Repository navigation
feat(runtime): audit publication invariants after every publishing stage (#261) - #365
Conversation
…age (#261) After every implement, review, resolve and rereview dispatch, whatever the stage reported, the runtime compares the audited state on origin with its state before the stage: the cycle's change requests stay open, the cycle branch only moves fast-forward, and the default and protected branches neither move nor disappear. A breach stops the cycle with publication_violation and before/after evidence; an unreadable state stops it with publication_unverified. Hosts other than GitHub keep their refs audited and report the rest as unverified. Telemetry schema 24. Closes #261
|
Implementation summary for #261: post-stage publication audit ( Pushed head: Local verification (agent-reported):
Checks of the pushed head: none reported yet when they were read once after publication (CI policy Routing: profile |
|
Cycle note (not a review): the Before resuming, the local HEAD, |
Initial review — PR #365 (work item #261)[CCR-20261029-001] · senior_reviewer · anthropic/claude-opus-5-5→? · high · schema:2Verdict: Summary and scopeI reviewed the whole accumulated diff from merge base
I checked these points and they hold:
Findings[REV-001] · medium · open · - · blocks:no — A cycle branch the host reports as protected cannot move forward
Security triageThe deterministic rule did not fire. The reviewer's judgement fired the audit. The change is security tooling: it is the runtime's last line of defence against a stage merging, force-pushing or deleting refs.
The fail-open paths I looked for all fail closed instead: a missing default branch on a non-empty remote, malformed JSON, and missing commits. Commands executed
Unverified
CI (head
|
| Check | State |
|---|---|
| Skills, manifests, and Bash installer | pass |
| Windows test shard (windows-1) | pass |
| Windows test shard (windows-2) | pass |
| Windows test shard (windows-3) | pass |
| Windows test shard (windows-4) | pass |
| PowerShell installer | pending |
The cycle's CI gate waits for the pending check and decides. This review neither waited for it nor reads it as success.
Residual risks
- The before and after snapshots cannot see a ref that is changed and then restored within one stage. The PR body states this.
- During
implement, only the checkout's branch is audited as the cycle branch. If the implementer pushes a branch under another name, that branch is first audited from the next stage on, through the change request'shead_ref. The new change request's open state is still checked. - Under the documented design, any move of the default branch during a stage stops the cycle, including a legitimate merge of another pull request. Long stages in busy repositories may stop more often. This follows the decision taken on security(runtime): verify publication invariants after each publishing stage #261.
Work item: #261 — security(runtime): verify publication invariants after each publishing stage.
Closes #261
Summary
After every publishing stage (
implement,review,resolve,rereview), whatever status the stage reported,run_cycle.pycompares the audited state with its state before the stage. It uses the newscripts/publication_audit.py:implementmay create the branch.originonly, once before the stage, and the same set is compared after it. A push to an unrelated branch passes (refactor(F0): limit the read-only review guard to the refs of the change under review #197).publication_violation, whoever caused it. The stop line says "changed outside the stage?". The audit runs before the stage's report is read, so a breach wins overBLOCKED/stage_not_completed, the order fix(runtime): reconcile a publishing stage that reported BLOCKED after a verified publish #364 depends on. No later stage runs.CycleReport.publication_auditholds the before/after SHA per ref, the before/after change-request state, and each breach's kind with up to 20 added and removed commits. The cycle journal gets onepublication_auditrow per stage and onefailurerow per breach.publication_unverified. That covers a failed protected-branch query, a failed change-request read, and missing commits that leave a move uncomparable. Nothing is read as "none". If the read fails before the stage, the stage is not dispatched.unverified, never as passed, until Make provider capabilities first-class and close 0.6.0 workflow gaps #238.--disallowedToolsor other adapter-level denies were added. Those belong to Recommended host permissions for Claude/Codex and per-project generalized approvals #142.Diagnosis
dispatch()fingerprints remote refs only for read-only review stages (_isolated_review_dispatch). Nothing checks after a publishing stage.BLOCKEDis never turned into a success.advance(), the single place every publishing stage passes through. It is the same for every executor, so no per-adapter rule is needed.docs/role-workspace-policy.mdsaid only that the audit "remains separate (security(runtime): verify publication invariants after each publishing stage #261)". That page now has a Publication audit section.docs/instrumentation.mdlists the stop reasons and schema 24, anddocs/review-cycle.mdadds a row to its exit table.Work units
WU-1scripts/publication_audit.py,scripts/run_cycle.py,scripts/telemetry.py,scripts/runtime.manifest,scripts/windows-test-shards.json,docs/role-workspace-policy.md,docs/instrumentation.md,docs/review-cycle.md,CHANGELOG.md,tests/test_publication_audit.py,tests/test_work_item_outcomes.pytests/test_publication_audit.py(25 tests: local bare remotes plus a fakegh)independent. Rows written with schema 24 stay readable, because the stop reasons are payload-only.Verification (agent-reported)
python3 -m unittest tests.test_publication_audit tests.test_installed_cyclepython3 -m unittest discover -s testspython3 scripts/validate-package.pypython3 -m unittestdiscover -s testsabove is the real suiteThe acceptance criteria are covered by tests:
publication_violationand its evidence, and no further stage runs.BLOCKEDreport with a merge stops withpublication_violation.unverified, and its refs are still audited.Environment the local run shares with CI: the tests use temporary bare repositories with their own Git identity and an in-memory fake
gh. They need no global Git config, credentials or network. The local run is Linux only, so the Windows shards were not run locally. The new module is listed inwindows-4.Residual risk
gh apiandgit ls-remotecalls.