@ai-sdk/code-mode 1.0.5 → 1.0.7
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/README.md +8 -5
- package/dist/approval-continuation.js +84 -0
- package/dist/approval-continuation.js.map +1 -0
- package/dist/approval.js +32 -0
- package/dist/approval.js.map +1 -0
- package/dist/continuation-capability.js +125 -0
- package/dist/continuation-capability.js.map +1 -0
- package/dist/direct-tool-call.js +5 -0
- package/dist/direct-tool-call.js.map +1 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -1
- package/dist/host-interrupt.js +28 -0
- package/dist/host-interrupt.js.map +1 -0
- package/dist/index.d.ts +152 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/interrupt-continuation.js +115 -0
- package/dist/interrupt-continuation.js.map +1 -0
- package/dist/runtime/guest-sources.js +91 -1
- package/dist/runtime/guest-sources.js.map +1 -1
- package/dist/runtime/manager.js +388 -8
- package/dist/runtime/manager.js.map +1 -1
- package/dist/runtime/worker-source.js +1 -1
- package/dist/tool-invocation.js +93 -16
- package/dist/tool-invocation.js.map +1 -1
- package/package.json +3 -3
- package/src/approval-continuation.ts +115 -0
- package/src/approval.ts +60 -0
- package/src/continuation-capability.ts +201 -0
- package/src/direct-tool-call.ts +6 -0
- package/src/errors.ts +65 -0
- package/src/host-interrupt.ts +49 -0
- package/src/index.ts +28 -0
- package/src/interrupt-continuation.ts +173 -0
- package/src/runtime/guest-sources.ts +92 -1
- package/src/runtime/manager.ts +473 -8
- package/src/runtime/protocol.ts +24 -2
- package/src/runtime/worker.ts +116 -6
- package/src/tool-invocation.ts +119 -18
- package/src/types.ts +139 -0
package/dist/tool-invocation.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { asSchema } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { CODE_MODE_TOOL_APPROVAL_KIND } from "./approval.js";
|
|
3
|
+
import {
|
|
4
|
+
CodeModeProtocolError,
|
|
5
|
+
CodeModeToolApprovalDeniedError,
|
|
6
|
+
CodeModeToolApprovalRequiredError,
|
|
7
|
+
CodeModeToolError
|
|
8
|
+
} from "./errors.js";
|
|
9
|
+
import { isCodeModeHostInterruptSignal } from "./host-interrupt.js";
|
|
3
10
|
import {
|
|
4
11
|
assertJsonSerializable,
|
|
5
12
|
toJsonPayload
|
|
@@ -9,9 +16,12 @@ async function invokeHostTool({
|
|
|
9
16
|
inputJson,
|
|
10
17
|
tools,
|
|
11
18
|
baseExecutionOptions,
|
|
19
|
+
codeModeOptions,
|
|
12
20
|
maxToolInputBytes,
|
|
13
21
|
maxToolOutputBytes,
|
|
14
|
-
toolCallId
|
|
22
|
+
toolCallId,
|
|
23
|
+
codeModeInterrupt,
|
|
24
|
+
skipApproval = false
|
|
15
25
|
}) {
|
|
16
26
|
throwIfAborted(baseExecutionOptions.abortSignal);
|
|
17
27
|
const hostTool = tools[toolName];
|
|
@@ -40,25 +50,92 @@ async function invokeHostTool({
|
|
|
40
50
|
}
|
|
41
51
|
const executionOptions = {
|
|
42
52
|
...baseExecutionOptions,
|
|
43
|
-
toolCallId
|
|
53
|
+
toolCallId,
|
|
54
|
+
...codeModeInterrupt !== void 0 ? { codeModeInterrupt } : {}
|
|
44
55
|
};
|
|
45
|
-
|
|
56
|
+
const needsApproval = !skipApproval && await raceAgainstAbort(
|
|
46
57
|
requiresApproval(hostTool, validation.value, executionOptions),
|
|
47
58
|
executionOptions.abortSignal
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
59
|
+
);
|
|
60
|
+
if (needsApproval) {
|
|
61
|
+
if (codeModeOptions.approval?.mode === "interrupt") {
|
|
62
|
+
return {
|
|
63
|
+
type: "interrupted",
|
|
64
|
+
toolName,
|
|
65
|
+
input: validation.value,
|
|
66
|
+
toolCallId,
|
|
67
|
+
payload: { kind: CODE_MODE_TOOL_APPROVAL_KIND }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const approval = await raceAgainstAbort(
|
|
71
|
+
Promise.resolve(
|
|
72
|
+
codeModeOptions.approval?.onApprovalRequired?.({
|
|
73
|
+
toolName,
|
|
74
|
+
input: validation.value,
|
|
75
|
+
toolCallId
|
|
76
|
+
})
|
|
77
|
+
),
|
|
78
|
+
baseExecutionOptions.abortSignal
|
|
52
79
|
);
|
|
80
|
+
if (approval === void 0) {
|
|
81
|
+
throw new CodeModeToolApprovalRequiredError(
|
|
82
|
+
toolName,
|
|
83
|
+
validation.value,
|
|
84
|
+
toolCallId
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const approved = typeof approval === "string" ? approval === "approved" : approval?.approved;
|
|
88
|
+
const reason = typeof approval === "string" ? void 0 : approval?.reason;
|
|
89
|
+
if (typeof approved !== "boolean") {
|
|
90
|
+
throw new CodeModeProtocolError(
|
|
91
|
+
`Tool "${toolName}" approval callback returned a malformed approval decision.`,
|
|
92
|
+
{ toolName, toolCallId }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (reason !== void 0 && typeof reason !== "string") {
|
|
96
|
+
throw new CodeModeProtocolError(
|
|
97
|
+
`Tool "${toolName}" approval callback returned a malformed approval reason.`,
|
|
98
|
+
{ toolName, toolCallId }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (!approved) {
|
|
102
|
+
throw new CodeModeToolApprovalDeniedError(
|
|
103
|
+
toolName,
|
|
104
|
+
validation.value,
|
|
105
|
+
toolCallId,
|
|
106
|
+
reason
|
|
107
|
+
);
|
|
108
|
+
}
|
|
53
109
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
110
|
+
let output;
|
|
111
|
+
try {
|
|
112
|
+
output = await raceAgainstAbort(
|
|
113
|
+
executeHostTool(hostTool.execute.bind(hostTool), {
|
|
114
|
+
input: validation.value,
|
|
115
|
+
options: executionOptions
|
|
116
|
+
}),
|
|
117
|
+
executionOptions.abortSignal
|
|
118
|
+
);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (isCodeModeHostInterruptSignal(error)) {
|
|
121
|
+
return {
|
|
122
|
+
type: "interrupted",
|
|
123
|
+
toolName,
|
|
124
|
+
input: validation.value,
|
|
125
|
+
toolCallId,
|
|
126
|
+
payload: error.payload
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
type: "success",
|
|
133
|
+
valueJson: toJsonPayload(
|
|
134
|
+
output,
|
|
135
|
+
maxToolOutputBytes,
|
|
136
|
+
`Tool "${toolName}" output`
|
|
137
|
+
)
|
|
138
|
+
};
|
|
62
139
|
}
|
|
63
140
|
async function requiresApproval(hostTool, input, options) {
|
|
64
141
|
if (hostTool.needsApproval == null) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/tool-invocation.ts"],"sourcesContent":["import { asSchema } from 'ai';\nimport { CodeModeToolError } from './errors.js';\nimport type { CodeModeToolExecutionOptions, CodeModeToolSet } from './types.js';\nimport {\n assertJsonSerializable,\n toJsonPayload,\n} from './utils/serialization.js';\n\nexport async function invokeHostTool({\n toolName,\n inputJson,\n tools,\n baseExecutionOptions,\n maxToolInputBytes,\n maxToolOutputBytes,\n toolCallId,\n}: {\n toolName: string;\n inputJson: string;\n tools: CodeModeToolSet;\n baseExecutionOptions: CodeModeToolExecutionOptions;\n maxToolInputBytes: number;\n maxToolOutputBytes: number;\n toolCallId: string;\n}): Promise<string> {\n throwIfAborted(baseExecutionOptions.abortSignal);\n\n const hostTool = tools[toolName];\n if (!hostTool) {\n throw new CodeModeToolError(`Unknown tool: ${toolName}`, {\n toolName,\n availableTools: Object.keys(tools),\n });\n }\n if (hostTool.execute == null) {\n throw new CodeModeToolError(`Tool \"${toolName}\" does not have execute().`, {\n toolName,\n });\n }\n\n const input = inputJson === '' ? undefined : JSON.parse(inputJson);\n assertJsonSerializable(input, maxToolInputBytes, `Tool \"${toolName}\" input`);\n\n const validation = await raceAgainstAbort(\n validateToolInput(hostTool.inputSchema, input),\n baseExecutionOptions.abortSignal,\n );\n if (!validation.success) {\n throw new CodeModeToolError(\n `Invalid input for tool \"${toolName}\": ${validation.error.message}`,\n { toolName, input, cause: validation.error.message },\n );\n }\n\n const executionOptions: CodeModeToolExecutionOptions = {\n ...baseExecutionOptions,\n toolCallId,\n };\n\n if (\n await raceAgainstAbort(\n requiresApproval(hostTool, validation.value, executionOptions),\n executionOptions.abortSignal,\n )\n ) {\n throw new CodeModeToolError(\n `Tool \"${toolName}\" requires approval, which code mode does not support yet.`,\n { toolName, input: validation.value, toolCallId },\n );\n }\n\n const output = await raceAgainstAbort(\n executeHostTool(hostTool.execute.bind(hostTool), {\n input: validation.value,\n options: executionOptions,\n }),\n executionOptions.abortSignal,\n );\n return toJsonPayload(output, maxToolOutputBytes, `Tool \"${toolName}\" output`);\n}\n\nasync function requiresApproval(\n hostTool: CodeModeToolSet[string],\n input: unknown,\n options: CodeModeToolExecutionOptions,\n): Promise<boolean> {\n if (hostTool.needsApproval == null) {\n return false;\n }\n if (typeof hostTool.needsApproval === 'boolean') {\n return hostTool.needsApproval;\n }\n return await hostTool.needsApproval(input as never, options as never);\n}\n\nasync function validateToolInput(\n schema: CodeModeToolSet[string]['inputSchema'],\n input: unknown,\n): Promise<\n { success: true; value: unknown } | { success: false; error: Error }\n> {\n const normalizedSchema = asSchema(schema);\n if (normalizedSchema.validate === undefined) {\n return { success: true, value: input };\n }\n return await normalizedSchema.validate(input);\n}\n\nasync function executeHostTool(\n execute: NonNullable<CodeModeToolSet[string]['execute']>,\n {\n input,\n options,\n }: {\n input: unknown;\n options: CodeModeToolExecutionOptions;\n },\n): Promise<unknown> {\n const output = execute(input as never, options as never);\n if (isAsyncIterable(output)) {\n let finalOutput: unknown;\n for await (const part of output) {\n finalOutput = part;\n }\n return finalOutput;\n }\n return await output;\n}\n\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n Symbol.asyncIterator in value &&\n typeof (value as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === 'function'\n );\n}\n\nasync function raceAgainstAbort<T>(\n operation: Promise<T>,\n abortSignal: AbortSignal | undefined,\n): Promise<T> {\n if (abortSignal === undefined) {\n return await operation;\n }\n throwIfAborted(abortSignal);\n\n let rejectOnAbort!: (reason?: unknown) => void;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectOnAbort = reject;\n });\n const onAbort = () => {\n rejectOnAbort(abortReason(abortSignal));\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n if (abortSignal.aborted) {\n onAbort();\n }\n\n try {\n return await Promise.race([operation, aborted]);\n } finally {\n abortSignal.removeEventListener('abort', onAbort);\n }\n}\n\nfunction throwIfAborted(abortSignal: AbortSignal | undefined): void {\n if (abortSignal?.aborted) {\n throw abortReason(abortSignal);\n }\n}\n\nfunction abortReason(abortSignal: AbortSignal): unknown {\n return (\n abortSignal.reason ??\n new DOMException('The operation was aborted.', 'AbortError')\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/tool-invocation.ts"],"sourcesContent":["import { asSchema } from 'ai';\nimport { CODE_MODE_TOOL_APPROVAL_KIND } from './approval.js';\nimport {\n CodeModeProtocolError,\n CodeModeToolApprovalDeniedError,\n CodeModeToolApprovalRequiredError,\n CodeModeToolError,\n} from './errors.js';\nimport { isCodeModeHostInterruptSignal } from './host-interrupt.js';\nimport type {\n CodeModeInterruptExecutionContext,\n CodeModeInterruptPayload,\n CodeModeOptions,\n CodeModeToolExecutionOptions,\n CodeModeToolSet,\n} from './types.js';\nimport {\n assertJsonSerializable,\n toJsonPayload,\n} from './utils/serialization.js';\n\nexport type HostToolInvocationResult =\n | { type: 'success'; valueJson: string }\n | {\n type: 'interrupted';\n toolName: string;\n input: unknown;\n toolCallId: string;\n payload: CodeModeInterruptPayload;\n };\n\nexport async function invokeHostTool({\n toolName,\n inputJson,\n tools,\n baseExecutionOptions,\n codeModeOptions,\n maxToolInputBytes,\n maxToolOutputBytes,\n toolCallId,\n codeModeInterrupt,\n skipApproval = false,\n}: {\n toolName: string;\n inputJson: string;\n tools: CodeModeToolSet;\n baseExecutionOptions: CodeModeToolExecutionOptions;\n codeModeOptions: CodeModeOptions;\n maxToolInputBytes: number;\n maxToolOutputBytes: number;\n toolCallId: string;\n codeModeInterrupt?: CodeModeInterruptExecutionContext;\n skipApproval?: boolean;\n}): Promise<HostToolInvocationResult> {\n throwIfAborted(baseExecutionOptions.abortSignal);\n\n const hostTool = tools[toolName];\n if (!hostTool) {\n throw new CodeModeToolError(`Unknown tool: ${toolName}`, {\n toolName,\n availableTools: Object.keys(tools),\n });\n }\n if (hostTool.execute == null) {\n throw new CodeModeToolError(`Tool \"${toolName}\" does not have execute().`, {\n toolName,\n });\n }\n\n const input = inputJson === '' ? undefined : JSON.parse(inputJson);\n assertJsonSerializable(input, maxToolInputBytes, `Tool \"${toolName}\" input`);\n\n const validation = await raceAgainstAbort(\n validateToolInput(hostTool.inputSchema, input),\n baseExecutionOptions.abortSignal,\n );\n if (!validation.success) {\n throw new CodeModeToolError(\n `Invalid input for tool \"${toolName}\": ${validation.error.message}`,\n { toolName, input, cause: validation.error.message },\n );\n }\n\n const executionOptions: CodeModeToolExecutionOptions = {\n ...baseExecutionOptions,\n toolCallId,\n ...(codeModeInterrupt !== undefined ? { codeModeInterrupt } : {}),\n };\n\n const needsApproval =\n !skipApproval &&\n (await raceAgainstAbort(\n requiresApproval(hostTool, validation.value, executionOptions),\n executionOptions.abortSignal,\n ));\n\n if (needsApproval) {\n if (codeModeOptions.approval?.mode === 'interrupt') {\n return {\n type: 'interrupted',\n toolName,\n input: validation.value,\n toolCallId,\n payload: { kind: CODE_MODE_TOOL_APPROVAL_KIND },\n };\n }\n\n const approval = await raceAgainstAbort(\n Promise.resolve(\n codeModeOptions.approval?.onApprovalRequired?.({\n toolName,\n input: validation.value,\n toolCallId,\n }),\n ),\n baseExecutionOptions.abortSignal,\n );\n if (approval === undefined) {\n throw new CodeModeToolApprovalRequiredError(\n toolName,\n validation.value,\n toolCallId,\n );\n }\n const approved =\n typeof approval === 'string'\n ? approval === 'approved'\n : approval?.approved;\n const reason = typeof approval === 'string' ? undefined : approval?.reason;\n if (typeof approved !== 'boolean') {\n throw new CodeModeProtocolError(\n `Tool \"${toolName}\" approval callback returned a malformed approval decision.`,\n { toolName, toolCallId },\n );\n }\n if (reason !== undefined && typeof reason !== 'string') {\n throw new CodeModeProtocolError(\n `Tool \"${toolName}\" approval callback returned a malformed approval reason.`,\n { toolName, toolCallId },\n );\n }\n if (!approved) {\n throw new CodeModeToolApprovalDeniedError(\n toolName,\n validation.value,\n toolCallId,\n reason,\n );\n }\n }\n\n let output: unknown;\n try {\n output = await raceAgainstAbort(\n executeHostTool(hostTool.execute.bind(hostTool), {\n input: validation.value,\n options: executionOptions,\n }),\n executionOptions.abortSignal,\n );\n } catch (error) {\n if (isCodeModeHostInterruptSignal(error)) {\n return {\n type: 'interrupted',\n toolName,\n input: validation.value,\n toolCallId,\n payload: error.payload,\n };\n }\n throw error;\n }\n return {\n type: 'success',\n valueJson: toJsonPayload(\n output,\n maxToolOutputBytes,\n `Tool \"${toolName}\" output`,\n ),\n };\n}\n\nasync function requiresApproval(\n hostTool: CodeModeToolSet[string],\n input: unknown,\n options: CodeModeToolExecutionOptions,\n): Promise<boolean> {\n if (hostTool.needsApproval == null) {\n return false;\n }\n if (typeof hostTool.needsApproval === 'boolean') {\n return hostTool.needsApproval;\n }\n return await hostTool.needsApproval(input as never, options as never);\n}\n\nasync function validateToolInput(\n schema: CodeModeToolSet[string]['inputSchema'],\n input: unknown,\n): Promise<\n { success: true; value: unknown } | { success: false; error: Error }\n> {\n const normalizedSchema = asSchema(schema);\n if (normalizedSchema.validate === undefined) {\n return { success: true, value: input };\n }\n return await normalizedSchema.validate(input);\n}\n\nasync function executeHostTool(\n execute: NonNullable<CodeModeToolSet[string]['execute']>,\n {\n input,\n options,\n }: {\n input: unknown;\n options: CodeModeToolExecutionOptions;\n },\n): Promise<unknown> {\n const output = execute(input as never, options as never);\n if (isAsyncIterable(output)) {\n let finalOutput: unknown;\n for await (const part of output) {\n finalOutput = part;\n }\n return finalOutput;\n }\n return await output;\n}\n\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n Symbol.asyncIterator in value &&\n typeof (value as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === 'function'\n );\n}\n\nasync function raceAgainstAbort<T>(\n operation: Promise<T>,\n abortSignal: AbortSignal | undefined,\n): Promise<T> {\n if (abortSignal === undefined) {\n return await operation;\n }\n throwIfAborted(abortSignal);\n\n let rejectOnAbort!: (reason?: unknown) => void;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectOnAbort = reject;\n });\n const onAbort = () => {\n rejectOnAbort(abortReason(abortSignal));\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n if (abortSignal.aborted) {\n onAbort();\n }\n\n try {\n return await Promise.race([operation, aborted]);\n } finally {\n abortSignal.removeEventListener('abort', onAbort);\n }\n}\n\nfunction throwIfAborted(abortSignal: AbortSignal | undefined): void {\n if (abortSignal?.aborted) {\n throw abortReason(abortSignal);\n }\n}\n\nfunction abortReason(abortSignal: AbortSignal): unknown {\n return (\n abortSignal.reason ??\n new DOMException('The operation was aborted.', 'AbortError')\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/code-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "QuickJS-backed code mode tool for AI SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"ai": "7.0.
|
|
40
|
+
"ai": "7.0.50"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "22.19.19",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"typescript": "5.8.3",
|
|
48
48
|
"zod": "3.25.76",
|
|
49
49
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
50
|
-
"ai": "7.0.
|
|
50
|
+
"ai": "7.0.50"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=22"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ModelMessage } from 'ai';
|
|
2
|
+
import {
|
|
3
|
+
assertCodeModeApprovalResponse,
|
|
4
|
+
CODE_MODE_TOOL_APPROVAL_KIND,
|
|
5
|
+
} from './approval.js';
|
|
6
|
+
import { CodeModeProtocolError } from './errors.js';
|
|
7
|
+
import {
|
|
8
|
+
continueCodeModeInterrupt,
|
|
9
|
+
isCodeModeInterrupt,
|
|
10
|
+
} from './interrupt-continuation.js';
|
|
11
|
+
import type {
|
|
12
|
+
CodeModeApprovalInterrupt,
|
|
13
|
+
CodeModeApprovalResponse,
|
|
14
|
+
CodeModeContinuationSecurityOptions,
|
|
15
|
+
CodeModeOptions,
|
|
16
|
+
CodeModeToolExecutionOptions,
|
|
17
|
+
CodeModeToolSet,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
|
|
20
|
+
export function isCodeModeApprovalInterrupt(
|
|
21
|
+
value: unknown,
|
|
22
|
+
continuationSecurity: CodeModeContinuationSecurityOptions = {},
|
|
23
|
+
): value is CodeModeApprovalInterrupt {
|
|
24
|
+
return (
|
|
25
|
+
isCodeModeInterrupt(value, continuationSecurity) &&
|
|
26
|
+
value.payload.kind === CODE_MODE_TOOL_APPROVAL_KIND
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function continueCodeModeApproval({
|
|
31
|
+
interrupt,
|
|
32
|
+
approvalResponse,
|
|
33
|
+
tools,
|
|
34
|
+
options = {},
|
|
35
|
+
toolExecutionOptions,
|
|
36
|
+
}: {
|
|
37
|
+
interrupt: CodeModeApprovalInterrupt;
|
|
38
|
+
approvalResponse: CodeModeApprovalResponse;
|
|
39
|
+
tools: CodeModeToolSet;
|
|
40
|
+
options?: CodeModeOptions;
|
|
41
|
+
toolExecutionOptions?: Partial<CodeModeToolExecutionOptions>;
|
|
42
|
+
}): Promise<unknown> {
|
|
43
|
+
assertCodeModeApprovalResponse(approvalResponse);
|
|
44
|
+
if (approvalResponse.approvalId !== interrupt.interruptId) {
|
|
45
|
+
throw new CodeModeProtocolError(
|
|
46
|
+
`Approval response ${approvalResponse.approvalId} does not match pending code-mode approval ${interrupt.interruptId}.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return await continueCodeModeInterrupt({
|
|
51
|
+
interrupt,
|
|
52
|
+
resolution: {
|
|
53
|
+
approved: approvalResponse.approved,
|
|
54
|
+
...(approvalResponse.reason !== undefined
|
|
55
|
+
? { reason: approvalResponse.reason }
|
|
56
|
+
: {}),
|
|
57
|
+
},
|
|
58
|
+
tools,
|
|
59
|
+
options: {
|
|
60
|
+
...options,
|
|
61
|
+
approval: { ...options.approval, mode: 'interrupt' },
|
|
62
|
+
},
|
|
63
|
+
...(toolExecutionOptions !== undefined ? { toolExecutionOptions } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toCodeModeApprovalMessages(
|
|
68
|
+
interrupt: CodeModeApprovalInterrupt,
|
|
69
|
+
): ModelMessage[] {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
role: 'assistant',
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'tool-call',
|
|
76
|
+
toolCallId: interrupt.toolCallId,
|
|
77
|
+
toolName: interrupt.toolName,
|
|
78
|
+
input: interrupt.input,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'tool-approval-request',
|
|
82
|
+
approvalId: interrupt.interruptId,
|
|
83
|
+
toolCallId: interrupt.toolCallId,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getCodeModeApprovalResponse(
|
|
91
|
+
messages: ModelMessage[],
|
|
92
|
+
interrupt: CodeModeApprovalInterrupt,
|
|
93
|
+
): CodeModeApprovalResponse | undefined {
|
|
94
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
95
|
+
const message = messages[index];
|
|
96
|
+
if (message?.role !== 'tool') {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const part of message.content) {
|
|
100
|
+
if (
|
|
101
|
+
part.type === 'tool-approval-response' &&
|
|
102
|
+
part.approvalId === interrupt.interruptId &&
|
|
103
|
+
typeof part.approved === 'boolean' &&
|
|
104
|
+
(part.reason === undefined || typeof part.reason === 'string')
|
|
105
|
+
) {
|
|
106
|
+
return {
|
|
107
|
+
approvalId: part.approvalId,
|
|
108
|
+
approved: part.approved,
|
|
109
|
+
...(part.reason !== undefined ? { reason: part.reason } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
package/src/approval.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CodeModeProtocolError } from './errors.js';
|
|
2
|
+
import type {
|
|
3
|
+
CodeModeApprovalInterruptPayload,
|
|
4
|
+
CodeModeApprovalResolution,
|
|
5
|
+
CodeModeApprovalResponse,
|
|
6
|
+
CodeModeInterruptPayload,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
export const CODE_MODE_TOOL_APPROVAL_KIND =
|
|
10
|
+
'ai-sdk-code-mode/tool-approval' as const;
|
|
11
|
+
|
|
12
|
+
export function isCodeModeApprovalInterruptPayload(
|
|
13
|
+
payload: CodeModeInterruptPayload,
|
|
14
|
+
): payload is CodeModeApprovalInterruptPayload {
|
|
15
|
+
return payload.kind === CODE_MODE_TOOL_APPROVAL_KIND;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function assertCodeModeApprovalResponse(
|
|
19
|
+
value: unknown,
|
|
20
|
+
): asserts value is CodeModeApprovalResponse {
|
|
21
|
+
if (
|
|
22
|
+
typeof value !== 'object' ||
|
|
23
|
+
value === null ||
|
|
24
|
+
Array.isArray(value) ||
|
|
25
|
+
typeof (value as { approvalId?: unknown }).approvalId !== 'string' ||
|
|
26
|
+
typeof (value as { approved?: unknown }).approved !== 'boolean' ||
|
|
27
|
+
('reason' in value &&
|
|
28
|
+
(value as { reason?: unknown }).reason !== undefined &&
|
|
29
|
+
typeof (value as { reason?: unknown }).reason !== 'string')
|
|
30
|
+
) {
|
|
31
|
+
throw new CodeModeProtocolError(
|
|
32
|
+
'Code mode approval response is malformed.',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeApprovalResolution(
|
|
38
|
+
resolution: unknown,
|
|
39
|
+
): CodeModeApprovalResolution {
|
|
40
|
+
if (
|
|
41
|
+
typeof resolution !== 'object' ||
|
|
42
|
+
resolution === null ||
|
|
43
|
+
Array.isArray(resolution) ||
|
|
44
|
+
typeof (resolution as { approved?: unknown }).approved !== 'boolean' ||
|
|
45
|
+
('reason' in resolution &&
|
|
46
|
+
(resolution as { reason?: unknown }).reason !== undefined &&
|
|
47
|
+
typeof (resolution as { reason?: unknown }).reason !== 'string')
|
|
48
|
+
) {
|
|
49
|
+
throw new CodeModeProtocolError(
|
|
50
|
+
'Code mode approval resolution must be a boolean approval decision.',
|
|
51
|
+
{ resolution },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { approved, reason } = resolution as CodeModeApprovalResolution;
|
|
56
|
+
return {
|
|
57
|
+
approved,
|
|
58
|
+
...(reason !== undefined ? { reason } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { CodeModeProtocolError } from './errors.js';
|
|
3
|
+
import type {
|
|
4
|
+
CodeModeContinuation,
|
|
5
|
+
CodeModeContinuationAuth,
|
|
6
|
+
CodeModeContinuationSecurityOptions,
|
|
7
|
+
UnsignedCodeModeContinuation,
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const SIGNATURE_ALGORITHM = 'HMAC-SHA256';
|
|
11
|
+
const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
let defaultSigningKey: Uint8Array = randomBytes(32);
|
|
14
|
+
let defaultMaxAgeMs = DEFAULT_MAX_AGE_MS;
|
|
15
|
+
|
|
16
|
+
export interface ResolvedCodeModeContinuationSecurity {
|
|
17
|
+
signingKey: Buffer;
|
|
18
|
+
maxAgeMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function setCodeModeContinuationSigningKey(
|
|
22
|
+
key?: string | Uint8Array,
|
|
23
|
+
options: { maxAgeMs?: number } = {},
|
|
24
|
+
): void {
|
|
25
|
+
const resolved = resolveCodeModeContinuationSecurity({
|
|
26
|
+
signingKey: key ?? randomBytes(32),
|
|
27
|
+
maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
|
|
28
|
+
});
|
|
29
|
+
defaultSigningKey = resolved.signingKey;
|
|
30
|
+
defaultMaxAgeMs = resolved.maxAgeMs;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveCodeModeContinuationSecurity(
|
|
34
|
+
options: CodeModeContinuationSecurityOptions = {},
|
|
35
|
+
): ResolvedCodeModeContinuationSecurity {
|
|
36
|
+
const signingKey =
|
|
37
|
+
options.signingKey === undefined
|
|
38
|
+
? Buffer.from(defaultSigningKey)
|
|
39
|
+
: typeof options.signingKey === 'string'
|
|
40
|
+
? Buffer.from(options.signingKey)
|
|
41
|
+
: Buffer.from(options.signingKey);
|
|
42
|
+
if (signingKey.byteLength === 0) {
|
|
43
|
+
throw new TypeError('Continuation signing key must not be empty.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const maxAgeMs = options.maxAgeMs ?? defaultMaxAgeMs;
|
|
47
|
+
if (
|
|
48
|
+
!Number.isInteger(maxAgeMs) ||
|
|
49
|
+
!Number.isFinite(maxAgeMs) ||
|
|
50
|
+
maxAgeMs <= 0
|
|
51
|
+
) {
|
|
52
|
+
throw new TypeError('Continuation maxAgeMs must be a positive integer.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { signingKey, maxAgeMs };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function signCodeModeContinuation(
|
|
59
|
+
continuation: UnsignedCodeModeContinuation,
|
|
60
|
+
security = resolveCodeModeContinuationSecurity(),
|
|
61
|
+
): CodeModeContinuation {
|
|
62
|
+
const issuedAtMs = Date.now();
|
|
63
|
+
const auth: Omit<CodeModeContinuationAuth, 'signature'> = {
|
|
64
|
+
alg: SIGNATURE_ALGORITHM,
|
|
65
|
+
nonce: randomBytes(16).toString('hex'),
|
|
66
|
+
issuedAtMs,
|
|
67
|
+
expiresAtMs: issuedAtMs + security.maxAgeMs,
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
...structuredClone(continuation),
|
|
71
|
+
auth: {
|
|
72
|
+
...auth,
|
|
73
|
+
signature: signContinuationPayload(
|
|
74
|
+
{ ...continuation, auth },
|
|
75
|
+
security.signingKey,
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function verifyCodeModeContinuation(
|
|
82
|
+
continuation: CodeModeContinuation,
|
|
83
|
+
security: CodeModeContinuationSecurityOptions = {},
|
|
84
|
+
): void {
|
|
85
|
+
assertAuthShape(continuation.auth);
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (continuation.auth.expiresAtMs < now) {
|
|
88
|
+
throw new CodeModeProtocolError('Code mode continuation has expired.', {
|
|
89
|
+
expiresAtMs: continuation.auth.expiresAtMs,
|
|
90
|
+
now,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (continuation.auth.issuedAtMs > now + 60_000) {
|
|
94
|
+
throw new CodeModeProtocolError(
|
|
95
|
+
'Code mode continuation was issued in the future.',
|
|
96
|
+
{ issuedAtMs: continuation.auth.issuedAtMs, now },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { signingKey } = resolveCodeModeContinuationSecurity(security);
|
|
101
|
+
const expected = signContinuationPayload(
|
|
102
|
+
stripSignature(continuation),
|
|
103
|
+
signingKey,
|
|
104
|
+
);
|
|
105
|
+
if (!constantTimeEqual(continuation.auth.signature, expected)) {
|
|
106
|
+
throw new CodeModeProtocolError(
|
|
107
|
+
'Code mode continuation signature is invalid.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function hasValidCodeModeContinuationCapability(
|
|
113
|
+
value: unknown,
|
|
114
|
+
security: CodeModeContinuationSecurityOptions = {},
|
|
115
|
+
): value is CodeModeContinuation {
|
|
116
|
+
try {
|
|
117
|
+
verifyCodeModeContinuation(value as CodeModeContinuation, security);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function signContinuationPayload(
|
|
125
|
+
continuation: UnsignedCodeModeContinuation & {
|
|
126
|
+
auth: Omit<CodeModeContinuationAuth, 'signature'>;
|
|
127
|
+
},
|
|
128
|
+
signingKey: Uint8Array,
|
|
129
|
+
): string {
|
|
130
|
+
return createHmac('sha256', signingKey)
|
|
131
|
+
.update(canonicalJson(continuation))
|
|
132
|
+
.digest('base64url');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stripSignature(
|
|
136
|
+
continuation: CodeModeContinuation,
|
|
137
|
+
): UnsignedCodeModeContinuation & {
|
|
138
|
+
auth: Omit<CodeModeContinuationAuth, 'signature'>;
|
|
139
|
+
} {
|
|
140
|
+
const { auth, ...rest } = continuation;
|
|
141
|
+
const { signature: _signature, ...unsignedAuth } = auth;
|
|
142
|
+
return { ...rest, auth: unsignedAuth };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function assertAuthShape(
|
|
146
|
+
auth: unknown,
|
|
147
|
+
): asserts auth is CodeModeContinuationAuth {
|
|
148
|
+
if (
|
|
149
|
+
typeof auth !== 'object' ||
|
|
150
|
+
auth === null ||
|
|
151
|
+
Array.isArray(auth) ||
|
|
152
|
+
(auth as { alg?: unknown }).alg !== SIGNATURE_ALGORITHM ||
|
|
153
|
+
typeof (auth as { nonce?: unknown }).nonce !== 'string' ||
|
|
154
|
+
!/^[0-9a-f]{32}$/i.test((auth as { nonce: string }).nonce) ||
|
|
155
|
+
typeof (auth as { issuedAtMs?: unknown }).issuedAtMs !== 'number' ||
|
|
156
|
+
!Number.isInteger((auth as { issuedAtMs: number }).issuedAtMs) ||
|
|
157
|
+
typeof (auth as { expiresAtMs?: unknown }).expiresAtMs !== 'number' ||
|
|
158
|
+
!Number.isInteger((auth as { expiresAtMs: number }).expiresAtMs) ||
|
|
159
|
+
(auth as { expiresAtMs: number }).expiresAtMs <=
|
|
160
|
+
(auth as { issuedAtMs: number }).issuedAtMs ||
|
|
161
|
+
typeof (auth as { signature?: unknown }).signature !== 'string' ||
|
|
162
|
+
(auth as { signature: string }).signature.length === 0
|
|
163
|
+
) {
|
|
164
|
+
throw new CodeModeProtocolError(
|
|
165
|
+
'Code mode continuation is missing valid signed auth metadata.',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function constantTimeEqual(left: string, right: string): boolean {
|
|
171
|
+
const leftBytes = Buffer.from(left);
|
|
172
|
+
const rightBytes = Buffer.from(right);
|
|
173
|
+
return (
|
|
174
|
+
leftBytes.byteLength === rightBytes.byteLength &&
|
|
175
|
+
timingSafeEqual(leftBytes, rightBytes)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function canonicalJson(value: unknown): string {
|
|
180
|
+
if (value === null) {
|
|
181
|
+
return 'null';
|
|
182
|
+
}
|
|
183
|
+
if (typeof value === 'string') {
|
|
184
|
+
return JSON.stringify(value);
|
|
185
|
+
}
|
|
186
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
187
|
+
return JSON.stringify(value);
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(value)) {
|
|
190
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === 'object') {
|
|
193
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
194
|
+
.filter(([, item]) => item !== undefined)
|
|
195
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
196
|
+
return `{${entries
|
|
197
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
|
|
198
|
+
.join(',')}}`;
|
|
199
|
+
}
|
|
200
|
+
throw new TypeError('Continuation contains a non-JSON-serializable value.');
|
|
201
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -112,6 +112,37 @@ export class CodeModeToolError extends CodeModeError {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export class CodeModeToolApprovalRequiredError extends CodeModeToolError {
|
|
116
|
+
constructor(toolName: string, input: unknown, toolCallId: string) {
|
|
117
|
+
super(`Tool "${toolName}" requires approval before execution.`, {
|
|
118
|
+
toolName,
|
|
119
|
+
input,
|
|
120
|
+
toolCallId,
|
|
121
|
+
});
|
|
122
|
+
this.code = 'CODE_MODE_TOOL_APPROVAL_REQUIRED';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class CodeModeToolApprovalDeniedError extends CodeModeToolError {
|
|
127
|
+
constructor(
|
|
128
|
+
toolName: string,
|
|
129
|
+
input: unknown,
|
|
130
|
+
toolCallId: string,
|
|
131
|
+
reason?: string,
|
|
132
|
+
) {
|
|
133
|
+
super(
|
|
134
|
+
`Tool "${toolName}" approval was denied${reason ? `: ${reason}` : '.'}`,
|
|
135
|
+
{
|
|
136
|
+
toolName,
|
|
137
|
+
input,
|
|
138
|
+
toolCallId,
|
|
139
|
+
...(reason !== undefined ? { reason } : {}),
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
this.code = 'CODE_MODE_TOOL_APPROVAL_DENIED';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
115
146
|
/**
|
|
116
147
|
* Converts an unknown thrown value into a worker-safe serializable shape.
|
|
117
148
|
*
|
|
@@ -250,6 +281,40 @@ export function deserializeError(error: SerializableError): Error {
|
|
|
250
281
|
return result;
|
|
251
282
|
}
|
|
252
283
|
|
|
284
|
+
if (error.code === 'CODE_MODE_TOOL_APPROVAL_REQUIRED') {
|
|
285
|
+
const details = error.details as
|
|
286
|
+
| { toolName?: string; input?: unknown; toolCallId?: string }
|
|
287
|
+
| undefined;
|
|
288
|
+
const result = new CodeModeToolApprovalRequiredError(
|
|
289
|
+
details?.toolName ?? 'unknown',
|
|
290
|
+
details?.input,
|
|
291
|
+
details?.toolCallId ?? 'unknown',
|
|
292
|
+
);
|
|
293
|
+
result.message = error.message;
|
|
294
|
+
restoreStack(result, error);
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (error.code === 'CODE_MODE_TOOL_APPROVAL_DENIED') {
|
|
299
|
+
const details = error.details as
|
|
300
|
+
| {
|
|
301
|
+
toolName?: string;
|
|
302
|
+
input?: unknown;
|
|
303
|
+
toolCallId?: string;
|
|
304
|
+
reason?: string;
|
|
305
|
+
}
|
|
306
|
+
| undefined;
|
|
307
|
+
const result = new CodeModeToolApprovalDeniedError(
|
|
308
|
+
details?.toolName ?? 'unknown',
|
|
309
|
+
details?.input,
|
|
310
|
+
details?.toolCallId ?? 'unknown',
|
|
311
|
+
details?.reason,
|
|
312
|
+
);
|
|
313
|
+
result.message = error.message;
|
|
314
|
+
restoreStack(result, error);
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
253
318
|
const result = new CodeModeError(
|
|
254
319
|
error.message,
|
|
255
320
|
error.code ?? 'CODE_MODE_ERROR',
|