withruntime 0.4.0 → 0.5.1

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.
Files changed (88) hide show
  1. package/DAYTONA.md +129 -0
  2. package/E2B.md +3 -3
  3. package/README.md +47 -9
  4. package/VERCEL.md +127 -0
  5. package/dist/ai/index.d.ts +33 -0
  6. package/dist/ai/index.js +38 -0
  7. package/dist/claude-agent-sdk/index.d.ts +7 -0
  8. package/dist/claude-agent-sdk/index.js +65 -0
  9. package/dist/cli.d.ts +6 -0
  10. package/dist/cli.js +959 -102
  11. package/dist/client.d.ts +25 -2
  12. package/dist/client.js +14 -1
  13. package/dist/credentials.d.ts +10 -0
  14. package/dist/credentials.js +56 -1
  15. package/dist/daytona/client.d.ts +44 -0
  16. package/dist/daytona/client.js +40 -0
  17. package/dist/daytona/context.d.ts +31 -0
  18. package/dist/daytona/context.js +25 -0
  19. package/dist/daytona/daytona.d.ts +185 -0
  20. package/dist/daytona/daytona.js +416 -0
  21. package/dist/daytona/errors.d.ts +106 -0
  22. package/dist/daytona/errors.js +182 -0
  23. package/dist/daytona/filesystem.d.ts +84 -0
  24. package/dist/daytona/filesystem.js +206 -0
  25. package/dist/daytona/git.d.ts +33 -0
  26. package/dist/daytona/git.js +128 -0
  27. package/dist/daytona/image.d.ts +45 -0
  28. package/dist/daytona/image.js +140 -0
  29. package/dist/daytona/index.d.ts +50 -0
  30. package/dist/daytona/index.js +50 -0
  31. package/dist/daytona/process.d.ts +86 -0
  32. package/dist/daytona/process.js +341 -0
  33. package/dist/daytona/sandbox.d.ts +196 -0
  34. package/dist/daytona/sandbox.js +448 -0
  35. package/dist/e2b/commands.js +6 -2
  36. package/dist/e2b/sandbox.d.ts +22 -2
  37. package/dist/e2b/sandbox.js +39 -7
  38. package/dist/errors.d.ts +10 -4
  39. package/dist/errors.js +26 -6
  40. package/dist/image-context.d.ts +39 -0
  41. package/dist/image-context.js +176 -0
  42. package/dist/index.d.ts +19 -2
  43. package/dist/index.js +17 -2
  44. package/dist/keys.d.ts +43 -0
  45. package/dist/keys.js +196 -0
  46. package/dist/login.js +14 -2
  47. package/dist/openai-agents/index.d.ts +132 -0
  48. package/dist/openai-agents/index.js +791 -0
  49. package/dist/products/audit.d.ts +48 -0
  50. package/dist/products/audit.js +10 -0
  51. package/dist/products/images.d.ts +113 -7
  52. package/dist/products/images.js +186 -27
  53. package/dist/products/index.d.ts +11 -0
  54. package/dist/products/index.js +17 -1
  55. package/dist/products/observability.d.ts +186 -0
  56. package/dist/products/observability.js +117 -0
  57. package/dist/products/secrets.d.ts +40 -0
  58. package/dist/products/secrets.js +24 -0
  59. package/dist/products/switching.d.ts +96 -0
  60. package/dist/products/switching.js +25 -0
  61. package/dist/sandbox.d.ts +26 -1
  62. package/dist/sandbox.js +101 -1
  63. package/dist/ssh.d.ts +21 -0
  64. package/dist/ssh.js +324 -0
  65. package/dist/tar.d.ts +1 -0
  66. package/dist/tar.js +4 -4
  67. package/dist/tools/index.d.ts +62 -0
  68. package/dist/tools/index.js +180 -0
  69. package/dist/transport.d.ts +12 -2
  70. package/dist/transport.js +31 -4
  71. package/dist/tunnel.d.ts +70 -0
  72. package/dist/tunnel.js +316 -0
  73. package/dist/types.d.ts +57 -1
  74. package/dist/vercel/client.d.ts +40 -0
  75. package/dist/vercel/client.js +39 -0
  76. package/dist/vercel/command.d.ts +70 -0
  77. package/dist/vercel/command.js +148 -0
  78. package/dist/vercel/errors.d.ts +53 -0
  79. package/dist/vercel/errors.js +113 -0
  80. package/dist/vercel/filesystem.d.ts +150 -0
  81. package/dist/vercel/filesystem.js +248 -0
  82. package/dist/vercel/index.d.ts +30 -0
  83. package/dist/vercel/index.js +38 -0
  84. package/dist/vercel/sandbox.d.ts +317 -0
  85. package/dist/vercel/sandbox.js +726 -0
  86. package/dist/vercel/snapshot.d.ts +93 -0
  87. package/dist/vercel/snapshot.js +123 -0
  88. package/package.json +56 -4
@@ -0,0 +1,726 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ import { clientFor, signalOf, } from "./client.js";
3
+ import { Command, CommandFinished, exitCodeOf } from "./command.js";
4
+ import { APIError, guard, NotSupportedError, translate } from "./errors.js";
5
+ import { FileSystem } from "./filesystem.js";
6
+ import { Snapshot } from "./snapshot.js";
7
+ /* Vercel Sandbox's `Sandbox` over Runtime's. Every call is one or a few calls
8
+ of the withruntime SDK; nothing here speaks HTTP. What Runtime cannot do the
9
+ same way throws NotSupportedError before anything happens. */
10
+ /** Vercel's default machine: 2 vCPUs with 2048 MB each (checked 23 September
11
+ * 2026). Runtime gives 2 vCPU and 4096 MiB. */
12
+ export const DEFAULT_VCPUS = 2;
13
+ export const MEMORY_MIB_PER_VCPU = 2048;
14
+ /** Vercel's default timeout, 5 minutes. */
15
+ export const DEFAULT_TIMEOUT_MS = 300_000;
16
+ const MIN_LEASE_SECONDS = 60;
17
+ const MAX_LEASE_SECONDS = 3600;
18
+ /** Commands without a timeout run until the sandbox ends: Runtime's longest. */
19
+ const LONGEST_MS = 86_400_000;
20
+ /** Vercel's working directory; `/workspace` on Runtime. */
21
+ export const HOME = "/vercel/sandbox";
22
+ const RUNTIME_HOME = "/workspace";
23
+ const HOME_LINK = "[ -e /vercel/sandbox ] || { sudo mkdir -p /vercel && sudo ln -s /workspace /vercel/sandbox; }";
24
+ /** Vercel's managed images and legacy runtimes: all Runtime's stock image. */
25
+ const STOCK_IMAGE = /^(vercel\/sandbox\/)?(universal|ubuntu|node:2[246]|python:3\.1[34])(:latest)?$/;
26
+ const LEGACY_RUNTIMES = new Set(["node22", "node24", "node26", "python3.13"]);
27
+ /** Vercel regions in the United States, where Runtime runs. */
28
+ const US_REGIONS = new Set(["iad1", "sfo1", "cle1", "pdx1"]);
29
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
30
+ function leaseSeconds(timeoutMs) {
31
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
32
+ throw new RangeError(`timeout must be a positive number of milliseconds, not ${timeoutMs}.`);
33
+ const seconds = Math.ceil(timeoutMs / 1000);
34
+ if (seconds > MAX_LEASE_SECONDS)
35
+ throw new NotSupportedError(`A sandbox timeout of ${timeoutMs} ms (over one hour)`, "Runtime leases last up to an hour (3_600_000 ms); call sandbox.extendTimeout(ms) before it ends to keep going, as often as needed.");
36
+ return Math.max(seconds, MIN_LEASE_SECONDS);
37
+ }
38
+ function refuseCreate(params) {
39
+ if (params.mounts && Object.keys(params.mounts).length)
40
+ throw new NotSupportedError("Vercel Drives (mounts)", "Use a Runtime volume: withruntime: { create: { volumes: [{ volumeId, path }] } } (runtime.volumes creates one).");
41
+ if (params.networkId !== undefined)
42
+ throw new NotSupportedError("Secure Compute networks (networkId)", "Remove it; restrict outbound traffic with networkPolicy instead.");
43
+ if (params.region !== undefined && !US_REGIONS.has(params.region))
44
+ throw new NotSupportedError(`The region ${params.region}`, "Runtime runs in one US region (east). Remove region, or use iad1.");
45
+ if (params.failoverRegions?.length)
46
+ throw new NotSupportedError("Failover regions (failoverRegions)", "Remove it: Runtime runs in one US region.");
47
+ }
48
+ /** Vercel's network policy as Runtime's rules. Header injection and
49
+ * forwarding rules have no Runtime counterpart and are refused. */
50
+ export function networkRules(policy) {
51
+ if (policy === "allow-all")
52
+ return { internet: true };
53
+ if (policy === "deny-all")
54
+ return { internet: false };
55
+ const allow = policy.allow;
56
+ let domains = [];
57
+ if (Array.isArray(allow))
58
+ domains = allow;
59
+ else if (allow) {
60
+ for (const [domain, rules] of Object.entries(allow)) {
61
+ if (rules.length)
62
+ throw new NotSupportedError(`Network rules that transform or forward requests (for ${domain})`, "Allow the domain with an empty rule list and pass credentials to the command as env instead.");
63
+ domains.push(domain);
64
+ }
65
+ }
66
+ const wildcard = domains.includes("*");
67
+ const listed = [...domains.filter((one) => one !== "*"), ...(policy.subnets?.allow ?? [])];
68
+ // A custom policy reaches only what it allows: with nothing allowed, nothing.
69
+ if (!wildcard && !listed.length)
70
+ return { internet: false };
71
+ return {
72
+ internet: true,
73
+ ...(!wildcard && listed.length ? { allow: listed } : {}),
74
+ ...(policy.subnets?.deny?.length ? { deny: policy.subnets.deny } : {}),
75
+ };
76
+ }
77
+ /** Where an image or legacy runtime sends the create: the stock image, or a
78
+ * Runtime image by name or id. */
79
+ async function resolveImage(client, params) {
80
+ if (params.source?.type === "snapshot")
81
+ return { snapshot: params.source.snapshotId };
82
+ if (params.runtime !== undefined && !LEGACY_RUNTIMES.has(params.runtime))
83
+ throw new NotSupportedError(`The Vercel runtime ${params.runtime}`, "Leave runtime out: Runtime's stock image has Node.js 24, Bun and Python 3.12.");
84
+ const image = params.image;
85
+ if (image === undefined || STOCK_IMAGE.test(image))
86
+ return {};
87
+ if (UUID.test(image))
88
+ return { image };
89
+ const page = await client.images.list({ name: image, state: "ready", limit: 1 });
90
+ const found = page.data[0];
91
+ if (found)
92
+ return { image: found.id };
93
+ throw new NotSupportedError(`The image ${image}, which is not a Runtime image`, `Build it as a Runtime image with that name and this call starts from it: \`npx withruntime image build --dockerfile Dockerfile --name ${image}\`, or runtime.images.build({ name: "${image}", image: "<registry reference>" }).`);
94
+ }
95
+ /** Runtime's state as Vercel's status: a paused sandbox is a stopped
96
+ * persistent one. */
97
+ function statusOf(state) {
98
+ switch (state) {
99
+ case "starting":
100
+ return "pending";
101
+ case "running":
102
+ case "resuming":
103
+ return "running";
104
+ case "pausing":
105
+ case "stopping":
106
+ return "stopping";
107
+ default:
108
+ return "stopped";
109
+ }
110
+ }
111
+ /** Runtime's `/workspace` stands for Vercel's `/vercel/sandbox`: relative
112
+ * paths resolve against it, and paths under /vercel/sandbox are rewritten. */
113
+ export function toRuntimePath(path, cwd) {
114
+ const base = cwd === undefined ? HOME : toVercelPath(cwd);
115
+ const absolute = path.startsWith("/") ? path : `${base}/${path.replace(/^\.\/?/, "")}`;
116
+ const normal = absolute.replace(/\/+$/, "") || "/";
117
+ if (normal === HOME || normal.startsWith(`${HOME}/`))
118
+ return RUNTIME_HOME + normal.slice(HOME.length);
119
+ return normal;
120
+ }
121
+ function toVercelPath(path) {
122
+ return path.startsWith("/") ? path : `${HOME}/${path.replace(/^\.\/?/, "")}`;
123
+ }
124
+ /** A sandbox with Vercel's methods. `sandbox.withruntime` is the Runtime
125
+ * sandbox underneath, for anything Vercel has no name for. */
126
+ export class Sandbox {
127
+ fs;
128
+ #rt;
129
+ #client;
130
+ #env;
131
+ #persistent;
132
+ #routes = new Map();
133
+ #onResume;
134
+ #linked;
135
+ #deleted = false;
136
+ /** Use Sandbox.create, Sandbox.get or Sandbox.getOrCreate. */
137
+ constructor(resolved) {
138
+ this.#rt = resolved.runtime;
139
+ this.#client = resolved.client;
140
+ this.#env = { ...resolved.params.env };
141
+ this.#persistent = resolved.params.persistent ?? resolved.runtime.info.onLeaseEnd !== "stop";
142
+ this.#onResume = resolved.params.onResume;
143
+ this.fs = new FileSystem({
144
+ name: this.name,
145
+ files: async () => (await this.#live()).files,
146
+ run: async (argv) => {
147
+ const live = await this.#live();
148
+ return live.exec(argv);
149
+ },
150
+ resolve: (path, cwd) => toRuntimePath(path, cwd),
151
+ });
152
+ }
153
+ /** The Runtime sandbox underneath: previews, network, snapshots, desktop,
154
+ * interpreter, processes and the rest of Runtime's SDK. */
155
+ get withruntime() {
156
+ return this.#rt;
157
+ }
158
+ /** The sandbox's name, or its Runtime id when it was made without one. */
159
+ get name() {
160
+ return this.#rt.info.name ?? this.#rt.id;
161
+ }
162
+ get routes() {
163
+ return [...this.#routes].map(([port, url]) => ({
164
+ port,
165
+ subdomain: new URL(url).hostname.split(".")[0],
166
+ url,
167
+ }));
168
+ }
169
+ get persistent() {
170
+ return this.#persistent;
171
+ }
172
+ get region() {
173
+ return "iad1";
174
+ }
175
+ get failoverRegions() {
176
+ return [];
177
+ }
178
+ get networkId() {
179
+ return undefined;
180
+ }
181
+ get vcpus() {
182
+ return this.#rt.info.vcpu;
183
+ }
184
+ /** Megabytes. */
185
+ get memory() {
186
+ return this.#rt.info.memoryMiB;
187
+ }
188
+ get runtime() {
189
+ return undefined;
190
+ }
191
+ get image() {
192
+ const image = this.#rt.info.image;
193
+ return typeof image === "string" ? image : undefined;
194
+ }
195
+ get createdAt() {
196
+ return new Date(this.#rt.info.createdAt);
197
+ }
198
+ get updatedAt() {
199
+ return new Date(this.#rt.info.pausedAt ?? this.#rt.info.readyAt ?? this.#rt.info.createdAt);
200
+ }
201
+ get cwd() {
202
+ return HOME;
203
+ }
204
+ get status() {
205
+ return statusOf(this.#rt.state);
206
+ }
207
+ /** Milliseconds. */
208
+ get timeout() {
209
+ return this.#rt.info.timeoutSeconds * 1000;
210
+ }
211
+ get expiresAt() {
212
+ return new Date(this.#rt.info.expiresAt);
213
+ }
214
+ get tags() {
215
+ return { ...this.#rt.info.labels };
216
+ }
217
+ get sourceSnapshotId() {
218
+ const snapshot = this.#rt.info.snapshot;
219
+ return typeof snapshot === "string" ? snapshot : undefined;
220
+ }
221
+ // ---- create, get, list ------------------------------------------------
222
+ /** Creates a sandbox and waits until it runs. Vercel's defaults: 2 vCPUs,
223
+ * 2048 MiB per vCPU, a 5-minute timeout, persistent. Funding is left to
224
+ * Runtime: the free trial while the account has trial time, then prepaid
225
+ * credit, exactly as withruntime's own create. */
226
+ static async create(params = {}) {
227
+ refuseCreate(params);
228
+ const client = clientFor(params);
229
+ const persistent = params.persistent ?? true;
230
+ const vcpus = params.resources?.vcpus ?? DEFAULT_VCPUS;
231
+ const source = await guard(() => resolveImage(client, params));
232
+ const input = {
233
+ ...(source.snapshot ? {} : { vcpu: vcpus, memoryMiB: vcpus * MEMORY_MIB_PER_VCPU }),
234
+ timeoutSeconds: leaseSeconds(params.timeout ?? DEFAULT_TIMEOUT_MS),
235
+ onLeaseEnd: persistent ? "pause" : "stop",
236
+ ...(params.name ? { name: params.name } : {}),
237
+ ...(params.tags && Object.keys(params.tags).length ? { labels: params.tags } : {}),
238
+ ...(params.networkPolicy ? { network: networkRules(params.networkPolicy) } : {}),
239
+ ...source,
240
+ ...params.withruntime?.create,
241
+ };
242
+ const runtime = await guard(() => client.sandboxes.create(input, signalOf(params)));
243
+ const sandbox = new Sandbox({ runtime, client, params: { ...params, persistent } });
244
+ try {
245
+ await sandbox.#setUp(params);
246
+ }
247
+ catch (error) {
248
+ await runtime.stop({ wait: false }).catch(() => undefined);
249
+ throw translate(error, sandbox.name);
250
+ }
251
+ return sandbox;
252
+ }
253
+ async #setUp(params) {
254
+ for (const port of params.ports ?? [])
255
+ await this.#share(port);
256
+ if (params.snapshotExpiration !== undefined && this.#persistent)
257
+ await this.#rt.setRetention(retentionDays(params.snapshotExpiration));
258
+ const source = params.source;
259
+ if (source?.type === "git") {
260
+ const argv = [
261
+ "git",
262
+ ...(source.username !== undefined
263
+ ? [
264
+ "-c",
265
+ 'credential.helper=!f() { echo "username=$GIT_USER"; echo "password=$GIT_PASS"; }; f',
266
+ ]
267
+ : []),
268
+ "clone",
269
+ ...(source.depth ? ["--depth", String(source.depth)] : []),
270
+ ...(source.revision && source.depth ? ["--no-single-branch"] : []),
271
+ "--",
272
+ source.url,
273
+ RUNTIME_HOME,
274
+ ];
275
+ const env = source.username !== undefined
276
+ ? { GIT_USER: source.username, GIT_PASS: source.password ?? "" }
277
+ : undefined;
278
+ await this.#setupStep(argv, env);
279
+ if (source.revision)
280
+ await this.#setupStep(["git", "-C", RUNTIME_HOME, "checkout", source.revision], env);
281
+ }
282
+ else if (source?.type === "tarball") {
283
+ await this.#setupStep([
284
+ "sh",
285
+ "-c",
286
+ 'curl -fsSL "$1" | tar -xz -C /workspace',
287
+ "sh",
288
+ source.url,
289
+ ]);
290
+ }
291
+ }
292
+ async #setupStep(argv, env) {
293
+ const result = await this.#rt.exec(argv, { timeoutMs: 600_000, ...(env ? { env } : {}) });
294
+ if (result.exitCode !== 0)
295
+ throw new APIError(new Response(null, { status: 400 }), {
296
+ message: `Setting up the sandbox's source failed (${argv.slice(0, 3).join(" ")}): ${result.stderr.trim()}`,
297
+ sandboxName: this.name,
298
+ });
299
+ }
300
+ /** A sandbox by name (or Runtime id), woken if it is paused unless
301
+ * `resume: false`. */
302
+ static async get(params) {
303
+ const client = clientFor(params);
304
+ const runtime = await guard(() => find(client, params.name), params.name);
305
+ const sandbox = new Sandbox({
306
+ runtime,
307
+ client,
308
+ params: params.onResume ? { onResume: params.onResume } : {},
309
+ });
310
+ if (params.resume !== false)
311
+ await sandbox.#live();
312
+ return sandbox;
313
+ }
314
+ /** The named sandbox, or a new one with that name when there is none. */
315
+ static async getOrCreate(params = {}) {
316
+ if (params.name !== undefined) {
317
+ try {
318
+ return await Sandbox.get({ ...params, name: params.name });
319
+ }
320
+ catch (error) {
321
+ if (!(error instanceof APIError && error.response.status === 404))
322
+ throw error;
323
+ }
324
+ }
325
+ const sandbox = await Sandbox.create(params);
326
+ await params.onCreate?.(sandbox);
327
+ return sandbox;
328
+ }
329
+ /** A new sandbox copied from a running one, memory and all (a Runtime
330
+ * fork). `sourceSandbox` is its name. */
331
+ static async fork(params) {
332
+ const unsupported = ["resources", "ports", "timeout", "networkPolicy", "image", "tags", "env"].find((field) => params[field] !== undefined);
333
+ if (unsupported)
334
+ throw new NotSupportedError(`Overriding ${unsupported} on a fork`, "Fork without it; the copy keeps the source's machine, lease and rules.");
335
+ refuseCreate(params);
336
+ const source = await Sandbox.get({ ...params, name: params.sourceSandbox });
337
+ const copy = await guard(() => source.#rt.fork({ ...(params.name ? { name: params.name } : {}), ...signalOf(params) }), params.sourceSandbox);
338
+ return new Sandbox({
339
+ runtime: copy,
340
+ client: source.#client,
341
+ params: { persistent: source.#persistent, env: source.#env },
342
+ });
343
+ }
344
+ /** Running and stopped (paused) sandboxes, oldest first, with Vercel's
345
+ * paginator: `for await (const s of await Sandbox.list())`. */
346
+ static async list(params = {}) {
347
+ for (const field of ["since", "until", "cursor", "sortBy", "sortOrder", "namePrefix"])
348
+ if (params[field] !== undefined)
349
+ throw new NotSupportedError(`Listing sandboxes by ${field}`, "List them all (oldest first) and filter the result yourself.");
350
+ const client = clientFor(params);
351
+ const page = await guard(() => client.sandboxes.list({
352
+ ...(params.tags ? { labels: params.tags } : {}),
353
+ ...(params.limit ? { limit: Math.min(params.limit, 100) } : {}),
354
+ }));
355
+ return paginate(page);
356
+ }
357
+ // ---- commands ---------------------------------------------------------
358
+ /** Wakes the sandbox if it was paused (a stopped persistent sandbox, in
359
+ * Vercel's words), as Vercel resumes one on the next call. */
360
+ async #live() {
361
+ if (this.#deleted)
362
+ throw new APIError(new Response(null, { status: 410 }), {
363
+ message: `Sandbox ${this.name} was deleted.`,
364
+ sandboxName: this.name,
365
+ });
366
+ const state = this.#rt.state;
367
+ if (state === "paused" || state === "pausing")
368
+ await this.#wake();
369
+ return this.#rt;
370
+ }
371
+ async #wake() {
372
+ await guard(() => this.#rt.wake({}), this.name);
373
+ await this.#onResume?.(this);
374
+ }
375
+ /** Runs `work` on the live sandbox; if the lease paused it meanwhile, wakes
376
+ * it and runs `work` once more. */
377
+ async #withResume(work) {
378
+ const runtime = await this.#live();
379
+ try {
380
+ return await work(runtime);
381
+ }
382
+ catch (error) {
383
+ if (!(error instanceof RuntimeError && error.code === "sandbox_paused"))
384
+ throw translate(error, this.name);
385
+ await guard(() => runtime.refresh(), this.name);
386
+ await this.#wake();
387
+ return guard(() => work(this.#rt), this.name);
388
+ }
389
+ }
390
+ async #linkHome(text) {
391
+ if (this.#linked || !text.includes(HOME))
392
+ return this.#linked;
393
+ this.#linked = (async () => {
394
+ await this.#rt.exec(HOME_LINK).catch(() => undefined);
395
+ })();
396
+ return this.#linked;
397
+ }
398
+ async runCommand(commandOrParams, args, opts = {}) {
399
+ const params = typeof commandOrParams === "string"
400
+ ? { cmd: commandOrParams, ...(args ? { args } : {}), ...opts }
401
+ : commandOrParams;
402
+ const argv = [
403
+ ...(params.sudo ? ["sudo", "--preserve-env"] : []),
404
+ params.cmd,
405
+ ...(params.args ?? []),
406
+ ];
407
+ const cwd = params.cwd === undefined ? RUNTIME_HOME : toRuntimePath(params.cwd);
408
+ const env = { ...this.#env, ...params.env };
409
+ await this.#live();
410
+ await this.#linkHome(`${argv.join(" ")}\n${params.cwd ?? ""}\n${Object.values(env).join("\n")}`);
411
+ const timeoutMs = params.timeoutMs ?? LONGEST_MS;
412
+ const common = {
413
+ cwd,
414
+ ...(Object.keys(env).length ? { env } : {}),
415
+ timeoutMs,
416
+ };
417
+ const startedAt = Date.now();
418
+ const vercelCwd = params.cwd === undefined ? HOME : toVercelPath(params.cwd);
419
+ if (params.detached) {
420
+ const process = await this.#withResume((runtime) => runtime.spawn(argv, common));
421
+ const command = new Command({
422
+ id: process.id,
423
+ cwd: vercelCwd,
424
+ startedAt,
425
+ sandboxName: this.name,
426
+ process,
427
+ });
428
+ if (params.stdout || params.stderr)
429
+ void pipe(command, params);
430
+ return command;
431
+ }
432
+ const lines = [];
433
+ const record = (stream, writer) => (data) => {
434
+ lines.push({ stream, data });
435
+ writer?.write(data);
436
+ };
437
+ const result = await this.#withResume((runtime) => runtime.exec(argv, {
438
+ ...common,
439
+ onStdout: record("stdout", params.stdout),
440
+ onStderr: record("stderr", params.stderr),
441
+ ...signalOf(params),
442
+ }));
443
+ return new CommandFinished({
444
+ id: result.processId ?? `cmd_${startedAt}`,
445
+ cwd: vercelCwd,
446
+ startedAt,
447
+ sandboxName: this.name,
448
+ lines,
449
+ }, exitCodeOf(result.exitCode, result.timedOut), result.durationMs ?? Date.now() - startedAt);
450
+ }
451
+ /** A command started earlier, by its cmdId. */
452
+ async getCommand(cmdId, _opts = {}) {
453
+ const process = await this.#withResume((runtime) => runtime.processes.get(cmdId));
454
+ return new Command({
455
+ id: process.id,
456
+ cwd: process.info.cwd,
457
+ startedAt: Date.parse(process.info.startedAt),
458
+ sandboxName: this.name,
459
+ process,
460
+ });
461
+ }
462
+ // ---- files ------------------------------------------------------------
463
+ async mkDir(path, _opts = {}) {
464
+ const target = toRuntimePath(path);
465
+ await this.#withResume((runtime) => runtime.files.mkdir(target, { parents: true }));
466
+ }
467
+ /** The file's bytes as a Node stream, or null when there is no file. */
468
+ async readFile(file, opts = {}) {
469
+ const buffer = await this.readFileToBuffer(file, opts);
470
+ if (buffer === null)
471
+ return null;
472
+ const { Readable } = await import("node:stream");
473
+ return Readable.from([buffer]);
474
+ }
475
+ /** The file as a Buffer, or null when there is no file. */
476
+ async readFileToBuffer(file, opts = {}) {
477
+ const target = toRuntimePath(file.path, file.cwd);
478
+ try {
479
+ return Buffer.from(await this.#withResume((runtime) => runtime.files.read(target, signalOf(opts))));
480
+ }
481
+ catch (error) {
482
+ if (error instanceof APIError && error.response.status === 404)
483
+ return null;
484
+ throw error;
485
+ }
486
+ }
487
+ /** Copies a sandbox file to the local disk; null when there is no file. */
488
+ async downloadFile(src, dst, opts = {}) {
489
+ const buffer = await this.readFileToBuffer(src, opts);
490
+ if (buffer === null)
491
+ return null;
492
+ const { mkdir, writeFile } = await import("node:fs/promises");
493
+ const paths = await import("node:path");
494
+ const target = paths.resolve(dst.cwd ?? process.cwd(), dst.path);
495
+ if (opts.mkdirRecursive)
496
+ await mkdir(paths.dirname(target), { recursive: true });
497
+ await writeFile(target, buffer);
498
+ return target;
499
+ }
500
+ /** Writes files, making their directories. Relative paths land in the
501
+ * working directory; `mode` sets permissions. */
502
+ async writeFiles(files, _opts = {}) {
503
+ const modes = [];
504
+ await this.#withResume(async (runtime) => {
505
+ for (const file of files) {
506
+ const target = toRuntimePath(file.path);
507
+ await runtime.files.write(target, file.content);
508
+ if (file.mode !== undefined)
509
+ modes.push(file.mode.toString(8), target);
510
+ }
511
+ if (!modes.length)
512
+ return;
513
+ const script = 'while [ "$#" -gt 0 ]; do chmod "$1" "$2" || exit 1; shift 2; done';
514
+ const result = await runtime.exec(["sh", "-c", script, "sh", ...modes]);
515
+ if (result.exitCode !== 0)
516
+ throw new APIError(new Response(null, { status: 400 }), {
517
+ message: `Setting file modes failed: ${result.stderr.trim()}`,
518
+ sandboxName: this.name,
519
+ });
520
+ });
521
+ }
522
+ // ---- ports ------------------------------------------------------------
523
+ async #share(port) {
524
+ const preview = await guard(() => this.#rt.previews.create(port, { visibility: "public" }), this.name);
525
+ this.#routes.set(port, preview.url.replace(/\/$/, ""));
526
+ }
527
+ /** The public address of a port listed in `ports`, such as
528
+ * `https://3000-<id>.runtimehost.com`. A browser sees a one-time page
529
+ * naming Runtime before the site. */
530
+ domain(port) {
531
+ const url = this.#routes.get(port);
532
+ if (!url)
533
+ throw new Error(`No route for port ${port}. List it in Sandbox.create({ ports: [${port}] }) or sandbox.update({ ports }).`);
534
+ return url;
535
+ }
536
+ // ---- lifecycle --------------------------------------------------------
537
+ /** Ends the session. A persistent sandbox is paused, keeping its files and
538
+ * memory, and wakes on the next call or Sandbox.get; any other ends. */
539
+ async stop(opts = {}) {
540
+ await guard(() => this.#rt.refresh(signalOf(opts)), this.name);
541
+ const state = this.#rt.state;
542
+ if (state !== "stopped" && state !== "stopping" && state !== "paused") {
543
+ if (this.#persistent)
544
+ await guard(() => this.#rt.pause(signalOf(opts)), this.name);
545
+ else
546
+ await guard(() => this.#rt.stop(signalOf(opts)), this.name);
547
+ }
548
+ return { name: this.name, status: this.status };
549
+ }
550
+ /** Ends the sandbox for good. */
551
+ async delete(opts = {}) {
552
+ if (opts.deleteOrphanSnapshots)
553
+ throw new NotSupportedError("Deleting a sandbox's snapshots with it (deleteOrphanSnapshots)", "Delete them with Snapshot.get({ snapshotId }) and snapshot.delete(), or runtime.snapshots.delete(id).");
554
+ await guard(() => this.#rt.refresh(signalOf(opts)), this.name);
555
+ if (this.#rt.state !== "stopped")
556
+ await guard(() => this.#rt.stop({ wait: false, ...signalOf(opts) }), this.name);
557
+ this.#deleted = true;
558
+ }
559
+ async [Symbol.asyncDispose]() {
560
+ if (this.#deleted)
561
+ return;
562
+ await this.stop().catch(() => undefined);
563
+ }
564
+ /** Moves the end `duration` milliseconds later (at most an hour ahead of
565
+ * now on Runtime). */
566
+ async extendTimeout(duration, opts = {}) {
567
+ if (!Number.isFinite(duration) || duration <= 0)
568
+ throw new RangeError(`duration must be a positive number of milliseconds, not ${duration}.`);
569
+ await this.#withResume((runtime) => runtime.extend(Math.ceil(duration / 1000), signalOf(opts)));
570
+ }
571
+ /** Replaces the network rules; returns the policy given. */
572
+ async updateNetworkPolicy(networkPolicy, _opts = {}) {
573
+ const rules = networkRules(networkPolicy);
574
+ await this.#withResume((runtime) => runtime.network.set(rules));
575
+ return networkPolicy;
576
+ }
577
+ /** Changes what Runtime can change on a sandbox: `timeout`, `ports`,
578
+ * `networkPolicy` and `snapshotExpiration`. Anything else is refused before
579
+ * anything changes. */
580
+ async update(params, _opts = {}) {
581
+ const allowed = new Set(["timeout", "ports", "networkPolicy", "snapshotExpiration"]);
582
+ const other = Object.keys(params).find((key) => params[key] !== undefined && !allowed.has(key));
583
+ if (other)
584
+ throw new NotSupportedError(`Changing ${other} on an existing sandbox`, "Create a new sandbox with it (Runtime cannot change a sandbox's machine, persistence, tags or region after creation).");
585
+ if (params.timeout !== undefined) {
586
+ const current = this.timeout;
587
+ if (params.timeout < current)
588
+ throw new NotSupportedError("Shortening a sandbox's timeout", "Runtime leases only move later. Call stop() when the work is done.");
589
+ if (params.timeout > current)
590
+ await this.extendTimeout(params.timeout - current);
591
+ }
592
+ if (params.networkPolicy !== undefined)
593
+ await this.updateNetworkPolicy(params.networkPolicy);
594
+ if (params.snapshotExpiration !== undefined)
595
+ await guard(() => this.#rt.setRetention(retentionDays(params.snapshotExpiration)), this.name);
596
+ if (params.ports !== undefined) {
597
+ const wanted = new Set(params.ports);
598
+ for (const port of [...this.#routes.keys()])
599
+ if (!wanted.has(port)) {
600
+ await guard(() => this.#rt.previews.delete(port), this.name);
601
+ this.#routes.delete(port);
602
+ }
603
+ for (const port of wanted)
604
+ if (!this.#routes.has(port))
605
+ await this.#share(port);
606
+ }
607
+ }
608
+ /** Keeps the sandbox's whole machine as a Runtime snapshot (files, memory
609
+ * and running processes), then stops the sandbox, as Vercel does. Start
610
+ * from it with `Sandbox.create({ source: { type: "snapshot", snapshotId } })`. */
611
+ async snapshot(opts = {}) {
612
+ const taken = await this.#withResume((runtime) => runtime.snapshot({
613
+ ...(opts.expiration !== undefined ? { retentionDays: retentionDays(opts.expiration) } : {}),
614
+ }));
615
+ await guard(() => this.#rt.stop({ wait: false }), this.name);
616
+ return new Snapshot(taken, this.#client);
617
+ }
618
+ // ---- what Runtime does differently -----------------------------------
619
+ openInteractive() {
620
+ return Promise.reject(new NotSupportedError("Vercel's interactive shell (openInteractive)", "Use `await sandbox.withruntime.terminal({ cols, rows, onData })`, or `npx withruntime sandbox shell <id>`."));
621
+ }
622
+ getDefaultUser() {
623
+ return Promise.resolve({ username: "runtime", group: "runtime" });
624
+ }
625
+ createUser() {
626
+ return usersRefused();
627
+ }
628
+ asUser() {
629
+ throw usersError();
630
+ }
631
+ createGroup() {
632
+ return usersRefused();
633
+ }
634
+ addUserToGroup() {
635
+ return usersRefused();
636
+ }
637
+ removeUserFromGroup() {
638
+ return usersRefused();
639
+ }
640
+ currentSession() {
641
+ throw new NotSupportedError("Vercel sessions (currentSession)", "A Runtime sandbox is its own session: call the methods on the sandbox.");
642
+ }
643
+ listSessions() {
644
+ return Promise.reject(new NotSupportedError("Listing a sandbox's sessions", "A Runtime sandbox has one session: itself."));
645
+ }
646
+ listSnapshots() {
647
+ return Promise.reject(new NotSupportedError("Listing a sandbox's snapshots from the sandbox", "Use Snapshot.list({ name }) or runtime.snapshots.list({ sandboxId })."));
648
+ }
649
+ }
650
+ function usersError() {
651
+ return new NotSupportedError("Extra Linux users and groups (createUser, asUser, groups)", "Commands run as the sandbox owner with passwordless sudo; run `sudo useradd ...` and `sudo -u <user> ...` with runCommand.");
652
+ }
653
+ function usersRefused() {
654
+ return Promise.reject(usersError());
655
+ }
656
+ /** Milliseconds as whole days for Runtime's retention: 1 to 365, and 0 (no
657
+ * expiration) as 365. */
658
+ export function retentionDays(ms) {
659
+ if (ms === 0)
660
+ return 365;
661
+ return Math.min(365, Math.max(1, Math.ceil(ms / 86_400_000)));
662
+ }
663
+ async function pipe(command, params) {
664
+ try {
665
+ for await (const line of command.logs())
666
+ (line.stream === "stdout" ? params.stdout : params.stderr)?.write(line.data);
667
+ }
668
+ catch {
669
+ // The command's own wait() reports a broken stream.
670
+ }
671
+ }
672
+ /** The newest live sandbox with this name, or the sandbox with this Runtime
673
+ * id. A 404 when there is none. */
674
+ async function find(client, name) {
675
+ if (UUID.test(name))
676
+ return client.sandboxes.get(name);
677
+ const page = await client.sandboxes.list({ name });
678
+ const all = await page.toArray(1000);
679
+ const found = all.filter((one) => one.state !== "stopped").at(-1);
680
+ if (!found)
681
+ throw new RuntimeError({
682
+ message: `Sandbox ${name} was not found.`,
683
+ code: "not_found",
684
+ status: 404,
685
+ hint: "Sandbox.getOrCreate({ name }) makes it when it is missing.",
686
+ });
687
+ return found;
688
+ }
689
+ function itemOf(runtime) {
690
+ const info = runtime.info;
691
+ return {
692
+ name: info.name ?? info.id,
693
+ persistent: info.onLeaseEnd !== "stop",
694
+ createdAt: Date.parse(info.createdAt),
695
+ updatedAt: Date.parse(info.pausedAt ?? info.readyAt ?? info.createdAt),
696
+ currentSessionId: info.id,
697
+ status: statusOf(info.state),
698
+ region: "iad1",
699
+ vcpus: info.vcpu,
700
+ memory: info.memoryMiB,
701
+ timeout: info.timeoutSeconds * 1000,
702
+ expiresAt: Date.parse(info.expiresAt),
703
+ tags: { ...info.labels },
704
+ };
705
+ }
706
+ /** Vercel's paginator over a Runtime page: the first page's fields, async
707
+ * iteration over every item, `pages()` and `toArray()`. */
708
+ function paginate(first) {
709
+ const toPage = (page) => ({
710
+ sandboxes: page.data.map(itemOf),
711
+ pagination: { count: page.data.length, next: page.nextCursor },
712
+ });
713
+ return Object.assign(toPage(first), {
714
+ async *pages() {
715
+ for await (const page of first.pages())
716
+ yield toPage(page);
717
+ },
718
+ async *[Symbol.asyncIterator]() {
719
+ for await (const item of first)
720
+ yield itemOf(item);
721
+ },
722
+ async toArray() {
723
+ return (await first.toArray()).map(itemOf);
724
+ },
725
+ });
726
+ }