mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
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:
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); }
|
||||
|
||||
Reference in new issue
Block a user