ai 6.0.201 → 6.0.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/index.d.mts +61 -15
  3. package/dist/index.d.ts +61 -15
  4. package/dist/index.js +853 -592
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +695 -430
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/internal/index.js +10 -12
  9. package/dist/internal/index.js.map +1 -1
  10. package/dist/internal/index.mjs +11 -13
  11. package/dist/internal/index.mjs.map +1 -1
  12. package/docs/03-ai-sdk-core/16-mcp-tools.mdx +32 -0
  13. package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +20 -2
  14. package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +1 -1
  15. package/docs/07-reference/05-ai-sdk-errors/ai-invalid-tool-approval-signature-error.mdx +31 -0
  16. package/package.json +3 -3
  17. package/src/error/index.ts +1 -0
  18. package/src/error/invalid-tool-approval-signature-error.ts +35 -0
  19. package/src/generate-text/generate-text.ts +48 -6
  20. package/src/generate-text/run-tools-transformation.ts +14 -1
  21. package/src/generate-text/stream-text.ts +47 -11
  22. package/src/generate-text/to-response-messages.ts +1 -0
  23. package/src/generate-text/tool-approval-request-output.ts +5 -0
  24. package/src/generate-text/tool-approval-signature.ts +125 -0
  25. package/src/generate-text/validate-tool-approvals.ts +124 -0
  26. package/src/middleware/extract-json-middleware.ts +2 -1
  27. package/src/middleware/extract-reasoning-middleware.ts +2 -1
  28. package/src/ui/convert-to-model-messages.ts +3 -0
  29. package/src/ui/process-ui-message-stream.ts +6 -1
  30. package/src/ui/ui-messages.ts +10 -0
  31. package/src/ui/validate-ui-messages.ts +10 -0
  32. package/src/ui-message-stream/create-ui-message-stream.ts +2 -3
  33. package/src/ui-message-stream/ui-message-chunks.ts +2 -0
  34. package/src/util/download/download.ts +11 -14
@@ -0,0 +1,35 @@
1
+ import { AISDKError } from '@ai-sdk/provider';
2
+
3
+ const name = 'AI_InvalidToolApprovalSignatureError';
4
+ const marker = `vercel.ai.error.${name}`;
5
+ const symbol = Symbol.for(marker);
6
+
7
+ export class InvalidToolApprovalSignatureError extends AISDKError {
8
+ private readonly [symbol] = true;
9
+
10
+ readonly approvalId: string;
11
+ readonly toolCallId: string;
12
+
13
+ constructor({
14
+ approvalId,
15
+ toolCallId,
16
+ reason,
17
+ }: {
18
+ approvalId: string;
19
+ toolCallId: string;
20
+ reason: string;
21
+ }) {
22
+ super({
23
+ name,
24
+ message: `Tool approval signature verification failed for approval "${approvalId}" (tool call "${toolCallId}"): ${reason}`,
25
+ });
26
+ this.approvalId = approvalId;
27
+ this.toolCallId = toolCallId;
28
+ }
29
+
30
+ static isInstance(
31
+ error: unknown,
32
+ ): error is InvalidToolApprovalSignatureError {
33
+ return AISDKError.hasMarker(error, marker);
34
+ }
35
+ }
@@ -69,6 +69,8 @@ import { extractTextContent } from './extract-text-content';
69
69
  import type { GenerateTextResult } from './generate-text-result';
70
70
  import { DefaultGeneratedFile } from './generated-file';
71
71
  import { isApprovalNeeded } from './is-approval-needed';
72
+ import { maybeSignApproval } from './tool-approval-signature';
73
+ import { validateApprovedToolApprovals } from './validate-tool-approvals';
72
74
  import { text, type Output } from './output';
73
75
  import type { InferCompleteOutput } from './output-utils';
74
76
  import { parseToolCall } from './parse-tool-call';
@@ -270,6 +272,7 @@ export async function generateText<
270
272
  experimental_repairToolCall: repairToolCall,
271
273
  experimental_download: download,
272
274
  experimental_context,
275
+ experimental_toolApprovalSecret,
273
276
  experimental_include: include,
274
277
  _internal: { generateId = originalGenerateId } = {},
275
278
  experimental_onStart: onStart,
@@ -411,6 +414,15 @@ export async function generateText<
411
414
  */
412
415
  experimental_context?: unknown;
413
416
 
417
+ /**
418
+ * Secret for HMAC-signing tool approval requests. When set, the server
419
+ * signs each approval request at issuance and verifies the signature when
420
+ * the approval is replayed, preventing client-forged approvals.
421
+ *
422
+ * Experimental (can break in patch releases).
423
+ */
424
+ experimental_toolApprovalSecret?: string | Uint8Array;
425
+
414
426
  /**
415
427
  * Settings for controlling what data is included in step results.
416
428
  * Disabling inclusion can help reduce memory usage when processing
@@ -548,12 +560,32 @@ export async function generateText<
548
560
  const initialMessages = initialPrompt.messages;
549
561
  const responseMessages: Array<ResponseMessage> = [];
550
562
 
551
- const { approvedToolApprovals, deniedToolApprovals } =
552
- collectToolApprovals<TOOLS>({ messages: initialMessages });
563
+ const {
564
+ approvedToolApprovals,
565
+ deniedToolApprovals: collectedDeniedToolApprovals,
566
+ } = collectToolApprovals<TOOLS>({ messages: initialMessages });
567
+
568
+ // Re-validate approvals reconstructed from the client-supplied message
569
+ // history before executing them: verify the HMAC signature (when a
570
+ // secret is configured), re-validate the input against the tool's
571
+ // schema, and re-resolve whether the tool requires approval.
572
+ const {
573
+ approvedToolApprovals: localApprovedToolApprovals,
574
+ deniedToolApprovals: revalidationDeniedToolApprovals,
575
+ } = await validateApprovedToolApprovals<TOOLS>({
576
+ approvedToolApprovals: approvedToolApprovals.filter(
577
+ toolApproval => !toolApproval.toolCall.providerExecuted,
578
+ ),
579
+ tools,
580
+ messages: initialMessages,
581
+ experimental_context,
582
+ toolApprovalSecret: experimental_toolApprovalSecret,
583
+ });
553
584
 
554
- const localApprovedToolApprovals = approvedToolApprovals.filter(
555
- toolApproval => !toolApproval.toolCall.providerExecuted,
556
- );
585
+ const deniedToolApprovals = [
586
+ ...collectedDeniedToolApprovals,
587
+ ...revalidationDeniedToolApprovals,
588
+ ];
557
589
 
558
590
  if (
559
591
  deniedToolApprovals.length > 0 ||
@@ -926,10 +958,20 @@ export async function generateText<
926
958
  experimental_context,
927
959
  })
928
960
  ) {
961
+ const approvalId = generateId();
962
+ const signature = await maybeSignApproval({
963
+ secret: experimental_toolApprovalSecret,
964
+ approvalId,
965
+ toolCallId: toolCall.toolCallId,
966
+ toolName: toolCall.toolName,
967
+ input: toolCall.input,
968
+ });
969
+
929
970
  toolApprovalRequests[toolCall.toolCallId] = {
930
971
  type: 'tool-approval-request',
931
- approvalId: generateId(),
972
+ approvalId,
932
973
  toolCall,
974
+ ...(signature != null ? { signature } : {}),
933
975
  };
934
976
  }
935
977
  }
@@ -28,6 +28,7 @@ import {
28
28
  type GeneratedFile,
29
29
  } from './generated-file';
30
30
  import { isApprovalNeeded } from './is-approval-needed';
31
+ import { maybeSignApproval } from './tool-approval-signature';
31
32
  import { parseToolCall } from './parse-tool-call';
32
33
  import type { ToolApprovalRequestOutput } from './tool-approval-request-output';
33
34
  import type { TypedToolCall } from './tool-call';
@@ -128,6 +129,7 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
128
129
  abortSignal,
129
130
  repairToolCall,
130
131
  experimental_context,
132
+ toolApprovalSecret,
131
133
  generateId,
132
134
  stepNumber,
133
135
  model,
@@ -143,6 +145,7 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
143
145
  abortSignal: AbortSignal | undefined;
144
146
  repairToolCall: ToolCallRepairFunction<TOOLS> | undefined;
145
147
  experimental_context: unknown;
148
+ toolApprovalSecret?: string | Uint8Array;
146
149
  generateId: IdGenerator;
147
150
  stepNumber?: number;
148
151
  model?: { provider: string; modelId: string };
@@ -328,10 +331,20 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
328
331
  experimental_context,
329
332
  })
330
333
  ) {
334
+ const approvalId = generateId();
335
+ const signature = await maybeSignApproval({
336
+ secret: toolApprovalSecret,
337
+ approvalId,
338
+ toolCallId: toolCall.toolCallId,
339
+ toolName: toolCall.toolName,
340
+ input: toolCall.input,
341
+ });
342
+
331
343
  toolResultsStreamController!.enqueue({
332
344
  type: 'tool-approval-request',
333
- approvalId: generateId(),
345
+ approvalId,
334
346
  toolCall,
347
+ ...(signature != null ? { signature } : {}),
335
348
  });
336
349
  break;
337
350
  }
@@ -75,6 +75,7 @@ import {
75
75
  type AsyncIterableStream,
76
76
  } from '../util/async-iterable-stream';
77
77
  import { consumeStream } from '../util/consume-stream';
78
+ import { createIdMap } from '../util/create-id-map';
78
79
  import { createStitchableStream } from '../util/create-stitchable-stream';
79
80
  import type { DownloadFunction } from '../util/download/download-function';
80
81
  import { mergeAbortSignals } from '../util/merge-abort-signals';
@@ -82,6 +83,7 @@ import { mergeObjects } from '../util/merge-objects';
82
83
  import { now as originalNow } from '../util/now';
83
84
  import { prepareRetries } from '../util/prepare-retries';
84
85
  import { collectToolApprovals } from './collect-tool-approvals';
86
+ import { validateApprovedToolApprovals } from './validate-tool-approvals';
85
87
  import type {
86
88
  OnFinishEvent,
87
89
  OnStartEvent,
@@ -359,6 +361,7 @@ export function streamText<
359
361
  experimental_onToolCallStart: onToolCallStart,
360
362
  experimental_onToolCallFinish: onToolCallFinish,
361
363
  experimental_context,
364
+ experimental_toolApprovalSecret,
362
365
  experimental_include: include,
363
366
  _internal: { now = originalNow, generateId = originalGenerateId } = {},
364
367
  ...settings
@@ -532,6 +535,15 @@ export function streamText<
532
535
  */
533
536
  experimental_context?: unknown;
534
537
 
538
+ /**
539
+ * Secret for HMAC-signing tool approval requests. When set, the server
540
+ * signs each approval request at issuance and verifies the signature when
541
+ * the approval is replayed, preventing client-forged approvals.
542
+ *
543
+ * Experimental (can break in patch releases).
544
+ */
545
+ experimental_toolApprovalSecret?: string | Uint8Array;
546
+
535
547
  /**
536
548
  * Settings for controlling what data is included in step results.
537
549
  * Disabling inclusion can help reduce memory usage when processing
@@ -608,6 +620,7 @@ export function streamText<
608
620
  now,
609
621
  generateId,
610
622
  experimental_context,
623
+ experimental_toolApprovalSecret,
611
624
  download,
612
625
  include,
613
626
  });
@@ -792,6 +805,7 @@ class DefaultStreamTextResult<
792
805
  onToolCallStart,
793
806
  onToolCallFinish,
794
807
  experimental_context,
808
+ experimental_toolApprovalSecret,
795
809
  download,
796
810
  include,
797
811
  }: {
@@ -828,6 +842,7 @@ class DefaultStreamTextResult<
828
842
  | undefined;
829
843
  originalAbortSignal: AbortSignal | undefined;
830
844
  experimental_context: unknown;
845
+ experimental_toolApprovalSecret: string | Uint8Array | undefined;
831
846
  download: DownloadFunction | undefined;
832
847
  include: { requestBody?: boolean } | undefined;
833
848
 
@@ -881,7 +896,7 @@ class DefaultStreamTextResult<
881
896
  text: string;
882
897
  providerMetadata: ProviderMetadata | undefined;
883
898
  }
884
- > = {};
899
+ > = createIdMap();
885
900
 
886
901
  let activeReasoningContent: Record<
887
902
  string,
@@ -890,7 +905,7 @@ class DefaultStreamTextResult<
890
905
  text: string;
891
906
  providerMetadata: ProviderMetadata | undefined;
892
907
  }
893
- > = {};
908
+ > = createIdMap();
894
909
 
895
910
  const eventProcessor = new TransformStream<
896
911
  EnrichedStreamPart<TOOLS, InferPartialOutput<OUTPUT>>,
@@ -1055,8 +1070,8 @@ class DefaultStreamTextResult<
1055
1070
  if (part.type === 'start-step') {
1056
1071
  // reset the recorded data when a new step starts:
1057
1072
  recordedContent = [];
1058
- activeReasoningContent = {};
1059
- activeTextContent = {};
1073
+ activeReasoningContent = createIdMap();
1074
+ activeTextContent = createIdMap();
1060
1075
 
1061
1076
  recordedRequest = part.request;
1062
1077
  recordedWarnings = part.warnings;
@@ -1402,12 +1417,29 @@ class DefaultStreamTextResult<
1402
1417
  deniedToolApprovals.length > 0 ||
1403
1418
  approvedToolApprovals.length > 0
1404
1419
  ) {
1405
- const localApprovedToolApprovals = approvedToolApprovals.filter(
1406
- toolApproval => !toolApproval.toolCall.providerExecuted,
1407
- );
1408
- const localDeniedToolApprovals = deniedToolApprovals.filter(
1409
- toolApproval => !toolApproval.toolCall.providerExecuted,
1410
- );
1420
+ // Re-validate approvals reconstructed from the client-supplied
1421
+ // message history before executing them: verify the HMAC signature
1422
+ // (when a secret is configured), re-validate the input against the
1423
+ // tool's schema, and re-resolve whether the tool requires approval.
1424
+ const {
1425
+ approvedToolApprovals: localApprovedToolApprovals,
1426
+ deniedToolApprovals: revalidationDeniedToolApprovals,
1427
+ } = await validateApprovedToolApprovals<TOOLS>({
1428
+ approvedToolApprovals: approvedToolApprovals.filter(
1429
+ toolApproval => !toolApproval.toolCall.providerExecuted,
1430
+ ),
1431
+ tools,
1432
+ messages: initialMessages,
1433
+ experimental_context,
1434
+ toolApprovalSecret: experimental_toolApprovalSecret,
1435
+ });
1436
+
1437
+ const localDeniedToolApprovals = [
1438
+ ...deniedToolApprovals.filter(
1439
+ toolApproval => !toolApproval.toolCall.providerExecuted,
1440
+ ),
1441
+ ...revalidationDeniedToolApprovals,
1442
+ ];
1411
1443
 
1412
1444
  const deniedProviderExecutedToolApprovals =
1413
1445
  deniedToolApprovals.filter(
@@ -1728,6 +1760,7 @@ class DefaultStreamTextResult<
1728
1760
  repairToolCall,
1729
1761
  abortSignal,
1730
1762
  experimental_context,
1763
+ toolApprovalSecret: experimental_toolApprovalSecret,
1731
1764
  generateId,
1732
1765
  stepNumber: recordedSteps.length,
1733
1766
  model: stepModelInfo,
@@ -2468,7 +2501,7 @@ class DefaultStreamTextResult<
2468
2501
  sendSources = false,
2469
2502
  sendStart = true,
2470
2503
  sendFinish = true,
2471
- onError = getErrorMessage,
2504
+ onError = () => 'An error occurred.', // prevent leaking server error details to the client by default
2472
2505
  }: UIMessageStreamOptions<UI_MESSAGE> = {}): AsyncIterableStream<
2473
2506
  InferUIMessageChunk<UI_MESSAGE>
2474
2507
  > {
@@ -2688,6 +2721,9 @@ class DefaultStreamTextResult<
2688
2721
  type: 'tool-approval-request',
2689
2722
  approvalId: part.approvalId,
2690
2723
  toolCallId: part.toolCall.toolCallId,
2724
+ ...(part.signature != null
2725
+ ? { signature: part.signature }
2726
+ : {}),
2691
2727
  });
2692
2728
  break;
2693
2729
  }
@@ -113,6 +113,7 @@ export async function toResponseMessages<TOOLS extends ToolSet>({
113
113
  type: 'tool-approval-request',
114
114
  approvalId: part.approvalId,
115
115
  toolCallId: part.toolCall.toolCallId,
116
+ ...(part.signature != null ? { signature: part.signature } : {}),
116
117
  });
117
118
  break;
118
119
  }
@@ -18,4 +18,9 @@ export type ToolApprovalRequestOutput<TOOLS extends ToolSet> = {
18
18
  * Tool call that the approval request is for.
19
19
  */
20
20
  toolCall: TypedToolCall<TOOLS>;
21
+
22
+ /**
23
+ * HMAC-SHA256 signature binding this approval request to its tool call.
24
+ */
25
+ signature?: string;
21
26
  };
@@ -0,0 +1,125 @@
1
+ import {
2
+ convertBase64ToUint8Array,
3
+ convertUint8ArrayToBase64,
4
+ } from '@ai-sdk/provider-utils';
5
+
6
+ const encoder = new TextEncoder();
7
+
8
+ function canonicalJSON(value: unknown): string {
9
+ if (value === null || value === undefined) {
10
+ return JSON.stringify(value);
11
+ }
12
+ if (typeof value !== 'object') {
13
+ return JSON.stringify(value);
14
+ }
15
+ if (Array.isArray(value)) {
16
+ return `[${value.map(canonicalJSON).join(',')}]`;
17
+ }
18
+ const keys = Object.keys(value as Record<string, unknown>).sort();
19
+ const entries = keys.map(
20
+ k =>
21
+ `${JSON.stringify(k)}:${canonicalJSON((value as Record<string, unknown>)[k])}`,
22
+ );
23
+ return `{${entries.join(',')}}`;
24
+ }
25
+
26
+ function toBase64url(bytes: Uint8Array): string {
27
+ return convertUint8ArrayToBase64(bytes)
28
+ .replace(/\+/g, '-')
29
+ .replace(/\//g, '_')
30
+ .replace(/=+$/g, '');
31
+ }
32
+
33
+ function fromBase64url(str: string): Uint8Array {
34
+ return convertBase64ToUint8Array(str);
35
+ }
36
+
37
+ async function importKey(secret: string | Uint8Array): Promise<CryptoKey> {
38
+ const keyData = typeof secret === 'string' ? encoder.encode(secret) : secret;
39
+ return crypto.subtle.importKey(
40
+ 'raw',
41
+ keyData,
42
+ { name: 'HMAC', hash: 'SHA-256' },
43
+ false,
44
+ ['sign', 'verify'],
45
+ );
46
+ }
47
+
48
+ async function hashInput(input: unknown): Promise<string> {
49
+ const canonical = canonicalJSON(input);
50
+ const digest = await crypto.subtle.digest(
51
+ 'SHA-256',
52
+ encoder.encode(canonical),
53
+ );
54
+ return toBase64url(new Uint8Array(digest));
55
+ }
56
+
57
+ function buildPayload(
58
+ approvalId: string,
59
+ toolCallId: string,
60
+ toolName: string,
61
+ inputDigest: string,
62
+ ): Uint8Array {
63
+ return encoder.encode(
64
+ `${approvalId}\n${toolCallId}\n${toolName}\n${inputDigest}`,
65
+ );
66
+ }
67
+
68
+ export async function signToolApproval({
69
+ secret,
70
+ approvalId,
71
+ toolCallId,
72
+ toolName,
73
+ input,
74
+ }: {
75
+ secret: string | Uint8Array;
76
+ approvalId: string;
77
+ toolCallId: string;
78
+ toolName: string;
79
+ input: unknown;
80
+ }): Promise<string> {
81
+ const key = await importKey(secret);
82
+ const inputDigest = await hashInput(input);
83
+ const payload = buildPayload(approvalId, toolCallId, toolName, inputDigest);
84
+ const sig = await crypto.subtle.sign('HMAC', key, payload);
85
+ return toBase64url(new Uint8Array(sig));
86
+ }
87
+
88
+ export async function verifyToolApprovalSignature({
89
+ secret,
90
+ signature,
91
+ approvalId,
92
+ toolCallId,
93
+ toolName,
94
+ input,
95
+ }: {
96
+ secret: string | Uint8Array;
97
+ signature: string;
98
+ approvalId: string;
99
+ toolCallId: string;
100
+ toolName: string;
101
+ input: unknown;
102
+ }): Promise<boolean> {
103
+ const key = await importKey(secret);
104
+ const inputDigest = await hashInput(input);
105
+ const payload = buildPayload(approvalId, toolCallId, toolName, inputDigest);
106
+ const sigBytes = fromBase64url(signature);
107
+ return crypto.subtle.verify('HMAC', key, sigBytes, payload);
108
+ }
109
+
110
+ export async function maybeSignApproval({
111
+ secret,
112
+ approvalId,
113
+ toolCallId,
114
+ toolName,
115
+ input,
116
+ }: {
117
+ secret: string | Uint8Array | undefined;
118
+ approvalId: string;
119
+ toolCallId: string;
120
+ toolName: string;
121
+ input: unknown;
122
+ }): Promise<string | undefined> {
123
+ if (secret == null) return undefined;
124
+ return signToolApproval({ secret, approvalId, toolCallId, toolName, input });
125
+ }
@@ -0,0 +1,124 @@
1
+ import {
2
+ asSchema,
3
+ safeValidateTypes,
4
+ type ModelMessage,
5
+ } from '@ai-sdk/provider-utils';
6
+ import { InvalidToolApprovalSignatureError } from '../error/invalid-tool-approval-signature-error';
7
+ import { InvalidToolInputError } from '../error/invalid-tool-input-error';
8
+ import type { CollectedToolApprovals } from './collect-tool-approvals';
9
+ import { isApprovalNeeded } from './is-approval-needed';
10
+ import { verifyToolApprovalSignature } from './tool-approval-signature';
11
+ import type { ToolSet } from './tool-set';
12
+
13
+ /**
14
+ * Re-validates approved tool approvals reconstructed from client-supplied
15
+ * message history before they are executed. Checks the HMAC signature (when
16
+ * `experimental_toolApprovalSecret` is configured), re-validates the tool-call
17
+ * input against the tool's input schema, and re-resolves whether the tool
18
+ * actually requires approval.
19
+ *
20
+ * Approvals that fail signature or schema validation throw (fail-closed).
21
+ * Approvals for tools that no longer require approval are moved to the denied
22
+ * list, since the server would never have issued an approval request for them.
23
+ */
24
+ export async function validateApprovedToolApprovals<TOOLS extends ToolSet>({
25
+ approvedToolApprovals,
26
+ tools,
27
+ messages,
28
+ experimental_context,
29
+ toolApprovalSecret,
30
+ }: {
31
+ approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
32
+ tools: TOOLS | undefined;
33
+ messages: ModelMessage[];
34
+ experimental_context: unknown;
35
+ toolApprovalSecret?: string | Uint8Array;
36
+ }): Promise<{
37
+ approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
38
+ deniedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
39
+ }> {
40
+ const approved: Array<CollectedToolApprovals<TOOLS>> = [];
41
+ const denied: Array<CollectedToolApprovals<TOOLS>> = [];
42
+
43
+ for (const approval of approvedToolApprovals) {
44
+ const { toolCall, approvalRequest } = approval;
45
+ const tool = tools?.[toolCall.toolName];
46
+
47
+ if (toolApprovalSecret != null) {
48
+ if (approvalRequest.signature == null) {
49
+ throw new InvalidToolApprovalSignatureError({
50
+ approvalId: approvalRequest.approvalId,
51
+ toolCallId: toolCall.toolCallId,
52
+ reason: 'missing signature',
53
+ });
54
+ }
55
+
56
+ const valid = await verifyToolApprovalSignature({
57
+ secret: toolApprovalSecret,
58
+ signature: approvalRequest.signature,
59
+ approvalId: approvalRequest.approvalId,
60
+ toolCallId: toolCall.toolCallId,
61
+ toolName: toolCall.toolName,
62
+ input: toolCall.input,
63
+ });
64
+
65
+ if (!valid) {
66
+ throw new InvalidToolApprovalSignatureError({
67
+ approvalId: approvalRequest.approvalId,
68
+ toolCallId: toolCall.toolCallId,
69
+ reason: 'invalid signature',
70
+ });
71
+ }
72
+ }
73
+
74
+ // Re-validate the (client-supplied) input against the tool's input schema
75
+ // for tools that are executed on the server.
76
+ if (
77
+ tool != null &&
78
+ typeof tool.execute === 'function' &&
79
+ tool.inputSchema != null
80
+ ) {
81
+ const validation = await safeValidateTypes({
82
+ value: toolCall.input,
83
+ schema: asSchema(tool.inputSchema),
84
+ });
85
+
86
+ if (!validation.success) {
87
+ throw new InvalidToolInputError({
88
+ toolName: toolCall.toolName,
89
+ toolInput: JSON.stringify(toolCall.input),
90
+ cause: validation.error,
91
+ });
92
+ }
93
+ }
94
+
95
+ // Re-resolve whether the tool requires approval. A tool that does not
96
+ // require approval would never have had an approval request issued by the
97
+ // server, so any approval for it is fabricated and is denied.
98
+ const approvalNeeded =
99
+ tool != null &&
100
+ (await isApprovalNeeded({
101
+ tool,
102
+ toolCall,
103
+ messages,
104
+ experimental_context,
105
+ }));
106
+
107
+ if (approvalNeeded) {
108
+ approved.push(approval);
109
+ } else {
110
+ denied.push({
111
+ ...approval,
112
+ approvalResponse: {
113
+ ...approval.approvalResponse,
114
+ approved: false,
115
+ reason:
116
+ approval.approvalResponse.reason ??
117
+ `Tool "${toolCall.toolName}" does not require approval`,
118
+ },
119
+ });
120
+ }
121
+ }
122
+
123
+ return { approvedToolApprovals: approved, deniedToolApprovals: denied };
124
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  LanguageModelV3StreamPart,
4
4
  } from '@ai-sdk/provider';
5
5
  import type { LanguageModelMiddleware } from '../types/language-model-middleware';
6
+ import { createIdMap } from '../util/create-id-map';
6
7
 
7
8
  /**
8
9
  * Default transform function that strips markdown code fences from text.
@@ -68,7 +69,7 @@ export function extractJsonMiddleware(options?: {
68
69
  buffer: string;
69
70
  prefixStripped: boolean;
70
71
  }
71
- > = {};
72
+ > = createIdMap();
72
73
 
73
74
  const SUFFIX_BUFFER_SIZE = 12;
74
75
 
@@ -3,6 +3,7 @@ import type {
3
3
  LanguageModelV3StreamPart,
4
4
  } from '@ai-sdk/provider';
5
5
  import type { LanguageModelMiddleware } from '../types/language-model-middleware';
6
+ import { createIdMap } from '../util/create-id-map';
6
7
  import { getPotentialStartIndex } from '../util/get-potential-start-index';
7
8
 
8
9
  /**
@@ -92,7 +93,7 @@ export function extractReasoningMiddleware({
92
93
  idCounter: number;
93
94
  textId: string;
94
95
  }
95
- > = {};
96
+ > = createIdMap();
96
97
 
97
98
  let delayedTextStart: LanguageModelV3StreamPart | undefined;
98
99
 
@@ -196,6 +196,9 @@ export async function convertToModelMessages<UI_MESSAGE extends UIMessage>(
196
196
  type: 'tool-approval-request' as const,
197
197
  approvalId: part.approval.id,
198
198
  toolCallId: part.toolCallId,
199
+ ...(part.approval.signature != null
200
+ ? { signature: part.approval.signature }
201
+ : {}),
199
202
  });
200
203
  }
201
204
 
@@ -675,7 +675,12 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
675
675
  case 'tool-approval-request': {
676
676
  const toolInvocation = getToolInvocation(chunk.toolCallId);
677
677
  toolInvocation.state = 'approval-requested';
678
- toolInvocation.approval = { id: chunk.approvalId };
678
+ toolInvocation.approval = {
679
+ id: chunk.approvalId,
680
+ ...(chunk.signature != null
681
+ ? { signature: chunk.signature }
682
+ : {}),
683
+ };
679
684
  write();
680
685
  break;
681
686
  }