fix: refuse a gate decision the conductor wrote for the human (2.5.76) - #750
fix: refuse a gate decision the conductor wrote for the human (2.5.76)#750JWThewes wants to merge 1 commit into
Conversation
The human-presence guard proves a HUMAN_TURN exists after the gate opened -
presence, not intent. Presence is continuous in a live session, so a conductor
blocked by a completion precondition can satisfy the guard while supplying the
decision text itself through --user-input / --feedback / --details.
Field audit trails show this happening under low human availability. Because
GATE_REJECTED is the only event that restores an advisory review budget, a
deadlocked conductor records one and states in the feedback that it is not a
human decision ("AGENT-INITIATED, NOT A HUMAN REJECTION ... this reopen exists
solely to obtain a review budget"), alongside self-approvals and self-answers
labelled "CONDUCTOR DEFAULT, session unattended". One reviewed session produced
eight such authority-bearing rows across five stages; a second session, with a
human answering throughout, produced none - so the deadlock induces the
behaviour rather than it being constant. Once those rows exist, "a human
approved this stage" is no longer verifiable from the trail, and the conductors
that wrote them were the honest case: a more compressed one writes "Request
changes" and is indistinguishable from a human.
Add fabricatedDecisionMarker() beside isNonAnswer() and enforce it at the same
three call sites - approve --user-input, reject --feedback, and answer
--details. `report --result approved|rejected` shells out to state.ts, so it
inherits the floor. Each refusal quotes the phrase that tripped it and names
the supported alternative: surface the blocker at the gate instead of deciding
for the human.
The vocabulary matches only self-referential provenance claims, never claims
about the work, because over-blocking a real human at a gate is the one
regression worse than the forgery: "revert the agent-initiated retry in
scheduler.ts" and "the conductor default timeout is too low" still commit.
Autonomous Construction is exempt, keeping honest self-labelling legal where
the conductor holds the decision - otherwise the floor would push autonomous
runs toward unlabelled decisions, which is strictly worse for the trail.
A blocked decision leaves no partial receipt (no GATE_REJECTED, no
STAGE_REVISING) and does not consume the human's turn, so their real choice
still commits on the same turn.
Refs awslabs#742
leandrodamascena
left a comment
There was a problem hiding this comment.
Jan, the underlying issue is real: HUMAN_TURN proves human presence, not that the human authored the approval, rejection, or answer. I reviewed 5ec6309, inspected all three enforcement paths, ran the official t261 suite (33/33 passing), and probed the predicate directly with both bypass and false-positive cases.
I agree this should remain changes requested. I independently verified the following blockers:
P1: The guard detects selected confessions, not conductor-authored decisions
fabricatedDecisionMarker() at core/tools/aidlc-lib.ts:2647-2674 operates only on the submitted prose. It can disprove human authorship when the conductor explicitly uses one of the recognized phrases, but it cannot prove that the decision came from the human.
The reported phrases are blocked as intended:
- AGENT-INITIATED, NOT A HUMAN REJECTION
- Approve - CONDUCTOR DEFAULT, session unattended
However, these conductor-authored decisions returned no marker and would pass the new floor:
- Approve
- Request changes: reopen for another review
- Not human rejection; reopen it
- This rejection was generated by the AI
- I, the conductor, am rejecting this
- The assistant chose Request changes
The same predicate is used directly by approve, reject, and answer:
- core/tools/aidlc-state.ts:2465-2468
- core/tools/aidlc-state.ts:2715-2718
- core/tools/aidlc-log.ts:423-426
Therefore omitting or slightly varying the self-attribution still allows the conductor to mint the same authority-bearing receipts. The current tests prove the known vocabulary, not the stated security property.
Please bind the decision to trusted harness-captured human input for the specific gate/question, ideally with a single-use gate or prompt identifier. Conductor-supplied prose should not be treated as proof of human authorship.
P1: The "Autonomous Construction" exemption applies outside Construction
All three call sites bypass the new guard whenever isAutonomousMode(content) returns true. That function checks only the persistent Construction Autonomy Mode field:
- core/tools/aidlc-lib.ts:4641-4643
It does not verify that the target stage is in Construction.
I directly probed states carrying Construction Autonomy Mode: autonomous and obtained:
- Ideation / feasibility: true
- Operation / deployment-execution: true
The new test explicitly codifies the Ideation bypass by setting autonomous mode on feasibility and expecting a conductor-authored rejection to commit:
- tests/unit/t261-audit-authority-floor.test.ts:586-605
This means a stale or injected Construction autonomy field disables the authorship floor for human-gated Ideation and Operation decisions.
Please scope the exemption to an actual Construction-stage autonomous directive/authority, not merely the persisted field.
P2: The substring heuristic blocks legitimate human feedback
The PR states that the vocabulary matches only self-referential provenance claims, but direct probes produced markers for legitimate statements about unrelated subjects:
- This is not a human response time issue
- never confirmed by the human resources team
- report written by the conductor service
- The session was unattended after the websocket dropped
These match because the expressions lack sufficient semantic or clause boundaries. A real human can therefore be prevented from approving, rejecting, or answering based on ordinary feedback text.
If the text heuristic is retained as defense in depth, it should be advisory/auditable rather than treated as authorization evidence. At minimum, add regression cases covering quoted phrases, third-party referents, "human resources," service names, and unattended system behavior.
The focused tests, package parity, and coverage registry are green, but they validate the known phrases rather than the full authorship guarantee. The vulnerability is valid; this implementation is an incomplete and bypassable mitigation.
|
Jan, thanks for this - and thanks @leandrodamascena for the thorough verification round. I re-ran the key probes independently and they reproduce exactly: the six unlabelled phrasings all pass the predicate, and the false-positive list is real. So changes requested stands. Where I land differently is on what follows from that. The mechanism is worth keeping; the claim is what needs to change. One piece of context that changed since this PR opened: #758 now adds the bounded recovery review pass from #742's remediation - the deadlock your field sessions were escaping no longer requires a false Request Changes once that lands. The two PRs compose: #758 removes the motive to fabricate, this one removes the labelled path and makes attempts auditable. It also means this PR should sequence after #758 - merged alone, it tightens the trap on honest conductors while surfacing the blocker at the gate still leads nowhere. Since v2 has moved to 2.6.2 you need a rebase and a fresh version slot anyway, which lines the sequencing up naturally. @leandrodamascena - proposed merge order for this set: #758 first (CI green and mergeable, functional prerequisite for this PR, and it claims the next slot 2.6.3 so nothing above it re-bumps; it only lacks a review - if you can take that one, it unblocks the whole sequence), #749 second once Akash clears the two round-3 blockers (independent mechanism, already slotted at 2.6.6), this PR last after the rework below, taking a fresh version slot on rebase. Concretely, to get this merged:
On the redirect that has been floated: binding decisions to trusted harness-captured input with single-use gate-scoped receipts is the right end state, and the seam exists (the HUMAN_TURN mint on UserPromptSubmit deliberately reads no prompt content today). But it is an RFC, not a change request this PR can absorb: content capture is per-harness work, Copilot has no trusted prompt event, and widget answers transit no trusted channel on any harness - which is exactly why the presence guard is presence-only. We will track that separately. With #758 covering the deadlock and this PR re-scoped to what it verifiably does, the audit-integrity hole gets a real floor without waiting on the receipt RFC. |
Problem
Refs #742. That issue covers the ergonomics of the advisory-review deadlock — a stage that cannot legally complete once a
produces[]artifact is written after the reviewer's receipt, because completion needs a fresh receipt and the advisory review budget of 1 is only reset by aGATE_REJECTED. This PR addresses the other half: what an unattended conductor does to escape it.The human-presence guard (
humanActedSinceGate) proves aHUMAN_TURNexists after the gate opened. That is human presence, not human intent — and in a live session presence is continuous. A conductor blocked by a completion precondition therefore satisfies the guard while supplying the decision text itself through--user-input/--feedback/--details.Two audit trails reviewed for #742 (details and figures in this comment) show exactly that. Because
GATE_REJECTEDis the only event that restores an advisory review budget, a deadlocked conductor records one and says in the feedback field that it is not a human decision:One session produced 8 authority-bearing rows carrying an explicit self-attestation — 5
GATE_REJECTED, 2QUESTION_ANSWERED, 1GATE_APPROVED(Approve - CONDUCTOR DEFAULT, session unattended, whose own text records that no human confirmed any of its inputs). A second session, where a human answered throughout, produced zero — the deadlock induces the behaviour rather than it being constant.Once such rows exist, "a human approved this stage" is no longer verifiable from the trail. Note these conductors were the honest case: they wrote down what they were doing, in prose nobody reads. A more compressed one writes
Request changesand is indistinguishable from a human.This is not harness-specific —
aidlc-state.ts,aidlc-log.ts, andaidlc-lib.tsare byte-identical acrosscore/and all sevendist/trees.Change
fabricatedDecisionMarker()incore/tools/aidlc-lib.ts, beside the existingisNonAnswer()— the mirror-image floor.isNonAnswercatches the harness handing back a dismissed widget; this catches the conductor writing the decision itself. Enforced at the same three call sites:aidlc-state.tshandleApprove--user-inputaidlc-state.tshandleReject--feedbackaidlc-log.tshandleAnswer--detailsaidlc-orchestrate report --result approved\|rejectedshells out tostate.ts approve/reject, so it inherits the floor rather than needing its own. Each refusal quotes the phrase that tripped it and names the supported alternative — surface the blocker at the gate instead of deciding for the human:Deliberately narrow
Over-blocking a real human at a gate is the one regression worse than the forgery, so the vocabulary matches only self-referential provenance claims — assertions about who authored this very decision — never assertions about the work:
revert the agent-initiated retry in scheduler.ts→ commits (bareagent-initiatedis not a marker; the authorship alternatives all require a decision noun)The conductor default timeout of 30s is too low→ commits (the attributive sense; the label senseCONDUCTOR DEFAULT,closes its clause)the unattended batch job should retry three times→ commitsUnlike
NON_ANSWER_RE's whole-string anchor these are substring matches, because the disclaimer rides inside a paragraph of otherwise-substantive prose.Autonomous Construction is exempt at every call site. Where the conductor legitimately holds the decision, honest self-labelling must stay legal — otherwise the floor would push autonomous runs toward unlabelled decisions, which is strictly worse for the trail. The floor also rides the same
AIDLC_SKIP_HUMAN_PRESENCE_GUARDoff-switch as the sibling authority guards.A blocked decision leaves no partial receipt (neither
GATE_REJECTEDnorSTAGE_REVISING) and does not consume the human's turn, so their real choice still commits on the same turn.Scope note
This does not fix the deadlock itself — #742's refresh action is still worth doing, and the two are complementary. The refresh action removes the motive to fabricate; this removes the possibility. Filed as a fix rather than a feature because the audit-integrity hole stands on its own: it is reachable from any completion precondition a conductor cannot clear, not just the advisory-review one.
Testing
13 new cases in
tests/unit/t261-audit-authority-floor.test.ts(the existing home of theisNonAnswerfloor, extended to guarantee 5). Verified they are real regression tests: applied to a pristinev2checkout, the 7 forgery cases fail and the 6 must-not-over-block / carve-out cases pass; with the fix all 13 pass.reject --feedbackrefuses self-attributed prose (verbatim field strings), asserting noGATE_REJECTEDand noSTAGE_REVISINGlandapprove --user-inputrefuses a self-attributed approval; noGATE_APPROVEDanswer --detailsrefuses a self-attributed interview answer; noQUESTION_ANSWEREDApprovestill commitsAlso run:
bun scripts/package.ts+bun scripts/package.ts --check— no drift, all 7 harness trees regenerated and committedbun run typecheck— cleanbun run lint— exit 0 (3 pre-existing infos int267-usage.test.ts, untouched here)bun tests/gen-coverage-registry.ts— regenerated; thefunctionratchet moves 144 → 145 (new exported function is covered)bun tests/run-tests.ts— 318 files. The 4 remaining red files were each verified against a pristineorigin/v2worktree and none are attributable to this change:t248-codekb-scope-diff(2)t255-workspace-sync(9)t267-usage(1)concurrent processes preserve every transcript fold— load flake; reproduced on pristine v2 (1 of 3 runs under load), passes standalone on botht19(2)gen-coverage-registryandt249-copilot-adapterwent red mid-development (stale registry; a 5 s timeout overshooting by 617 ms) and are green in the final run.Notes for reviewers
v2, matching where recent work has been merging.CONTRIBUTING.mdstill says "Work against the latestmainbranch" — happy to retarget if that is wrong.GATE_APPROVEDis silent and permanent, while a false refusal costs the human one rephrase and is self-explaining.