Skip to content

fix(security): isolate Pi child credentials and make farm writes symlink-safe#420

Closed
SUaDtL wants to merge 5 commits into
mainfrom
fix/pi-credential-projection-and-farm-fs-safety
Closed

fix(security): isolate Pi child credentials and make farm writes symlink-safe#420
SUaDtL wants to merge 5 commits into
mainfrom
fix/pi-credential-projection-and-farm-fs-safety

Conversation

@SUaDtL

@SUaDtL SUaDtL commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Lands the deferred security commit 980688f, which was authored on the local-only branch codex/tribunal-critical-fixes and never pushed. Its Pi sibling work reached main via #313 (feat/pi-support), but this security commit did not — verified: child-env.ts on 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-0021 in the decision log, dated 2026-07-22, Decided by: SUaDtL@... ("1") — the maintainer's literal conflict-option selection, matching the established DECISION-0016 ("a") format. Re-confirmed by the maintainer before this rebase.

  • Pi child credentialsca-pi reads the operator auth.json only 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.
  • Farm worker writes — new worktree-fs.ts primitive 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.

Suite Before After
ca-pi (vitest) 545 passed 551 passed, 1 skipped
ca-farm (vitest) 198 passed 207 passed, 1 skipped
python-hooks 1057 OK 1057 OK

tsc --noEmit clean in both TS packages. git diff --check clean (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:dev enter/exit pair in overrides.log, and an H-01 branch-guard block in gate-events.log (the gate correctly refusing a commit evaluated on main during this session).

https://claude.ai/code/session_01Y8UPz5RZwgnda3NbAVfd6L

SUaDtL added 3 commits July 24, 2026 17:36
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())"`;
SUaDtL added 2 commits July 24, 2026 17:42
…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
@SUaDtL

SUaDtL commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded and split.

Landing 980688f on current main surfaced two things:

  1. A contract conflict (resolved). test_pi_child_live.py asserted that none of the five Pi child sources may reference auth.json — the ADR-0014 opaque-auth clause. ADR-0016 supersedes exactly that clause and sanctions child-env.ts as a narrow secret-transport boundary, so the blanket ban contradicted the accepted decision and failed all six adapter-contract matrix cells. Fix drafted here: narrow the ban to the four non-transport sources and replace it, for child-env.ts, with positive assertions of ADR-0016's bounded contract (size bound, private mkdtemp root, exact selected-provider record, scrub set, credential truncation). Every assertion was mutation-tested and bites.

  2. A live regression (unresolved). With that cleared, all six cells still fail — now at the live child contract:

    live child did not complete through production runner:
    result={'terminal': 'degraded', 'diagnostic': 'Pi child isolation failed safely; no inline promotion is available; run /ca-doctor.'}
    provider_turns=0 runner_stderr=''
    

    The credential-projection path fails closed against a real Pi 0.80.5/0.80.10 runtime with no reason string. This is a genuine defect in the projection code as authored on the stale branch — and the likely reason the commit was never pushed.

The farm half of 980688f (#373) is independent and green, so it ships separately as #425.

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.

@SUaDtL SUaDtL closed this Jul 24, 2026
SUaDtL added a commit that referenced this pull request Jul 25, 2026
…, 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[critical] Isolate Pi child HOME from the operator credential store [critical] Make farm worker writes symlink-safe at the filesystem boundary

2 participants