fix(testbed): L5 — adopt-by-read identity, current host-add grammar, L3 bind+bearer carried; L6 drives a committed scenario

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]>
This commit is contained in:
v-openrig-buildandClaude Fable 5 committed 2026-08-07 19:22:34 +00:00
1 parent 8c5cd5b78b
commit 3dffc83598
2 files changed
+48 -11

No files matched your search

@@ -15,28 +15,62 @@ the received signature names the origin and the reply lands on the origin, not t
## L5.1 — boot two named self-hosts on one network
**IDENTITY IS READ, NEVER PREDICTED (PM ruling — capture-not-compose applied to identity).** The
self-host id is minted daemon-side and stored as a never-rewritten singleton; the runbook CAPTURES
each container's id from its own surface (`/healthz`, whose `selfHostId` field is emitted at
`packages/daemon/src/server.ts:589`) and then uses that CAPTURED value verbatim. It never predicts,
hardcodes, or derives an id. (`OPENRIG_SELF_HOST_ID` was NOT the knob: `docker/testbed/entrypoint.sh:8`
only ECHOES it for `docker logs`; nothing in the daemon reads it. Do not reintroduce it.)
**BIND + BEARER, carried from L3 verbatim.** L5 needs cross-container HTTP, so each daemon must be
reachable off-loopback — the same three coupled parts L3 proved, for the same reasons, with the same
never-weaken-the-guard rail: explicit `OPENRIG_HOST=0.0.0.0`, `OPENRIG_AUTH_BEARER_TOKEN` set (the
guard at `packages/daemon/src/middleware/auth-bearer-token.ts:240-264` REFUSES a non-loopback bind
without it), and an explicit published port when a host-side probe is used. Registry rows carry a
bearer POINTER (`--bearer-env` NAME), never a token value.
```bash
GIT_SHA="$(git rev-parse HEAD)"; IMAGE="openrig-testbed:${GIT_SHA}"; EVID="dist/testbed-image/evidence/${GIT_SHA}"; mkdir -p "${EVID}"
NET="orig-net-${GIT_SHA:0:8}"; docker network create "${NET}" >/dev/null
TESTBEARER="l5-testbed-$(date +%s)" # throwaway, container-scoped, never a real credential
# Each container adopts a distinct self-host id via the entrypoint (plan §3 / 51-09 adopt path).
docker run -d -t --name H_A --hostname H_A --network "${NET}" -e OPENRIG_SELF_HOST_ID=A "${IMAGE}"
docker run -d -t --name H_B --hostname H_B --network "${NET}" -e OPENRIG_SELF_HOST_ID=B "${IMAGE}"
for h in H_A H_B; do docker exec "$h" bash -lc 'rig daemon start && sleep 2 && rig whoami --json'; done | tee "${EVID}/L5-selfids.txt"
for h in H_A H_B; do
docker run -d -t --name "$h" --hostname "$h" --network "${NET}" \
-e OPENRIG_HOST=0.0.0.0 \
-e OPENRIG_AUTH_BEARER_TOKEN="${TESTBEARER}" \
-e OPENRIG_TESTBED_BEARER_ENV=OPENRIG_AUTH_BEARER_TOKEN \
"${IMAGE}"
done
for h in H_A H_B; do docker exec "$h" bash -lc 'rig daemon start && sleep 2 && rig status || true'; done | tee "${EVID}/L5-start.txt"
# CAPTURE each minted id from its own daemon (adopt-by-read; ids are random per boot).
ID_A="$(docker exec H_A bash -lc 'curl -fsS http://127.0.0.1:7433/healthz' | python3 -c "import json,sys; print(json.load(sys.stdin).get('selfHostId',''))")"
ID_B="$(docker exec H_B bash -lc 'curl -fsS http://127.0.0.1:7433/healthz' | python3 -c "import json,sys; print(json.load(sys.stdin).get('selfHostId',''))")"
printf 'H_A selfHostId=%s\nH_B selfHostId=%s\n' "${ID_A}" "${ID_B}" | tee "${EVID}/L5-selfids.txt"
[ -n "${ID_A}" ] && [ -n "${ID_B}" ] && [ "${ID_A}" != "${ID_B}" ] || { echo "L5.1 FAIL: missing or identical self-host ids"; exit 1; }
```
**PASS:** each `rig whoami` reports its distinct self-id (`A`, `B`) — both non-`local`, registry-valid,
and stable across a daemon restart (51-09 incr 1). **FAIL:** duplicate/`local` self-ids.
**PASS:** both containers report a NON-EMPTY, DISTINCT `selfHostId` on their own `/healthz`, and each
id is stable across a daemon restart in that container (51-09 incr 1 — the singleton is never
re-keyed). **FAIL:** an empty id / identical ids / an id that changes on restart.
*Readback note (captured product observation, NOT worked around here): `rig whoami` reports a SEAT
identity and a bare host container has no seat, so it cannot answer "which host am I". The
daemon-surface read above is the shipped substitute. The declared-identity + host-readback gap is
pooled at PM's desk (identity family); this runbook consumes what ships.*
## L5.2 — compose via the host registry over HTTP
Register `H_B` in `H_A`'s host registry pointing at the container-DNS name `H_B` (reachable on `${NET}`):
```bash
docker exec H_A bash -lc 'rig host add B --url http://H_B:7433 && rig host ls --json' | tee "${EVID}/L5-registry.txt"
# CURRENT grammar (packages/cli/src/commands/host.ts:427-433): --id and --transport are REQUIRED;
# http transport takes --url; the bearer rides as a POINTER (env var NAME), never a value.
# The id is the CAPTURED ${ID_B} — adopt-by-read, never an authored name.
docker exec H_A bash -lc "rig host add --id '${ID_B}' --transport http --url http://H_B:7433 --bearer-env OPENRIG_AUTH_BEARER_TOKEN && rig host ls --json" | tee "${EVID}/L5-registry.txt"
```
**PASS:** `H_A`'s registry lists host `B` at the container-DNS URL; a probe reaches `H_B`'s daemon over
**PASS:** `H_A`'s registry lists the CAPTURED id `${ID_B}` at the container-DNS URL; a probe reaches `H_B`'s daemon over
the network only. **FAIL:** unreachable / resolves off-network. (Ground the exact `rig host` flag shape
against the shipped CLI at execution — read `--json`, don't assert a remembered field.)
@@ -61,7 +61,10 @@ const docker = (args) => new Promise((res) => {
// Container-mode: inject the ScenarioDaemon-shaped container adapter + the manifest id.
const daemon = (scaffold /*, opts */) => spawnContainerDaemon(scaffold, { image, docker });
const scenario = `${SCENARIO}/` + (process.env.L6_SCENARIO ?? "scenario-collision.yaml");
// COMMITTED scenario (the prior literal `scenario-collision.yaml` DOES NOT EXIST in the tree —
// operator-caught at the L4/L6 re-entry). L6 proves the container RUNNER, so it drives a real
// shipped scenario; override with L6_SCENARIO to run another COMMITTED file, never an improvised one.
const scenario = `${SCENARIO}/` + (process.env.L6_SCENARIO ?? "scenario-02-baton.yaml");
const baseEnv = { HOME: process.env.HOME, PATH: process.env.PATH, TERM: "xterm" };
const records = [];
@@ -87,7 +90,7 @@ Run the IDENTICAL scenario in **host-mode** (no `daemon` override, no `imageId`)
```bash
L6_MODE=host node --import tsx packages/daemon/scripts/run-scenarios.mjs \
packages/daemon/test/fixtures/scenarios/scenario-collision.yaml | tee "${EVID}/L6-host.txt"
packages/daemon/test/fixtures/scenarios/scenario-02-baton.yaml | tee "${EVID}/L6-host.txt"
```
**PASS:** the host-mode verdict for the same scenario matches the container-mode verdict from L6.1 —
@@ -108,7 +111,7 @@ import { spawnContainerDaemon } from "./packages/daemon/test/helpers/scenario-co
const [image] = process.argv.slice(2);
const daemon = (scaffold) => spawnContainerDaemon(scaffold, { image, docker: async () => ({ stdout: "", stderr: "", code: 0 }) });
try {
await runScenarioFile(resolve("packages/daemon/test/fixtures/scenarios/scenario-collision.yaml"),
await runScenarioFile(resolve("packages/daemon/test/fixtures/scenarios/scenario-02-baton.yaml"),
{ rigBin: resolve("packages/cli/dist/bin-wrapper.js"), baseEnv: process.env, daemon });
console.log("NO-REFUSAL"); process.exit(0);
} catch (e) { console.error("REFUSED: " + e.message); process.exit(7); }