ai 6.0.201 → 6.0.202
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 +14 -0
- package/dist/index.d.mts +58 -13
- package/dist/index.d.ts +58 -13
- package/dist/index.js +836 -575
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +674 -408
- package/dist/index.mjs.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.mjs +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 +6 -6
- 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 +41 -6
- 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/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/ui-message-chunks.ts +2 -0
package/dist/internal/index.js
CHANGED
|
@@ -152,7 +152,7 @@ function detectMediaType({
|
|
|
152
152
|
var import_provider_utils2 = require("@ai-sdk/provider-utils");
|
|
153
153
|
|
|
154
154
|
// src/version.ts
|
|
155
|
-
var VERSION = true ? "6.0.
|
|
155
|
+
var VERSION = true ? "6.0.202" : "0.0.0-test";
|
|
156
156
|
|
|
157
157
|
// src/util/download/download.ts
|
|
158
158
|
var download = async ({
|
package/dist/internal/index.mjs
CHANGED
|
@@ -124,6 +124,38 @@ transports by providing an `authProvider`.
|
|
|
124
124
|
|
|
125
125
|
</Note>
|
|
126
126
|
|
|
127
|
+
### OAuth Authorization Server Validation
|
|
128
|
+
|
|
129
|
+
When using MCP OAuth in server-side applications, the MCP server can advertise
|
|
130
|
+
the OAuth authorization server to use. If you connect to MCP servers outside
|
|
131
|
+
your control, implement `validateAuthorizationServerURL` on your
|
|
132
|
+
`authProvider` to allow only the authorization server origins you trust:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const allowedAuthorizationServerOrigins = new Set([
|
|
136
|
+
'https://accounts.example.com',
|
|
137
|
+
'https://tenant.auth0.com',
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const myOAuthClientProvider = {
|
|
141
|
+
// ...other OAuthClientProvider methods
|
|
142
|
+
|
|
143
|
+
validateAuthorizationServerURL(serverUrl, authorizationServerUrl) {
|
|
144
|
+
const origin = new URL(authorizationServerUrl).origin;
|
|
145
|
+
|
|
146
|
+
if (!allowedAuthorizationServerOrigins.has(origin)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Unexpected OAuth authorization server for ${serverUrl}: ${origin}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This hook is called before the SDK fetches authorization server metadata, so a
|
|
156
|
+
rejected URL is not requested. It is optional and does not change existing OAuth
|
|
157
|
+
flows unless you implement it.
|
|
158
|
+
|
|
127
159
|
### Closing the MCP Client
|
|
128
160
|
|
|
129
161
|
After initialization, you should close the MCP client based on your usage pattern:
|
|
@@ -40,6 +40,9 @@ import { existsSync, mkdirSync } from 'fs';
|
|
|
40
40
|
import { writeFile } from 'fs/promises';
|
|
41
41
|
import path from 'path';
|
|
42
42
|
|
|
43
|
+
// Treat chat IDs as opaque tokens before using them in file paths.
|
|
44
|
+
const chatIdRegex = /^[A-Za-z0-9_-]+$/;
|
|
45
|
+
|
|
43
46
|
export async function createChat(): Promise<string> {
|
|
44
47
|
const id = generateId(); // generate a unique chat ID
|
|
45
48
|
await writeFile(getChatFile(id), '[]'); // create an empty chat file
|
|
@@ -47,12 +50,27 @@ export async function createChat(): Promise<string> {
|
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
function getChatFile(id: string): string {
|
|
50
|
-
|
|
53
|
+
if (!chatIdRegex.test(id)) {
|
|
54
|
+
throw new Error('Invalid chat ID');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const chatDir = path.resolve(process.cwd(), '.chats');
|
|
58
|
+
const chatFile = path.resolve(chatDir, `${id}.json`);
|
|
59
|
+
|
|
60
|
+
// Defense in depth: keep the resolved file inside the chat directory.
|
|
61
|
+
if (!chatFile.startsWith(`${chatDir}${path.sep}`)) {
|
|
62
|
+
throw new Error('Invalid chat ID');
|
|
63
|
+
}
|
|
64
|
+
|
|
51
65
|
if (!existsSync(chatDir)) mkdirSync(chatDir, { recursive: true });
|
|
52
|
-
return
|
|
66
|
+
return chatFile;
|
|
53
67
|
}
|
|
54
68
|
```
|
|
55
69
|
|
|
70
|
+
The chat ID can come from the URL or the request body, so validate it as an
|
|
71
|
+
opaque token before using it in a file path. The resolved path check ensures the
|
|
72
|
+
file stays inside the intended `.chats` directory.
|
|
73
|
+
|
|
56
74
|
## Loading an existing chat
|
|
57
75
|
|
|
58
76
|
When the user navigates to the chat page with a chat ID, we need to load the chat messages from storage.
|
|
@@ -106,7 +106,7 @@ It currently does not support accepting notifications from an MCP server, and cu
|
|
|
106
106
|
type: 'OAuthClientProvider',
|
|
107
107
|
isOptional: true,
|
|
108
108
|
description:
|
|
109
|
-
'Optional OAuth provider for authorization to access protected remote MCP servers.',
|
|
109
|
+
'Optional OAuth provider for authorization to access protected remote MCP servers. Implement `validateAuthorizationServerURL` on the provider to allowlist discovered OAuth authorization server URLs before metadata is fetched.',
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
name: 'redirect',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: AI_InvalidToolApprovalSignatureError
|
|
3
|
+
description: Learn how to fix AI_InvalidToolApprovalSignatureError
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI_InvalidToolApprovalSignatureError
|
|
7
|
+
|
|
8
|
+
This error occurs when `experimental_toolApprovalSecret` is configured and a tool approval replayed from the message history has a missing or invalid HMAC signature. The server rejects the approval before the tool executes.
|
|
9
|
+
|
|
10
|
+
Common causes:
|
|
11
|
+
|
|
12
|
+
- The approval was fabricated by the client (no signature present)
|
|
13
|
+
- The tool arguments were modified after the approval was signed
|
|
14
|
+
- The server secret changed between issuance and verification (e.g. after key rotation without overlap)
|
|
15
|
+
|
|
16
|
+
## Properties
|
|
17
|
+
|
|
18
|
+
- `approvalId`: The approval ID that failed verification
|
|
19
|
+
- `toolCallId`: The tool call ID the approval was for
|
|
20
|
+
|
|
21
|
+
## Checking for this Error
|
|
22
|
+
|
|
23
|
+
You can check if an error is an instance of `AI_InvalidToolApprovalSignatureError` using:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { InvalidToolApprovalSignatureError } from 'ai';
|
|
27
|
+
|
|
28
|
+
if (InvalidToolApprovalSignatureError.isInstance(error)) {
|
|
29
|
+
// Handle the error
|
|
30
|
+
}
|
|
31
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.202",
|
|
4
4
|
"description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@opentelemetry/api": "^1.9.0",
|
|
48
|
-
"@ai-sdk/gateway": "3.0.
|
|
49
|
-
"@ai-sdk/provider": "
|
|
50
|
-
"@ai-sdk/provider
|
|
48
|
+
"@ai-sdk/gateway": "3.0.128",
|
|
49
|
+
"@ai-sdk/provider-utils": "4.0.28",
|
|
50
|
+
"@ai-sdk/provider": "3.0.10"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"tsx": "^4.19.2",
|
|
59
59
|
"typescript": "5.8.3",
|
|
60
60
|
"zod": "3.25.76",
|
|
61
|
-
"@ai-
|
|
62
|
-
"@
|
|
61
|
+
"@vercel/ai-tsconfig": "0.0.0",
|
|
62
|
+
"@ai-sdk/test-server": "1.0.5"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"zod": "^3.25.76 || ^4.1.8"
|
package/src/error/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
export { InvalidArgumentError } from './invalid-argument-error';
|
|
18
18
|
export { InvalidStreamPartError } from './invalid-stream-part-error';
|
|
19
19
|
export { InvalidToolApprovalError } from './invalid-tool-approval-error';
|
|
20
|
+
export { InvalidToolApprovalSignatureError } from './invalid-tool-approval-signature-error';
|
|
20
21
|
export { InvalidToolInputError } from './invalid-tool-input-error';
|
|
21
22
|
export { ToolCallNotFoundForApprovalError } from './tool-call-not-found-for-approval-error';
|
|
22
23
|
export { MissingToolResultsError } from './missing-tool-result-error';
|
|
@@ -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
|
}
|
|
@@ -82,6 +82,7 @@ import { mergeObjects } from '../util/merge-objects';
|
|
|
82
82
|
import { now as originalNow } from '../util/now';
|
|
83
83
|
import { prepareRetries } from '../util/prepare-retries';
|
|
84
84
|
import { collectToolApprovals } from './collect-tool-approvals';
|
|
85
|
+
import { validateApprovedToolApprovals } from './validate-tool-approvals';
|
|
85
86
|
import type {
|
|
86
87
|
OnFinishEvent,
|
|
87
88
|
OnStartEvent,
|
|
@@ -359,6 +360,7 @@ export function streamText<
|
|
|
359
360
|
experimental_onToolCallStart: onToolCallStart,
|
|
360
361
|
experimental_onToolCallFinish: onToolCallFinish,
|
|
361
362
|
experimental_context,
|
|
363
|
+
experimental_toolApprovalSecret,
|
|
362
364
|
experimental_include: include,
|
|
363
365
|
_internal: { now = originalNow, generateId = originalGenerateId } = {},
|
|
364
366
|
...settings
|
|
@@ -532,6 +534,15 @@ export function streamText<
|
|
|
532
534
|
*/
|
|
533
535
|
experimental_context?: unknown;
|
|
534
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Secret for HMAC-signing tool approval requests. When set, the server
|
|
539
|
+
* signs each approval request at issuance and verifies the signature when
|
|
540
|
+
* the approval is replayed, preventing client-forged approvals.
|
|
541
|
+
*
|
|
542
|
+
* Experimental (can break in patch releases).
|
|
543
|
+
*/
|
|
544
|
+
experimental_toolApprovalSecret?: string | Uint8Array;
|
|
545
|
+
|
|
535
546
|
/**
|
|
536
547
|
* Settings for controlling what data is included in step results.
|
|
537
548
|
* Disabling inclusion can help reduce memory usage when processing
|
|
@@ -608,6 +619,7 @@ export function streamText<
|
|
|
608
619
|
now,
|
|
609
620
|
generateId,
|
|
610
621
|
experimental_context,
|
|
622
|
+
experimental_toolApprovalSecret,
|
|
611
623
|
download,
|
|
612
624
|
include,
|
|
613
625
|
});
|
|
@@ -792,6 +804,7 @@ class DefaultStreamTextResult<
|
|
|
792
804
|
onToolCallStart,
|
|
793
805
|
onToolCallFinish,
|
|
794
806
|
experimental_context,
|
|
807
|
+
experimental_toolApprovalSecret,
|
|
795
808
|
download,
|
|
796
809
|
include,
|
|
797
810
|
}: {
|
|
@@ -828,6 +841,7 @@ class DefaultStreamTextResult<
|
|
|
828
841
|
| undefined;
|
|
829
842
|
originalAbortSignal: AbortSignal | undefined;
|
|
830
843
|
experimental_context: unknown;
|
|
844
|
+
experimental_toolApprovalSecret: string | Uint8Array | undefined;
|
|
831
845
|
download: DownloadFunction | undefined;
|
|
832
846
|
include: { requestBody?: boolean } | undefined;
|
|
833
847
|
|
|
@@ -1402,12 +1416,29 @@ class DefaultStreamTextResult<
|
|
|
1402
1416
|
deniedToolApprovals.length > 0 ||
|
|
1403
1417
|
approvedToolApprovals.length > 0
|
|
1404
1418
|
) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
)
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1419
|
+
// Re-validate approvals reconstructed from the client-supplied
|
|
1420
|
+
// message history before executing them: verify the HMAC signature
|
|
1421
|
+
// (when a secret is configured), re-validate the input against the
|
|
1422
|
+
// tool's schema, and re-resolve whether the tool requires approval.
|
|
1423
|
+
const {
|
|
1424
|
+
approvedToolApprovals: localApprovedToolApprovals,
|
|
1425
|
+
deniedToolApprovals: revalidationDeniedToolApprovals,
|
|
1426
|
+
} = await validateApprovedToolApprovals<TOOLS>({
|
|
1427
|
+
approvedToolApprovals: approvedToolApprovals.filter(
|
|
1428
|
+
toolApproval => !toolApproval.toolCall.providerExecuted,
|
|
1429
|
+
),
|
|
1430
|
+
tools,
|
|
1431
|
+
messages: initialMessages,
|
|
1432
|
+
experimental_context,
|
|
1433
|
+
toolApprovalSecret: experimental_toolApprovalSecret,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
const localDeniedToolApprovals = [
|
|
1437
|
+
...deniedToolApprovals.filter(
|
|
1438
|
+
toolApproval => !toolApproval.toolCall.providerExecuted,
|
|
1439
|
+
),
|
|
1440
|
+
...revalidationDeniedToolApprovals,
|
|
1441
|
+
];
|
|
1411
1442
|
|
|
1412
1443
|
const deniedProviderExecutedToolApprovals =
|
|
1413
1444
|
deniedToolApprovals.filter(
|
|
@@ -1728,6 +1759,7 @@ class DefaultStreamTextResult<
|
|
|
1728
1759
|
repairToolCall,
|
|
1729
1760
|
abortSignal,
|
|
1730
1761
|
experimental_context,
|
|
1762
|
+
toolApprovalSecret: experimental_toolApprovalSecret,
|
|
1731
1763
|
generateId,
|
|
1732
1764
|
stepNumber: recordedSteps.length,
|
|
1733
1765
|
model: stepModelInfo,
|
|
@@ -2688,6 +2720,9 @@ class DefaultStreamTextResult<
|
|
|
2688
2720
|
type: 'tool-approval-request',
|
|
2689
2721
|
approvalId: part.approvalId,
|
|
2690
2722
|
toolCallId: part.toolCall.toolCallId,
|
|
2723
|
+
...(part.signature != null
|
|
2724
|
+
? { signature: part.signature }
|
|
2725
|
+
: {}),
|
|
2691
2726
|
});
|
|
2692
2727
|
break;
|
|
2693
2728
|
}
|
|
@@ -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
|
+
}
|