A clean install of the CLI couldn't start: bare `rig` and `rig --help` exited 1 with `ERR_MODULE_NOT_FOUND` for `@modelcontextprotocol/sdk`. `packages/cli/src/commands/environment.ts` imports the SDK client at module load and `index.ts` loads the environment command statically, but the SDK was declared only in the CLI's `devDependencies`, so an install without development dependencies didn't have it. The SDK (`^1.32.2`) is now a production dependency of `packages/cli`; the lockfile update is metadata only (its dev flags), with no package added, removed or re-versioned. `scripts/cli-runtime-dependencies.test.mjs` walks the import closure from the CLI's entry points and fails if any external package it reaches isn't a production dependency, and the testbed image now runs `rig --help` after `rig --version`.
4 files, +77/-87.
The internal testbed image moves from Node 22.22.1 to 22.23.4 (CVE-2026-111497). Its download is checked against Node's official SHASUMS256, and the check binds the extracted archive, so a mismatch stops the build.
The scenario runner now settles its containers by recorded id instead of `rm -f`. It removes the cidfile only after a confirmed removal, so a re-run into the same folder isn't refused, and it prints a retained container's name.
This image isn't shipped. Its rebuild hasn't been qualified on its own: the first ordinary run of the route that consumes it is the proof.
* test: bind scenario seeds and exercise the library baton in CI
* test: run named scenarios through a remote Docker executor
* test: bound and clean up the testbed image-load check
---------
Co-authored-by: dev-driver <dev-driver@openrig-build>
Co-authored-by: dev-qa <dev-qa@openrig-build>
* ci: add installed baton scenario and regression control to PR tests
* ci: retain healthy return after the durability fault control
* Bind seeded failure to queue observation and propagate pack errors
* test: repair hosted fixture drift and bound temporary IPC
* ci: record a bounded self-process probe before package suites
* test: keep hosted process and transport probes confined
---------
Co-authored-by: dev-driver <dev-driver@openrig-build>
Co-authored-by: dev-qa <dev-qa@openrig-build>
Founder-ruled engine-independent leg (2026-08-21 ~18:40Z; containers land 5.3,
this fix lands now). Root cause on record in the scoping report: the L6 runbook's
inline execFile invoker predated the stdinFrom tar-pipe contract (9d2f1f2cc) and
ignored it — an open never-fed pipe stdin left the in-container `tar -xf -`
blocked on read forever (row 42576855, the 08-12 seven-minute hang).
makeRealStagingDocker implements the contract as committed, tested code:
- no stdinFrom → stdin IGNORED (closed), so a stdin-reading child terminates on
EOF (the hang class dies even on a misused step);
- stdinFrom → two processes, producer piped into consumer with EOF forwarded,
DUAL-EXIT checked (a failing tar into a succeeding exec no longer masks an
empty stage), EPIPE on the wiring swallowed and the producer's death reported;
- consumer-exits-early → producer killed and the step FAILS named (this
invoker's own first cut hung exactly there — the producer's stdout backed up
against a pipe nobody drained; the RED test caught it at 20s);
- every step bounded by a timeout (default 120s) that kills both and returns a
NAMED failure, never an unbounded wait.
The runbook now IMPORTS the helper — an invoker inlined in a document drifts
behind the code contract with nothing to fail. 8/8 hermetic (sh stand-ins, no
engine) + 19/19 adjacent container suites; typecheck clean. NO live containment
claim: the real-engine proof (docker vs Apple container exec-stdin semantics)
explicitly waits on the 5.3 engine-substrate ruling, and the helper's docs say so.
Claude-Session: https://claude.ai/code/session_01DWhNDDavuNoFWK8ZrYtSdH
(cherry picked from commit 1f6d82c71df86d8fba149c0cfc99d18bc003baaa)
L1-L4 are green (operator receipt 814cd300); L5 failed on four components,
three of which are runbook-vs-shipped-surface staleness and are fixed here.
(1) IDENTITY — adopt-by-read (PM ruling): the runbook now CAPTURES each
container's minted selfHostId from its own /healthz (server.ts:589) and
uses that value verbatim, at every provisioning. It never predicts,
hardcodes, or derives an id. OPENRIG_SELF_HOST_ID was never a knob:
entrypoint.sh:8 only echoes it for docker logs and nothing in the daemon
reads it — the runbook no longer sets it.
(2) HOST-ADD GRAMMAR: "rig host add B --url ..." -> the shipped form
(host.ts:427-433) with required --id/--transport, --url for http, and a
bearer POINTER (--bearer-env NAME), never a token value.
(3) BIND + BEARER CARRIED FROM L3 verbatim: L5 needs cross-container HTTP, so
each daemon binds 0.0.0.0 with OPENRIG_AUTH_BEARER_TOKEN set — satisfying
assertBindAuthInvariant (auth-bearer-token.ts:240-264), never weakening it.
(4) whoami-without-seat is left as a captured product observation with a note,
not worked around; the daemon-surface read is the shipped substitute.
L6: the literal scenario-collision.yaml DOES NOT EXIST in the tree (operator
caught it at re-entry). L6 proves the container RUNNER, so it now drives the
committed scenario-02-baton.yaml in both container- and host-mode, with
L6_SCENARIO documented as an override that must also name a COMMITTED file.
Authoring a real cross-host collision scenario is a separate scenario-
authoring item, not an improvisation inside a runbook.
Co-Authored-By: Claude Fable 5 <[email protected]>
L3.1 passed on the published-daemon procedure (operator receipt dfcdd798:
published healthz green, authenticated transport routed 404-not-401,
unauthenticated negative 401, sqlite container-local). L3.2 then RED:
"missing required argument 'source'".
Two causes, both in the runbook, both now fixed:
- STAGED PATH: the build verb preserves each census entry's full
repo-relative path (build-testbed-image.sh:51-52) and the Dockerfile copies
that context dir wholesale (Dockerfile:52), so the trio lands in-container
at /opt/openrig-testbed/stub-assets/docker/testbed/stub-assets/ — not the
stub-assets root the runbook assumed. The nesting is load-bearing: the same
relative paths are hashed into the byte-reproducible census receipt
(build-testbed-image.sh:101), so flattening the staging would change receipt
bytes r1 verified. The RUNBOOK adapts; the staging stands.
- EXPLICIT SOURCE: rig up takes a required positional (up.ts:77); a bare
invocation is the exact error the operator hit. Now "rig up rig.yaml".
Added a pre-flight fence: assert rig.yaml at the staged path and fail LOUD
naming it (plus a payload listing) rather than surfacing as a downstream
argument error, so a future staging change is self-diagnosing.
PROVENANCE: this failure was PREDICTED and scoped to this runbook by
dev-driver, endorsed in 51-04-STUB-ASSET-TRIO-REVIEW-VERDICT-review-r1.md
(sha-16 5d4a8bb1d0a1605b, "Honest forward-flag") — the note never reached the
runbook text until now; it is quoted in place so it cannot be lost again.
SIBLING SWEEP (desk ask): L4/L5/L6 reference neither the staged payload, nor
rig up, nor the health probe — both defects were L3-only.
Co-Authored-By: Claude Fable 5 <[email protected]>
The A/B at pin feeda3a80 was BLOCKED-BEFORE-MEASUREMENT: L3 failed on BOTH
arms (Docker curl 52, Apple curl 56) because the runbook started a
LOOPBACK-BOUND daemon and probed it through a published port — unreachable by
construction, on any runtime. A control that cannot pass cannot judge the
variable arm, so no verdict was produced (correct) and thresholds stayed
unengaged. Operator receipt: Q2-AB-feeda3a80-20260807-host/AB-RESULT.md
sha256 3433e95f427828ce.
Four coupled mechanics, all grounded at source, IDENTICAL on both arms:
(a) explicit bind OPENRIG_HOST=0.0.0.0 (index.ts:148 reads it; unset defaults
to 127.0.0.1 at :167 — that default is the whole defect);
(b) OPENRIG_AUTH_BEARER_TOKEN set, because assertBindAuthInvariant
(auth-bearer-token.ts:240-264) REFUSES to start a non-loopback bind
without it — the procedure SATISFIES the guard, never weakens or
special-cases it (PM rail: build the missing piece, never soften the gate);
(c) an explicit host port, never 0 — Apple 1.2.0 rejects ephemeral publish
where Docker accepts it (captured parity difference, not an A/B variable);
(d) two probes, two distinct facts, both recorded.
VERIFIED-AT-SOURCE CORRECTION to the dispatched shape: /healthz is registered
directly on the app (server.ts:574) and is UNAUTHENTICATED — there is no
global auth middleware (the app.use("*") at :441 only injects deps) and the
bearer middleware is mounted inside exactly six route modules. So the health
probe does NOT 401 without a header and must not be scored on one. The bearer
is proven separately on a genuinely guarded surface (/api/transport/*, whose
router guards "*" at transport.ts:9), plus a NEGATIVE CONTROL asserting the
same call without a bearer IS 401 — otherwise the runbook would claim to test
an auth path it never exercised.
Acceptance gate written into the runbook: prove the DOCKER BASELINE GREEN
under this exact text before any A/B rerun is requested.
Co-Authored-By: Claude Fable 5 <[email protected]>
Break #4 in the Q2 series (Dockerfile unchanged since fold 54; revealed by fixing
break #3 — the self-contained tarball now lets npm reach better-sqlite3's native
install). better-sqlite3's install is `prebuild-install || node-gyp rebuild`; the
image had neither a prebuilt path nor a toolchain, so layer 3 died
('prebuild-install: not found' → 'Could not find any Python installation', exit 1).
FIX (a), per the sealed Q2 packaging ruling (better-sqlite3 builds FRESH ON TARGET,
never prebuilt/nested → no wrong-arch binary): layer 1 installs python3 make g++.
Testbed-image scope only (no multi-stage gold-plating).
RIDER (PM, ratified) — close the CLASS 'green gate over a broken install'
(assert-the-EFFECT-not-the-command): the build verb now LOADS the daemon inside the
freshly-built container (rig daemon start → opens the DB → better-sqlite3 must have
bound), reusing the proven L3-daemon-in-container sequence. A host clean-dir install
can't catch this (host has the toolchain); only a container load does. Rides the
pre-pin build verb (not a per-fold docker build); the A/B pin package requires it green.
PROOF STATUS — HONEST: docker is absent in the VM seat BY RULING (build is host-side;
build-testbed-image.sh itself guards `command -v docker` → exit 3). VM-authorable
structural fences GREEN 12/12 (build-testbed-image.test.mjs: toolchain present +
effect-proof load step present) + bash -n clean. The behavioral RED→GREEN (docker
build fails at layer 3 on this branch's parent → builds + daemon loads with this fix)
MUST run HOST-SIDE — handing off. NOT claiming a verified image build.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
First real host run of the 51-04 testbed build verb (reviewed+folded, never host-RUN
— the runtime-proof gap) surfaced two blockers:
A. build-testbed-image.sh npm-pack'd the private monorepo ROOT ([email protected], NO
bin) → the image's rig --version smoke died exit 127. FIX: assemble via
build-package.sh, then pack packages/cli (@openrig/cli, ships the rig bin).
B. plain docker build → the legacy builder never populates the automatic TARGETARCH
→ the Dockerfile defaulted amd64 → x64 Node in an arm64 image → exit 133. FIX:
resolve the host arch (uname -m) and pass --build-arg TARGETARCH explicitly
(builder-agnostic), fail-CLOSED on unknown; the Dockerfile also fails-closed on an
empty TARGETARCH instead of the silent amd64 default.
RED pins: 3 structural (packs @openrig/cli via build-package.sh not root; resolves+passes
TARGETARCH fail-closed; Dockerfile no :-amd64 default) + an offline ARTIFACT effect proof
(the packed tarball is @openrig/cli and SHIPS dist/bin-wrapper.js — a root pack ships none).
KNOWN-REMAINING (flagged to orch, arch-class): the cli tarball is NOT yet standalone-
installable — @openrig/cli hard-depends on @openrig/daemon ("*"), which is 404/unpublished
and imported at runtime via a bare specifier, while build-package.sh bundles daemon to
<cli>/daemon/ not <cli>/node_modules/@openrig/daemon. So npm install -g still fails to
resolve @openrig/daemon. The full install+run proof + operator success ride that packaging
fix. Structural+artifact tests green (11/11).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Resolves the VM-side half of the stub census slot: authors the concrete zero-token
stub payload and populates docker/testbed/stub-assets.list (was empty => the build
verb refused, L0.2 FAIL). Base-image digest slot stays host-resolved (L0.1).
- docker/testbed/stub-assets/{rig.yaml, culture.md, agents/worker/agent.yaml}: a
runtime:stub, one-pod/one-seat topology. Shape grounded on the PROVEN 51-02 runner
fixture (test/fixtures/scenarios/topo-stub-baton.yaml) — member-key + top-key parity
MATCH — which the scenario runner settles headlessly in real tmux with zero LLM
tokens; both YAML files parse clean. Not guessed from memory (L3.3 grounding).
- stub-assets.list: exactly the trio's repo-relative paths (the census scope the
manifest hashes + the build verb stages; census-scope-match-code-path).
RED-first: scripts/testbed-stub-assets.test.mjs pins the L0.2 check (census non-empty,
names exactly the trio, every file exists, receipt each 64-hex) — RED on the empty
list (deriveStubAssetsHash loud-fails), GREEN after. 3/3 + full testbed suites 46/46.
L0.2 receipt stubAssetsHash: 3b24066fc0cc2e0a12ce03b32c7a18c170e62c394add583ffef0c105d8bd8349
Co-Authored-By: Claude Opus 4.8 <[email protected]>
L6 is the live proof of the step-3 mechanism the VM units injected: the runner's
container-mode drives a REAL scenario through the testbed image BY MANIFEST
IDENTITY (host-side reads -> published container port), stamps the manifest id
onto every ledger row, and carries two riders:
- L6.2 host-mode PARITY: the same scenario in host-mode must match container-mode
(the additive-opt-in fence, proven behaviorally where the VM units prove it
structurally; a divergence is a PM 51-02 gate, not a fold).
- L6.3 the L4 fail-closed STEP-3 FORM: a foreign OPENRIG_URL hard-refuses before
any container is created (the container is not an excuse to weaken the guard).
Wiring composes only already-tested helpers (spawnContainerDaemon, runScenarioFile,
withImageId); the only new-at-runtime piece is the real execFile docker invoker
(the container analogue of runRig). README index + sequencing updated (L6 after
L3); L4's grounding note now names L6.3 as its step-3 form.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Step-2 (plan §2): the L1-L4 live legs authored as host-executed, evidence-defined
runbooks (the 51-09 live-legs pattern; the VM seat has no runtime per the locus ruling).
Each leg = exact commands + real-byte capture + PASS/FAIL verdict-on-bytes (never from
memory) + hashed evidence dir (census-receipt discipline):
- L0 resolve inputs (base-image digest + stub-assets.list census — the two host-resolved slots)
- L1 PTY allocation (docker run -t + tmux capture real bytes + resize propagation)
- L2 tmux server lifecycle (detach-survive, multi-pane, exact send-keys/capture round-trip)
- L3 daemon-in-container (container-local sqlite, healthz on published port, rig up stub
topology settles — where stub-assets.list is grounded/finalized; zero tokens)
- L4 hermetic fail-closed (foreign OPENRIG_URL hard-refused INSIDE the container; grounding
note flags the packaged-vs-runner-mode form honestly)
- L5 multi-host (N named self-hosts on one docker network via the host registry, plan §3)
+ the 51-09 live-leg RIDER: executes the shipped collision/alignment RUNBOOK.md (LEG A
cross-host triple, LEG B founder-collision) against the testbed containers in one session.
Fences carried on every leg: no real-HOME mounts, network-only shared surface, scenarios
byte-unchanged, docker-gap-watch routes to PM.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Completes step-1 (Dockerfile + build verb + manifest). The build verb is a thin
host-side shell (scripts/build-testbed-image.sh) over the tested node core:
- testbed-emit-manifest.mjs wires host-resolved base digest -> stub census ->
computeTestbedManifest -> byte-stable manifest.json + census receipt, with a CLI
shim the shell invokes (spawn-tested: emits on good input, loud non-zero on unresolved).
- The shell builds openrig-testbed:<git-sha> from the TREE (npm pack, never registry)
against the digest-pinned base, NEVER pushes; fences guarded by build-testbed-image.test.mjs.
- docker/testbed/base-image + stub-assets.list: documented HOST-RESOLVED slots (locus
ruling) — both refuse an unresolved/empty state, so the tree can never build floating.
docker build runs host-side (the VM seat has no runtime); all logic is VM-unit-tested.
RED-first throughout. 43/43 across the 5 testbed suites; bash -n clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The openrig-testbed Dockerfile (docker/testbed/) as the deliverable: digest-pinned
base parameterized via ARG BASE_IMAGE (the verb refuses a tag-floating ref), tmux+git
PTY substrate, pinned in-range node, OpenRig from the LOCAL pack tarball never the npm
registry, staged stub assets, a non-root openrig user, tini PID1 entrypoint that seeds
the 51-09 self-host-id adopt path. docker build runs host-side (locus ruling); the
plan-§1 fences are pinned as an executable contract (scripts/testbed-dockerfile.test.mjs
8/8, negative-asserts the registry-install fence), RED-first.
Co-Authored-By: Claude Opus 4.8 <[email protected]>