Repository navigation
Refuse to accept an attempt whose plan edit was restored - #25
Conversation
Layer B F2 on a real host: after a crash, the pickup host saw the worker's forged [x] and "Dispatched: … accepted" line (plan_touched), restored the plan, re-inspected, and accepted with --scope-override, blaming "an interrupted host session". The helper's own refusal had suggested "restore, re-inspect and pass --scope-override", which design §12.3 rule 5 still allowed from the PDN-0003 rule, while dispatch step 14 said to reject plan_tampered. Each inspection now records plan_touched (an optional member of the receipt's inspection entries), and accept is refused once any inspection saw the plan touched and the latest no longer does. The refusal message no longer suggests restoring. The skill states that a plan change seen while the verdict is pending is never the host's own: the host edits the plan only after the verdict. The reference host rejects on the history too; tests/attempt.sh and Layer A case F2d reproduce the real host's steps and fail without the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In F2 the pickup session also did task 1.2, and its new file made the oracle's final-tree check fail for a reason unrelated to the scenario. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Layer B host loads passdown from the plugin channel pinned to release/v0.5.0; the new beta makes the plugin update pick up the restored-plan guard before F2 is run again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vunm-dev
left a comment
There was a problem hiding this comment.
Reviewed cd5b25b. Found one P2 issue in recovery across the beta.2 → beta.3 upgrade; please address it before merging.
Validation: acceptance (7), F2d (1), skills (156), contracts (39), bundle synchronization and version checks passed. All PR CI checks are green. The additional cross-version reproduction fails as described in the inline comment.
AI-Reviewed-By: Codex
| @@ -1705,7 +1705,7 @@ cmd_inspect() { | |||
| | .artifact.digest = $a.digest | .artifact.changed = $a.changed | |||
| | .artifact.out_of_scope = $a.out_of_scope | .artifact.overlaps_baseline = $a.overlaps_baseline | |||
| | .artifact.plan_touched = $a.plan_touched | |||
There was a problem hiding this comment.
[P2] Preserve earlier plan-tampering evidence when upgrading an existing receipt
A beta.2 inspection stores artifact.plan_touched: true, but its artifact.inspections[] entries do not contain plan_touched. If a recovery session upgrades to beta.3, restores the plan and runs inspect, this assignment overwrites the existing true with false and appends only a false history entry. The new acceptance guard therefore finds no historical true and accepts the attempt, even without --scope-override. This leaves the restored-plan loophole open for pending receipts created before the upgrade, which remain schema-valid.
Reproduced using the actual helper from the PR base for the first inspection and the PR helper for re-inspection and acceptance: the new restoration regression expects exit 3 but gets exit 0 (accepted). Preserve the previous evidence before overwriting it, or recover it from the existing inspect.json history, and add a beta.2-to-beta.3 recovery regression test.
There was a problem hiding this comment.
Confirmed: a beta.2 inspection leaves plan_touched only in artifact.plan_touched and inspect.json, and the beta.3 inspect overwrote it. Fixed in 7aa1abd, both ways you suggested: inspect now moves the previous artifact.plan_touched onto the latest entry before overwriting it, and the accept guard also reads inspect.json (every beta has written each inspection's full record there). Regression tests in tests/attempt.sh use receipts shaped like beta.2's: (a) one beta.2 inspection, then restore and re-inspect on beta.3 (backfill path, inspect.json removed); (b) inspection and restore both under beta.2 (only inspect.json remembers). Removing either half of the fix makes its case fail.
A receipt inspected by a beta.2 helper has inspection entries without plan_touched; the evidence lives only in artifact.plan_touched and in inspect.json. A beta.3 re-inspection after restoring the plan overwrote it with false and appended a false entry, so the new guard found no earlier true and accepted. Inspect now moves the previous artifact.plan_touched onto the latest entry before overwriting it, and the accept guard also reads inspect.json, which has always held every inspection's full record. tests/attempt.sh covers both paths with receipts shaped like beta.2's; each half of the fix is caught by its own check when removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All seven gate scenarios (F1, F2, F11, F14a, F14b, F18, F27) pass on a real host: claude -p 2.1.193 with the passdown plugin pinned to release/v0.5.0, the owner attesting every stop by hand. The gate found two protocol gaps, fixed in #24 and #25 before those scenarios were run again; the failing runs are kept as F1-fail-1 and F2-fail-1. docs/evidence/v0.5.0/README.md summarizes the runs, the findings, the harness changes and what was observed but not blocking. Two harness changes come with the evidence: - oracle.sh judges must-not-project on the interrupted attempt. The first oracle failed F14a because the host, correctly, redid the changed task in a new attempt and projected that one; the interrupted verdict must still never be projected, and a tick must be backed by a projected attempt on the current task revision. The first judgment is kept in F14a/oracle.first.txt. - sanitize.sh prepares a run for publication: local paths, the operator's e-mail, the init, hook and rate-limit events, thinking counters and signatures. oracle.sh gives the same verdict on the sanitized records, and they pass the workspace's boundary denylist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDN-0004 S7 — second release-blocking fix found by the Layer B gate (scenario F2).
What Layer B found
F2 on a real host (
claude -p, plugin 0.5.0-beta.2), owner attesting by hand:fakeworker wrotesrc/hello.txt, ticked its own box and forgedDispatched: fake (2026-09-21) — accepted; verified: grep; attempt: <id>.pending).plan_touched: true, twice), then calledverdict accept. The helper refused, but its message said "reject scope_violation/plan_tampered, or restore, re-inspect and pass --scope-override".plan_touched: false) and accepted with the override "plan change attributable to interrupted host session (step 16 written before verdict)". That attribution is impossible: the host writes the plan only at step 16, after the verdict, and the killed session had stopped at the result.Oracle:
FAIL a receipt was accepted,FAIL task 1.1 is ticked. The run is kept asdocs/evidence/v0.5.0/F2-fail-1, to land with the evidence PR. (A third FAIL, the final-tree digest, came from the pickup session also doing task 1.2 — a harness gap, fixed here too.)Cause: design §12.3 rule 5 kept the PDN-0003 rule "restore the plan, re-inspect, accept with an override", while dispatch step 14 (S4) says restore and reject
plan_tampered. The helper implemented the design and advertised the path.Changes
plan_touched(new optional member ofartifact.inspections[]inpassdown.receipt/v1; older receipts stay valid).verdict acceptis refused once any inspection saw the plan touched and the latest no longer does — with or without--scope-override. The refusal message no longer suggests restoring; the override records only a plan change the owner made and keeps.plan_tampered.tests/attempt.sh(restore → accept refused, with and without override → rejectplan_tampered), Layer A case F2d reproducing the real host's steps after a crash, and two prose checks. Both new tests fail with the guard removed (checked locally).0.5.0-beta.3.Verification
tests/skills.sh,docs.sh,version.sh,attempt.sh,contracts.shandinterrupt.sh(Layer A, now 38 cases) pass;sync-bundled.sh --check,check-version.shand shellcheck are clean. After merge: update the plugin to beta.3 and rerun F2 on the real host.🤖 Generated with Claude Code