fix(plugins): tribunal quick-kills: atomic writes, typed spawn opts, fail-closed CI#197
Merged
Conversation
The version-bump-sandbox gate swallowed every git-show failure as "plugin is new" and exited 0, silently skipping the check that stops a changed payload shipping on a published version. Only a genuine absence at the base ref (git cat-file -e) now takes that path; any other git failure fails the step. release.yml's workflow_dispatch inputs were expanded directly inside run: blocks on a contents:write runner (CWE-78); they are now env-bound and referenced as quoted shell variables. Security review: PASS (0 critical/high; 2 LOW hardening notes carried in the PR body). Closes #174 Closes #185 Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa
Three tribunal hardening fixes with their coupled tests. write_provenance
used a bare open('w'); a crash mid-write truncated the record and
read_provenance mapped it to None, silently disabling drift detection for
that doc. It now routes through _hooklib.write_text_atomic, which gains an
inert-by-default newline param so the record stays LF-canonical on
Windows. run()'s opts in ca/tools/exec.ts and ca-sandbox build.ts were
bare object spread after env, letting a future caller silently defeat the
secret scrub (or drop MSYS_NO_PATHCONV); both are now narrowed types that
make the invariant compiler-enforced. statusline.py drops six dead
_ledgerlib re-binds left from the T-12 extraction; _tx_accumulate stays,
nine existing tests exercise it.
CHANGELOG: provenance records survive interrupted writes; the farm's
secret-scrub and the sandbox docker env can no longer be overridden by an
untyped spawn-opts spread.
Closes #195
Closes #177
Ref: #191 (reliability-016 only; the group's other members remain open)
Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa
The sanctioned board mutator's uninitialized-repo branch (exit 1, 'no board at' on stderr, no side-effect file) had no test; every existing CLI case pre-created the board. A regression removing the None-guard would have shipped green. Closes #184 Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa
routing-table.md row 14 routed /spike to a nonexistent spike skill; the command is self-contained and the row now says so. tech-stack.md's test list omitted the plugins/ca/hooks/tests unittest suite that CI runs via unittest discover, so a by-the-book local verification skipped 592 tests. Closes #176 Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa
.claude/settings.local.json can carry machine-local secrets (the tribunal found a live key there guarded only by a per-developer global gitignore); the exclusion now travels with the repo. The exposed key was rotated by the maintainer on 2026-07-02. Version bumps satisfy the per-PR payload gate for both plugins; README badge kept in sync (badge guard green). Closes #182 Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa
Both reviewers flagged the same residual flank in the new RunOpts types: shell:true could still reach the spawn spread and reintroduce shell interpolation. No caller passes it; omitting it is a zero-behavior-change tightening that makes the argv-array invariant compiler-enforced too. CHANGELOG: spawn opts in the farm and sandbox tools can no longer opt into shell interpolation. Ref: #195 Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa
This was referenced Jul 2, 2026
Closed
SUaDtL
added a commit
that referenced
this pull request
Jul 2, 2026
…e-bound helper containers (#180 #173) (#204) * refactor(sandbox): single docker primitive; fix(sandbox): label+time-bound helper containers Extract docker.ts as the ONE shared docker-primitive module for ca-sandbox (architecture-007, #180): DOCKER_ENV (the MSYS_NO_PATHCONV Windows/Git-Bash guard) and defaultDockerRun were copy-pasted verbatim across build, cp, create, exec, registry, run, claude-inside, and cli; the {code,stdout,stderr} result shape recurred under three names (RunResult / ClaudeRunResult / DockerResult). Every module now imports the single definitions from docker.ts and re-exports the old type names for backward compatibility; the injectable-runner test seam is unchanged. Behavior-preserving: the pre-existing vitest suite passes unmodified (one existing test call site updated to add a required arg introduced by the #173 fix below, no assertions changed). typesafety-002 (build.ts's opts.env silently dropping DOCKER_ENV) was already fixed in #197's typed RunOpts. Label + time-bound the sandbox helper containers (reliability-015, #173): the throwaway clone container and the docker-cp helper carried no ca.sandbox label and no timeout, so a hung clone or a host crash mid-create could orphan a networked container prune() could never reclaim. Both now carry ca.sandbox=1 + ca.sandbox.id=<id>, and each docker step (clone, cp-helper create, cp) is wall-clock timed out. createSandbox's failure teardown now removes containers before the volume (matching destroySandbox's order), so an in-use volume is no longer left un-retried. Tests: pure unit coverage for the new labels + teardown order; docker-gated integration tests proving prune() reclaims an orphaned, labeled helper-shaped container (self-skip without a Docker engine, as documented in tech-stack.md). Closes #180 Closes #173 Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa * fix(sandbox): create.ts label + timeout wiring (reliability-015, #173) Completes the previous commit: create.ts's clone container and docker-cp helper now carry ca.sandbox=1 + ca.sandbox.id=<id>, each docker step is wall-clock timed out, and createSandbox's failure teardown removes containers before the volume. Adds create.test.ts coverage (pure + the docker-gated prune() proxy tests). This was already reflected in the previously-committed rebuilt sandbox.js; only the .ts sources were left unstaged in the prior commit. Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa * chore(release): bump ca-sandbox 0.1.2 -> 0.1.3 (docker primitive + helper labels) Payload change in plugins/ca-sandbox/tools/ requires a version bump (per-PR CI gate). Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa --------- Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
SUaDtL
added a commit
that referenced
this pull request
Jul 2, 2026
…arse prune transcript (#191 #188) (#212) * fix(hooks): atomic writes for prune/settings/githooks state; single-parse prune transcript reliability-008/009/010 route bare open('w') writers through _hooklib.write_text_atomic (temp + os.replace); the SessionStart settings-heal now reloads-before-save with a per-call temp so concurrent sessions can't clobber each other. #188: hook_run threads its single transcript read+parse through run() instead of re-reading it. reliability-016 (write_provenance) was already atomic since PR #197. Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa * chore(release): bump ca 2.8.6 -> 2.8.7 (atomic writes + prune single-parse) Version bump + README badge sync for the crash-atomic state writers (#191) and the single-parse prune-transcript fix (#188). Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa * test(prune): make the raw-open regression guard platform-robust (<=6) write_in_place opens the transcript one extra time on POSIX (os.replace/verify), so the no-duplicate baseline is 5 on Windows / 6 on POSIX; the hardcoded <=5 red on ubuntu/macos CI. Bound raised to 6 — still catches the +2 duplicate-read regression on either platform. The platform-independent single-parse proof is the sibling load_lines-count-1 test. Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa --------- Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
This was referenced Jul 2, 2026
SUaDtL
added a commit
that referenced
this pull request
Jul 2, 2026
#218) The ca-sandbox manifest is at 0.1.3 but its CHANGELOG stopped at 0.1.1; add the missing 0.1.2 (tribunal quick-kills, #197) and 0.1.3 (single docker primitive + helper-container hardening, #180/#173) entries. Prepares ca-sandbox-v0.1.3. Claude-Session: https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa Co-authored-by: SUaDtL <SUaDtL@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships the ten small, contained fixes from the 2026-07-01 tribunal audit (run dir:
.codearbiter/reports/2026-07-01-repo-root/). Six commits, one per conventional type plus a reviewer-driven tightening.What changed and why
git showfailure as "plugin is new" and exited 0; only a genuine absence at the base ref (git cat-file -e) now takes that path.release.yml's dispatch inputs are env-bound instead of expanded insiderun:blocks (CWE-78 on acontents: writerunner). Closes Interpolate release.yml workflow_dispatch inputs into the run shell (script injection) #174, closes version-bump-sandbox gate treats any git-show failure as plugin is new, silently skipping the gate #185.write_provenancewas a bareopen('w'); a crash mid-write truncated the record and silently disabled drift detection for that doc. It now routes through_hooklib.write_text_atomic, which gains an inert-by-defaultnewlineparam so records stay LF-canonical on Windows. Ref Atomic writes: several state/artifact writers use bare open('w') instead of temp+os.replace #191 (reliability-016 only; the group's other members remain open).run()'soptsinca/tools/exec.tsandca-sandbox/tools/build.tswas bareobjectspread afterenv, letting a future caller silently defeat the secret scrub (or dropMSYS_NO_PATHCONV). Both are now narrowed types; a follow-up commit also omitsshellafter both security reviewers flagged the same residual flank. Closes Typed spawn opts: bare object typing on run() lets a caller silently override env #195._ledgerlibre-binds removed fromstatusline.py(_tx_accumulatestays; nine existing tests exercise it). Closes statusline.py re-binds 7 dead _ledgerlib members after the T-12 extraction #177.taskwrite's uninitialized-repo exit path. Closes taskwrite.py: missing-board error path (uninitialized repo) is untested #184./spiketo a nonexistent skill; tech-stack.md now documents theplugins/ca/hooks/tests/suite CI runs. Closes routing-table.md routes /spike to a nonexistent spike skill #176..claude/settings.local.jsonis excluded by the repo's tracked.gitignore(the tribunal found a live key there guarded only by a per-developer global ignore; the key was rotated 2026-07-02). Payload bumps: ca 2.8.0 -> 2.8.1, ca-sandbox 0.1.1 -> 0.1.2, README badge in sync. Closes Live-looking FARM_API_KEY was stored in cleartext in a repo-root config file #182.Tradeoff note: the CI gate change prefers a spurious step failure over a silently skipped version check (conflict hierarchy level 2, correctness, over level 5, velocity).
Test plan
.github/scripts/plusunittest discover(592 tests) greenca/tools: typecheck, 180 vitest tests, build;farm.jsunchangedca-sandbox/tools: typecheck, 159 vitest tests (29 docker-gated self-skipped), build;sandbox.jsunchangedcheck-plugin-refs.pyandcheck_badge_consistency.pygreengit check-ignoreresolves via the tracked rule; nogithub.event.inputsexpansion remains inrun:blocks; crash-simulation tests hold the prior provenance record intactReview notes (non-blocking nits, filed as #198 and #199)
ci.yml:321:git cat-file -estill conflates exit 1 (absent) with exit 128 (fatal); capturing the exit code would fully separate them.release.yml:steps.v.outputs.*are still${{ }}-expanded inrun:; they derive from the manifest onmain(trusted), but env-binding them too would make the workflow uniformly expansion-free.https://claude.ai/code/session_01PVTCDji6bEuf9SifQ8tfsa