Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agent/climb.jsonl
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{"id": "cli-bridge-interface4-receiver-20261011", "project": "cli-bridge", "layer": "system", "hill": "Crash-safe cloud-to-local context admission", "lever": "adopt canonical request and durable allocated environment binding", "prediction": "The exact profile route will accept after the public contract is upgraded; unsupported allocation fields will have no durable admission", "outcome": "PENDING", "timestamp": "2026-10-11T23:25:00Z", "baseline": {"status": "Public request material rejects profile at Interface 2.13.1", "codeVersion": "7ee852e9079638f59e514aa73110e4103a2e7478", "instrument": "tests/context-transfer.test.ts"}, "result": {"status": "Implementation authored; published Interface floor and green gate pending"}, "reasoning": "Frozen narrow Beelink gate failed in canonical request digest serialization before HTTP. The receiver also needs a separately persisted actual environment identity for an unbound accepted plan."}
{"id": "cli-bridge-native-context-owner-20261012", "project": "cli-bridge", "layer": "system", "hill": "Crash-safe cloud-to-local context admission", "lever": "backend-owned typed native initialization and durable native acknowledgement phases", "prediction": "One real pinned Codex conformance case preserves exact selected role and text-part history across receiver restart into the first model input, with zero model calls before authoritative receipt and zero repeated native allocation/import after lost HTTP receipt; uncertain native ACK cases remain fenced", "outcome": "PENDING", "timestamp": "2026-10-12T00:50:44Z", "baseline": {"status": "Receiver source issued a context receipt after readiness and fed selected history through the prompt path; no qualified native first-turn evidence", "codeVersion": "b8585902da9e77872c23c63b836cd5ec8bb42b1d", "instrument": "tests/codex-context-transfer-real.test.ts"}, "result": {"status": "Backend native import, durable phase CAS, ordinary dispatch fence and real-binary conformance authored; frozen dependency floor and gate pending"}, "reasoning": "Use the backend owning profile materialization, executor, auth, and native state. Official Codex public injection checkpoints before ACK. Owned native probes show public history projections cannot reconcile an uncertain injection; hold that operation instead of repeating it. Qualify the first real model input through the consumer."}
{"id": "cli-bridge-codex-attach-20261014", "project": "cli-bridge", "layer": "system", "hill": "CLI Bridge routes that accept a Braid profile handoff", "lever": "selected-only Interface 5.2.1 request, attach reported only for the qualified Codex native import, first turn bound to the provider attach route", "prediction": "Braid main plan_fork and execute_fork to a Codex profile over this bridge is allowed, the transfer is imported into a fresh native Codex thread before the first turn, and that turn is admitted with only the new user input", "outcome": "PARTIAL", "timestamp": "2026-10-14T21:45:00Z", "baseline": {"value": 0, "measurement": {"metric": "bridge model routes reporting contextTransfer.attach", "unit": "routes", "selection": "the codex and claude-code routes of a bridge built from cli-bridge main before #258", "selectBy": "version", "n": 2, "basis": "measured", "codeVersion": "cli-bridge@68ece80", "perRelease": true, "instrument": "GET /v1/capabilities?model=<route> on main source (src/sessions/retained/capabilities.ts reports no attach)", "calibration": {"verdict": "UNCALIBRATED", "next": "read the same endpoint on a deployed main bridge"}}}, "result": {"value": 1, "measurement": {"metric": "bridge model routes reporting contextTransfer.attach", "unit": "routes", "selection": "the codex/openai/gpt-5.5 and claude-code/anthropic/claude-opus-5-5 routes of a bridge started from fix/context-transfer-interface4-20261011 with codex-cli 0.162.1", "selectBy": "version", "n": 2, "basis": "measured", "codeVersion": "cli-bridge@afe3da1", "perRelease": true, "instrument": "GET /v1/capabilities?model=<route> on beelink1, then a Braid main 5193bc1 braid rpc plan_fork and execute_fork with profileRef and runner codex", "calibration": {"verdict": "PASS", "check": "the same Braid run reached execute_fork acceptance, a Codex rollout holding the selected messages before the first turn, and first-turn admission", "against": "the claude-code route, which reports no contextTransfer and is refused"}}}, "reasoning": "Codex is the one route that imports into a fresh native session. The first answer could not be produced: every Codex seat is at its usage limit until 2026-10-19 and the Beelink host Codex logins fail refresh (401), so the model step of the live proof remains open."}
{"id": "cli-runtime-fixed-high-20261023", "project": "cli-bridge", "layer": "system", "hill": "Vendor-fixable HIGH findings in the shipped CLI runtime", "lever": "publisher-qualified npm self-upgrade, shared Kimi overrides and append-only per-slot runtime transactions", "prediction": "Reduce the ten retained vendor-fixable HIGH findings to zero in the rebuilt runtime, retain zero CRITICAL, then serve the artifact on all eight independently drained slots without losing credentials or predecessor evidence.", "outcome": "PENDING", "timestamp": "2026-10-23T01:23:19.601645+00:00", "baseline": {"value": 10, "measurement": {"metric": "vendor-fixable HIGH findings in shipped runtime image", "unit": "findings", "n": 1, "selection": "all CRITICAL/HIGH findings with FixedVersion in the retained f24a12 runtime Trivy report; one immutable artifact serves eight slots", "selectBy": "version", "basis": "computed", "codeVersion": "sha256:f24a12b08eb3deafec20639413bdd6c538846b501a00e77f3f36ec18005bdd10", "perRelease": true, "instrument": "retained runtime-trivy.json plus live authenticated pool maintenance receipts", "calibration": {"verdict": "PASS", "check": "six package names and exact package paths were matched to retained scanner findings; eight live slot image/config/rootfs identities verified", "against": "retained immutable runtime Trivy JSON and eight Docker image bindings"}}}, "result": {"status": "Real Docker/authenticated HTTP repeated-artifact and restart integration passed209s at9367486; image native/scan pass awaits admitted floor+12GiB producer, and live second roll remains pending.", "value": 209, "measurement": {"metric": "actual repeated-artifact Docker and authenticated HTTP integration gate duration", "unit": "seconds", "n": 1, "selection": "the exact9367486 frozen install/typecheck/Docker/HTTP gate beelink2-a2bb99ec5e50c9cddefb6c8842500c8b", "selectBy": "sample", "basis": "measured", "codeVersion": "936748602e3ca28535b39441cc5cb11776cd95d9", "perRelease": true, "instrument": "beelink-gate durable receipt and real Docker/HTTP proof output"}}, "reasoning": "Patch the existing image owners and roll through the existing authenticated slot owner. Count UV cache archives as shipped packages and retain remaining unfixed HIGH findings; the scheduled native host-security owner will scan actual local source-labeled immutable images."}
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,8 @@ Retained native-session API jobs do not yet enter this one-shot capture scope.
### Individually maintained Docker pool slots

The bearer-protected `/admin/pools/status` endpoint reports explicit adopted pool
identities and durable roll receipts. Maintenance requires `BRIDGE_BEARER`, even
identities, per-slot immutable image bindings, and durable roll receipts/history.
Maintenance requires `BRIDGE_BEARER`, even
on loopback. Adoption requires Linux with the existing `ss` socket inventory tool;
unavailable or ambiguous listener evidence refuses ownership transfer. Run adoption
from a native Linux bridge service in the same host network namespace as the
Expand Down Expand Up @@ -1270,7 +1271,12 @@ No image is pulled and no volume is deleted by this interface.

`pool-maintenance.json` under the bridge data directory records exact old and
candidate identities before each Docker transition. After an interrupted roll,
retry the same original ID and image; a different intent is refused. After owner
retry the same original ID and image; a different unfinished intent is refused.
Once a transaction is promoted, drain its current candidate ID and request a
different immutable local image to begin the next transaction. The owner appends
the completed receipt to history before recording the new intent, retaining the
original adoption identities and every stopped predecessor. Missing local images
refuse without changing the completed journal. After owner
restart, adopted slots remain drained until the same roll request verifies and
re-admits each completed candidate. The journal contains no credential values.
Use the registered service deployer to activate merged source; an unchanged
Expand Down
5 changes: 3 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM node:24.22.1-trixie-slim AS base
LABEL org.opencontainers.image.source="https://github.com/drewstone/cli-bridge"
WORKDIR /app

# Build toolchain + CLI-install prereqs. curl is needed for the opencode
Expand Down Expand Up @@ -35,8 +36,8 @@ RUN curl -fsSL https://opencode.ai/install | bash \
# install with an absolute path. Symlink both into /usr/local/bin so
# they're on PATH at runtime AND survive the /root/.kimi volume mount
# (which only covers auth state).
COPY scripts/install-harness.sh scripts/python-security-overrides.txt /usr/local/share/cli-bridge/
RUN sh /usr/local/share/cli-bridge/install-harness.sh kimi
COPY scripts/install-harness.sh scripts/python-security-overrides.txt scripts/npm-security.json /usr/local/share/cli-bridge/
RUN CLI_BRIDGE_UPGRADE_IMAGE_NPM=1 sh /usr/local/share/cli-bridge/install-harness.sh kimi

# Factory droid, Sourcegraph amp, Forge left out by default — install
# manually on hosts that need them. They change rapidly.
Expand Down
5 changes: 3 additions & 2 deletions docker/Dockerfile.cli-runtime
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
# active backend needs.

FROM node:24.22.1-trixie-slim
LABEL org.opencontainers.image.source="https://github.com/drewstone/cli-bridge"

ARG CLI_BRIDGE_HARNESSES=all

Expand All @@ -50,8 +51,8 @@ RUN apt-get update \
# --- CLI installs (Pi is pinned to its qualified RPC version) ---
# Source of truth lives in scripts/install-harness.sh so host installs,
# subset images, and the all-in-one runtime cannot drift.
COPY scripts/install-harness.sh scripts/python-security-overrides.txt /usr/local/share/cli-bridge/
RUN set -eu; \
COPY scripts/install-harness.sh scripts/python-security-overrides.txt scripts/npm-security.json /usr/local/share/cli-bridge/
RUN set -eu; export CLI_BRIDGE_UPGRADE_IMAGE_NPM=1; \
if [ "$CLI_BRIDGE_HARNESSES" = "all" ]; then \
sh /usr/local/share/cli-bridge/install-harness.sh all; \
else \
Expand Down
21 changes: 21 additions & 0 deletions scripts/install-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ if [ -z "$harness" ]; then
exit 2
fi

# Image-only npm self-upgrade; host harness installation keeps its existing npm.
# The descriptor owns the qualified publisher version and archive integrity.
if [ "${CLI_BRIDGE_UPGRADE_IMAGE_NPM:-0}" = "1" ]; then
npm_security_tmp="$(mktemp -d)"
trap 'rm -rf "$npm_security_tmp"' EXIT HUP INT TERM
npm_security_version="$(node -p 'require(process.argv[1]).version' "$harness_script_dir/npm-security.json")"
curl --fail --silent --show-error --location --max-time 120 \
"https://registry.npmjs.org/npm/-/npm-$npm_security_version.tgz" --output "$npm_security_tmp/npm.tgz"
node --input-type=module - "$harness_script_dir/npm-security.json" "$npm_security_tmp/npm.tgz" <<'VERIFY_NPM'
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
const descriptor = JSON.parse(readFileSync(process.argv[2], 'utf8'))
const actual = 'sha512-' + createHash('sha512').update(readFileSync(process.argv[3])).digest('base64')
if (actual !== descriptor.integrity) throw new Error('npm security archive integrity mismatch')
VERIFY_NPM
npm install --global --ignore-scripts --no-audit --no-fund "$npm_security_tmp/npm.tgz"
test "$(npm --version)" = "$npm_security_version"
rm -rf "$npm_security_tmp"
trap - EXIT HUP INT TERM
fi

install_claude() {
npm install -g @anthropic-ai/claude-code@latest
}
Expand Down
4 changes: 4 additions & 0 deletions scripts/npm-security.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "11.21.1",
"integrity": "sha512-YTzL6QBEJXyHcTu0JiUc7RKMp+mzlJtl7rGLmERaXUQeFHIMc9atdr6G9M9C8vDWbv40epoTQAzNL2/PtZVDhQ=="
}
6 changes: 5 additions & 1 deletion scripts/python-security-overrides.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
# Kimi pins an older Pillow; this image owns the audited security override.
# Audited Kimi dependency overrides; shared by both shipped image recipes.
pillow==13.0.0
aiohttp==3.14.4
asyncssh==2.23.1
lxml==6.1.0
lxml_html_clean==0.4.5
32 changes: 32 additions & 0 deletions scripts/verify-pool-runtime-roll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const owned = new Set<string>()
const volumes = new Set<string>()
const checks: string[] = []
let oldImage = ''
let nextImage = ''
let pool: ContainerPool | undefined
let appOwner: Awaited<ReturnType<typeof buildApp>> | undefined
let server: ReturnType<typeof serve> | undefined
Expand Down Expand Up @@ -64,6 +65,7 @@ try {
const seed = await command(['create', '--name', `${prefix}-seed`, image])
owned.add(seed)
oldImage = await command(['commit', '--change', `LABEL cli-bridge.proof=${suffix}`, seed])
nextImage = await command(['commit', '--change', `LABEL cli-bridge.proof-next=${suffix}`, seed])
await command(['rm', '-v', seed]); owned.delete(seed)

pool = await ContainerPool.create({ size: 2, image: oldImage, namePrefix: `${prefix}-leases`,
Expand Down Expand Up @@ -144,7 +146,36 @@ try {
assert.ok(status.pools[0]!.slots.every((slot) => !slot.draining))
assert.equal(readFileSync(join(data, 'pool-maintenance.json'), 'utf8').includes(token), false)
checks.push('actual HTTP owner restart reconciles completed receipts and re-admits verified slots without secrets in journal')
const firstRoll = JSON.parse(readFileSync(join(data, 'pool-maintenance.json'), 'utf8')).pools[0].rolls['0'] as RollReceipt
const currentId = firstRoll.candidateId!
assert.equal((await api(`/admin/pools/${prefix}/slots/0/drain`, { expectedId: currentId })).status, 200)
const completedJournal = readFileSync(join(data, 'pool-maintenance.json'), 'utf8')
assert.equal((await api(`/admin/pools/${prefix}/slots/0/roll`, { expectedId: currentId, imageId: `sha256:${'0'.repeat(64)}` })).status, 409)
assert.equal(readFileSync(join(data, 'pool-maintenance.json'), 'utf8'), completedJournal)
const secondResponse = await api(`/admin/pools/${prefix}/slots/0/roll`, { expectedId: currentId, imageId: nextImage })
assert.equal(secondResponse.status, 200)
const second = (await secondResponse.json() as { receipt: RollReceipt }).receipt
owned.add(second.candidateId!)
assert.notEqual(second.nonce, firstRoll.nonce)
assert.equal(second.oldId, currentId)
assert.equal((await dockerEngine('GET', `/containers/${currentId}/json`)).State.Running, false)
assert.equal((await dockerEngine('GET', `/containers/${slots[0]!.id}/json`)).State.Running, false)
assert.equal(await command(['exec', second.candidateId!, 'cat', '/root/.config/opencode/proof-marker']), 'retained')
await stopApp()
await startApp()
assert.equal((await api(`/admin/pools/${prefix}/slots/0/roll`, { expectedId: currentId, imageId: nextImage })).status, 200)
assert.equal((await api(`/admin/pools/${prefix}/slots/0/roll`, { expectedId: slots[0]!.id, imageId: image })).status, 409)
const journal = JSON.parse(readFileSync(join(data, 'pool-maintenance.json'), 'utf8')).pools[0]
assert.deepEqual(journal.slots, slots)
assert.deepEqual(journal.history['0'], [firstRoll])
assert.equal(journal.rolls['0'].candidateId, second.candidateId)
const repeatedStatus = await (await api('/admin/pools/status')).json() as { pools: Array<{ slots: Array<{ containerId: string; draining: boolean }> }> }
assert.equal(repeatedStatus.pools[0]!.slots[0]!.containerId, second.candidateId)
assert.equal(repeatedStatus.pools[0]!.slots[0]!.draining, false)
checks.push('second immutable artifact appends prior receipt, preserves original identities and volumes, rejects stale intent, and reconciles after owner restart')
checks.push('missing second artifact refuses without changing completed journal')
const result = { complete: true, checks, old_fixture_image: oldImage, candidate_image: image,
second_candidate_image: nextImage,
production_node_upgrade_proven: false, no_model_calls: true }
const output = process.env.CLI_ROLL_PROOF_OUTPUT
if (output) writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 })
Expand All @@ -156,5 +187,6 @@ try {
for (const id of owned) await dockerCli(['rm', '-f', '-v', id])
for (const volume of volumes) await dockerCli(['volume', 'rm', volume])
if (oldImage) await dockerCli(['image', 'rm', oldImage])
if (nextImage) await dockerCli(['image', 'rm', nextImage])
rmSync(root, { recursive: true, force: true })
}
Loading