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.
- package/CHANGELOG.md +41 -0
- package/dist/index.d.mts +61 -15
- package/dist/index.d.ts +61 -15
- package/dist/index.js +853 -592
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +695 -430
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +10 -12
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/index.mjs +11 -13
- package/dist/internal/index.mjs.map +1 -1
- package/docs/03-ai-sdk-core/16-mcp-tools.mdx +32 -0
- package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +20 -2
- package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +1 -1
- package/docs/07-reference/05-ai-sdk-errors/ai-invalid-tool-approval-signature-error.mdx +31 -0
- package/package.json +3 -3
- package/src/error/index.ts +1 -0
- package/src/error/invalid-tool-approval-signature-error.ts +35 -0
- package/src/generate-text/generate-text.ts +48 -6
- package/src/generate-text/run-tools-transformation.ts +14 -1
- package/src/generate-text/stream-text.ts +47 -11
- package/src/generate-text/to-response-messages.ts +1 -0
- package/src/generate-text/tool-approval-request-output.ts +5 -0
- package/src/generate-text/tool-approval-signature.ts +125 -0
- package/src/generate-text/validate-tool-approvals.ts +124 -0
- package/src/middleware/extract-json-middleware.ts +2 -1
- package/src/middleware/extract-reasoning-middleware.ts +2 -1
- package/src/ui/convert-to-model-messages.ts +3 -0
- package/src/ui/process-ui-message-stream.ts +6 -1
- package/src/ui/ui-messages.ts +10 -0
- package/src/ui/validate-ui-messages.ts +10 -0
- package/src/ui-message-stream/create-ui-message-stream.ts +2 -3
- package/src/ui-message-stream/ui-message-chunks.ts +2 -0
- 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 {
|
|
552
|
-
|
|
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
|
|
555
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1406
|
-
|
|
1407
|
-
)
|
|
1408
|
-
|
|
1409
|
-
|
|
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 =
|
|
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 = {
|
|
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
|
}
|