@ai-sdk/provider-utils 5.0.0-beta.30 → 5.0.0-beta.49

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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Options for executing a command in the sandbox via `run` or `spawn`.
3
+ */
4
+ type SandboxProcessOptions = {
5
+ /**
6
+ * Command to execute in the sandbox.
7
+ */
8
+ command: string;
9
+
10
+ /**
11
+ * Working directory to execute the command in.
12
+ */
13
+ workingDirectory?: string;
14
+
15
+ /**
16
+ * Environment variables to set for this command. Merged with the
17
+ * sandbox's default environment; values here take precedence.
18
+ * Supporting environment variables as an option is preferable from a
19
+ * security perspective, e.g. to avoid them leaking in logs.
20
+ */
21
+ env?: Record<string, string>;
22
+
23
+ /**
24
+ * Signal that can be used to abort the command. When aborted, the running
25
+ * process is killed; for `spawn`, `wait()` rejects with the abort reason.
26
+ */
27
+ abortSignal?: AbortSignal;
28
+ };
29
+
30
+ /**
31
+ * Options for reading a file from the sandbox.
32
+ */
33
+ type ReadFileOptions = {
34
+ /**
35
+ * Path of the file to read.
36
+ */
37
+ path: string;
38
+
39
+ /**
40
+ * Signal that can be used to abort the read.
41
+ */
42
+ abortSignal?: AbortSignal;
43
+ };
44
+
45
+ /**
46
+ * Options for writing a file to the sandbox. `CONTENT` is the payload written
47
+ * to the file: a byte stream, raw bytes, or a string.
48
+ */
49
+ type WriteFileOptions<CONTENT> = {
50
+ /**
51
+ * Path of the file to write.
52
+ */
53
+ path: string;
54
+
55
+ /**
56
+ * Content to write to the file.
57
+ */
58
+ content: CONTENT;
59
+
60
+ /**
61
+ * Signal that can be used to abort the write.
62
+ */
63
+ abortSignal?: AbortSignal;
64
+ };
65
+
66
+ /**
67
+ * Sandbox session that can execute commands and read/write files.
68
+ */
69
+ export type SandboxSession = {
70
+ /**
71
+ * Description of the sandbox environment that can be added to the agent's instructions
72
+ * so that the agent knows about relevant details such as the root directory, exposed
73
+ * ports, the public hostname, etc.
74
+ */
75
+ readonly description: string;
76
+
77
+ /**
78
+ * Read one file from the sandbox as a stream of bytes. Resolves to `null`
79
+ * when the file does not exist.
80
+ *
81
+ * Relative path handling is implementation-defined. This is the lowest-level
82
+ * read primitive; prefer `readBinaryFile` or `readTextFile` unless you need
83
+ * to stream bytes.
84
+ */
85
+ readonly readFile: (
86
+ options: ReadFileOptions,
87
+ ) => PromiseLike<ReadableStream<Uint8Array> | null>;
88
+
89
+ /**
90
+ * Read one file from the sandbox as raw bytes. Resolves to `null` when the
91
+ * file does not exist.
92
+ */
93
+ readonly readBinaryFile: (
94
+ options: ReadFileOptions,
95
+ ) => PromiseLike<Uint8Array | null>;
96
+
97
+ /**
98
+ * Read one text file from the sandbox, decoded using the requested encoding.
99
+ * Resolves to `null` when the file does not exist.
100
+ *
101
+ * Line ranges are 1-based and inclusive. When `endLine` is past EOF the read
102
+ * returns through EOF without error.
103
+ */
104
+ readonly readTextFile: (
105
+ options: ReadFileOptions & {
106
+ /**
107
+ * Text encoding used to decode the file bytes. Defaults to `"utf-8"`.
108
+ */
109
+ encoding?: string;
110
+
111
+ /**
112
+ * 1-based inclusive start line. Defaults to 1.
113
+ */
114
+ startLine?: number;
115
+
116
+ /**
117
+ * 1-based inclusive end line. When past the file's line count, the read
118
+ * returns through EOF without error.
119
+ */
120
+ endLine?: number;
121
+ },
122
+ ) => PromiseLike<string | null>;
123
+
124
+ /**
125
+ * Write one file to the sandbox from a stream of bytes. Creates parent
126
+ * directories recursively and overwrites any existing file.
127
+ *
128
+ * This is the lowest-level write primitive; prefer `writeBinaryFile` or
129
+ * `writeTextFile` when the full content is already materialized in memory.
130
+ */
131
+ readonly writeFile: (
132
+ options: WriteFileOptions<ReadableStream<Uint8Array>>,
133
+ ) => PromiseLike<void>;
134
+
135
+ /**
136
+ * Write one file to the sandbox from raw bytes. Creates parent directories
137
+ * recursively and overwrites any existing file.
138
+ */
139
+ readonly writeBinaryFile: (
140
+ options: WriteFileOptions<Uint8Array>,
141
+ ) => PromiseLike<void>;
142
+
143
+ /**
144
+ * Write one file to the sandbox from a string, encoded using the requested
145
+ * encoding. Creates parent directories recursively and overwrites any
146
+ * existing file.
147
+ */
148
+ readonly writeTextFile: (
149
+ options: WriteFileOptions<string> & {
150
+ /**
151
+ * Text encoding used to encode the string to bytes. Defaults to `"utf-8"`.
152
+ */
153
+ encoding?: string;
154
+ },
155
+ ) => PromiseLike<void>;
156
+
157
+ /**
158
+ * Spawn a long-running process in the sandbox. Returns immediately with a
159
+ * handle that streams stdout/stderr, can be waited on, and can be killed.
160
+ *
161
+ * `run` is conceptually a thin wrapper over this primitive: spawn,
162
+ * collect both streams to strings, await `wait()`, return the result.
163
+ */
164
+ readonly spawn: (
165
+ options: SandboxProcessOptions,
166
+ ) => PromiseLike<SandboxProcess>;
167
+
168
+ /**
169
+ * Run a command in the sandbox.
170
+ */
171
+ readonly run: (options: SandboxProcessOptions) => PromiseLike<{
172
+ /**
173
+ * Exit code returned by the command.
174
+ */
175
+ exitCode: number;
176
+
177
+ /**
178
+ * Standard output produced by the command.
179
+ */
180
+ stdout: string;
181
+
182
+ /**
183
+ * Standard error produced by the command.
184
+ */
185
+ stderr: string;
186
+ }>;
187
+ };
188
+
189
+ /**
190
+ * Handle to a long-running process started via `SandboxSession.spawn`.
191
+ */
192
+ export type SandboxProcess = {
193
+ /**
194
+ * Process identifier, if the sandbox implementation exposes one.
195
+ */
196
+ readonly pid?: number;
197
+
198
+ /**
199
+ * Stream of bytes written by the process to standard output.
200
+ */
201
+ readonly stdout: ReadableStream<Uint8Array>;
202
+
203
+ /**
204
+ * Stream of bytes written by the process to standard error.
205
+ */
206
+ readonly stderr: ReadableStream<Uint8Array>;
207
+
208
+ /**
209
+ * Resolve when the process exits, yielding its exit code.
210
+ */
211
+ wait(): PromiseLike<{ exitCode: number }>;
212
+
213
+ /**
214
+ * Terminate the process. Idempotent.
215
+ */
216
+ kill(): PromiseLike<void>;
217
+ };
@@ -20,4 +20,10 @@ export type ToolApprovalRequest = {
20
20
  * @default false
21
21
  */
22
22
  isAutomatic?: boolean;
23
+
24
+ /**
25
+ * HMAC-SHA256 signature binding this approval to its tool call.
26
+ * Present only when `experimental_toolApprovalSecret` is configured.
27
+ */
28
+ signature?: string;
23
29
  };
@@ -1,5 +1,6 @@
1
1
  import type { Context } from './context';
2
2
  import type { ModelMessage } from './model-message';
3
+ import type { SandboxSession } from './sandbox';
3
4
 
4
5
  /**
5
6
  * Additional options that are sent into each tool execution.
@@ -35,6 +36,11 @@ export interface ToolExecutionOptions<
35
36
  * in `prepareStep` and update it there.
36
37
  */
37
38
  context: CONTEXT;
39
+
40
+ /**
41
+ * The sandbox environment that the tool is operating in.
42
+ */
43
+ experimental_sandbox?: SandboxSession;
38
44
  }
39
45
 
40
46
  /**
package/src/types/tool.ts CHANGED
@@ -1,15 +1,16 @@
1
- import type { JSONValue, SharedV4ProviderMetadata } from '@ai-sdk/provider';
1
+ import type { JSONValue, JSONObject } from '@ai-sdk/provider';
2
2
  import type { FlexibleSchema } from '../schema';
3
3
  import type { ToolResultOutput } from './content-part';
4
4
  import type { Context } from './context';
5
+ import type { ExecutableTool } from './executable-tool';
5
6
  import type { NeverOptional } from './never-optional';
6
7
  import type { ProviderOptions } from './provider-options';
7
- import type { SensitiveContext } from './sensitive-context';
8
8
  import type {
9
9
  ToolExecuteFunction,
10
10
  ToolExecutionOptions,
11
11
  } from './tool-execute-function';
12
12
  import type { ToolNeedsApprovalFunction } from './tool-needs-approval-function';
13
+ import type { SandboxSession } from './sandbox';
13
14
 
14
15
  /**
15
16
  * Helper type to determine the outputSchema and execute function properties of a tool.
@@ -59,6 +60,8 @@ type BaseTool<
59
60
  > = {
60
61
  /**
61
62
  * An optional title of the tool.
63
+ *
64
+ * @deprecated Use `providerMetadata` for source-specific tool display metadata.
62
65
  */
63
66
  title?: string;
64
67
 
@@ -74,11 +77,11 @@ type BaseTool<
74
77
  *
75
78
  * Unlike `providerOptions`, this metadata is not sent to the language
76
79
  * model. Instead, it is propagated onto the resulting tool call's
77
- * `providerMetadata` so consumers can read it from tool call / result
78
- * parts and UI message parts. This is useful for sources of dynamic
79
- * tools (e.g. an MCP server) to identify themselves.
80
+ * `toolMetadata` so consumers can read it from tool call / result parts
81
+ * and UI message parts. This is useful for sources of dynamic tools (e.g.
82
+ * an MCP server) to identify themselves.
80
83
  */
81
- providerMetadata?: SharedV4ProviderMetadata;
84
+ metadata?: JSONObject;
82
85
 
83
86
  /**
84
87
  * The schema of the input that the tool expects.
@@ -96,12 +99,6 @@ type BaseTool<
96
99
  */
97
100
  contextSchema?: FlexibleSchema<CONTEXT>;
98
101
 
99
- /**
100
- * Marks top-level context properties that contain sensitive data and should be excluded from telemetry.
101
- * Properties marked as `true` are omitted from telemetry integrations.
102
- */
103
- sensitiveContext?: SensitiveContext<CONTEXT>;
104
-
105
102
  /**
106
103
  * Whether the tool needs approval before it can be executed.
107
104
  *
@@ -180,10 +177,21 @@ type BaseFunctionTool<
180
177
  CONTEXT extends Context | unknown | never = any,
181
178
  > = BaseTool<INPUT, OUTPUT, CONTEXT> & {
182
179
  /**
183
- * An optional description of what the tool does.
184
- * Will be used by the language model to decide whether to use the tool.
180
+ * Optional description of what the tool does.
181
+ *
182
+ * Included in the tool definition sent to the language model so it can
183
+ * decide when and how to call the tool.
184
+ *
185
+ * Provide a string for a fixed description, or a function that returns a
186
+ * string from the current `context` (and optional `experimental_sandbox`) when the
187
+ * description should vary per call.
185
188
  */
186
- description?: string;
189
+ description?:
190
+ | string
191
+ | ((options: {
192
+ context: NoInfer<CONTEXT>;
193
+ experimental_sandbox?: SandboxSession;
194
+ }) => string);
187
195
 
188
196
  /**
189
197
  * Strict mode setting for the tool.
@@ -208,7 +216,7 @@ type BaseFunctionTool<
208
216
  };
209
217
 
210
218
  /**
211
- * Tool with user-defined input and output schemas.
219
+ * Tool with user-defined input and output schemas that is executed by the AI SDK.
212
220
  */
213
221
  export type FunctionTool<
214
222
  INPUT extends JSONValue | unknown | never = any,
@@ -219,8 +227,10 @@ export type FunctionTool<
219
227
  };
220
228
 
221
229
  /**
222
- * Tool that is defined at runtime (e.g. an MCP tool).
230
+ * Tool that is defined at runtime.
223
231
  * The types of input and output are not known at development time.
232
+ *
233
+ * For example, MCP tools that are not known at development time.
224
234
  */
225
235
  export type DynamicTool<
226
236
  INPUT extends JSONValue | unknown | never = any,
@@ -259,6 +269,8 @@ type BaseProviderTool<
259
269
  /**
260
270
  * Tool with provider-defined input and output schemas that is executed by the
261
271
  * user.
272
+ *
273
+ * For example, shell tools that are executed in a local shell, but have provider-defined input and output schemas.
262
274
  */
263
275
  export type ProviderDefinedTool<
264
276
  INPUT extends JSONValue | unknown | never = any,
@@ -277,6 +289,8 @@ export type ProviderDefinedTool<
277
289
  /**
278
290
  * Tool with provider-defined input and output schemas that is executed by the
279
291
  * provider.
292
+ *
293
+ * For example, web search tools and code execution tools that are executed by the provider itself.
280
294
  */
281
295
  export type ProviderExecutedTool<
282
296
  INPUT extends JSONValue | unknown | never = any,
@@ -325,8 +339,20 @@ export type Tool<
325
339
  * Infer the tool type from a tool object.
326
340
  *
327
341
  * This is useful for type inference when working with tool objects.
342
+ *
343
+ * When the input has an `execute` function, the return type narrows to
344
+ * `ExecutableTool<Tool<...>>` so that `.execute` is non-nullable without
345
+ * needing `isExecutableTool` or a `!` assertion at the call site.
328
346
  */
329
- // Note: overload order is important for auto-completion
347
+ // Note: overload order is important for auto-completion.
348
+ // The "with execute" overload comes first so calls that include an
349
+ // `execute` function get the narrowed return type. Calls without
350
+ // `execute` fall through to the overloads below.
351
+ export function tool<INPUT, OUTPUT, CONTEXT extends Context>(
352
+ tool: Tool<INPUT, OUTPUT, CONTEXT> & {
353
+ execute: ToolExecuteFunction<INPUT, OUTPUT, CONTEXT>;
354
+ },
355
+ ): ExecutableTool<Tool<INPUT, OUTPUT, CONTEXT>>;
330
356
  export function tool<INPUT, OUTPUT, CONTEXT extends Context>(
331
357
  tool: Tool<INPUT, OUTPUT, CONTEXT>,
332
358
  ): Tool<INPUT, OUTPUT, CONTEXT>;
@@ -4,6 +4,10 @@ import { DownloadError } from './download-error';
4
4
  * Validates that a URL is safe to download from, blocking private/internal addresses
5
5
  * to prevent SSRF attacks.
6
6
  *
7
+ * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
8
+ * hostname that resolves to a private address is not blocked here (see callers, which
9
+ * should additionally constrain egress at the network layer when handling untrusted URLs).
10
+ *
7
11
  * @param url - The URL string to validate.
8
12
  * @throws DownloadError if the URL is unsafe.
9
13
  */
@@ -31,7 +35,9 @@ export function validateDownloadUrl(url: string): void {
31
35
  });
32
36
  }
33
37
 
34
- const hostname = parsed.hostname;
38
+ // Strip a trailing dot so a fully-qualified name like `localhost.` (which resolves
39
+ // identically to `localhost`) cannot bypass the hostname blocklist below.
40
+ const hostname = parsed.hostname.toLowerCase().replace(/\.+$/, '');
35
41
 
36
42
  // Block empty hostname
37
43
  if (!hostname) {
@@ -90,59 +96,135 @@ function isIPv4(hostname: string): boolean {
90
96
 
91
97
  function isPrivateIPv4(ip: string): boolean {
92
98
  const parts = ip.split('.').map(Number);
93
- const [a, b] = parts;
99
+ const [a, b, c] = parts;
94
100
 
95
101
  // 0.0.0.0/8
96
102
  if (a === 0) return true;
97
103
  // 10.0.0.0/8
98
104
  if (a === 10) return true;
105
+ // 100.64.0.0/10 (CGNAT, used by some cloud providers for internal traffic)
106
+ if (a === 100 && b >= 64 && b <= 127) return true;
99
107
  // 127.0.0.0/8
100
108
  if (a === 127) return true;
101
109
  // 169.254.0.0/16
102
110
  if (a === 169 && b === 254) return true;
103
111
  // 172.16.0.0/12
104
112
  if (a === 172 && b >= 16 && b <= 31) return true;
113
+ // 192.0.0.0/24 (IETF protocol assignments)
114
+ if (a === 192 && b === 0 && c === 0) return true;
105
115
  // 192.168.0.0/16
106
116
  if (a === 192 && b === 168) return true;
117
+ // 198.18.0.0/15 (benchmarking)
118
+ if (a === 198 && (b === 18 || b === 19)) return true;
119
+ // 240.0.0.0/4 (reserved, includes 255.255.255.255 broadcast)
120
+ if (a >= 240) return true;
107
121
 
108
122
  return false;
109
123
  }
110
124
 
111
- function isPrivateIPv6(ip: string): boolean {
112
- const normalized = ip.toLowerCase();
113
-
114
- // ::1 (loopback)
115
- if (normalized === '::1') return true;
116
- // :: (unspecified)
117
- if (normalized === '::') return true;
118
-
119
- // Check for IPv4-mapped addresses (::ffff:x.x.x.x or ::ffff:HHHH:HHHH)
120
- if (normalized.startsWith('::ffff:')) {
121
- const mappedPart = normalized.slice(7);
122
- // Dotted-decimal form: ::ffff:127.0.0.1
123
- if (isIPv4(mappedPart)) {
124
- return isPrivateIPv4(mappedPart);
125
- }
126
- // Hex form: ::ffff:7f00:1 (URL parser normalizes to this)
127
- const hexParts = mappedPart.split(':');
128
- if (hexParts.length === 2) {
129
- const high = parseInt(hexParts[0], 16);
130
- const low = parseInt(hexParts[1], 16);
131
- if (!isNaN(high) && !isNaN(low)) {
132
- const a = (high >> 8) & 0xff;
133
- const b = high & 0xff;
134
- const c = (low >> 8) & 0xff;
135
- const d = low & 0xff;
136
- return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
125
+ /**
126
+ * Expands an IPv6 address string into its 8 16-bit groups, handling `::`
127
+ * compression and an optional dotted-decimal IPv4 tail (e.g. `::ffff:127.0.0.1`).
128
+ *
129
+ * @returns the 8 groups, or null if the input is not a parseable IPv6 address.
130
+ */
131
+ function parseIPv6(ip: string): number[] | null {
132
+ // Strip an optional zone id (e.g. `fe80::1%eth0`).
133
+ let address = ip.toLowerCase();
134
+ const zoneIndex = address.indexOf('%');
135
+ if (zoneIndex !== -1) {
136
+ address = address.slice(0, zoneIndex);
137
+ }
138
+
139
+ // At most one `::` compression marker is allowed.
140
+ const halves = address.split('::');
141
+ if (halves.length > 2) return null;
142
+
143
+ const toGroups = (segment: string): number[] | null => {
144
+ if (segment === '') return [];
145
+ const groups: number[] = [];
146
+ const parts = segment.split(':');
147
+ for (let i = 0; i < parts.length; i++) {
148
+ const part = parts[i];
149
+ // A dotted-decimal IPv4 tail is only valid as the final part.
150
+ if (part.includes('.')) {
151
+ if (i !== parts.length - 1 || !isIPv4(part)) return null;
152
+ const [a, b, c, d] = part.split('.').map(Number);
153
+ groups.push((a << 8) | b, (c << 8) | d);
154
+ continue;
137
155
  }
156
+ if (!/^[0-9a-f]{1,4}$/.test(part)) return null;
157
+ groups.push(parseInt(part, 16));
138
158
  }
159
+ return groups;
160
+ };
161
+
162
+ const head = toGroups(halves[0]);
163
+ if (head === null) return null;
164
+
165
+ if (halves.length === 2) {
166
+ const tail = toGroups(halves[1]);
167
+ if (tail === null) return null;
168
+ const fill = 8 - head.length - tail.length;
169
+ if (fill < 0) return null;
170
+ return [...head, ...new Array<number>(fill).fill(0), ...tail];
139
171
  }
140
172
 
141
- // fc00::/7 (unique local addresses - fc00:: and fd00::)
142
- if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
173
+ // No `::` compression: the address must contain exactly 8 groups.
174
+ return head.length === 8 ? head : null;
175
+ }
176
+
177
+ function isPrivateIPv6(ip: string): boolean {
178
+ const groups = parseIPv6(ip);
179
+
180
+ // Fail closed: if the address cannot be parsed, treat it as unsafe.
181
+ if (groups === null) return true;
182
+
183
+ const topZero = (count: number) =>
184
+ groups.slice(0, count).every(group => group === 0);
185
+
186
+ // ::1 (loopback) and :: (unspecified)
187
+ if (topZero(7) && (groups[7] === 0 || groups[7] === 1)) return true;
188
+
189
+ // fc00::/7 (unique local addresses)
190
+ if ((groups[0] & 0xfe00) === 0xfc00) return true;
143
191
 
144
192
  // fe80::/10 (link-local)
145
- if (normalized.startsWith('fe80')) return true;
193
+ if ((groups[0] & 0xffc0) === 0xfe80) return true;
194
+
195
+ // fec0::/10 (site-local, deprecated but still routable internally)
196
+ if ((groups[0] & 0xffc0) === 0xfec0) return true;
197
+
198
+ // ff00::/8 (multicast)
199
+ if ((groups[0] & 0xff00) === 0xff00) return true;
200
+
201
+ // Addresses that embed an IPv4 address in their last 32 bits. For these we
202
+ // extract the embedded IPv4 and reuse the IPv4 private-range checks, so that
203
+ // e.g. ::ffff:127.0.0.1 or 64:ff9b::169.254.169.254 are blocked.
204
+ const embedsIPv4 =
205
+ // ::/96 — IPv4-compatible (deprecated)
206
+ topZero(6) ||
207
+ // ::ffff:0:0/96 — IPv4-mapped (ffff in group 5)
208
+ (topZero(5) && groups[5] === 0xffff) ||
209
+ // ::ffff:0:0/96 — IPv4-translated form (ffff in group 4, group 5 zero)
210
+ (topZero(4) && groups[4] === 0xffff && groups[5] === 0) ||
211
+ // 64:ff9b::/96 — NAT64 well-known prefix
212
+ (groups[0] === 0x0064 &&
213
+ groups[1] === 0xff9b &&
214
+ groups[2] === 0 &&
215
+ groups[3] === 0 &&
216
+ groups[4] === 0 &&
217
+ groups[5] === 0) ||
218
+ // 64:ff9b:1::/48 — NAT64 local-use prefix
219
+ (groups[0] === 0x0064 && groups[1] === 0xff9b && groups[2] === 0x0001);
220
+
221
+ if (embedsIPv4) {
222
+ const a = (groups[6] >> 8) & 0xff;
223
+ const b = groups[6] & 0xff;
224
+ const c = (groups[7] >> 8) & 0xff;
225
+ const d = groups[7] & 0xff;
226
+ return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
227
+ }
146
228
 
147
229
  return false;
148
230
  }
@@ -1,9 +0,0 @@
1
- import type { Context } from './context';
2
-
3
- /**
4
- * Top-level context properties that contain sensitive data and should be
5
- * excluded from telemetry.
6
- */
7
- export type SensitiveContext<CONTEXT extends Context | unknown | never> =
8
- | { [KEY in keyof CONTEXT]?: boolean }
9
- | undefined;