Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
11 KiB
Row 178: Console proxy proof
Issue #1676 / P19 385. Owner Rocko; reviewer Dewey; gate owner Sage.
Brief: docs/plans/2026-10-19_console-proxy-proof.md, blob 1da7ac8753.
Claim rev 2125. Initial base c7421cabd (plans only above 7f519242e).
Boundaries and design
Only code and fixture tests. No slot token, Traefik middleware, live unit flags, user config, live bus or Console action. No real proof file is created. No credentials, push or source commit. Header and path alone enter unit flags and argv; the value lives only in a buffer read once before listen.
Serve's proofOptions is the shared startup validator. It opens O_NOFOLLOW and O_NONBLOCK, fstats the opened descriptor, requires a regular file owned by the process uid and exact permission bits 0400/0600, then reads that descriptor. A single optional final LF is removed; every remaining byte must be base64url and there must be at least 32. All file errors become one fixed refusal that names neither path nor contents. Headers use the existing header-name grammar, and Host, Origin, auth and tester names cannot collide with proof.
The public-Host request guard precedes auth-header reads, compares exactly one header via equal-length buffers and timingSafeEqual, and returns the same fixed 401 for missing, incorrect and repeated proof. Wrong-Host and tester ingress refuse the proof header with 403. Existing auth, Origin, Fetch Metadata and human write checks remain in force. The tester receives the header name only.
Service render checks metadata and canonical location, not contents. Proof flags precede --send and --tester-port, with the send-status regex extended to accept the optional proof pair; the tester status parsing is unchanged. stack-release already carries arbitrary Console flags through its pointer and target resolution, so its product change is an explicit preservation comment; new lifecycle tests pin that existing behavior for the proof pair, including tester-port replacement. No new activation flag or target probe.
Verification plan and review mapping
- proof.test.mjs: public read/send success, read once, config/log non-disclosure; missing/wrong/equal-length/short/repeated proofs, before-auth ordering and zero reads; wrong Hosts and tester rejection; header grammar and collisions.
- proof-cli.test.mjs: partial flags and preview requirement; missing/directory/ symlink/0644/other uid/short/interior newline/extra final LF/bad-byte refusals; generic errors; /proc argv and startup on/off; real main/tester wiring.
- service.test.mjs: both render flags, metadata/location refusal and no content validation; existing stand-in systemctl, no live unit action.
- stack-release.test.mjs: exact pointer and unit flags after re-pin, rollback and tester-port replacement. All units and copies are fixtures.
All suites and mutants wait for Sage's explicit slot after row 180. Before review, rebase onto any landed row 180/181 and rerun every touched test singly. Sage owns the full CLI/WebUI and shell landing gate. No timing benchmark is a gate: the constant-time primitive and equal-length check are reviewed directly.
Sandbox and residual boundary
Pending fixture-only probe and source evidence. The proof is a partial fix: same-uid T3 seats can read anything the Console uid can read. This row does not claim to prevent that. Service uid separation remains Jason's decision on #385. Activation is a separate Mos plan-check window, middleware first, then Console flags/restart and smoke; rollback drops flags before middleware. This packet never authorizes those steps.
Prepared checks (not yet run): render-no-open.py watches a temp fixture with Linux inotify IN_OPEN/IN_ACCESS, so reading and discarding bytes cannot pass the render check. Its mutant adds a discarded cat read. sandbox-probe.mjs uses the existing bwrap bind builder with both harness mount lists and a fixture marker outside all mounts, checks the host-config path only inside that sandbox, and also checks the run-task container's exact two mount sources in compose.yaml. It reads no live config. The legacy container check is source evidence only, not a claimed Docker runtime run; an operator override that mounts an ancestor of the proof could expose it. Same-uid callers remain outside this partial fix.
Seventeen exact-once mutant sites are prepared. Tests were written before the implementation, but execution is deferred under sage-1177; no red/green result is claimed before the slot. Only node --check, bash -n, git diff --check and mutation-site text preflight have run during preparation.
Rebase after row 180 (sage-1179)
Before the first run, rebuilt on a757f8fe6, including row 180 landing 1905e385c.
One manual conflict: packages/cli/tests/stack-release.test.mjs, both rows appended
a tail. Kept row 180's new timestamp, status and plan tests intact and closed
that test block, then appended the two independent T178 proof lifecycle tests.
The other shared files merged cleanly:
- scripts/stack-release.sh: row 180's activation-id and plan changes remain; row 178 adds only the proof-pair preservation comment at console_for_target.
- scripts/webui-service.sh: row 180's tester status parsing remains; row 178 adds proof flags/metadata checking without changing that parser.
- packages/webui/tests/service.test.mjs: row 180's status cases remain before the two appended proof render tests.
- docs/TOOLS.md: row 180's operational notes remain; proof notes are appended.
Base red: proof.test.mjs against the rebased original serve.mjs failed all four new tests because proofOptions did not exist, then the candidate was restored. The focused candidate runs follow; no red result is claimed for other files. The first shell command printed CLEAR before red/green began at 03:55:31Z.
Initial suite cleanup stall
The first WebUI suite stalled with proof-cli.test.mjs as its only remaining child. No serve child remained. After terminating that test process group, Node reported one cancelled file, zero assertion failures, and a pending Promise with the event loop resolved. The new test's cleanup checked only child.exitCode, which stays null after signal termination; it could await an already-emitted close event. Both new cleanup guards now also check signalCode. The corrected file passed 3/3 singly before the full suite rerun. The original log is retained as webui-suite-cleanup-stall.log; its counts are not a pass. No product change was made. The busy/sweep scripts also recognize node-* comm names (this Node build reports node-MainThread), as well as node/chromium.
Send-status regression found before packet freeze
After the first full package runs and all 17 original mutants passed, code review found that proof flags between --allow-user and --send made the service status regex say send off. A new service fixture test reproduced exactly that wrong line (service-status-red.log). The regex now accepts the optional proof pair; no tester-status logic changes. The service file passed 31/31 afterward. The final lifecycle and package runs are repeated after this local product fix. M18 removes only the new regex group and must fail the new status assertion. The final batch therefore has the original 17 plus this regression mutant.
The sandbox probe passed for the two harness mount lists, with fixture host config, the host config path and private HOME config all absent inside bwrap. It also verified compose.yaml's exact two mount sources: dataRoot and read-only PI_AUTH_FILE. This establishes no host-config mount in that source definition; no Docker runtime run or live-config-dependent claim is made. Overrides that mount an ancestor of proof can defeat that boundary. Same-uid readers remain outside this partial fix.
Final verification
Final base a757f8fe6. Slot span 2026-10-20T03:55:31Z–04:17:04Z, including the documented cleanup cancellation and local status regression repair. Final WebUI: 269 tests, 268 passed, one pinned esbuild rebuild skipped (0.28.1 installed, 0.28.2 required), zero failed/cancelled. Final CLI: 408/408, zero failed/cancelled/skipped. awk package sum: 677 tests, 676 passed, one skip. Focused file sum: 149/149. render-no-open and sandbox fixture commands passed. No Docker or shell suites were run; Sage owns the shell landing gate. All runners exited; process-sweep.json is empty. Slot clear sent at 04:17:04Z. No recurrence of the named infrastructure flakes was observed.
No source or packet commits. Source byte reconstruction and preserved transport pins are recorded by verification.json and pins.json. reader-and-pin-search.json and rg-path-search.json/rg-64hex-pins.txt record path, basename and hash searches across tests and script readers.
Mutant table
| Mutant | What it removes or changes | Killing test / pattern | Result |
|---|---|---|---|
| M01-auth-before-proof | proof refusal precedes auth | packages/webui/tests/proof.test.mjs / refuses missing wrong short | killed |
| M02-wrong-equal-length | same-length incorrect proof refuses | packages/webui/tests/proof.test.mjs / refuses missing wrong short | killed |
| M03-missing-proof | absent proof refuses | packages/webui/tests/proof.test.mjs / refuses missing wrong short | killed |
| M04-loopback-proof | proof on nonpublic Host refuses | packages/webui/tests/proof.test.mjs / header is refused on other Hosts | killed |
| M05-mode-0644 | 0644 file refused | packages/webui/tests/proof-cli.test.mjs / startup refuses | killed |
| M06-log-refusal | refusal log never includes proof | packages/webui/tests/proof.test.mjs / refuses missing wrong short | killed |
| M07-no-follow | fixture symlink refused | packages/webui/tests/proof-cli.test.mjs / startup refuses | killed |
| M08-owner | another uid refused | packages/webui/tests/proof-cli.test.mjs / startup refuses | killed |
| M09-line-bytes | short/newline/bad-byte file refused | packages/webui/tests/proof-cli.test.mjs / startup refuses | killed |
| M10-duplicate-proof | duplicate header refused | packages/webui/tests/proof.test.mjs / refuses missing wrong short | killed |
| M11-tester-proof | tester proof header refused | packages/webui/tests/proof.test.mjs / header is refused on other Hosts | killed |
| M12-main-wiring | actual CLI main listener checks proof | packages/webui/tests/proof-cli.test.mjs / CLI applies proof | killed |
| M13-tester-wiring | actual CLI tester rejects proof header | packages/webui/tests/proof-cli.test.mjs / CLI applies proof | killed |
| M14-service-location | render refuses repository/copy path | packages/webui/tests/service.test.mjs / proxy proof render refuses | killed |
| M15-service-propagation | render carries both flags | packages/webui/tests/service.test.mjs / proxy proof render carries | killed |
| M16-release-preservation | pointer/unit preserve pair through re-pin and rollback | packages/cli/tests/stack-release.test.mjs / T178 proxy proof header | killed |
| M17-render-no-open | render never opens or reads the fixture | render-no-open.py / inotify probe | killed |
| M18-proof-send-status | proof flags retain send-on status | packages/webui/tests/service.test.mjs / proxy proof status | killed |