Deterministic, provider-free reproduction for the approval-provenance boundary discussed in Vercel AI SDK issues #19964 and #19979.
This fixture does not call a real model or payment API. chargeCard only increments an in-memory counter with a dummy amount.
- Node.js: 22.18.0
ai: 7.0.84@ai-sdk/workflow: 2.0.14zod: 4.4.3
| Arm | Input | Expected tool executions | Observed |
|---|---|---|---|
| W1 | fresh tool call with needsApproval: true |
0 | 0 |
| W2 | matching tool-approval-request plus client-authored approved: true history |
1 | 1 |
| W3 | W2 plus a forced, unsupported experimental_toolApprovalSecret option |
1 | 1 |
W1 confirms that needsApproval blocks the tool before approval. W2 confirms that the tested WorkflowAgent resume path accepts matching approval history and then executes the tool. W3 confirms that forcing a same-named option into the constructor through as any does not add signature verification.
The upstream implementation in PR #19976 shipped after the baseline fixture above. I reran the same provider-free approval boundary on:
- Node.js 22.18.0
ai@7.0.86@ai-sdk/workflow@2.0.16
Observed behavior:
| Current-release arm | Observed result |
|---|---|
fresh needsApproval: true call |
pauses before tool execution |
| unsigned approval replay with no approval secret configured | still executes, preserving the backward-compatible path |
unsigned / forged replay with experimental_toolApprovalSecret configured |
no tool-result; execution counter remains unchanged |
This gives the repository a clean before/after boundary: the original 2.0.14 fixture records the pre-feature behavior, while 2.0.16 verifies that signed approval replay is enforced when the secret is explicitly configured. The unsigned path remains opt-in compatible when no secret is configured.
Upstream release confirmation: vercel/ai#19964.
src/type-boundary.ts also uses @ts-expect-error to pin the public type boundary: WorkflowAgent 2.0.14 does not expose experimental_toolApprovalSecret as a supported option. If a future release adds it, the typecheck intentionally becomes a signal to re-test this fixture.
npm install
npm run typecheck
npm testA successful run prints three sections:
W1 fresh approval request
W2 client-authored approval replay
W3 unsupported secret forced into WorkflowAgent options
The important deterministic assertions are the tool-execution counts, not the timing values printed by the mock stream.
This fixture does not prove that an attacker can bypass authentication, CSRF protection, anti-replay controls, or server-side authorization. It demonstrates a narrower application-design boundary: client-supplied message history should not be the only authority for high-impact approval provenance.
For payment, deletion, email, production writes, or other sensitive tools, keep a server-owned approval record that binds at least:
approval_id
+ authenticated_approver
+ tenant_id
+ tool_name
+ canonical_tool_input_hash
+ target_resource
+ approval_status
+ expires_at
+ idempotency_key
Fail closed if that record is missing, expired, revoked, belongs to another tenant, or no longer matches the tool input being executed.