fix(hooks): key the environment contract by harness so a second CLI never clobbers a live run - #570
Conversation
…ever clobbers a live run The environment contract was one untracked file per checkout, but it carries harness-specific facts (skills= path=, harness= name=). A second harness opening the same checkout rewrote it at SessionStart, silently changing the skills tree, helper paths, and hook verdicts of a run already in flight -- including a false "wrong plugin path" advisory against a path that was actually correct for the running harness. Each harness now writes and prefers its own .agent/env-contract.<harness>.txt; the bare, un-suffixed name is kept as a read-only legacy fallback for one release and is never written again. SessionStart also records mode=observer (with the harness it is observing) when another harness's own contract was still fresh at start, and PreToolUse now refuses -- once, retry allowed -- a write into the checkout root from an observer session, so two harnesses on one machine no longer fight over the same files. Co-Authored-By: Claude claude-sonnet-5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
…g them Adversarial review on PR #570 found two gaps in contract_other_harness_active (the mode=observer liveness check added by the harness-keyed contract fix): - F1 [P1]: it accepted ANY readable file matching .agent/env-contract.*.txt with no untracked/ownership check, and interpolated the unvalidated filename suffix into a grep pattern and then verbatim into this session's own mode=observer line -- a tracked or otherwise hostile file could inject arbitrary bytes into an established contract. Fixed by requiring guard_contract_is_ours before reading any candidate, and constraining the harness value (from the filename for a keyed candidate, from its harness= line for the legacy one) to the same safe single-token vocabulary contract_cache_harness_name itself requires. - F2 [P2]: the scan only looked at keyed files, so an older run of the other harness sitting on only a fresh, untracked bare contract was invisible -- a new session would record mode=owner and could race it. Fixed by adding the validated legacy bare contract as an active-run candidate. Both add regression tests (a tracked hostile keyed file, a malformed-suffix keyed file, and a fresh legacy-only contract from the other harness); confirmed all four new assertions fail against the pre-fix function and pass with it. Co-Authored-By: Claude claude-sonnet-5 <noreply@anthropic.com>
|
This was written agentically; verify its assertions: Adversarial review receipt
🤖 Co-authored by Claude Fable 5 (root). |
|
This was written agentically; verify its assertions: Review ledgerMachine-readable record of every review already performed on this PR. {
"version": 1,
"pr": 570,
"repo": "wrzonance/agent-kit",
"reviews": [
{
"kind": "adversarial",
"provider": "openai",
"model": "gpt-5.6-sol",
"effort": "xhigh",
"mode": "cross-provider",
"harness": "claude",
"head_sha": "93ba80a167ccbf70f63c5c95f8461a920faf96c1",
"covered_heads": [
"864a9ebe68ad26c5f93a52f5e7ad8e75110b9dcc",
"93ba80a167ccbf70f63c5c95f8461a920faf96c1"
],
"diff_payload": "wrzonance/agent-kit:570:7081b6b56e25118e2c9b9f89c12bbdc590f0912aacc7469a50e51c4e34835c9f",
"counts": {
"p1": 1,
"p2": 1
},
"reviewed_at": "2026-08-30T06:18:49Z",
"coverage": [
{
"sha": "864a9ebe68ad26c5f93a52f5e7ad8e75110b9dcc",
"reason": "merge-down:a8543f3924bb11a19614b0f511ca44de73b15237",
"covered_at": "2026-08-30T06:37:50Z"
}
]
}
]
}🤖 Co-authored by Claude Fable 5 (root). |
This was written agentically; verify its assertions:
Why
.agent/env-contract.txtwas one untracked file per checkout carrying harness-specific facts (skills= path=,harness= name=). A second harness opening the same checkout rewrote it at SessionStart, silently changing the skills tree, helper paths, and hook verdicts of a run already in flight — observed 2026-08-28 when a Claude session opened to observe a Codexparallel-issuesroot clobbered its contract mid-wave.What
.agent/env-contract.<harness>.txt(SessionStart passes--writeexplicitly); the bare name is a read-only legacy fallback for one release and is forwarded into the keyed file on first reuse.contract-read.sh,contract-cache.sh, and the hook guard library resolve the running harness's file first, so the pinned-plugin-path lesson no longer fires against a session's own tree.mode=observer other-harness=<x>; PreToolUse denies-once a content-bearing write into the checkout root from an observer.create-issue-worktree.sh --inherit-sessionprefers the root's keyed contract.Decisions
CONTRACT_MAX_AGE_MINUTES), deliberately not a new heartbeat/lock subsystem; a false positive only costs an unnecessary observer mode.agent-preflight.sh's bare-name--writedefault and the per-worktree contract flow are unchanged (outside the declared write set; worktrees are single-harness). Consequence: every SKILL.md Step-0 resolver still reads the bare name, which the skill's own preflight still writes — works today; migrating that prose to the keyed name is a follow-up.base=origin/main
files=10
total.insertions=414
total.deletions=36
total.lines=450
operational.files=10
operational.insertions=414
operational.deletions=36
operational.lines=450
generated.files=0
generated.insertions=0
generated.deletions=0
generated.lines=0
lockfile.files=0
lockfile.insertions=0
lockfile.deletions=0
lockfile.lines=0
fixture.files=0
fixture.insertions=0
fixture.deletions=0
fixture.lines=0
non_operational.files=0
non_operational.insertions=0
non_operational.deletions=0
non_operational.lines=0
Testing
tests/run-tests.sh --only hooks,session-contract-freshnessgreen (new keyed-contract, observer, and legacy-forward assertions)agent-run.sh --cmd testALL GREEN on the final tree (.agent/logs/20260830T052658Z-test.log)🤖 Co-authored by Claude Fable 5 (root) / claude-sonnet-5 high (worker).
Closes #551