[AAASM-4129] 🔒 (gateway): Fail closed on pending verdict without an approval channel#234
Conversation
A `pending` verdict routed to waitForApproval but the native client stub
resolved `{denied:false}`, so an approval-required verdict was silently
downgraded to allow even under enforce. Deny under failClosed when no
approval channel is wired, matching python (_resolve_pending_approval)
and go (WaitForApproval). Advisory postures stay neutral.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz
Add a native-gateway test that a pending verdict with no wired approval channel blocks the tool (never runs its body) under enforce, and clarify the advisory-posture PENDING case stays fail-open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Review — AAASM-4129 pending verdict fails closedVerdict: Approve-ready (comment only, no approval per review protocol). All four dimensions green. 1. CI — 25/25 checks passing, 0 failed. No fix needed. 2. Scope vs ticket — Matches AAASM-4129 exactly. The ticket calls out 3. Side effects — Verified the gate is
4. FE — N/A. Local validation (worktree, Touches only — Claude Code |



Target
Task summary:
A
pending(approval-required) verdict from the runtime was silently downgraded to allow at the SDK layer. The native gateway client'swaitForApprovalstub resolved{ denied: false }, which won thePromise.raceagainst the approval timeout inenforceGovernance, so the wrapped tool ran with no approval ever solicited — even underenforce. Node was the only SDK of the three that did this: python's_resolve_pending_approvaland go'sWaitForApprovalboth DENY a pending verdict when no approval channel is wired.This makes a pending verdict fail closed under
failClosed(enforce):waitForApprovalnow resolves{ denied: true, reason }when no approval channel is configured, so the wrapper throwsPolicyViolationErrorand the tool never runs. Advisory postures (observe / disabled / unset) stay neutral so a missing approval channel never blocks the agent — matching the existingcheck()fault posture.Task tickets:
Key point change (optional):
waitForApprovalresolves{ denied: false }→ tool executes with no approval.enforcewith no wired approval channel →{ denied: true }→PolicyViolationError, tool blocked. Non-enforce postures unchanged (fail-open).createNativeGatewayClient(the only client that can emitpending: true); the no-op client'scheck()never returns pending, so it is untouched.Effecting Scope
No new dependencies. No public API change. No native (Rust) glue touched — pure TypeScript.
Description
src/gateway/client.ts:createNativeGatewayClient.waitForApprovalnow denies underfailClosed(enforce) with an explanatory reason; stays neutral in advisory postures.tests/native-gateway-enforcement.test.ts: adds a test that a pending verdict + enforce + no approval channel blocks the tool (never runs its body); clarifies the existing advisory-posture PENDING test.How to verify
pnpm typecheck && pnpm lintclean.pnpm test— full suite green (the newPENDING under enforce ... fails closedcase plus the unchanged advisory PENDING case).Closes AAASM-4129.
🤖 Generated with Claude Code
https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz