Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/gateway/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 28 additions & 2 deletions tests/native-gateway-enforcement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

/**
Expand Down
Loading