From 8f2bc46a72ee2ba10f0a90564bd9e9cddad25f4a Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 4 Jul 2026 23:45:19 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=20(gateway):=20Fail=20closed?= =?UTF-8?q?=20on=20pending=20verdict=20without=20an=20approval=20channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz --- src/gateway/client.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/gateway/client.ts b/src/gateway/client.ts index e8212bbb..aac747ec 100644 --- a/src/gateway/client.ts +++ b/src/gateway/client.ts @@ -117,7 +117,18 @@ export function createNativeGatewayClient( return { denied: failClosed, pending: false }; } }, - waitForApproval: async () => ({ denied: false }), + // A `pending` verdict routes here to solicit an approval decision. The node + // SDK does not yet wire a real approval channel (poll/stream), so no + // decision can be obtained (AAASM-4129). Under `enforce` this must fail + // closed — deny — rather than silently downgrade an approval-required + // verdict to allow, matching python's `_resolve_pending_approval` and go's + // `WaitForApproval`, both of which deny when no approval channel is wired. + // In any advisory posture (observe / disabled / unset) it stays neutral so + // a missing approval channel never blocks the agent. + waitForApproval: async () => + failClosed + ? { denied: true, reason: "approval required but no approval channel is configured" } + : { denied: false }, record: async () => undefined, recordResult: async () => undefined, scanPrompts: async () => undefined From 038a7a7a99cc80d163f196112163194016f57093 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Sat, 4 Jul 2026 23:45:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20(gateway):=20Assert=20pending?= =?UTF-8?q?=20verdict=20fails=20closed=20under=20enforce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01R7vqjjo5nrebYNt8WnCNbz --- tests/native-gateway-enforcement.test.ts | 30 ++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/native-gateway-enforcement.test.ts b/tests/native-gateway-enforcement.test.ts index 63421bc7..1c20a448 100644 --- a/tests/native-gateway-enforcement.test.ts +++ b/tests/native-gateway-enforcement.test.ts @@ -169,13 +169,39 @@ describe("native gateway enforcement (AAASM-3050)", () => { const executeFn = vi.fn(async () => "approved-and-ran"); const tools = { wire_transfer: { description: "money", execute: executeFn } }; - // No approval push is wired in the node SDK yet, so the noop approval path - // resolves to "not denied" and the tool proceeds (per the shared contract). + // No approval channel is wired in the node SDK yet; in the ADVISORY posture + // (no enforcementMode) that resolves to "not denied" and the tool proceeds, + // so a missing approval channel never blocks the agent (AAASM-4129). withAssembly(tools, { gatewayClient: gateway }); await expect(tools.wire_transfer.execute()).resolves.toBe("approved-and-ran"); expect(executeFn).toHaveBeenCalledOnce(); }); + + it("PENDING under enforce: no approval channel fails closed and blocks the tool (AAASM-4129)", async () => { + // py/go parity: python's `_resolve_pending_approval` and go's + // `WaitForApproval` DENY a pending verdict when no approval channel is + // wired. Under enforce the node SDK must not auto-approve — the tool is + // blocked (never runs its body) rather than silently downgraded to allow. + const gateway = createNativeGatewayClient( + "napi-inprocess", + fakeNativeClient(async () => ({ + denied: false, + pending: true, + reason: "awaiting approval" + })), + "agent-1", + undefined, + "enforce" + ); + + const executeFn = vi.fn(async () => "should-not-run-without-approval"); + const tools = { wire_transfer: { description: "money", execute: executeFn } }; + withAssembly(tools, { gatewayClient: gateway }); + + await expect(tools.wire_transfer.execute()).rejects.toThrow(PolicyViolationError); + expect(executeFn).not.toHaveBeenCalled(); + }); }); /**