Skip to content

fix: an unattended driver must not mint the human-presence token (2.6.6) - #766

Open
warren830 wants to merge 1 commit into
awslabs:v2from
warren830:fix/unattended-human-turn-mint
Open

fix: an unattended driver must not mint the human-presence token (2.6.6)#766
warren830 wants to merge 1 commit into
awslabs:v2from
warren830:fix/unattended-human-turn-mint

Conversation

@warren830

Copy link
Copy Markdown

Summary

The human-presence gate can be satisfied by something that is not a person.

hooks/aidlc-record-human-turn.ts records a HUMAN_TURN on UserPromptSubmit — the evidence humanActedSinceGate() spends to authorize an approval or an interview answer. But the hook has no evidence for the assertion it is making: the event carries no signal about who submitted, and the hook reads no stdin ("Presence-only: the prompt text is irrelevant"). Its only guard is "does a state file exist".

That is sound while every prompt comes from a person. It stops being sound the moment something drives /aidlc on a schedule — an overnight runner, a CI job, a cron. Each cycle submits a prompt, so each cycle mints fresh, spendable presence.

This is not a regression of #427 — it is the premise #427's fix was verified against. That verification reads "At every approval gate the agent paused and ended its turn; walking away (no input) did not auto-approve", which holds precisely because no prompt means no presence. An unattended driver submits prompts, so "walking away" no longer implies "no new human turn".

Evidence

Measured on a real unattended run against a stock v2 install (10 detached cycles, no human involved at any point):

HUMAN_TURN rows in the ledger: 10   (every one minted by a runner-submitted prompt)
gate resolutions:              0
humanActedSinceGate():         true

The new t188 case then proves the consequence rather than assuming it. Reverting only the hook change turns it red with Expected: not 0 — i.e. without this change the gate approves on a turn minted solely by an unattended prompt. The remaining barrier today is the conductor's prose ("NEVER self-answer questions"), not a mechanism.

Changes

AIDLC_UNATTENDED=1 suppresses the mint. One env check in one hook.

  • Covers both of the hook's paths — the typed prompt and the answered AskUserQuestion widget — since an unattended driver should mint from neither.
  • Consistent with doctrine already in the engine. An unattended autonomous Construction run "has no human at the gate" (aidlc-utility.ts), which is why it already refuses scope changes and plan re-shapes, and why aidlc-state.ts's autonomy guard refuses park. This closes the one path where an unattended turn still manufactured a human.
  • Fail direction is deliberate: the flag can only ever WITHHOLD authority. If it leaks into an interactive shell, approvals get refused until it is unset — annoying, and safe. The inverse mistake cannot be undone, because the ledger is append-only.
  • Nothing changes for an interactive session. With the flag unset the mint is byte-for-byte what it was, which is what the "an attended one still does" half of the new test pins.

One choice I am flagging rather than making silently

The .aidlc-human-turn marker is still written. It is not an authority signal, and suppressing it would change the Stop hook's conversational carve-out — separate behaviour, with its own tests, that I have not exercised here. It is a one-line follow-on if you want it; I would rather you decide than have it ride along.

Two further design calls that are yours, not mine:

  1. Skip vs. distinct event. I skip the mint. The alternative is recording something like RUNNER_TURN that humanActedSinceGate does not accept, which keeps the trail richer at the cost of a new VALID_EVENT_TYPES member plus the state-machine chapter tables under the same-commit rule. SESSION_STARTED already records that a session happened, so I took the smaller surface.
  2. Naming. AIDLC_UNATTENDED reuses the word the codebase already uses for this condition. It is the symmetric half of the existing AIDLC_SKIP_HUMAN_PRESENCE_GUARD (which turns the guard off); what was missing was a way to say "this prompt is not a person's".

User experience

An operator running AI-DLC unattended sets AIDLC_UNATTENDED=1 in the driving process, and gates then refuse exactly as they do when nobody has acted — the workflow parks and waits for a real human instead of being able to walk past its own approval. Interactive users see no change.

Related: #530 ("a first unattended-gate story") is where this belongs as a design thread; I have posted the finding there.

Checklist

  • I have read the contributing guidelines
  • I have performed a self-review of my code
  • I have tested my changes
  • I have documented my changes (CHANGELOG + docs/reference/06-hooks-and-tools.md hook table, same commit per the Documentation Policy)

Test Plan

Two cases added to tests/unit/t188-human-presence-gate.test.ts, whose covers: header already carries file:hooks/aidlc-record-human-turn.ts, so no coverage-registry change. Both spawn the real hook as a process (the env read is the contract under test) with AIDLC_PROJECT_DIR pointed at the fixture:

  • an unattended prompt mints NO HUMAN_TURN; an attended one still does — the flag is the only difference between the two halves, which is what stops the test passing for the wrong reason (a hook that never mints at all).
  • a gate REFUSES on a turn minted only by an unattended prompt — the consequence, asserted end to end through the real approve CLI.

Red-green proven: stash the hook change, regenerate dist/, and both fail — the second with Expected: not 0, i.e. the gate approved. Restore, and 24/24 pass.

Verified locally (bun 1.3.11; CI pins 1.3.14):

bun scripts/package.ts && bun scripts/package.ts --check
bun tests/gen-coverage-registry.ts --check
bun run check
bun tests/run-tests.ts --smoke --unit --parallel 8

The first three exit 0. The suite result matches the baseline on this branch point: the same 2 files fail (t248-codekb-scope-diff, t255-workspace-sync), both unrelated to presence. t255's failures are ~5s timeouts on live git remote queries and its count moves with load — 16 when the file runs alone here, 7-8 under --parallel 8 — so it is environmental. t188 is 24/24.

Version bumped to 2.6.6: 2.6.3 is claimed by #754 and #758, 2.6.4 by #759, 2.6.5 by #760.

Acknowledgment

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

The `UserPromptSubmit` mint hook records a `HUMAN_TURN` — the token the approval
and interview gates spend — but it has no evidence for the assertion it is
making. The event carries no signal about who submitted, and the hook reads no
stdin ("Presence-only: the prompt text is irrelevant"). That is sound while
every prompt comes from a person, and unsound the moment something drives
`/aidlc` on a schedule: an overnight runner, a CI job, a cron. Each cycle mints
a fresh, spendable turn, so "walking away" stops meaning "no new human turn" —
which is the premise awslabs#427's fix was verified against.

Measured on a real unattended run: 10 runner-submitted prompts, zero humans, and
`humanActedSinceGate()` answered true. The new t188 case proves the consequence
rather than assuming it — without this change a gate APPROVES on a turn minted
only by an unattended prompt.

`AIDLC_UNATTENDED=1` now suppresses the mint, on both of the hook's paths (typed
prompt and answered AskUserQuestion widget). This is the doctrine the engine
already applies elsewhere: an unattended autonomous Construction run "has no
human at the gate", which is why aidlc-utility refuses scope changes and plan
re-shapes under it and aidlc-state refuses park. This closes the one path where
an unattended turn still manufactured a human.

Fail direction is deliberate: the flag can only WITHHOLD authority. A leak into
an interactive shell refuses your approvals until you unset it — annoying, and
safe. The inverse mistake cannot be undone, because the ledger is append-only.

The `.aidlc-human-turn` marker is deliberately still written. It is not an
authority signal, and suppressing it would change the Stop hook's conversational
carve-out, which is separate behaviour with its own tests. Flagged in the code as
a reviewable choice rather than made silently.

Nothing changes for an interactive session: with the flag unset the mint is
byte-for-byte what it was.
@apackeer

Copy link
Copy Markdown
Contributor

Warren, this PR addresses a real gap in the human-presence guard: a scheduled driver can submit the same prompt event as a person and thereby mint the HUMAN_TURN token that approval and interview gates trust. The proposed fail-closed AIDLC_UNATTENDED=1 declaration is a proportionate way for the driver, which is the component that knows it is unattended, to withhold that authority.

STAMP: tests/logs/2026-08-14T18-07-21Z-p2
TRACES: /home/ubuntu/src/aidlc-workflows/.claude/worktrees/pr-766/tests/logs/2026-08-14T18-07-21Z-p2/{sdk,tui,kiro-acp}-drive-*.ndjson (0 files)
SUMMARY: tests/logs/2026-08-14T18-07-21Z-p2/summary.txt + failures.txt
RESULT: unit . 31 pass/0 fail . reds: none . live vars set: none . invariant grep hits: 0 (path-excluded).

Part A - Direction: sound-but-misplaced

The problem exists on current origin/v2, and the fail direction is sound. The implementation boundary is incomplete, though: the repository has five production mint implementations, not one shared hook. Claude uses the changed hook, and Cursor/opencode forward to it, but Codex, Copilot, Kiro CLI, and Kiro IDE append HUMAN_TURN directly in their adapters. This policy should live in a shared mint predicate/helper used by every seam, or be applied explicitly to every adapter.

Part B - Review: request changes

  1. core/hooks/aidlc-record-human-turn.ts:68 does not protect the direct adapter mints at harness/codex/hooks/aidlc-codex-adapter.ts:597, harness/copilot/hooks/aidlc-copilot-adapter.ts:625, harness/kiro/hooks/aidlc-kiro-adapter.ts:236, and harness/kiro-ide/hooks/aidlc-kiro-adapter.ts:239. I confirmed behaviorally that the packaged Codex adapter still records one HUMAN_TURN with AIDLC_UNATTENDED=1. Please apply the contract across all shipped harnesses and add focused coverage for each independent mint path.

  2. The leaked-flag failure mode is opaque. core/tools/aidlc-state.ts:2485 and the aidlc-log answer/summary refusal paths tell the user to type or acknowledge again. With AIDLC_UNATTENDED=1 still set, that cannot mint presence, so the advice loops forever. Please append a conditional diagnostic naming the flag on these refusal paths.

  3. CHANGELOG.md:6 says an unattended driver can no longer satisfy the gate, but this is an opt-in declaration: an unflagged driver still mints, even after the cross-harness gap is fixed. Please describe the conditional contract explicitly. Also update docs/guide/07-interaction-modes.md:76 to document the declaration and how an operator returns to interactive mode.

Non-blocking release note: open PR #749 currently also declares 2.6.6. The version, README badge, and CHANGELOG heading are internally synchronized here, but whichever PR merges second needs the documented rebase/re-bump.

Part C - UX impact

This adds one operator environment setting and no new command, stage, or default. With the flag honored, unattended prompt cycles no longer grant approval/interview authority and the workflow waits for a person; interactive sessions remain unchanged. The marker used by the Stop-hook conversational carve-out is intentionally unchanged. The submitted code currently delivers that UX only on Claude, Cursor, and opencode, and a leaked flag produces an unexplained retry loop, so the release behavior is not yet consistent or diagnosable.

Verification: focused t188 plus t68 passed 31/31; package drift, coverage registry, and static checks also passed. No e2e or live-service variables were needed.

@apackeer apackeer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the cross-harness, diagnostic, and documentation gaps described in my review comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants