awslabs / awslabs/aidlc-workflows
# RFC: Gate Human-Presence Guard Blocks Legitimate Tool Calls
- Dominant language
- TypeScript
- Stars
- 4.7k
- Forks
- 853
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 165
Description
### Summary
The human-presence gate guard in the Kiro harness (`block` PreToolUse target in `aidlc-kiro-adapter.ts`) exits 2 on **all** tool calls while a stage is in `awaiting-approval` state and no `HUMAN_TURN` has been recorded since the last gate resolution. The guard's intent is correct — prevent the conductor from auto-approving its own gate — but its scope is too wide:
1. **Notification tools** (e.g. Slack MCP) can never fire while a gate is open, so the human waiting for that gate never receives a notification they explicitly requested.
2. **Unrelated questions** sent in a new session (or the same session) are also blocked because the guard intercepts every `PreToolUse` event project-wide, regardless of whether the incoming prompt is related to the open gate.
Both failure modes are reproducible with the shipped code.
### Motivation
### Layer 1 — prose floor (SKILL.md)
```
STOP your turn here — do NOT call any tool until the user explicitly responds with their choice.
```
This is a conductor directive, scoped to the conductor's own turn.
### Layer 2 — hard floor (`aidlc-kiro-adapter.ts`, target `block`)
```ts
if (hasOpenGate(content) && !humanActedSinceGate(pd)) {
process.stderr.write("An approval gate is open …");
return 2; // exit 2 → Kiro BLOCKS the tool call
}
```
`hasOpenGate` returns `true` whenever the state file contains at least one
`awaiting-approval` checkbox. `humanActedSinceGate` returns `true` only after
a `HUMAN_TURN` audit event appears after the last `GATE_APPROVED`,
`GATE_REJECTED`, or `QUESTION_ANSWERED` event.
The matcher on the registered hook is **empty** (all tools). So:
- `mcp_slack_slack_post_message` → **blocked**
- `fs_read` in a new chat session about an unrelated topic → **blocked**
- Any MCP tool the human explicitly invokes → **blocked**
This is a **regression from the guard's original contract**, which was limited
to: "the conductor must not fabricate its own gate approval". The current
implementation imposes the same constraint on every agent, every session, and
every tool.
### Detailed Proposal
### Reproducible Cases
#### Case A — Slack notification blocked while gate is open
**Setup:** An intent is running; a stage completes and the conductor presents
the approval gate (`report --result awaiting-approval`). State file contains `- [?] …`.
**Action:** The human (or the conductor, acting on a pre-gate instruction) calls
`mcp_slack_slack_post_message` to notify a team channel that approval is needed.
**Actual result:** `PreToolUse` fires, `hasOpenGate` → `true`,
`humanActedSinceGate` → `false`, hook exits 2, Kiro blocks the call. No notification is sent.
**Expected result:** Notification tools should be allowed — they do not mutate
workflow state and cannot fabricate an approval.
---
#### Case B — Unrelated question blocked in a new or same session
**Setup:** Gate is open on project X.
**Action:** In a new Kiro session (or the same session), the human asks an
unrelated question: "Can you summarise the README?"
**Actual result:** `PreToolUse` fires for `fs_read`, `hasOpenGate` → `true`,
`humanActedSinceGate` → `false`, hook exits 2, tool call blocked. The question
cannot be answered.
**Root cause:** `process.cwd()` is the project dir in both sessions. A new
session contains no `HUMAN_TURN` event in the audit log for this gate period,
so `humanActedSinceGate` returns `false`. The guard does not distinguish
"conductor continuing after gate" from "human asking an unrelated question in a
separate session".
---
#### Case C — Unrelated question silently advances the workflow past an open gate
**Setup:** Gate is open on an intent.
**Action:** In the same or a new session, the human types any message unrelated
to the gate (e.g. "what time is it?").
**Sequence:**
1. `UserPromptSubmit` fires → `aidlc-mint` runs → `HUMAN_TURN` appended to audit shard.
2. `PreToolUse` fires for the first tool the conductor calls to answer the
question. `humanActedSinceGate` finds the freshly minted `HUMAN_TURN` →
returns **true**. `block` exits 0. Tool call proceeds.
3. The conductor answers the question.
4. In the **same turn**, the conductor calls `aidlc-orchestrate.ts next` (or
`report --result approved`). `humanActedSinceGate` is still **true** (same token).
Gate commits the approval. Next stage begins.
**Actual result:** The intent advances past the gate without the human ever
choosing Approve / Request Changes.
**Root cause:** `aidlc-mint` (UserPromptSubmit) mints `HUMAN_TURN` on **every**
human message regardless of intent. The `block` guard checks for *some*
`HUMAN_TURN` after the last gate resolution — not that the human responded
*at the gate*. This is the inverse of Cases A/B: the guard is **too permissive**
because the mint is unconditional.
---
### Proposed Solutions
All three cases share a common root: `HUMAN_TURN` conflates "a human spoke"
with "a human responded at a gate". Cases A/B fail when the guard is too
aggressive; Case C fails when it is too permissive.
#### Option A — Tool allowlist for the block guard *(short term, Cases A/B)*
Add a set of tool-name patterns that the block guard always passes through,
regardless of gate state:
```ts
const ALLOWED_DURING_GATE = /^(mcp_slack_|mcp_.*_post_message|mcp_.*_notify)/;
if (ALLOWED_DURING_GATE.test(kiro.tool_name ?? "")) return 0;
```
**Pros:** Minimal diff, no change to audit semantics, backward-compatible.
**Cons:** Requires maintaining the allowlist. Does not fix Case C.
---
#### Option B — `HUMAN_TURN_AT_GATE` variant event *(short term, Case C)*
Add a `HUMAN_TURN_AT_GATE` variant event distinct from the unconditional
`HUMAN_TURN`. The conductor SKILL.md gate-present step emits this variant when
it stops for the gate; `humanActedSinceGate` checks for `HUMAN_TURN_AT_GATE`
instead of bare `HUMAN_TURN`. An unrelated question mints only `HUMAN_TURN`
→ does not satisfy the gate predicate → `block` guard stays active → conductor
cannot advance.
Required changes: `aidlc-kiro-adapter.ts` (mint target),
`core/tools/aidlc-lib.ts` (`GATE_RESOLUTION_EVENTS` / `humanActedSinceGate`),
SKILL.md gate-stop instruction.
**Pros:** Surgically fixes the mint/guard conflation. Preserves the full hard floor.
**Cons:** Adds a new audit event kind; requires coordinated changes across three files.
---
#### Option C — Intent-scoped gate check *(long term, all cases)*
Scope `hasOpenGate` to the **active intent** rather than the entire state file.
An unrelated question does not activate the current intent's gate, so
`hasOpenGate` returns `false` for it. Requires passing intent context into the
block hook (currently unavailable in the Kiro-IDE hook payload).
**Pros:** Correct by construction. Fixes all three cases.
**Cons:** Largest change; depends on harness-level payload improvements.
---
### Recommended Path
| Horizon | Action | Cases fixed |
|---------|--------|-------------|
| Short term | Option A — tool allowlist | A, B |
| Short term | Option B — `HUMAN_TURN_AT_GATE` event | C |
| Long term | Option C — intent-scoped gate check | A, B, C (supersedes A+B) |
---
### Acceptance Criteria
- [ ] Calling `mcp_slack_slack_post_message` while a gate is open is not blocked.
- [ ] A new Kiro session asking an unrelated question on a project with an open
gate can answer the question (read files, call tools).
- [ ] An unrelated question in the same or a new session does **not** allow the
conductor to advance past the open gate without an explicit Approve /
Request Changes response from the human.
- [ ] The original guard contract is preserved: the conductor cannot call
`aidlc-orchestrate.ts report --result approved` in the same turn it
presented the gate, without a prior gate-targeted human turn in the audit log.
- [ ] `t68` (version/changelog sync) and `t42` (human-presence guard) continue to pass.
- [ ] No new test failures in `bash tests/run-tests.sh smoke integration`.
---
### Affected Files
| File | Change |
|------|--------|
| `harness/kiro-ide/hooks/aidlc-kiro-adapter.ts` | Option A: allowlist in `block` target; Option B: emit `HUMAN_TURN_AT_GATE` in `mint` target |
| `harness/kiro/hooks/aidlc-kiro-adapter.ts` | Same for Kiro CLI harness |
| `core/tools/aidlc-lib.ts` | Option B: `HUMAN_TURN_AT_GATE` in gate predicate; Option C: intent-scoped `hasOpenGate` |
| `harness/kiro-ide/skills/aidlc/SKILL.md` | Option B: gate-stop instruction emits `HUMAN_TURN_AT_GATE` |
| `tests/integration/t42-*.test.ts` | Regression cases for all three scenarios |
| `CHANGELOG.md` | Entry under next patch bump |
| `core/tools/aidlc-version.ts` | Patch bump |
### Alternatives Considered
**Session-aware `HUMAN_TURN` minting (rejected)**
Minting a `HUMAN_TURN` event at `SessionStart` was considered. This would make
`humanActedSinceGate` return `true` for the first turn of any new session,
unblocking Case B. Rejected because `HUMAN_TURN` is the audit evidence that a
real person responded at a gate — auto-minting on session open would allow any
automated script to bypass an open gate simply by starting a new session, making
the hard floor meaningless. The fix must address *which tools are allowed* and
*what constitutes a gate-directed human turn*, not *how to satisfy the audit
predicate artificially*.
### Drawbacks
- Option A requires an allowlist to be maintained; a newly added MCP tool is
blocked by default until explicitly listed.
- Option B adds a new audit event kind (`HUMAN_TURN_AT_GATE`) that must be
understood by any future audit tooling or test that inspects the ledger.
- Options A and B together are still a partial fix — Option C is the only
complete solution but requires platform changes not yet available.
### Additional Context
- `harness/kiro-ide/hooks/aidlc-kiro-adapter.ts` — `target === "block"` section
- `harness/kiro/hooks/aidlc-kiro-adapter.ts` — `target === "pretool-block"` section
- `core/tools/aidlc-lib.ts` — `hasOpenGate()`, `humanActedSinceGate()`
- `harness/kiro-ide/skills/aidlc/SKILL.md` — prose gate-stop instruction
- `harness/kiro-ide/hooks/aidlc-mint.json` — `UserPromptSubmit` hook (unconditional mint)
Contributor guide
Assessment
This issue has not been assessed yet.