safety(controller): bind the approval actuator to a ready controller_approval verdict + drift-guard, before any approval wiring #127
Labels
No labels
agent/claude-code
agent/codex
agent/gemini
agent/hermes
agent/iskra
agent/ollama
agent/patchwarden
area:business-model
area:competitive
area:discovery
area:forgejo
area:metrics
area:product-strategy
area:v0-core
cagan-grade-approved
client:platform
dependency/blocked
dependency/blocks-others
dependency/cross-repo
dependency/needs-confirmation
domain:agents
domain:ci
domain:docs
domain:forgejo
domain:infra
domain:memory
domain:runtime
domain:signal
domain:ux
flow/architecture
flow/blocked
flow/deployed
flow/done
flow/implementation
flow/intake
flow/maintained
flow/observed
flow/ready
flow/refining
flow/retired
flow/review
judge/codex-candidate
judge/hermes-candidate
judge/low-confidence
judge/needs-refinement
judge/operator-needed
judge/p0
judge/p1
judge/p2
judge/p3
judge/park
judge/patchwarden-candidate
judge/stale-priority
kind/adr
kind/bug
kind/chore
kind/feature
kind/infra
kind/ops
kind/refactor
kind/research
kind:artifact
kind:decision
kind:dogfood
kind:epic
kind:implementation
kind:research
merge/auto
merge/manual
merge/manual-dependency-conflict
merge/manual-failing-tests
merge/manual-merge-conflict
merge/manual-missing-review
merge/manual-operator-preference
merge/manual-red-zone
merge/manual-security-sensitive
merge/manual-unclear-scope
merge/manual-unknown
mode:operator-only
mode:patchwarden-iskra-approved
mode:safe-auto
observed/erroring
observed/needs-followup
observed/pending
observed/retire-candidate
observed/unused
observed/used
priority:p0
priority:p1
priority:p2
priority:p3
ready-for-agent
review:claude-reviewed
review:codex-reviewed
review:dziadek-reviewed
review:needs-human
safety:external-write
safety:no-prod-mutation
safety:prod-impact
safety:secret-touch
size/large
size/medium
size/small
size/tiny
size/unknown
source/adr
source/agent-generated
source/manual
source/operator-chat
source/voice-note
status:blocked
status:blocked-on-discovery
status:cagan-grade-review-pending
status:codex-ready
status:merged:pending-evidence
status:needs-evidence
status:needs-operator-decision
status:operator-needed
status:parked
tier:0-anchor
tier:0-platform-substrate
tier:1-core
tier:1-iskra-value-layer
tier:2-supporting
tier:2-tools-products-modules
type:bug
type:chore
type:docs
type:feat
type:policy
type:research
wave:1-foundation
wave:2-positioning
wave:3-validation
wave:4-economics
wave:5-operating
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
pdurlej/patchwarden#127
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context (architect note from the review loop)
The read-only controller chain is now complete and disciplined:
controller_intake→controller_approval(#121, no-self-approval / hard-manual / head-bound / fail-closed verdict, read-only) →executor_preflight(#122) →executor_live_state(#124, GET-only). All write actuators inforgejo_client.py(ensure_pr_approval,create_pr_review,set_status,post_comment, …) currently have zero callers — dormant by design. 👏The
test_d20_architectural_boundarylint already does a lot:merge_pull_request/submit_approval(call, def, and raw/mergeURL),ensure_pr_approval(/create_pr_review(to the designated publication adapter.The gap this issue records (forward-looking, nothing is broken)
The lint guards where approval may be called (the adapter only). It does not guarantee that the adapter, before calling
ensure_pr_approval, actually consulted a readycontroller_approvalverdict. Today that's moot (no caller). The moment the approval adapter is wired — the single most dangerous upcoming PR — the safety of the whole D24 autonomy grant rests on that call site, and right now the two halves (controller_approvalverdict ↔forgejo_clientactuator) are coupled only by convention, not structure.This repo's idiom is "every invariant gets a build-time guard" (D20 lint, module-inventory drift-guard, cloud_review egress guard). The approval boundary deserves the same.
Definition-of-done for the upcoming approval-adapter PR (inspiration, not a spec)
controller_approvalready-verdict as a required precondition input — it must not re-derive eligibility ad-hoc. Unknown/blocked/missing verdict → no approval (fail-closed).ensure_pr_approval/create_pr_review(event="APPROVED")are unreachable without a ready verdict. The verdict must carry, and the guard must assert:self_approval_risk/*_identity_unknownblockers must hold at the actuator, not just the preflight);head_shabeing approved; stale head → refuse;create_pr_reviewdefaultsevent="APPROVED". Even adapter-restricted, a fail-unsafe default invites accidents — consider defaulting toCOMMENTand requiringAPPROVEDexplicitly.Why now
#121 already built the preflight half correctly. This issue just writes down the binding + guard requirement so the actuator can't land decoupled from it — catching it as a pre-stated contract is cheaper than catching it in review after the live write surface exists.
Refs #121, #122, #124; D20, D24. — claude (architect loop)
✅ Resolved by #129 — verified fail-closed, closing
#129 (
feat(controller): bind approvals to controller verdict) implements every point of this issue, and then some. Verified againstorigin/main@4d4ba6d:1. Structural binding ✅ —
contract_publishrequires acontroller_approvalready-verdict artifact (controller_approval_file); missing → blockercontroller_approval_file_missing, invalid →controller_approval_file_invalid(fail-closed).approve_on_passdefaultsFalse. The actualensure_pr_approvalwrite is reached only when_approval_preflight_blockers(...)returns empty.2. Drift-guard ✅ —
test_d20_architectural_boundary.test_contract_publish_approval_surface_requires_controller_guardasserts the adapter calls_approval_preflight_blockers+controller_approval_actuation_blockers, requirescontroller_approval_file, and — crucially — that the preflight call precedesensure_pr_approvalin source order. The approval surface can't drift out of the gate without a test failing.3. Fail-safe default ✅ —
create_pr_reviewdefault flippedAPPROVED→COMMENT, withtest_create_pr_review_default_is_not_approvedguarding it.Invariants enforced by
controller_approval_actuation_blockers:controller_approval.blockersto be an empty array — so any #121 verdict blocker (self_approval_risk,*_identity_unknown, hard-manual class) transitively refuses actuation.controller_approval.target_shaandcontract.head_shamust both equal the expected head._publication_guardfetches the live PR head and refuses onhead_sha_changedorlive_pr_fetch_failed— gating status/comment/approval alike. Tested bytest_publish_contract_execute_refuses_stale_live_headand..._refuses_when_live_preflight_fails.So approval now requires: explicit
approve_on_pass+ a present/valid/zero-blocker controller_approval verdict + artifact-consistent head + live head match + contract verdictpass— every uncertainty fails closed. 613 tests green.This is the read-only → guarded-actuation crossing of the D24 grant, done exactly to spec. Textbook. Closing as resolved. — claude (architect loop)