@ai-sdk/sandbox-vercel 1.0.124 → 1.0.126
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 +19 -0
- package/README.md +13 -68
- package/dist/index.d.ts +60 -32
- package/dist/index.js +381 -275
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +10 -1
- package/src/utils.ts +313 -0
- package/src/vercel-legacy-sandbox-provider.ts +221 -0
- package/src/vercel-network-sandbox-session.ts +5 -5
- package/src/vercel-sandbox-session.ts +5 -5
- package/src/vercel-sandbox.ts +156 -430
package/src/utils.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { HarnessSandboxAuthenticationError } from '@ai-sdk/harness';
|
|
2
|
+
import { Sandbox } from '@vercel/sandbox';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flattens an intersection of object types into a single object type so the
|
|
6
|
+
* resolved shape displays as its named properties rather than a chain of
|
|
7
|
+
* `A & B & C`.
|
|
8
|
+
*/
|
|
9
|
+
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Distributes `Omit` across each member of a union instead of collapsing the
|
|
13
|
+
* union to its common keys. `Sandbox.create`'s parameter is a union (a
|
|
14
|
+
* git/tarball/no-source create variant and a snapshot-source create variant),
|
|
15
|
+
* so a plain `Omit` would discard keys absent from any one member (e.g.
|
|
16
|
+
* `runtime`, which the snapshot variant lacks) and merge the `source` shapes.
|
|
17
|
+
* Applying `Omit` per-member preserves every variant intact; the `Prettify`
|
|
18
|
+
* wrapper collapses each member's intersections into a readable object shape.
|
|
19
|
+
*/
|
|
20
|
+
export type DistributiveOmit<T, K extends keyof any> = T extends unknown
|
|
21
|
+
? Prettify<Omit<T, K>>
|
|
22
|
+
: never;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parameters forwarded to `@vercel/sandbox`'s `Sandbox.create` when creating
|
|
26
|
+
* a sandbox from scratch. Aliased directly from the underlying SDK so the
|
|
27
|
+
* full surface — every option Vercel supports, including its native
|
|
28
|
+
* `NetworkPolicy` — is available without us re-declaring it.
|
|
29
|
+
*/
|
|
30
|
+
export type VercelSandboxCreateParams = DistributiveOmit<
|
|
31
|
+
NonNullable<Parameters<typeof Sandbox.create>[0]>,
|
|
32
|
+
'onResume'
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Base shape of `Sandbox.create` params extracted from the union (excludes
|
|
37
|
+
* the `source: { type: 'snapshot' }` variant) so all create-time fields
|
|
38
|
+
* are typed as present.
|
|
39
|
+
*/
|
|
40
|
+
export type BaseCreateSandboxParams = Exclude<
|
|
41
|
+
VercelSandboxCreateParams,
|
|
42
|
+
{ source: { type: 'snapshot'; snapshotId: string } }
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export const VERCEL_PROVIDER_ID = 'vercel-sandbox';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 30 minutes. The `@vercel/sandbox` SDK defaults to 5 minutes which is
|
|
49
|
+
* too short for multi-step workflows — the VM expires between steps.
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_SANDBOX_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
52
|
+
export const DEFAULT_SANDBOX_RUNTIME = 'node24';
|
|
53
|
+
|
|
54
|
+
const SNAPSHOT_POLL_INTERVAL_MS = 500;
|
|
55
|
+
const SNAPSHOT_POLL_TIMEOUT_MS = 30_000;
|
|
56
|
+
|
|
57
|
+
type SandboxLookupParams = {
|
|
58
|
+
fetch?: typeof fetch;
|
|
59
|
+
projectId?: string;
|
|
60
|
+
teamId?: string;
|
|
61
|
+
token?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type VercelSandboxAuthenticationSettings = SandboxLookupParams & {
|
|
65
|
+
sandbox?: Sandbox;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function getSandboxLookupParams(
|
|
69
|
+
settings: VercelSandboxAuthenticationSettings,
|
|
70
|
+
): SandboxLookupParams {
|
|
71
|
+
if ('sandbox' in settings && settings.sandbox != null) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { fetch, projectId, teamId, token } = settings as SandboxLookupParams;
|
|
76
|
+
return {
|
|
77
|
+
...(fetch ? { fetch } : {}),
|
|
78
|
+
...(projectId ? { projectId } : {}),
|
|
79
|
+
...(teamId ? { teamId } : {}),
|
|
80
|
+
...(token ? { token } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const VERCEL_SANDBOX_AUTHENTICATION_MESSAGE =
|
|
85
|
+
'Vercel Sandbox authentication failed. Set VERCEL_OIDC_TOKEN, or pass token, teamId, and projectId to createVercelSandbox(), then verify that they can access Vercel Sandbox.';
|
|
86
|
+
|
|
87
|
+
export async function withVercelSandboxAuthenticationError<T>({
|
|
88
|
+
settings,
|
|
89
|
+
operation,
|
|
90
|
+
}: {
|
|
91
|
+
settings: VercelSandboxAuthenticationSettings;
|
|
92
|
+
operation: () => Promise<T>;
|
|
93
|
+
}): Promise<T> {
|
|
94
|
+
try {
|
|
95
|
+
return await operation();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (
|
|
98
|
+
!isVercelSandboxAuthenticationFailure({
|
|
99
|
+
error,
|
|
100
|
+
hasConfiguredCredentials: hasConfiguredCredentials(settings),
|
|
101
|
+
})
|
|
102
|
+
) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
throw new HarnessSandboxAuthenticationError({
|
|
106
|
+
message: VERCEL_SANDBOX_AUTHENTICATION_MESSAGE,
|
|
107
|
+
sandboxProviderId: VERCEL_PROVIDER_ID,
|
|
108
|
+
cause: error,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasConfiguredCredentials(
|
|
114
|
+
settings: VercelSandboxAuthenticationSettings,
|
|
115
|
+
): boolean {
|
|
116
|
+
if (process.env.VERCEL_OIDC_TOKEN) return true;
|
|
117
|
+
if ('sandbox' in settings && settings.sandbox != null) return true;
|
|
118
|
+
const { token, teamId, projectId } = getSandboxLookupParams(settings);
|
|
119
|
+
return Boolean(token && teamId && projectId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isVercelSandboxAuthenticationFailure({
|
|
123
|
+
error,
|
|
124
|
+
hasConfiguredCredentials,
|
|
125
|
+
}: {
|
|
126
|
+
error: unknown;
|
|
127
|
+
hasConfiguredCredentials: boolean;
|
|
128
|
+
}): boolean {
|
|
129
|
+
const seen = new Set<unknown>();
|
|
130
|
+
let current = error;
|
|
131
|
+
while (current != null && !seen.has(current)) {
|
|
132
|
+
seen.add(current);
|
|
133
|
+
if (typeof current === 'object') {
|
|
134
|
+
const candidate = current as {
|
|
135
|
+
name?: unknown;
|
|
136
|
+
message?: unknown;
|
|
137
|
+
code?: unknown;
|
|
138
|
+
cause?: unknown;
|
|
139
|
+
response?: { status?: unknown; statusCode?: unknown };
|
|
140
|
+
};
|
|
141
|
+
if (
|
|
142
|
+
candidate.response?.status === 401 ||
|
|
143
|
+
candidate.response?.status === 403 ||
|
|
144
|
+
candidate.response?.statusCode === 401 ||
|
|
145
|
+
candidate.response?.statusCode === 403
|
|
146
|
+
) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
typeof candidate.name === 'string' &&
|
|
151
|
+
VERCEL_AUTHENTICATION_ERROR_NAMES.has(candidate.name)
|
|
152
|
+
) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
typeof candidate.message === 'string' &&
|
|
157
|
+
VERCEL_AUTHENTICATION_ERROR_MESSAGE.test(candidate.message)
|
|
158
|
+
) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
!hasConfiguredCredentials &&
|
|
163
|
+
candidate.code === 'ERR_INVALID_ARG_TYPE' &&
|
|
164
|
+
typeof candidate.message === 'string' &&
|
|
165
|
+
candidate.message.includes('"path" argument') &&
|
|
166
|
+
candidate.message.includes('Received undefined')
|
|
167
|
+
) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
current = candidate.cause;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const VERCEL_AUTHENTICATION_ERROR_NAMES = new Set([
|
|
179
|
+
'AccessTokenMissingError',
|
|
180
|
+
'LocalOidcContextError',
|
|
181
|
+
'OAuthError',
|
|
182
|
+
'RefreshAccessTokenFailedError',
|
|
183
|
+
'VercelOidcContextError',
|
|
184
|
+
'VercelOidcTokenError',
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const VERCEL_AUTHENTICATION_ERROR_MESSAGE =
|
|
188
|
+
/Could not get credentials from OIDC context|No authentication found|Failed to (?:retrieve|refresh) authentication token|Missing credentials parameters to access the Vercel API|Authentication failed/i;
|
|
189
|
+
|
|
190
|
+
export function hasExplicitSandboxEnvironment(params: object): boolean {
|
|
191
|
+
return (
|
|
192
|
+
('runtime' in params && params.runtime != null) ||
|
|
193
|
+
('image' in params && params.image != null) ||
|
|
194
|
+
('source' in params &&
|
|
195
|
+
typeof params.source === 'object' &&
|
|
196
|
+
params.source != null &&
|
|
197
|
+
'type' in params.source &&
|
|
198
|
+
params.source.type === 'snapshot')
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function withDefaultSandboxSettings(
|
|
203
|
+
createParams: BaseCreateSandboxParams,
|
|
204
|
+
): BaseCreateSandboxParams {
|
|
205
|
+
return {
|
|
206
|
+
...(hasExplicitSandboxEnvironment(createParams)
|
|
207
|
+
? {}
|
|
208
|
+
: { runtime: DEFAULT_SANDBOX_RUNTIME }),
|
|
209
|
+
...createParams,
|
|
210
|
+
timeout: createParams.timeout ?? DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
211
|
+
} as BaseCreateSandboxParams;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function ensureTemplateSnapshot({
|
|
215
|
+
baseParams,
|
|
216
|
+
templateName,
|
|
217
|
+
onCreate,
|
|
218
|
+
lookupParams,
|
|
219
|
+
abortSignal,
|
|
220
|
+
snapshotCache,
|
|
221
|
+
}: {
|
|
222
|
+
baseParams: BaseCreateSandboxParams;
|
|
223
|
+
templateName: string;
|
|
224
|
+
onCreate: (sandbox: Sandbox) => Promise<void>;
|
|
225
|
+
lookupParams: SandboxLookupParams;
|
|
226
|
+
abortSignal?: AbortSignal;
|
|
227
|
+
snapshotCache?: Map<string, string>;
|
|
228
|
+
}): Promise<string> {
|
|
229
|
+
const cachedSnapshotId = snapshotCache?.get(templateName);
|
|
230
|
+
if (cachedSnapshotId != null) {
|
|
231
|
+
return cachedSnapshotId;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const prepared = await Sandbox.getOrCreate({
|
|
235
|
+
...baseParams,
|
|
236
|
+
name: templateName,
|
|
237
|
+
persistent: true,
|
|
238
|
+
snapshotExpiration: baseParams.snapshotExpiration ?? 0,
|
|
239
|
+
onCreate,
|
|
240
|
+
...(abortSignal ? { signal: abortSignal } : {}),
|
|
241
|
+
});
|
|
242
|
+
let snapshotId = prepared.currentSnapshotId;
|
|
243
|
+
if (snapshotId == null) {
|
|
244
|
+
const stopped = await prepared.stop(
|
|
245
|
+
abortSignal ? { signal: abortSignal } : undefined,
|
|
246
|
+
);
|
|
247
|
+
snapshotId =
|
|
248
|
+
stopped.snapshot?.id ??
|
|
249
|
+
(await pollForTemplateSnapshot({
|
|
250
|
+
name: templateName,
|
|
251
|
+
lookupParams,
|
|
252
|
+
abortSignal,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
snapshotCache?.set(templateName, snapshotId);
|
|
256
|
+
return snapshotId;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function createLiveSandboxFromSnapshot({
|
|
260
|
+
baseParams,
|
|
261
|
+
snapshotId,
|
|
262
|
+
liveName,
|
|
263
|
+
abortSignal,
|
|
264
|
+
}: {
|
|
265
|
+
baseParams: BaseCreateSandboxParams;
|
|
266
|
+
snapshotId: string;
|
|
267
|
+
liveName?: string;
|
|
268
|
+
abortSignal?: AbortSignal;
|
|
269
|
+
}): Promise<Sandbox> {
|
|
270
|
+
const {
|
|
271
|
+
runtime: _runtime,
|
|
272
|
+
image: _image,
|
|
273
|
+
source: _source,
|
|
274
|
+
persistent: _persistent,
|
|
275
|
+
...forkParams
|
|
276
|
+
} = baseParams;
|
|
277
|
+
return Sandbox.create({
|
|
278
|
+
...forkParams,
|
|
279
|
+
source: { type: 'snapshot', snapshotId },
|
|
280
|
+
...(liveName != null ? { name: liveName } : {}),
|
|
281
|
+
...(abortSignal ? { signal: abortSignal } : {}),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function pollForTemplateSnapshot({
|
|
286
|
+
name,
|
|
287
|
+
lookupParams,
|
|
288
|
+
abortSignal,
|
|
289
|
+
}: {
|
|
290
|
+
name: string;
|
|
291
|
+
lookupParams: SandboxLookupParams;
|
|
292
|
+
abortSignal: AbortSignal | undefined;
|
|
293
|
+
}): Promise<string> {
|
|
294
|
+
const deadline = Date.now() + SNAPSHOT_POLL_TIMEOUT_MS;
|
|
295
|
+
while (Date.now() < deadline) {
|
|
296
|
+
abortSignal?.throwIfAborted();
|
|
297
|
+
const refreshed = await Sandbox.get({
|
|
298
|
+
...lookupParams,
|
|
299
|
+
name,
|
|
300
|
+
resume: false,
|
|
301
|
+
...(abortSignal ? { signal: abortSignal } : {}),
|
|
302
|
+
});
|
|
303
|
+
if (refreshed.currentSnapshotId) {
|
|
304
|
+
return refreshed.currentSnapshotId;
|
|
305
|
+
}
|
|
306
|
+
await new Promise<void>(resolve =>
|
|
307
|
+
setTimeout(resolve, SNAPSHOT_POLL_INTERVAL_MS),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Timed out waiting for snapshot of template "${name}" to publish.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HarnessV1NetworkSandboxSession,
|
|
3
|
+
HarnessV1SandboxProvider,
|
|
4
|
+
} from '@ai-sdk/harness';
|
|
5
|
+
import type { Experimental_SandboxSession as SandboxSession } from '@ai-sdk/provider-utils';
|
|
6
|
+
import { Sandbox } from '@vercel/sandbox';
|
|
7
|
+
import { VercelNetworkSandboxSession } from './vercel-network-sandbox-session';
|
|
8
|
+
import { VercelSandboxSession } from './vercel-sandbox-session';
|
|
9
|
+
import {
|
|
10
|
+
VERCEL_PROVIDER_ID,
|
|
11
|
+
createLiveSandboxFromSnapshot,
|
|
12
|
+
ensureTemplateSnapshot,
|
|
13
|
+
getSandboxLookupParams,
|
|
14
|
+
withVercelSandboxAuthenticationError,
|
|
15
|
+
withDefaultSandboxSettings,
|
|
16
|
+
type BaseCreateSandboxParams,
|
|
17
|
+
} from './utils';
|
|
18
|
+
|
|
19
|
+
type VercelSandboxCreateParams = NonNullable<
|
|
20
|
+
Parameters<typeof Sandbox.create>[0]
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
type DistributiveOmit<T, K extends keyof any> = T extends unknown
|
|
24
|
+
? Omit<T, K>
|
|
25
|
+
: never;
|
|
26
|
+
|
|
27
|
+
type VercelSandboxCreationSettings = DistributiveOmit<
|
|
28
|
+
VercelSandboxCreateParams,
|
|
29
|
+
'onResume'
|
|
30
|
+
> & {
|
|
31
|
+
sandbox?: never;
|
|
32
|
+
name?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Settings for `createVercelSandbox`. Two mutually-exclusive shapes:
|
|
37
|
+
*
|
|
38
|
+
* - `{ sandbox }` — wrap an already-created `@vercel/sandbox` `Sandbox`. The
|
|
39
|
+
* caller owns its lifecycle; the provider's `stop()` and `destroy()` are
|
|
40
|
+
* no-ops.
|
|
41
|
+
* - {@link VercelSandboxCreateParams} fields — provider creates the underlying
|
|
42
|
+
* sandbox. When the adapter declares a bootstrap recipe the provider uses
|
|
43
|
+
* `Sandbox.getOrCreate` to maintain a persistent named template snapshot
|
|
44
|
+
* keyed by the recipe identity, and forks an ephemeral sandbox per session
|
|
45
|
+
* from the snapshot. Use `name` to override the auto-derived template name.
|
|
46
|
+
*
|
|
47
|
+
* @deprecated Use `VercelNetworkSandboxSessionCreateOptions` for new sessions.
|
|
48
|
+
*/
|
|
49
|
+
export type VercelSandboxSettings =
|
|
50
|
+
| { sandbox: Sandbox }
|
|
51
|
+
| VercelSandboxCreationSettings;
|
|
52
|
+
|
|
53
|
+
const SESSION_NAME_PREFIX = 'ai-sdk-harness-session';
|
|
54
|
+
|
|
55
|
+
function sessionSandboxName(sessionId: string): string {
|
|
56
|
+
return `${SESSION_NAME_PREFIX}-${sessionId}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @deprecated Use `createVercelNetworkSandboxSession` instead. */
|
|
60
|
+
export function createVercelSandbox(
|
|
61
|
+
settings: VercelSandboxSettings = {} as VercelSandboxSettings,
|
|
62
|
+
): HarnessV1SandboxProvider {
|
|
63
|
+
console.warn(
|
|
64
|
+
'createVercelSandbox is deprecated. Use createVercelNetworkSandboxSession and HarnessAgent.createSession({ sandboxSession }) instead.',
|
|
65
|
+
);
|
|
66
|
+
return new VercelSandboxProvider(settings);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `HarnessV1SandboxProvider` implementation backed by `@vercel/sandbox`.
|
|
71
|
+
* Construct one via {@link createVercelSandbox} at module scope and pass it
|
|
72
|
+
* to a `HarnessAgent` (or call `createSession()` directly if you want raw
|
|
73
|
+
* access to a network sandbox session).
|
|
74
|
+
*/
|
|
75
|
+
/** @deprecated Use `createVercelNetworkSandboxSession` instead. */
|
|
76
|
+
export class VercelSandboxProvider implements HarnessV1SandboxProvider {
|
|
77
|
+
readonly specificationVersion = 'harness-sandbox-v1' as const;
|
|
78
|
+
readonly providerId = VERCEL_PROVIDER_ID;
|
|
79
|
+
|
|
80
|
+
constructor(private readonly settings: VercelSandboxSettings) {
|
|
81
|
+
console.warn(
|
|
82
|
+
'VercelSandboxProvider is deprecated. Use createVercelNetworkSandboxSession instead.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
createSession = async (options?: {
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
abortSignal?: AbortSignal;
|
|
89
|
+
identity?: string;
|
|
90
|
+
onFirstCreate?: (
|
|
91
|
+
session: SandboxSession,
|
|
92
|
+
opts: { abortSignal?: AbortSignal },
|
|
93
|
+
) => Promise<void>;
|
|
94
|
+
}): Promise<HarnessV1NetworkSandboxSession> => {
|
|
95
|
+
options?.abortSignal?.throwIfAborted();
|
|
96
|
+
|
|
97
|
+
if ('sandbox' in this.settings && this.settings.sandbox != null) {
|
|
98
|
+
return new VercelNetworkSandboxSession({
|
|
99
|
+
sandbox: this.settings.sandbox,
|
|
100
|
+
ownsLifecycle: false,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type CreateNewBranch = BaseCreateSandboxParams & {
|
|
105
|
+
sandbox?: never;
|
|
106
|
+
name?: string;
|
|
107
|
+
};
|
|
108
|
+
const settings = this.settings as CreateNewBranch;
|
|
109
|
+
const {
|
|
110
|
+
sandbox: _ignoredSandbox,
|
|
111
|
+
name: explicitName,
|
|
112
|
+
...createParams
|
|
113
|
+
} = settings;
|
|
114
|
+
// Sandbox v3 changed its implicit default from the Node 24 runtime to the
|
|
115
|
+
// Universal managed image. Keep this adapter's existing default stable while
|
|
116
|
+
// allowing callers to opt into managed images explicitly.
|
|
117
|
+
const baseParams = withDefaultSandboxSettings(
|
|
118
|
+
createParams as BaseCreateSandboxParams,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const identity = options?.identity;
|
|
122
|
+
const onFirstCreate = options?.onFirstCreate;
|
|
123
|
+
|
|
124
|
+
// When sessionId is supplied, name the per-session sandbox deterministically
|
|
125
|
+
// so a future `resumeSession({ sessionId })` can locate it via
|
|
126
|
+
// `Sandbox.get({ name })`. Absent sessionId (e.g. prewarm), fall back to
|
|
127
|
+
// Vercel's auto-naming.
|
|
128
|
+
const sessionNameOverride = options?.sessionId
|
|
129
|
+
? { name: sessionSandboxName(options.sessionId) }
|
|
130
|
+
: {};
|
|
131
|
+
|
|
132
|
+
if (identity == null || onFirstCreate == null) {
|
|
133
|
+
const sandbox = await withVercelSandboxAuthenticationError({
|
|
134
|
+
settings: this.settings,
|
|
135
|
+
operation: () =>
|
|
136
|
+
Sandbox.create({
|
|
137
|
+
...baseParams,
|
|
138
|
+
...sessionNameOverride,
|
|
139
|
+
...(options?.abortSignal ? { signal: options.abortSignal } : {}),
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
return new VercelNetworkSandboxSession({ sandbox, ownsLifecycle: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const templateName = explicitName ?? `ai-sdk-harness-${identity}`;
|
|
146
|
+
const snapshotId = await withVercelSandboxAuthenticationError({
|
|
147
|
+
settings: this.settings,
|
|
148
|
+
operation: () =>
|
|
149
|
+
ensureTemplateSnapshot({
|
|
150
|
+
baseParams,
|
|
151
|
+
templateName,
|
|
152
|
+
lookupParams: getSandboxLookupParams(baseParams),
|
|
153
|
+
abortSignal: options?.abortSignal,
|
|
154
|
+
snapshotCache: getSnapshotCache(),
|
|
155
|
+
onCreate: async sbx => {
|
|
156
|
+
await onFirstCreate(new VercelSandboxSession(sbx), {
|
|
157
|
+
abortSignal: options?.abortSignal,
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const fork = await withVercelSandboxAuthenticationError({
|
|
164
|
+
settings: this.settings,
|
|
165
|
+
operation: () =>
|
|
166
|
+
createLiveSandboxFromSnapshot({
|
|
167
|
+
baseParams,
|
|
168
|
+
snapshotId,
|
|
169
|
+
liveName: sessionNameOverride.name,
|
|
170
|
+
abortSignal: options?.abortSignal,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
return new VercelNetworkSandboxSession({
|
|
174
|
+
sandbox: fork,
|
|
175
|
+
ownsLifecycle: true,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
resumeSession = async (options: {
|
|
180
|
+
sessionId: string;
|
|
181
|
+
abortSignal?: AbortSignal;
|
|
182
|
+
}): Promise<HarnessV1NetworkSandboxSession> => {
|
|
183
|
+
options.abortSignal?.throwIfAborted();
|
|
184
|
+
|
|
185
|
+
// Wrap-existing case: caller owns the sandbox. Same session as createSession.
|
|
186
|
+
if ('sandbox' in this.settings && this.settings.sandbox != null) {
|
|
187
|
+
return new VercelNetworkSandboxSession({
|
|
188
|
+
sandbox: this.settings.sandbox,
|
|
189
|
+
ownsLifecycle: false,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sandbox = await withVercelSandboxAuthenticationError({
|
|
194
|
+
settings: this.settings,
|
|
195
|
+
operation: () =>
|
|
196
|
+
Sandbox.get({
|
|
197
|
+
...getSandboxLookupParams(this.settings),
|
|
198
|
+
name: sessionSandboxName(options.sessionId),
|
|
199
|
+
...(options.abortSignal ? { signal: options.abortSignal } : {}),
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
return new VercelNetworkSandboxSession({ sandbox, ownsLifecycle: true });
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const SNAPSHOT_CACHE_KEY = Symbol.for(
|
|
207
|
+
'ai-sdk.harness.vercel-template-snapshots',
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
type SnapshotCache = Map<string, string>;
|
|
211
|
+
function getSnapshotCache(): SnapshotCache {
|
|
212
|
+
const globals = globalThis as {
|
|
213
|
+
[SNAPSHOT_CACHE_KEY]?: SnapshotCache;
|
|
214
|
+
};
|
|
215
|
+
let cache = globals[SNAPSHOT_CACHE_KEY];
|
|
216
|
+
if (cache == null) {
|
|
217
|
+
cache = new Map();
|
|
218
|
+
globals[SNAPSHOT_CACHE_KEY] = cache;
|
|
219
|
+
}
|
|
220
|
+
return cache;
|
|
221
|
+
}
|
|
@@ -14,11 +14,11 @@ const VERCEL_PROVIDER_ID = 'vercel-sandbox';
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* `HarnessV1NetworkSandboxSession` backed by a `@vercel/sandbox` `Sandbox`. The
|
|
17
|
-
*
|
|
18
|
-
* {@link VercelSandboxSession} with
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* native adaptation and sandbox creation functions return one of these. It
|
|
18
|
+
* extends {@link VercelSandboxSession} with ports, lifecycle, and network
|
|
19
|
+
* policy. Explicit `stop()` and `destroy()` calls delegate to the native
|
|
20
|
+
* sandbox unless the session was wrapped through the compatibility provider's
|
|
21
|
+
* existing-sandbox path, where those calls are intentionally no-ops.
|
|
22
22
|
*/
|
|
23
23
|
export class VercelNetworkSandboxSession
|
|
24
24
|
extends VercelSandboxSession
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { posix } from 'node:path';
|
|
2
2
|
import {
|
|
3
3
|
extractLines,
|
|
4
|
-
type Experimental_SandboxSession,
|
|
5
|
-
type Experimental_SandboxProcess,
|
|
4
|
+
type Experimental_SandboxSession as SandboxSession,
|
|
5
|
+
type Experimental_SandboxProcess as SandboxProcess,
|
|
6
6
|
} from '@ai-sdk/provider-utils';
|
|
7
7
|
import type { Sandbox, Command } from '@vercel/sandbox';
|
|
8
8
|
|
|
@@ -13,7 +13,7 @@ import type { Sandbox, Command } from '@vercel/sandbox';
|
|
|
13
13
|
* constructed directly by consumers. The network sandbox session owns the
|
|
14
14
|
* lifetime of the underlying sandbox.
|
|
15
15
|
*/
|
|
16
|
-
export class VercelSandboxSession implements
|
|
16
|
+
export class VercelSandboxSession implements SandboxSession {
|
|
17
17
|
constructor(protected readonly sandbox: Sandbox) {}
|
|
18
18
|
|
|
19
19
|
get description(): string {
|
|
@@ -66,7 +66,7 @@ export class VercelSandboxSession implements Experimental_SandboxSession {
|
|
|
66
66
|
workingDirectory?: string;
|
|
67
67
|
env?: Record<string, string>;
|
|
68
68
|
abortSignal?: AbortSignal;
|
|
69
|
-
}): Promise<
|
|
69
|
+
}): Promise<SandboxProcess> {
|
|
70
70
|
abortSignal?.throwIfAborted();
|
|
71
71
|
|
|
72
72
|
const live = await this.sandbox.runCommand({
|
|
@@ -201,7 +201,7 @@ export class VercelSandboxSession implements Experimental_SandboxSession {
|
|
|
201
201
|
function createSandboxProcess(
|
|
202
202
|
command: Command,
|
|
203
203
|
abortSignal: AbortSignal | undefined,
|
|
204
|
-
):
|
|
204
|
+
): SandboxProcess {
|
|
205
205
|
const encoder = new TextEncoder();
|
|
206
206
|
const controllers: {
|
|
207
207
|
stdout?: ReadableStreamDefaultController<Uint8Array>;
|