fix(security): isolate Pi child credentials and make farm writes symlink-safe#420
fix(security): isolate Pi child credentials and make farm writes symlink-safe#420SUaDtL wants to merge 5 commits into
Conversation
The 2026-07-24 /ca:dev entry and exit pair was left uncommitted by the prior session. overrides.log is an append-only audit artifact; persist it. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L
Give Pi workers only an ephemeral selected-provider credential projection and make every model-controlled farm write reject link, junction, hardlink, and replacement races before mutation. Document the ADR-0016 supersession and keep raw child stderr and exact credential values outside result and audit boundaries. Closes #372 Closes #373
The pre-bash gate correctly refused a compound checkout+commit that was still evaluated on main. Persist the block record. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L
| "fs.rmSync('src',{recursive:true,force:true});", | ||
| `fs.symlinkSync(${JSON.stringify(external)},'src',process.platform==='win32'?'junction':'dir');`, | ||
| ].join(""); | ||
| const swapCommand = `node -e "eval(Buffer.from('${Buffer.from(script).toString("base64")}','base64').toString())"`; |
…payload The credential-projection fix changes the plugins/ca-pi payload, so the payload-version gate requires the manifest to advance. Conventional type in the payload-scoped window is fix -> patch. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L
The Task 6 source contract banned any auth.json reference across all five child sources. ADR-0016 supersedes that ADR-0014 clause and sanctions exactly one narrow secret-transport file, so the blanket ban contradicted the accepted decision and failed all six adapter-contract matrix cells. Narrow the ban rather than weaken it: the four non-transport sources keep the full credential-store prohibition, and child-env.ts gains positive assertions for ADR-0016's bounded contract - strict size bound, fresh private child root, exact selected-provider record, scrub set, and credential truncation on cleanup. Widening the exemption to a second file now requires a superseding ADR rather than an edit here. Every assertion mutation-tested: removing any one of MAX_AUTH_FILE_BYTES, mkdtemp, input.provider, sensitiveValues, or truncate(0) from child-env.ts fails the test, and adding an auth.json reference to runner.ts fails it. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L
|
Superseded and split. Landing
The farm half of The Pi half (#372, ADR-0016) is held until the live-child regression is fixed. Reopening as a fresh PR once it passes the live contract. |
…, deadlines, advisory canary (#423) * fix(ci): restructure the Pi job graph - full suite, one canonical job, deadlines, advisory canary Four CI-structure defects around the Pi jobs, fixed together because they all reshape the same job graph in .github/workflows/ci.yml. #405 - required PR CI named 6 of the 23 committed plugins/ca-pi/tools/test/ *.test.ts files, so policy, plan-mode, dispatch, background-jobs, activity, doctor, footer, notices, runtime-resolver, and windows-supervisor could all regress with every required check green. The new canonical Pi job runs an UNFILTERED `npm test`, and test_ci_impact.py now computes the partition from the workflow text and fails when any committed test file is unassigned to a required shard. #390 - the six-cell os x pi-version matrix re-ran package-metadata checking, typecheck, the deterministic Vitest suites, the static security/parity scripts, the bundle rebuild + staleness gate, the release package contract, and a 100-sample benchmark once per cell (1,423 runner-seconds on run 29870271440). Those steps consume neither matrix.os nor matrix.pi-version, so they move to a single canonical ca-pi-checks job; the matrix keeps only the installed-Pi admission boundary, live package RPC activation, and the cross-platform platform contract. ca-pi-checks is registered in BOTH ci-passed's `needs:` list AND its `required_results` string, and a new contract test asserts the two registrations agree for every job (only the advisory canary may diverge). The canonical job resolves its Pi version from the repository's own support policy rather than hardcoding it, so a Pi promotion cannot leave it pinned to a stale runtime. #399 - every Pi CI and promotion job now declares timeout-minutes derived from recorded runtimes with bounded headroom, and a contract test rejects a future Pi job that omits one. #381 - the [WATCH] upstream-compatibility canary reported a FAILED check run whenever upstream published a Pi outside the validated window (live on PR #420 while main was green). A job-level continue-on-error does not change the check conclusion, so it is removed; the two upstream probes now absorb their own failure at step level and an always-published receipt records the version and each probe outcome in the job summary. A break in the canary HARNESS still turns the check red. Closes #405 Closes #390 Closes #399 Closes #381 Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L * fix(ca-pi): make plan-mode bridge tests portable off Windows The first required-CI run of the complete Pi Vitest suite (this PR's #405 fix) immediately caught a defect the old 6-of-23 file filter had been hiding: plan-mode.test.ts hardcodes "C:/repo" as the repositoryRoot argument to operatePlanFile(). node:path isAbsolute() is platform-native, so that string is absolute on Windows and RELATIVE on Linux and macOS - operatePlanFile() rejects a non-absolute root and returns OPERATION_FAILED, so four of the canonical plan-file bridge cases asserted nothing on the two hosts where they never ran. Observed on run 30129967015, job "[CHECK] | [PI ] | Host-independent adapter contract" (ubuntu-latest): FAIL test/plan-mode.test.ts > canonical plan-file bridge operations AssertionError: expected { ok: false } to deeply equal { ok: true, ...(2) } Test Files 1 failed | 22 passed (23) Tests 4 failed | 535 passed | 7 skipped (546) The fix is test-only: one REPOSITORY_ROOT constant picking a platform-absolute spelling. Production plan-mode.ts is unchanged and was always correct. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L * chore(ca-pi): advance to 0.1.2 for the plan-mode test portability fix The Pi release guard requires any plugins/ca-pi/** change to advance the manifest version together with regenerated root metadata and a new exact Keep-a-Changelog heading. The previous commit's test-portability fix touches plugins/ca-pi/tools/test/, so 0.1.1 -> 0.1.2 (patch: test-only, no payload behavior change). ca-pi-v0.1.1 was never tagged, so the initial release version named in docs/pi-parity-testing.md moves with it. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L * fix(ci): keep platform-gated Pi suites in the OS matrix Review of the #390 split found a real, undisclosed Windows coverage subtraction. Moving activation/commands/status/tool-guard into the single ubuntu ca-pi-checks job pulled two genuinely host-DEPENDENT suites out of required Windows CI: - activation.test.ts:388 `test.skipIf(process.platform !== "win32")` (fixed user-global update cache + symlinked-update-state rejection) now executed in NO required job on any platform. - commands.test.ts's four `symlink(..., win32 ? "junction" : "dir")` containment sites only ever exercised the POSIX "dir" branch, so the Windows junction escape vector lost its gate. Re-audited the whole Vitest tree against an explicit rule instead of a hand-kept list. A file is host-dependent exactly when it compares the LIVE `process.platform` against a literal - either a `.skipIf`/`.runIf` gate that skips the whole test off the wrong OS, or a ternary that selects a different operating-system primitive. A platform value that is INJECTED (`buildChildEnv({ platform: "win32" })`) or merely forwarded into a pure function (`platform: process.platform`) does not qualify: child-env, compaction, status and tool-guard run identical code on every host, so they stay on the canonical lane. That partition yields 11 host-dependent files; bridge/package/process-tree/runner-isolation already run per cell via test_pi_platform_contract.py, and the other seven are restored to the matrix. The host-independent savings #390 asked for are untouched - typecheck, the bundle build, the benchmark and the Python scripts stay factored out, so the matrix is still lighter than the 452s pre-split cell. Also tightens the #405 partition contract, which was file-granular and blind to a test's own gate - it reported activation.test.ts as "assigned" while the ubuntu lane skipped its only win32 case. Two new invariants, each proven to bite by mutation: - every file that branches on the live platform must run in a job that fans out over all three operating systems (asserted independently in test_ci_impact.py and in test_pi_package.py's pi_ci_contract_violations oracle, both deriving the set from the sources); - no committed Pi test may be disabled by a static `.skip`/`.todo`/`.only`, which is the other way a file stays "assigned" while contributing no verdict. A platform gate must be `.skipIf`/`.runIf` so the rule above can hold it. Two gaps this lane's own NEEDS-TRIAGE had deferred are closed here, since both weaken an attestation the lane created: - verify_pi_support.py REQUIRED_HOSTED_CHECKS now names "[CHECK] | [PI ] | Host-independent adapter contract" directly instead of reaching it only through Merge readiness. - .github/ci-impact-map.json gains the `pi-checks` id, and `pi-adapter`'s reproduce command is corrected: the matrix no longer runs the whole suite, so the accurate local repro is test_pi_platform_contract.py. Claude-Session: https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L --------- Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
Lands the deferred security commit
980688f, which was authored on the local-only branchcodex/tribunal-critical-fixesand never pushed. Its Pi sibling work reached main via #313 (feat/pi-support), but this security commit did not — verified:child-env.tson main is 122 lines with zero credential projection, and ADR-0016 was absent from.codearbiter/decisions/.Closes #372
Closes #373
What lands
ADR-0016 — bounded selected-provider credential projection (supersedes the opaque-auth clause of ADR-0014). Corroborated by
DECISION-0021in the decision log, dated 2026-07-22,Decided by: SUaDtL@... ("1")— the maintainer's literal conflict-option selection, matching the establishedDECISION-0016 ("a")format. Re-confirmed by the maintainer before this rebase.ca-pireads the operatorauth.jsononly while preparing a child, under a strict size bound, and projects only the one selected provider record into a fresh private ephemeral root. Never in argv, prompts, results, logs, telemetry, or.codearbiter/. Scrubbed on every path including failure; unprovable cleanup returns a bounded degraded failure.worktree-fs.tsprimitive makes every model-controlled write reject symlink, junction, hardlink, and replacement races before mutation.Verification
Cherry-picked onto current main with no conflicts. Both committed bundles (
plugins/ca/tools/farm.js,plugins/ca-pi/extensions/codearbiter.js) were rebuilt from source and are byte-identical to what the commit carried, so neither is stale against post-#313 main.tsc --noEmitclean in both TS packages.git diff --checkclean (LF preserved).Conflict hierarchy
Resolved at Level 1 (security & audit-trail correctness) — the credential boundary and the audit-attributed ADR both sit above velocity concerns.
Also included
Two audit-trail records left uncommitted by prior sessions: a
/ca:deventer/exit pair inoverrides.log, and anH-01branch-guard block ingate-events.log(the gate correctly refusing a commit evaluated on main during this session).https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L