awslabs / awslabs/aidlc-workflows

# RFC: Gate Human-Presence Guard Blocks Legitimate Tool Calls

Open
#708 0 comments 0 reactions 1 assignee Claimed by @apackeer View on GitHub
rfc
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.