agentbox-sdk 0.0.0 → 0.1.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.
@@ -0,0 +1,1824 @@
1
+ import {
2
+ AsyncQueue,
3
+ UnsupportedProviderError,
4
+ collectAllAgentReservedPorts,
5
+ pipeReadableStream,
6
+ readNodeStream,
7
+ readStreamAsText,
8
+ sleep,
9
+ suppressUnhandledRejection
10
+ } from "./chunk-O7HCJXKW.js";
11
+ import {
12
+ shellQuote,
13
+ toShellCommand
14
+ } from "./chunk-NSJM57Z4.js";
15
+ import {
16
+ SandboxProvider
17
+ } from "./chunk-2NKMDGYH.js";
18
+
19
+ // src/sandboxes/git.ts
20
+ function encodeExtraHeader(name, value) {
21
+ return `http.extraHeader=${name}: ${value}`;
22
+ }
23
+ function buildGitCloneCommand(options) {
24
+ const targetDir = options.targetDir ?? ".";
25
+ const cloneArgs = [];
26
+ if (options.depth) {
27
+ cloneArgs.push("--depth", String(options.depth));
28
+ }
29
+ if (options.branch) {
30
+ cloneArgs.push("--branch", options.branch, "--single-branch");
31
+ }
32
+ const configArgs = [];
33
+ if (options.token) {
34
+ configArgs.push(
35
+ "-c",
36
+ encodeExtraHeader("Authorization", `Bearer ${options.token}`)
37
+ );
38
+ }
39
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
40
+ configArgs.push("-c", encodeExtraHeader(name, value));
41
+ }
42
+ const command = [
43
+ "git",
44
+ ...configArgs.map(shellQuote),
45
+ "clone",
46
+ ...cloneArgs.map(shellQuote),
47
+ shellQuote(options.repoUrl),
48
+ shellQuote(targetDir)
49
+ ].join(" ");
50
+ if (targetDir === ".") {
51
+ return command;
52
+ }
53
+ return `mkdir -p ${shellQuote(targetDir)} && rm -rf ${shellQuote(targetDir)} && ${command}`;
54
+ }
55
+
56
+ // src/sandboxes/base.ts
57
+ var SandboxAdapter = class {
58
+ options;
59
+ secrets = {};
60
+ baseEnv;
61
+ provisioned = false;
62
+ provisioning;
63
+ constructor(options) {
64
+ this.options = options;
65
+ this.baseEnv = { ...options.env ?? {} };
66
+ }
67
+ async uploadFile(_content, _targetPath) {
68
+ void _content;
69
+ void _targetPath;
70
+ throw new Error(
71
+ `uploadFile is not supported by the ${this.provider} provider.`
72
+ );
73
+ }
74
+ async downloadFile(_sourcePath) {
75
+ void _sourcePath;
76
+ throw new Error(
77
+ `downloadFile is not supported by the ${this.provider} provider.`
78
+ );
79
+ }
80
+ async ensureProvisioned() {
81
+ if (this.provisioned) {
82
+ return;
83
+ }
84
+ if (!this.provisioning) {
85
+ this.provisioning = (async () => {
86
+ await this.provision();
87
+ this.provisioned = true;
88
+ })().finally(() => {
89
+ this.provisioning = void 0;
90
+ });
91
+ }
92
+ await this.provisioning;
93
+ }
94
+ get tags() {
95
+ return { ...this.options.tags ?? {} };
96
+ }
97
+ get workingDir() {
98
+ return this.options.workingDir ?? "/workspace";
99
+ }
100
+ /**
101
+ * Headers that callers should attach to HTTP / WebSocket requests they make
102
+ * against this sandbox's preview URL. Default is empty; providers like
103
+ * Vercel override this to inject Deployment Protection bypass tokens.
104
+ */
105
+ get previewHeaders() {
106
+ return {};
107
+ }
108
+ getMergedEnv(extra) {
109
+ return {
110
+ ...this.baseEnv,
111
+ ...this.secrets,
112
+ ...extra ?? {}
113
+ };
114
+ }
115
+ setSecret(name, value) {
116
+ this.secrets[name] = value;
117
+ }
118
+ setSecrets(values) {
119
+ Object.assign(this.secrets, values);
120
+ }
121
+ async gitClone(options) {
122
+ await this.ensureProvisioned();
123
+ return this.run(buildGitCloneCommand(options), {
124
+ cwd: this.workingDir,
125
+ env: this.getMergedEnv()
126
+ });
127
+ }
128
+ };
129
+
130
+ // src/sandboxes/providers/daytona.ts
131
+ import { Daytona } from "@daytonaio/sdk";
132
+
133
+ // src/sandboxes/image-utils.ts
134
+ function resolveSandboxImage(image) {
135
+ return image;
136
+ }
137
+ function resolveSandboxResources(resources) {
138
+ return resources && Object.values(resources).some((value) => value !== void 0) ? resources : void 0;
139
+ }
140
+
141
+ // src/sandboxes/providers/daytona.ts
142
+ var DaytonaSandboxAdapter = class extends SandboxAdapter {
143
+ client;
144
+ sandbox;
145
+ constructor(options) {
146
+ super(options);
147
+ this.client = new Daytona({
148
+ apiKey: options.provider?.apiKey,
149
+ jwtToken: options.provider?.jwtToken,
150
+ organizationId: options.provider?.organizationId,
151
+ apiUrl: options.provider?.apiUrl,
152
+ target: options.provider?.target
153
+ });
154
+ }
155
+ get provider() {
156
+ return SandboxProvider.Daytona;
157
+ }
158
+ get raw() {
159
+ return {
160
+ client: this.client,
161
+ sandbox: this.sandbox
162
+ };
163
+ }
164
+ get id() {
165
+ return this.sandbox?.id;
166
+ }
167
+ async provision() {
168
+ const existing = await this.findMatchingSandbox();
169
+ if (existing) {
170
+ this.sandbox = existing;
171
+ await existing.start();
172
+ return;
173
+ }
174
+ const labels = this.getLabels();
175
+ const autoStopInterval = this.options.idleTimeoutMs ? Math.max(1, Math.ceil(this.options.idleTimeoutMs / 6e4)) : void 0;
176
+ const autoDeleteInterval = this.options.autoStopMs ? Math.max(1, Math.ceil(this.options.autoStopMs / 6e4)) : void 0;
177
+ const image = resolveSandboxImage(this.options.image);
178
+ const resources = resolveSandboxResources(this.options.resources);
179
+ if (!image) {
180
+ throw new Error(
181
+ "daytona sandboxes require options.image to reference a prebuilt Daytona snapshot."
182
+ );
183
+ }
184
+ if (resources) {
185
+ throw new Error(
186
+ "daytona sandbox sizing is embedded in the image id and cannot be set via options.resources."
187
+ );
188
+ }
189
+ const createBase = {
190
+ name: this.options.provider?.name,
191
+ language: this.options.provider?.language ?? "typescript",
192
+ user: this.options.provider?.user,
193
+ envVars: this.getMergedEnv(),
194
+ labels,
195
+ public: this.options.provider?.public ?? true,
196
+ autoStopInterval,
197
+ autoDeleteInterval
198
+ };
199
+ const sandbox = await this.client.create({
200
+ ...createBase,
201
+ snapshot: image
202
+ });
203
+ await sandbox.start();
204
+ this.sandbox = sandbox;
205
+ }
206
+ async run(command, options) {
207
+ await this.ensureProvisioned();
208
+ const sandbox = this.requireSandbox();
209
+ const result = await sandbox.process.executeCommand(
210
+ toShellCommand(command),
211
+ options?.cwd ?? this.workingDir,
212
+ this.getMergedEnv(options?.env),
213
+ options?.timeoutMs ? Math.ceil(options.timeoutMs / 1e3) : void 0
214
+ );
215
+ const output = result.result ?? "";
216
+ return {
217
+ exitCode: result.exitCode,
218
+ stdout: output,
219
+ stderr: output,
220
+ combinedOutput: output,
221
+ raw: result
222
+ };
223
+ }
224
+ async runAsync(command, options) {
225
+ await this.ensureProvisioned();
226
+ const sandbox = this.requireSandbox();
227
+ const sessionId = `agentbox-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
228
+ await sandbox.process.createSession(sessionId);
229
+ const response = await sandbox.process.executeSessionCommand(
230
+ sessionId,
231
+ {
232
+ command: this.buildSessionCommand(command, options),
233
+ runAsync: true
234
+ },
235
+ options?.timeoutMs ? Math.ceil(options.timeoutMs / 1e3) : void 0
236
+ );
237
+ const commandId = response.cmdId;
238
+ const queue = new AsyncQueue();
239
+ let stdout = "";
240
+ let stderr = "";
241
+ let exitCode = 0;
242
+ let killed = false;
243
+ const streamLogs = sandbox.process.getSessionCommandLogs(
244
+ sessionId,
245
+ commandId,
246
+ (chunk) => {
247
+ stdout += chunk;
248
+ queue.push({
249
+ type: "stdout",
250
+ chunk,
251
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
252
+ });
253
+ },
254
+ (chunk) => {
255
+ stderr += chunk;
256
+ queue.push({
257
+ type: "stderr",
258
+ chunk,
259
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
260
+ });
261
+ }
262
+ );
263
+ const pollTimeoutMs = options?.timeoutMs ?? 0;
264
+ const completion = (async () => {
265
+ const pollStart = Date.now();
266
+ while (true) {
267
+ let status;
268
+ try {
269
+ status = await sandbox.process.getSessionCommand(
270
+ sessionId,
271
+ commandId
272
+ );
273
+ } catch (error) {
274
+ if (killed) {
275
+ break;
276
+ }
277
+ throw error;
278
+ }
279
+ if (status.exitCode !== null && status.exitCode !== void 0) {
280
+ if (!killed) {
281
+ exitCode = status.exitCode;
282
+ }
283
+ break;
284
+ }
285
+ if (pollTimeoutMs > 0 && Date.now() - pollStart > pollTimeoutMs) {
286
+ await sandbox.process.deleteSession(sessionId).catch(() => void 0);
287
+ killed = true;
288
+ exitCode = 130;
289
+ break;
290
+ }
291
+ await sleep(500);
292
+ }
293
+ try {
294
+ await streamLogs;
295
+ } catch (error) {
296
+ if (!killed) {
297
+ throw error;
298
+ }
299
+ }
300
+ queue.push({
301
+ type: "exit",
302
+ exitCode,
303
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
304
+ });
305
+ queue.finish();
306
+ return {
307
+ exitCode,
308
+ stdout,
309
+ stderr,
310
+ combinedOutput: `${stdout}${stderr}`,
311
+ raw: { sessionId, commandId }
312
+ };
313
+ })().catch((error) => {
314
+ queue.fail(error);
315
+ throw error;
316
+ });
317
+ suppressUnhandledRejection(completion);
318
+ return {
319
+ id: commandId,
320
+ raw: { sessionId, commandId },
321
+ wait: () => completion,
322
+ kill: async () => {
323
+ killed = true;
324
+ exitCode = 130;
325
+ await sandbox.process.deleteSession(sessionId).catch(() => void 0);
326
+ },
327
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
328
+ };
329
+ }
330
+ async list(options) {
331
+ const result = await this.client.list(options?.tags ?? this.getLabels());
332
+ return result.items.map((sandbox) => ({
333
+ provider: this.provider,
334
+ id: sandbox.id,
335
+ state: sandbox.state,
336
+ tags: sandbox.labels ?? {},
337
+ createdAt: sandbox.createdAt,
338
+ raw: sandbox
339
+ }));
340
+ }
341
+ async snapshot() {
342
+ return null;
343
+ }
344
+ async stop() {
345
+ const sandbox = this.sandbox;
346
+ if (!sandbox) {
347
+ return;
348
+ }
349
+ await sandbox.stop();
350
+ this.sandbox = void 0;
351
+ }
352
+ async delete() {
353
+ const sandbox = this.sandbox;
354
+ if (!sandbox) {
355
+ return;
356
+ }
357
+ await sandbox.delete();
358
+ this.sandbox = void 0;
359
+ }
360
+ async openPort(port) {
361
+ await this.ensureProvisioned();
362
+ await this.requireSandbox().getPreviewLink(port);
363
+ }
364
+ async getPreviewLink(port) {
365
+ await this.ensureProvisioned();
366
+ const sandbox = this.requireSandbox();
367
+ const preview = await sandbox.getPreviewLink(port);
368
+ return preview.url;
369
+ }
370
+ getLabels() {
371
+ return {
372
+ "agentbox.provider": this.provider,
373
+ ...this.options.tags ?? {}
374
+ };
375
+ }
376
+ buildSessionCommand(command, options) {
377
+ const statements = [];
378
+ const cwd = options?.cwd ?? this.workingDir;
379
+ const env = this.getMergedEnv(options?.env);
380
+ if (cwd) {
381
+ statements.push(`cd ${shellQuote(cwd)}`);
382
+ }
383
+ for (const [name, value] of Object.entries(env)) {
384
+ statements.push(`export ${name}=${shellQuote(value)}`);
385
+ }
386
+ statements.push(toShellCommand(command));
387
+ return statements.join(" && ");
388
+ }
389
+ async findMatchingSandbox() {
390
+ const result = await this.client.list(this.getLabels());
391
+ return result.items[0];
392
+ }
393
+ requireSandbox() {
394
+ if (!this.sandbox) {
395
+ throw new Error("Daytona sandbox has not been provisioned.");
396
+ }
397
+ return this.sandbox;
398
+ }
399
+ };
400
+
401
+ // src/sandboxes/providers/e2b.ts
402
+ var e2bModulePromise;
403
+ async function loadE2bModule() {
404
+ if (!e2bModulePromise) {
405
+ e2bModulePromise = import("e2b");
406
+ }
407
+ return e2bModulePromise;
408
+ }
409
+ var E2bSandboxAdapter = class extends SandboxAdapter {
410
+ sandbox;
411
+ get provider() {
412
+ return SandboxProvider.E2B;
413
+ }
414
+ get raw() {
415
+ return {
416
+ sandbox: this.sandbox
417
+ };
418
+ }
419
+ get id() {
420
+ return this.sandbox?.sandboxId;
421
+ }
422
+ async provision() {
423
+ const { Sandbox: E2bSandbox } = await loadE2bModule();
424
+ const existing = await this.findMatchingSandbox();
425
+ if (existing) {
426
+ this.sandbox = existing;
427
+ return;
428
+ }
429
+ const template = resolveSandboxImage(this.options.image);
430
+ const resources = resolveSandboxResources(this.options.resources);
431
+ if (!template) {
432
+ throw new Error(
433
+ "e2b sandboxes require options.image to reference an existing E2B template name or tag."
434
+ );
435
+ }
436
+ if (resources) {
437
+ throw new Error(
438
+ "e2b sandbox sizing must be defined when building the E2B template and cannot be set via options.resources."
439
+ );
440
+ }
441
+ const timeout = this.resolveTimeoutConfig();
442
+ this.sandbox = await E2bSandbox.create(template, {
443
+ ...this.getConnectionOptions(),
444
+ metadata: this.getMetadata(),
445
+ envs: this.getMergedEnv(),
446
+ secure: this.options.provider?.secure,
447
+ allowInternetAccess: this.options.provider?.allowInternetAccess,
448
+ ...timeout
449
+ });
450
+ }
451
+ async run(command, options) {
452
+ await this.ensureProvisioned();
453
+ const sandbox = this.requireSandbox();
454
+ const { CommandExitError } = await loadE2bModule();
455
+ if (options?.pty) {
456
+ const queue = new AsyncQueue();
457
+ const handle = await this.runAsyncWithPty(
458
+ sandbox,
459
+ command,
460
+ options,
461
+ queue
462
+ );
463
+ const result = await handle.wait();
464
+ return result;
465
+ }
466
+ try {
467
+ const result = await sandbox.commands.run(
468
+ toShellCommand(command),
469
+ this.getCommandStartOptions(options)
470
+ );
471
+ return this.toCommandResult(result, result);
472
+ } catch (error) {
473
+ if (error instanceof CommandExitError) {
474
+ return this.toCommandResult(error, error);
475
+ }
476
+ throw error;
477
+ }
478
+ }
479
+ async runAsync(command, options) {
480
+ await this.ensureProvisioned();
481
+ const sandbox = this.requireSandbox();
482
+ const { CommandExitError } = await loadE2bModule();
483
+ const queue = new AsyncQueue();
484
+ let stdout = "";
485
+ let stderr = "";
486
+ if (options?.pty) {
487
+ return this.runAsyncWithPty(sandbox, command, options, queue);
488
+ }
489
+ const handle = await sandbox.commands.run(toShellCommand(command), {
490
+ ...this.getCommandStartOptions(options),
491
+ timeoutMs: options?.timeoutMs ?? 0,
492
+ background: true,
493
+ stdin: true,
494
+ onStdout: async (chunk) => {
495
+ stdout += chunk;
496
+ queue.push({
497
+ type: "stdout",
498
+ chunk,
499
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
500
+ });
501
+ },
502
+ onStderr: async (chunk) => {
503
+ stderr += chunk;
504
+ queue.push({
505
+ type: "stderr",
506
+ chunk,
507
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
508
+ });
509
+ }
510
+ });
511
+ const completion = handle.wait().then((result) => {
512
+ const mapped = this.toCommandResult(
513
+ {
514
+ exitCode: result.exitCode,
515
+ stdout: stdout || result.stdout,
516
+ stderr: stderr || result.stderr,
517
+ error: result.error
518
+ },
519
+ result
520
+ );
521
+ queue.push({
522
+ type: "exit",
523
+ exitCode: mapped.exitCode,
524
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
525
+ });
526
+ queue.finish();
527
+ return mapped;
528
+ }).catch((error) => {
529
+ if (error instanceof CommandExitError) {
530
+ const mapped = this.toCommandResult(
531
+ {
532
+ exitCode: error.exitCode,
533
+ stdout: stdout || error.stdout,
534
+ stderr: stderr || error.stderr,
535
+ error: error.error
536
+ },
537
+ error
538
+ );
539
+ queue.push({
540
+ type: "exit",
541
+ exitCode: mapped.exitCode,
542
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
543
+ });
544
+ queue.finish();
545
+ return mapped;
546
+ }
547
+ queue.fail(error);
548
+ throw error;
549
+ });
550
+ suppressUnhandledRejection(completion);
551
+ return {
552
+ id: String(handle.pid),
553
+ raw: handle,
554
+ write: async (input) => {
555
+ await sandbox.commands.sendStdin(handle.pid, input);
556
+ },
557
+ wait: () => completion,
558
+ kill: async () => {
559
+ await handle.kill();
560
+ },
561
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
562
+ };
563
+ }
564
+ async runAsyncWithPty(sandbox, command, options, queue) {
565
+ const decoder = new TextDecoder();
566
+ const encoder = new TextEncoder();
567
+ let output = "";
568
+ const handle = await sandbox.pty.create({
569
+ cols: 120,
570
+ rows: 40,
571
+ cwd: options?.cwd ?? this.workingDir,
572
+ envs: this.getMergedEnv(options?.env),
573
+ timeoutMs: options?.timeoutMs ?? 0,
574
+ onData: async (chunk) => {
575
+ const text = decoder.decode(chunk, { stream: true });
576
+ output += text;
577
+ queue.push({
578
+ type: "stdout",
579
+ chunk: text,
580
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
581
+ });
582
+ }
583
+ });
584
+ await sandbox.pty.sendInput(
585
+ handle.pid,
586
+ encoder.encode(`${toShellCommand(command)}
587
+ `)
588
+ );
589
+ const completion = handle.wait().then((result) => {
590
+ const flush = decoder.decode();
591
+ if (flush) {
592
+ output += flush;
593
+ queue.push({
594
+ type: "stdout",
595
+ chunk: flush,
596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
597
+ });
598
+ }
599
+ queue.push({
600
+ type: "exit",
601
+ exitCode: result.exitCode,
602
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
603
+ });
604
+ queue.finish();
605
+ return {
606
+ exitCode: result.exitCode,
607
+ stdout: output || result.stdout,
608
+ stderr: result.stderr,
609
+ combinedOutput: `${output || result.stdout}${result.stderr}`,
610
+ raw: result
611
+ };
612
+ }).catch((error) => {
613
+ queue.fail(error);
614
+ throw error;
615
+ });
616
+ suppressUnhandledRejection(completion);
617
+ return {
618
+ id: String(handle.pid),
619
+ raw: handle,
620
+ write: async (input) => {
621
+ await sandbox.pty.sendInput(handle.pid, encoder.encode(input));
622
+ },
623
+ wait: () => completion,
624
+ kill: async () => {
625
+ await handle.kill();
626
+ },
627
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
628
+ };
629
+ }
630
+ async list(options) {
631
+ const { Sandbox: E2bSandbox } = await loadE2bModule();
632
+ const paginator = E2bSandbox.list({
633
+ ...this.getConnectionOptions(),
634
+ query: {
635
+ metadata: options?.tags ?? this.getMetadata(),
636
+ state: ["running", "paused"]
637
+ }
638
+ });
639
+ const sandboxes = [];
640
+ while (paginator.hasNext) {
641
+ const items = await paginator.nextItems();
642
+ for (const sandbox of items) {
643
+ sandboxes.push({
644
+ provider: this.provider,
645
+ id: sandbox.sandboxId,
646
+ state: sandbox.state,
647
+ tags: sandbox.metadata,
648
+ createdAt: sandbox.startedAt.toISOString(),
649
+ raw: sandbox
650
+ });
651
+ }
652
+ }
653
+ return sandboxes;
654
+ }
655
+ async snapshot() {
656
+ await this.ensureProvisioned();
657
+ const sandbox = this.requireSandbox();
658
+ const snapshot = await sandbox.createSnapshot();
659
+ return snapshot.snapshotId;
660
+ }
661
+ async stop() {
662
+ const sandbox = this.sandbox;
663
+ if (!sandbox) {
664
+ return;
665
+ }
666
+ await sandbox.kill();
667
+ this.sandbox = void 0;
668
+ }
669
+ async delete() {
670
+ await this.stop();
671
+ }
672
+ async openPort(_port) {
673
+ void _port;
674
+ }
675
+ async getPreviewLink(port) {
676
+ await this.ensureProvisioned();
677
+ const sandbox = this.requireSandbox();
678
+ const host = sandbox.getHost(port);
679
+ return host.startsWith("localhost:") ? `http://${host}` : `https://${host}`;
680
+ }
681
+ async findMatchingSandbox() {
682
+ const { Sandbox: E2bSandbox } = await loadE2bModule();
683
+ const matches = await this.list();
684
+ const match = matches[0];
685
+ if (!match) {
686
+ return void 0;
687
+ }
688
+ const timeout = this.resolveTimeoutConfig();
689
+ return E2bSandbox.connect(match.id, {
690
+ ...this.getConnectionOptions(),
691
+ timeoutMs: timeout.timeoutMs
692
+ });
693
+ }
694
+ getMetadata() {
695
+ return {
696
+ "agentbox.provider": this.provider,
697
+ ...this.options.tags ?? {}
698
+ };
699
+ }
700
+ getConnectionOptions() {
701
+ return {
702
+ accessToken: this.options.provider?.accessToken,
703
+ apiKey: this.options.provider?.apiKey,
704
+ apiUrl: this.options.provider?.apiUrl,
705
+ debug: this.options.provider?.debug,
706
+ domain: this.options.provider?.domain,
707
+ headers: this.options.provider?.headers,
708
+ requestTimeoutMs: this.options.provider?.requestTimeoutMs,
709
+ sandboxUrl: this.options.provider?.sandboxUrl
710
+ };
711
+ }
712
+ getCommandStartOptions(options) {
713
+ return {
714
+ cwd: options?.cwd ?? this.workingDir,
715
+ envs: this.getMergedEnv(options?.env),
716
+ timeoutMs: options?.timeoutMs
717
+ };
718
+ }
719
+ resolveTimeoutConfig() {
720
+ const providerTimeoutMs = this.options.provider?.timeoutMs;
721
+ const providerLifecycle = this.options.provider?.lifecycle;
722
+ const hasProviderTimeoutOverride = providerTimeoutMs !== void 0 || providerLifecycle !== void 0;
723
+ const normalizedLifecycle = providerLifecycle?.onTimeout ? {
724
+ onTimeout: providerLifecycle.onTimeout,
725
+ autoResume: providerLifecycle.autoResume
726
+ } : void 0;
727
+ if (hasProviderTimeoutOverride && (this.options.idleTimeoutMs !== void 0 || this.options.autoStopMs !== void 0)) {
728
+ throw new Error(
729
+ "e2b sandbox timeout configuration must use either provider.timeoutMs/provider.lifecycle or the shared idleTimeoutMs/autoStopMs fields, but not both."
730
+ );
731
+ }
732
+ if (providerLifecycle?.autoResume && providerLifecycle.onTimeout !== "pause") {
733
+ throw new Error(
734
+ "e2b provider.lifecycle.autoResume can only be enabled when provider.lifecycle.onTimeout is 'pause'."
735
+ );
736
+ }
737
+ if (hasProviderTimeoutOverride) {
738
+ return {
739
+ timeoutMs: providerTimeoutMs,
740
+ lifecycle: normalizedLifecycle
741
+ };
742
+ }
743
+ if (this.options.idleTimeoutMs !== void 0 && this.options.autoStopMs !== void 0) {
744
+ throw new Error(
745
+ "e2b sandboxes do not support combining idleTimeoutMs and autoStopMs because the provider exposes a single timeout/lifecycle configuration."
746
+ );
747
+ }
748
+ if (this.options.idleTimeoutMs !== void 0) {
749
+ return {
750
+ timeoutMs: this.options.idleTimeoutMs,
751
+ lifecycle: {
752
+ onTimeout: "pause",
753
+ autoResume: false
754
+ }
755
+ };
756
+ }
757
+ if (this.options.autoStopMs !== void 0) {
758
+ return {
759
+ timeoutMs: this.options.autoStopMs,
760
+ lifecycle: {
761
+ onTimeout: "kill",
762
+ autoResume: false
763
+ }
764
+ };
765
+ }
766
+ return {};
767
+ }
768
+ requireSandbox() {
769
+ if (!this.sandbox) {
770
+ throw new Error("E2B sandbox has not been provisioned.");
771
+ }
772
+ return this.sandbox;
773
+ }
774
+ toCommandResult(result, raw) {
775
+ return {
776
+ exitCode: result.exitCode,
777
+ stdout: result.stdout,
778
+ stderr: result.stderr,
779
+ combinedOutput: `${result.stdout}${result.stderr}`,
780
+ raw
781
+ };
782
+ }
783
+ };
784
+
785
+ // src/sandboxes/providers/local-docker.ts
786
+ import Docker from "dockerode";
787
+ import tar from "tar-stream";
788
+ import { PassThrough } from "stream";
789
+ import { finished } from "stream/promises";
790
+ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
791
+ client = new Docker();
792
+ container;
793
+ get provider() {
794
+ return SandboxProvider.LocalDocker;
795
+ }
796
+ get raw() {
797
+ return {
798
+ client: this.client,
799
+ container: this.container
800
+ };
801
+ }
802
+ get id() {
803
+ return this.container?.id;
804
+ }
805
+ async provision() {
806
+ const existing = await this.findMatchingContainer();
807
+ if (existing) {
808
+ this.container = this.client.getContainer(existing.Id);
809
+ if (existing.State !== "running") {
810
+ await this.container.start();
811
+ }
812
+ return;
813
+ }
814
+ const normalizedImage = resolveSandboxImage(this.options.image);
815
+ const image = await this.resolveContainerImage(normalizedImage);
816
+ const resources = resolveSandboxResources(this.options.resources);
817
+ const labels = this.getLabels();
818
+ const env = Object.entries(this.getMergedEnv()).map(
819
+ ([key, value]) => `${key}=${value}`
820
+ );
821
+ const publishedPorts = this.options.provider?.publishedPorts ?? [];
822
+ const portBindings = publishedPorts.length > 0 ? Object.fromEntries(
823
+ publishedPorts.map((port) => [
824
+ `${port}/tcp`,
825
+ [{ HostIp: "127.0.0.1", HostPort: String(port) }]
826
+ ])
827
+ ) : void 0;
828
+ const exposedPorts = publishedPorts.length > 0 ? Object.fromEntries(publishedPorts.map((port) => [`${port}/tcp`, {}])) : void 0;
829
+ const container = await this.client.createContainer({
830
+ Image: image,
831
+ name: this.options.provider?.name,
832
+ Cmd: this.options.provider?.command ?? ["sleep", "infinity"],
833
+ WorkingDir: this.workingDir,
834
+ Env: env,
835
+ Labels: labels,
836
+ Tty: false,
837
+ OpenStdin: true,
838
+ ...exposedPorts ? { ExposedPorts: exposedPorts } : {},
839
+ HostConfig: {
840
+ AutoRemove: this.options.provider?.autoRemove ?? false,
841
+ Binds: this.options.provider?.binds,
842
+ NetworkMode: this.options.provider?.networkMode,
843
+ ExtraHosts: ["host.docker.internal:host-gateway"],
844
+ ...portBindings ? { PortBindings: portBindings } : {},
845
+ ...resources?.cpu ? { NanoCpus: Math.round(resources.cpu * 1e9) } : {},
846
+ ...resources?.memoryMiB ? { Memory: resources.memoryMiB * 1024 * 1024 } : {}
847
+ }
848
+ });
849
+ this.container = container;
850
+ await container.start();
851
+ }
852
+ async run(command, options) {
853
+ await this.ensureProvisioned();
854
+ const container = this.requireContainer();
855
+ const exec = await container.exec({
856
+ AttachStdout: true,
857
+ AttachStderr: true,
858
+ Cmd: ["/bin/sh", "-lc", toShellCommand(command)],
859
+ Env: this.toDockerEnv(options?.env),
860
+ WorkingDir: options?.cwd ?? this.workingDir
861
+ });
862
+ const stream = await exec.start({ hijack: true, stdin: false, Tty: false });
863
+ const { stdout, stderr } = this.demuxExecStream(container, stream);
864
+ const work = Promise.all([
865
+ readNodeStream(stdout),
866
+ readNodeStream(stderr),
867
+ finished(stream)
868
+ ]).then(([out, err]) => [out, err]);
869
+ let result;
870
+ if (options?.timeoutMs && options.timeoutMs > 0) {
871
+ const timeout = new Promise(
872
+ (_, reject) => setTimeout(
873
+ () => reject(
874
+ new Error(`Command timed out after ${options.timeoutMs}ms.`)
875
+ ),
876
+ options.timeoutMs
877
+ )
878
+ );
879
+ result = await Promise.race([work, timeout]);
880
+ } else {
881
+ result = await work;
882
+ }
883
+ const [stdoutBuffer, stderrBuffer] = result;
884
+ const inspect = await exec.inspect();
885
+ const stdoutText = stdoutBuffer.toString("utf8");
886
+ const stderrText = stderrBuffer.toString("utf8");
887
+ return {
888
+ exitCode: inspect.ExitCode ?? 0,
889
+ stdout: stdoutText,
890
+ stderr: stderrText,
891
+ combinedOutput: `${stdoutText}${stderrText}`,
892
+ raw: { exec, inspect }
893
+ };
894
+ }
895
+ async runAsync(command, options) {
896
+ await this.ensureProvisioned();
897
+ const container = this.requireContainer();
898
+ const exec = await container.exec({
899
+ AttachStdin: true,
900
+ AttachStdout: true,
901
+ AttachStderr: true,
902
+ Cmd: ["/bin/sh", "-lc", toShellCommand(command)],
903
+ Env: this.toDockerEnv(options?.env),
904
+ WorkingDir: options?.cwd ?? this.workingDir,
905
+ Tty: options?.pty ?? false
906
+ });
907
+ const tty = options?.pty ?? false;
908
+ const stream = await exec.start({ hijack: true, stdin: true, Tty: tty });
909
+ const { stdout, stderr } = tty ? this.wrapTtyExecStream(stream) : this.demuxExecStream(container, stream);
910
+ const queue = new AsyncQueue();
911
+ let stdoutText = "";
912
+ let stderrText = "";
913
+ stdout.on("data", (chunk) => {
914
+ const text = chunk.toString("utf8");
915
+ stdoutText += text;
916
+ queue.push({
917
+ type: "stdout",
918
+ chunk: text,
919
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
920
+ });
921
+ });
922
+ stderr.on("data", (chunk) => {
923
+ const text = chunk.toString("utf8");
924
+ stderrText += text;
925
+ queue.push({
926
+ type: "stderr",
927
+ chunk: text,
928
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
929
+ });
930
+ });
931
+ const completion = (async () => {
932
+ await finished(stream);
933
+ const inspect = await exec.inspect();
934
+ const exitCode = inspect.ExitCode ?? 0;
935
+ queue.push({
936
+ type: "exit",
937
+ exitCode,
938
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
939
+ raw: inspect
940
+ });
941
+ queue.finish();
942
+ return {
943
+ exitCode,
944
+ stdout: stdoutText,
945
+ stderr: stderrText,
946
+ combinedOutput: `${stdoutText}${stderrText}`,
947
+ raw: { exec, inspect }
948
+ };
949
+ })().catch((error) => {
950
+ queue.fail(error);
951
+ throw error;
952
+ });
953
+ suppressUnhandledRejection(completion);
954
+ return {
955
+ id: exec.id,
956
+ raw: { exec, stream },
957
+ write: async (input) => {
958
+ stream.write(input);
959
+ },
960
+ wait: () => completion,
961
+ kill: async () => {
962
+ try {
963
+ stream.write("");
964
+ stream.end();
965
+ } catch {
966
+ }
967
+ },
968
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
969
+ };
970
+ }
971
+ demuxExecStream(container, stream) {
972
+ const stdout = new PassThrough();
973
+ const stderr = new PassThrough();
974
+ container.modem.demuxStream(stream, stdout, stderr);
975
+ const endOutputs = () => {
976
+ stdout.end();
977
+ stderr.end();
978
+ };
979
+ stream.on("end", endOutputs);
980
+ stream.on("close", endOutputs);
981
+ stream.on("error", (error) => {
982
+ stdout.destroy(error);
983
+ stderr.destroy(error);
984
+ });
985
+ return { stdout, stderr };
986
+ }
987
+ wrapTtyExecStream(stream) {
988
+ const stdout = new PassThrough();
989
+ const stderr = new PassThrough();
990
+ stream.on("data", (chunk) => {
991
+ stdout.write(chunk);
992
+ });
993
+ stream.on("end", () => {
994
+ stdout.end();
995
+ stderr.end();
996
+ });
997
+ stream.on("close", () => {
998
+ stdout.end();
999
+ stderr.end();
1000
+ });
1001
+ stream.on("error", (error) => {
1002
+ stdout.destroy(error);
1003
+ stderr.destroy(error);
1004
+ });
1005
+ return { stdout, stderr };
1006
+ }
1007
+ async list(options) {
1008
+ const filters = {
1009
+ label: this.toDockerLabelFilters(options?.tags)
1010
+ };
1011
+ const containers = await this.client.listContainers({ all: true, filters });
1012
+ return containers.map((container) => ({
1013
+ provider: this.provider,
1014
+ id: container.Id,
1015
+ state: container.State,
1016
+ tags: container.Labels ?? {},
1017
+ raw: container
1018
+ }));
1019
+ }
1020
+ async snapshot() {
1021
+ return null;
1022
+ }
1023
+ async stop() {
1024
+ const container = this.container;
1025
+ if (!container) {
1026
+ return;
1027
+ }
1028
+ await container.stop().catch(() => void 0);
1029
+ }
1030
+ async delete() {
1031
+ const container = this.container;
1032
+ if (!container) {
1033
+ return;
1034
+ }
1035
+ await container.remove({ force: true }).catch(() => void 0);
1036
+ this.container = void 0;
1037
+ }
1038
+ async openPort(port) {
1039
+ const provider = this.options.provider ?? (this.options.provider = {});
1040
+ if (provider.networkMode === "host") {
1041
+ return;
1042
+ }
1043
+ provider.publishedPorts = provider.publishedPorts?.includes(port) ? provider.publishedPorts : [...provider.publishedPorts ?? [], port];
1044
+ }
1045
+ async getPreviewLink(port) {
1046
+ const networkMode = this.options.provider?.networkMode;
1047
+ if (networkMode === "host") {
1048
+ return `http://127.0.0.1:${port}`;
1049
+ }
1050
+ if (this.options.provider?.publishedPorts?.includes(port)) {
1051
+ return `http://127.0.0.1:${port}`;
1052
+ }
1053
+ throw new Error(
1054
+ `Port ${port} is not reachable from the host. Use local-docker provider.networkMode="host" or provider.publishedPorts to expose it.`
1055
+ );
1056
+ }
1057
+ async uploadFile(content, targetPath) {
1058
+ await this.ensureProvisioned();
1059
+ const container = this.requireContainer();
1060
+ const pack = tar.pack();
1061
+ const body = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
1062
+ pack.entry({ name: targetPath.replace(/^\/+/, "") }, body);
1063
+ pack.finalize();
1064
+ await container.putArchive(pack, { path: "/" });
1065
+ }
1066
+ async downloadFile(sourcePath) {
1067
+ await this.ensureProvisioned();
1068
+ const container = this.requireContainer();
1069
+ const archive = await container.getArchive({ path: sourcePath });
1070
+ const chunks = [];
1071
+ for await (const chunk of archive) {
1072
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1073
+ }
1074
+ return Buffer.concat(chunks);
1075
+ }
1076
+ requireContainer() {
1077
+ if (!this.container) {
1078
+ throw new Error("Docker container is not provisioned.");
1079
+ }
1080
+ return this.container;
1081
+ }
1082
+ getLabels() {
1083
+ return {
1084
+ "agentbox.provider": this.provider,
1085
+ ...this.options.tags ?? {}
1086
+ };
1087
+ }
1088
+ toDockerEnv(extra) {
1089
+ return Object.entries(this.getMergedEnv(extra)).map(
1090
+ ([key, value]) => `${key}=${value}`
1091
+ );
1092
+ }
1093
+ toDockerLabelFilters(tags) {
1094
+ const labels = {
1095
+ "agentbox.provider": this.provider,
1096
+ ...tags ?? {}
1097
+ };
1098
+ return Object.entries(labels).map(([key, value]) => `${key}=${value}`);
1099
+ }
1100
+ async findMatchingContainer() {
1101
+ const containers = await this.client.listContainers({
1102
+ all: true,
1103
+ filters: { label: this.toDockerLabelFilters(this.options.tags) }
1104
+ });
1105
+ return containers[0];
1106
+ }
1107
+ async pullImage(image) {
1108
+ const stream = await this.client.pull(image);
1109
+ await new Promise((resolve, reject) => {
1110
+ this.client.modem.followProgress(stream, (error) => {
1111
+ if (error) {
1112
+ reject(error);
1113
+ return;
1114
+ }
1115
+ resolve();
1116
+ });
1117
+ });
1118
+ }
1119
+ async resolveContainerImage(image) {
1120
+ if (!image) {
1121
+ throw new Error(
1122
+ "local-docker sandboxes require options.image to reference a local Docker image."
1123
+ );
1124
+ }
1125
+ if (this.options.provider?.pull) {
1126
+ await this.pullImage(image);
1127
+ }
1128
+ return image;
1129
+ }
1130
+ };
1131
+
1132
+ // src/sandboxes/providers/modal.ts
1133
+ import { ModalClient } from "modal";
1134
+ var ModalSandboxAdapter = class extends SandboxAdapter {
1135
+ client;
1136
+ sandbox;
1137
+ clientClosed = false;
1138
+ constructor(options) {
1139
+ super(options);
1140
+ this.client = new ModalClient({
1141
+ tokenId: options.provider?.tokenId,
1142
+ tokenSecret: options.provider?.tokenSecret,
1143
+ environment: options.provider?.environment,
1144
+ endpoint: options.provider?.endpoint
1145
+ });
1146
+ }
1147
+ get provider() {
1148
+ return SandboxProvider.Modal;
1149
+ }
1150
+ get raw() {
1151
+ return {
1152
+ client: this.client,
1153
+ sandbox: this.sandbox
1154
+ };
1155
+ }
1156
+ get id() {
1157
+ return this.sandbox?.sandboxId;
1158
+ }
1159
+ async provision() {
1160
+ const existing = await this.findMatchingSandbox();
1161
+ if (existing) {
1162
+ this.sandbox = existing;
1163
+ return;
1164
+ }
1165
+ const appName = this.options.provider?.appName ?? "agentbox";
1166
+ const app = await this.client.apps.fromName(appName, {
1167
+ createIfMissing: true,
1168
+ environment: this.options.provider?.environment
1169
+ });
1170
+ const image = await this.resolveModalImage();
1171
+ const resources = resolveSandboxResources(this.options.resources);
1172
+ const unencryptedPorts = this.resolveDefaultUnencryptedPorts();
1173
+ const sandbox = await this.client.sandboxes.create(app, image, {
1174
+ cpu: resources?.cpu,
1175
+ memoryMiB: resources?.memoryMiB,
1176
+ timeoutMs: this.options.autoStopMs,
1177
+ idleTimeoutMs: this.options.idleTimeoutMs,
1178
+ workdir: this.workingDir,
1179
+ command: this.options.provider?.command ?? ["sleep", "infinity"],
1180
+ env: this.getMergedEnv(),
1181
+ encryptedPorts: this.options.provider?.encryptedPorts,
1182
+ unencryptedPorts,
1183
+ verbose: this.options.provider?.verbose
1184
+ });
1185
+ await sandbox.setTags(this.getTags());
1186
+ this.sandbox = sandbox;
1187
+ }
1188
+ /**
1189
+ * Modal requires ports to be declared at sandbox creation time — a running
1190
+ * sandbox cannot gain new tunnels. To make `openPort` work predictably
1191
+ * across providers, we pre-declare all well-known agent-harness ports on
1192
+ * every Modal sandbox we create, unless the caller has explicitly pinned
1193
+ * them to a specific (possibly empty) list.
1194
+ */
1195
+ resolveDefaultUnencryptedPorts() {
1196
+ const declared = this.options.provider?.unencryptedPorts;
1197
+ const encrypted = new Set(this.options.provider?.encryptedPorts ?? []);
1198
+ const reserved = collectAllAgentReservedPorts().filter(
1199
+ (port) => !encrypted.has(port)
1200
+ );
1201
+ if (declared === void 0) {
1202
+ return reserved.length > 0 ? reserved : void 0;
1203
+ }
1204
+ const merged = new Set(declared);
1205
+ for (const port of reserved) {
1206
+ merged.add(port);
1207
+ }
1208
+ return Array.from(merged);
1209
+ }
1210
+ async run(command, options) {
1211
+ await this.ensureProvisioned();
1212
+ const sandbox = this.requireSandbox();
1213
+ const process2 = await sandbox.exec(
1214
+ ["/bin/sh", "-lc", toShellCommand(command)],
1215
+ {
1216
+ workdir: options?.cwd ?? this.workingDir,
1217
+ timeoutMs: options?.timeoutMs,
1218
+ env: this.getMergedEnv(options?.env),
1219
+ pty: options?.pty,
1220
+ mode: "text"
1221
+ }
1222
+ );
1223
+ const [stdout, stderr, exitCode] = await Promise.all([
1224
+ readStreamAsText(process2.stdout),
1225
+ readStreamAsText(process2.stderr),
1226
+ process2.wait()
1227
+ ]);
1228
+ return {
1229
+ exitCode,
1230
+ stdout,
1231
+ stderr,
1232
+ combinedOutput: `${stdout}${stderr}`,
1233
+ raw: process2
1234
+ };
1235
+ }
1236
+ async runAsync(command, options) {
1237
+ await this.ensureProvisioned();
1238
+ const sandbox = this.requireSandbox();
1239
+ const process2 = await sandbox.exec(
1240
+ ["/bin/sh", "-lc", toShellCommand(command)],
1241
+ {
1242
+ workdir: options?.cwd ?? this.workingDir,
1243
+ timeoutMs: options?.timeoutMs,
1244
+ env: this.getMergedEnv(options?.env),
1245
+ pty: options?.pty,
1246
+ mode: "text"
1247
+ }
1248
+ );
1249
+ const queue = new AsyncQueue();
1250
+ let stdout = "";
1251
+ let stderr = "";
1252
+ const stdoutPump = pipeReadableStream(process2.stdout, (chunk) => {
1253
+ stdout += chunk;
1254
+ queue.push({
1255
+ type: "stdout",
1256
+ chunk,
1257
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1258
+ });
1259
+ });
1260
+ const stderrPump = pipeReadableStream(process2.stderr, (chunk) => {
1261
+ stderr += chunk;
1262
+ queue.push({
1263
+ type: "stderr",
1264
+ chunk,
1265
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1266
+ });
1267
+ });
1268
+ const completion = Promise.all([stdoutPump, stderrPump, process2.wait()]).then(([, , exitCode]) => {
1269
+ queue.push({
1270
+ type: "exit",
1271
+ exitCode,
1272
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1273
+ });
1274
+ queue.finish();
1275
+ return {
1276
+ exitCode,
1277
+ stdout,
1278
+ stderr,
1279
+ combinedOutput: `${stdout}${stderr}`,
1280
+ raw: process2
1281
+ };
1282
+ }).catch((error) => {
1283
+ queue.fail(error);
1284
+ throw error;
1285
+ });
1286
+ suppressUnhandledRejection(completion);
1287
+ return {
1288
+ id: `${sandbox.sandboxId}:${Date.now()}`,
1289
+ raw: process2,
1290
+ write: async (input) => {
1291
+ await process2.stdin.writeText(input);
1292
+ },
1293
+ wait: () => completion,
1294
+ kill: async () => {
1295
+ try {
1296
+ await process2.stdin.writeText("");
1297
+ } catch {
1298
+ }
1299
+ },
1300
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
1301
+ };
1302
+ }
1303
+ async list(options) {
1304
+ const sandboxes = [];
1305
+ for await (const sandbox of this.client.sandboxes.list({
1306
+ tags: options?.tags ?? this.tags
1307
+ })) {
1308
+ sandboxes.push({
1309
+ provider: this.provider,
1310
+ id: sandbox.sandboxId,
1311
+ state: await sandbox.poll() === null ? "running" : "finished",
1312
+ tags: await sandbox.getTags(),
1313
+ raw: sandbox
1314
+ });
1315
+ }
1316
+ return sandboxes;
1317
+ }
1318
+ async snapshot() {
1319
+ await this.ensureProvisioned();
1320
+ const sandbox = this.requireSandbox();
1321
+ const image = await sandbox.snapshotFilesystem();
1322
+ return image.imageId;
1323
+ }
1324
+ async stop() {
1325
+ const sandbox = this.sandbox;
1326
+ if (!sandbox) {
1327
+ return;
1328
+ }
1329
+ await sandbox.terminate();
1330
+ this.sandbox = void 0;
1331
+ }
1332
+ async delete() {
1333
+ await this.stop();
1334
+ await this.closeClient();
1335
+ }
1336
+ async openPort(port) {
1337
+ const provider = this.options.provider ?? (this.options.provider = {});
1338
+ if (provider.encryptedPorts?.includes(port)) {
1339
+ return;
1340
+ }
1341
+ const alreadyDeclared = provider.unencryptedPorts?.includes(port) ?? false;
1342
+ if (!alreadyDeclared) {
1343
+ provider.unencryptedPorts = [...provider.unencryptedPorts ?? [], port];
1344
+ }
1345
+ if (!this.sandbox) {
1346
+ return;
1347
+ }
1348
+ try {
1349
+ const tunnels = await this.sandbox.tunnels();
1350
+ if (tunnels[port]) {
1351
+ return;
1352
+ }
1353
+ } catch {
1354
+ return;
1355
+ }
1356
+ throw new Error(
1357
+ `Modal sandbox is already running and cannot expose port ${port} dynamically. Declare it at creation time via \`provider.unencryptedPorts\` (e.g. \`provider: { unencryptedPorts: [${port}] }\`) or use \`AGENT_RESERVED_PORTS\` / \`collectAllAgentReservedPorts()\` from agentbox-sdk to pre-declare the agent harness ports.`
1358
+ );
1359
+ }
1360
+ async getPreviewLink(port) {
1361
+ await this.ensureProvisioned();
1362
+ const sandbox = this.requireSandbox();
1363
+ const tunnels = await sandbox.tunnels();
1364
+ const tunnel = tunnels[port];
1365
+ if (!tunnel) {
1366
+ throw new Error(`Modal sandbox does not expose port ${port}.`);
1367
+ }
1368
+ return tunnel.url;
1369
+ }
1370
+ async findMatchingSandbox() {
1371
+ for await (const sandbox of this.client.sandboxes.list({
1372
+ tags: this.getTags()
1373
+ })) {
1374
+ return sandbox;
1375
+ }
1376
+ return void 0;
1377
+ }
1378
+ getTags() {
1379
+ return {
1380
+ "agentbox.provider": this.provider,
1381
+ ...this.options.tags ?? {}
1382
+ };
1383
+ }
1384
+ async closeClient() {
1385
+ if (this.clientClosed) {
1386
+ return;
1387
+ }
1388
+ this.clientClosed = true;
1389
+ const swallowGrpcShutdown = (reason) => {
1390
+ if (reason instanceof Error && reason.message.includes("Channel has been shut down")) {
1391
+ return;
1392
+ }
1393
+ throw reason;
1394
+ };
1395
+ process.on("unhandledRejection", swallowGrpcShutdown);
1396
+ try {
1397
+ this.client.close();
1398
+ await new Promise((resolve) => setTimeout(resolve, 250));
1399
+ } catch {
1400
+ } finally {
1401
+ process.off("unhandledRejection", swallowGrpcShutdown);
1402
+ }
1403
+ }
1404
+ requireSandbox() {
1405
+ if (!this.sandbox) {
1406
+ throw new Error("Modal sandbox has not been provisioned.");
1407
+ }
1408
+ return this.sandbox;
1409
+ }
1410
+ async resolveModalImage() {
1411
+ const image = resolveSandboxImage(this.options.image);
1412
+ if (!image) {
1413
+ throw new Error(
1414
+ "modal sandboxes require options.image to reference an existing Modal image id."
1415
+ );
1416
+ }
1417
+ return this.client.images.fromId(image);
1418
+ }
1419
+ };
1420
+
1421
+ // src/sandboxes/providers/vercel.ts
1422
+ import { Sandbox as VercelSandbox } from "@vercel/sandbox";
1423
+ function pickFirstTag(tags) {
1424
+ if (!tags) return void 0;
1425
+ const entries = Object.entries(tags);
1426
+ if (entries.length === 0) return void 0;
1427
+ const [key, value] = entries[0];
1428
+ return { [key]: value };
1429
+ }
1430
+ function matchesAllTags(candidateTags, required) {
1431
+ return Object.entries(required).every(
1432
+ ([key, value]) => candidateTags?.[key] === value
1433
+ );
1434
+ }
1435
+ function describeVercelApiError(error, action) {
1436
+ if (error && typeof error === "object" && "response" in error && error.response instanceof Response) {
1437
+ const apiError = error;
1438
+ const status = apiError.response.status;
1439
+ const body = apiError.json !== void 0 ? JSON.stringify(apiError.json) : apiError.text ?? "";
1440
+ return new Error(
1441
+ `Vercel ${action} failed with HTTP ${status}: ${body || apiError.message}`
1442
+ );
1443
+ }
1444
+ return error instanceof Error ? error : new Error(String(error));
1445
+ }
1446
+ async function wrapVercelApiError(action, fn) {
1447
+ try {
1448
+ return await fn();
1449
+ } catch (error) {
1450
+ throw describeVercelApiError(error, action);
1451
+ }
1452
+ }
1453
+ function buildTimeoutSignal(timeoutMs) {
1454
+ if (!timeoutMs || timeoutMs <= 0) return void 0;
1455
+ return AbortSignal.timeout(timeoutMs);
1456
+ }
1457
+ function getCredentials(options) {
1458
+ const token = options.provider?.token ?? process.env.VERCEL_TOKEN;
1459
+ const teamId = options.provider?.teamId ?? process.env.VERCEL_TEAM_ID;
1460
+ const projectId = options.provider?.projectId ?? process.env.VERCEL_PROJECT_ID;
1461
+ if (token && teamId && projectId) {
1462
+ return { token, teamId, projectId };
1463
+ }
1464
+ return {};
1465
+ }
1466
+ var VercelSandboxAdapter = class extends SandboxAdapter {
1467
+ sandbox;
1468
+ get provider() {
1469
+ return SandboxProvider.Vercel;
1470
+ }
1471
+ get raw() {
1472
+ return { sandbox: this.sandbox };
1473
+ }
1474
+ get id() {
1475
+ return this.sandbox?.name;
1476
+ }
1477
+ get workingDir() {
1478
+ return this.options.workingDir ?? "/vercel/sandbox";
1479
+ }
1480
+ get previewHeaders() {
1481
+ const token = this.options.provider?.protectionBypass;
1482
+ return token ? { "x-vercel-protection-bypass": token } : {};
1483
+ }
1484
+ async provision() {
1485
+ const existing = await this.findExistingSandbox();
1486
+ if (existing) {
1487
+ this.sandbox = existing;
1488
+ return;
1489
+ }
1490
+ const credentials = getCredentials(this.options);
1491
+ const provider = this.options.provider;
1492
+ const snapshotId = provider?.snapshotId;
1493
+ const timeout = provider?.timeoutMs ?? 12e4;
1494
+ const runtime = provider?.runtime ?? "node24";
1495
+ const resources = resolveSandboxResources(this.options.resources);
1496
+ const vcpus = resources?.cpu ? { resources: { vcpus: resources.cpu } } : {};
1497
+ const base = {
1498
+ ...credentials,
1499
+ timeout,
1500
+ env: this.getMergedEnv(),
1501
+ ...vcpus,
1502
+ tags: this.getTags(),
1503
+ ...provider?.ports?.length ? { ports: provider.ports } : {}
1504
+ };
1505
+ const sandbox = await wrapVercelApiError("create sandbox", () => {
1506
+ if (snapshotId) {
1507
+ return VercelSandbox.create({
1508
+ ...base,
1509
+ source: { type: "snapshot", snapshotId }
1510
+ });
1511
+ }
1512
+ if (provider?.gitSource) {
1513
+ const git = provider.gitSource;
1514
+ const source = {
1515
+ type: "git",
1516
+ url: git.url,
1517
+ depth: git.depth,
1518
+ revision: git.revision,
1519
+ ...git.username && git.password ? { username: git.username, password: git.password } : {}
1520
+ };
1521
+ return VercelSandbox.create({ ...base, runtime, source });
1522
+ }
1523
+ return VercelSandbox.create({ ...base, runtime });
1524
+ });
1525
+ this.sandbox = sandbox;
1526
+ if (this.workingDir !== "/vercel/sandbox") {
1527
+ await wrapVercelApiError(
1528
+ "create working directory",
1529
+ () => sandbox.runCommand({
1530
+ cmd: "mkdir",
1531
+ args: ["-p", this.workingDir],
1532
+ sudo: true
1533
+ })
1534
+ );
1535
+ }
1536
+ }
1537
+ async run(command, options) {
1538
+ await this.ensureProvisioned();
1539
+ const sandbox = this.requireSandbox();
1540
+ const signal = buildTimeoutSignal(options?.timeoutMs);
1541
+ const result = await wrapVercelApiError(
1542
+ "run command",
1543
+ () => sandbox.runCommand({
1544
+ cmd: "sh",
1545
+ args: ["-lc", toShellCommand(command)],
1546
+ cwd: options?.cwd ?? this.workingDir,
1547
+ env: this.getMergedEnv(options?.env),
1548
+ ...signal ? { signal } : {}
1549
+ })
1550
+ );
1551
+ const [stdout, stderr] = await Promise.all([
1552
+ result.stdout(),
1553
+ result.stderr()
1554
+ ]);
1555
+ return {
1556
+ exitCode: result.exitCode,
1557
+ stdout,
1558
+ stderr,
1559
+ combinedOutput: `${stdout}${stderr}`,
1560
+ raw: result
1561
+ };
1562
+ }
1563
+ async runAsync(command, options) {
1564
+ await this.ensureProvisioned();
1565
+ const sandbox = this.requireSandbox();
1566
+ const signal = buildTimeoutSignal(options?.timeoutMs);
1567
+ const cmd = await wrapVercelApiError(
1568
+ "start async command",
1569
+ () => sandbox.runCommand({
1570
+ cmd: "sh",
1571
+ args: ["-lc", toShellCommand(command)],
1572
+ cwd: options?.cwd ?? this.workingDir,
1573
+ env: this.getMergedEnv(options?.env),
1574
+ detached: true,
1575
+ ...signal ? { signal } : {}
1576
+ })
1577
+ );
1578
+ const queue = new AsyncQueue();
1579
+ let stdout = "";
1580
+ let stderr = "";
1581
+ const completion = (async () => {
1582
+ for await (const log of cmd.logs()) {
1583
+ if (log.stream === "stdout") {
1584
+ stdout += log.data;
1585
+ queue.push({
1586
+ type: "stdout",
1587
+ chunk: log.data,
1588
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1589
+ });
1590
+ } else {
1591
+ stderr += log.data;
1592
+ queue.push({
1593
+ type: "stderr",
1594
+ chunk: log.data,
1595
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1596
+ });
1597
+ }
1598
+ }
1599
+ const finished2 = await cmd.wait();
1600
+ const exitCode = finished2.exitCode;
1601
+ queue.push({
1602
+ type: "exit",
1603
+ exitCode,
1604
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1605
+ });
1606
+ queue.finish();
1607
+ return {
1608
+ exitCode,
1609
+ stdout,
1610
+ stderr,
1611
+ combinedOutput: `${stdout}${stderr}`,
1612
+ raw: finished2
1613
+ };
1614
+ })().catch((error) => {
1615
+ queue.fail(error);
1616
+ throw error;
1617
+ });
1618
+ suppressUnhandledRejection(completion);
1619
+ return {
1620
+ id: cmd.cmdId,
1621
+ raw: cmd,
1622
+ wait: () => completion,
1623
+ kill: async () => {
1624
+ await cmd.kill();
1625
+ },
1626
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
1627
+ };
1628
+ }
1629
+ async list(options) {
1630
+ const filterTags = options?.tags ?? this.getTags();
1631
+ const sandboxes = await this.listSandboxesByTags(filterTags);
1632
+ return sandboxes.map(
1633
+ (s) => ({
1634
+ provider: this.provider,
1635
+ id: s.name,
1636
+ state: s.status,
1637
+ tags: s.tags ?? {},
1638
+ createdAt: new Date(s.createdAt).toISOString(),
1639
+ raw: s
1640
+ })
1641
+ );
1642
+ }
1643
+ async snapshot() {
1644
+ await this.ensureProvisioned();
1645
+ const sandbox = this.requireSandbox();
1646
+ const snap = await wrapVercelApiError(
1647
+ "snapshot sandbox",
1648
+ () => sandbox.snapshot()
1649
+ );
1650
+ return snap.snapshotId;
1651
+ }
1652
+ async stop() {
1653
+ const sandbox = this.sandbox;
1654
+ if (!sandbox) {
1655
+ return;
1656
+ }
1657
+ await wrapVercelApiError("stop sandbox", () => sandbox.stop());
1658
+ this.sandbox = void 0;
1659
+ }
1660
+ async delete() {
1661
+ await this.stop();
1662
+ }
1663
+ async openPort(_port) {
1664
+ void _port;
1665
+ }
1666
+ async getPreviewLink(port) {
1667
+ await this.ensureProvisioned();
1668
+ const sandbox = this.requireSandbox();
1669
+ return sandbox.domain(port);
1670
+ }
1671
+ async uploadFile(content, targetPath) {
1672
+ await this.ensureProvisioned();
1673
+ const sandbox = this.requireSandbox();
1674
+ const data = typeof content === "string" ? content : new Uint8Array(content);
1675
+ await sandbox.writeFiles([{ path: targetPath, content: data }]);
1676
+ }
1677
+ async downloadFile(sourcePath) {
1678
+ await this.ensureProvisioned();
1679
+ const sandbox = this.requireSandbox();
1680
+ const result = await sandbox.readFileToBuffer({ path: sourcePath });
1681
+ if (!result) {
1682
+ throw new Error(`File not found in Vercel sandbox: ${sourcePath}`);
1683
+ }
1684
+ return result;
1685
+ }
1686
+ getTags() {
1687
+ return {
1688
+ "agentbox.provider": this.provider,
1689
+ ...this.options.tags ?? {}
1690
+ };
1691
+ }
1692
+ async listSandboxesByTags(tags) {
1693
+ const credentials = getCredentials(this.options);
1694
+ const result = await wrapVercelApiError(
1695
+ "list sandboxes",
1696
+ () => VercelSandbox.list({
1697
+ ...credentials,
1698
+ tags: pickFirstTag(tags)
1699
+ })
1700
+ );
1701
+ return result.sandboxes.filter(
1702
+ (s) => matchesAllTags(s.tags, tags)
1703
+ );
1704
+ }
1705
+ async findExistingSandbox() {
1706
+ const credentials = getCredentials(this.options);
1707
+ const sandboxes = await this.listSandboxesByTags(this.getTags());
1708
+ const match = sandboxes.find((s) => s.status === "running");
1709
+ if (!match) {
1710
+ return void 0;
1711
+ }
1712
+ return wrapVercelApiError(
1713
+ "get sandbox",
1714
+ () => VercelSandbox.get({ ...credentials, name: match.name })
1715
+ );
1716
+ }
1717
+ requireSandbox() {
1718
+ if (!this.sandbox) {
1719
+ throw new Error("Vercel sandbox has not been provisioned.");
1720
+ }
1721
+ return this.sandbox;
1722
+ }
1723
+ };
1724
+
1725
+ // src/sandboxes/Sandbox.ts
1726
+ function createSandboxAdapter(provider, options) {
1727
+ switch (provider) {
1728
+ case SandboxProvider.LocalDocker:
1729
+ return new LocalDockerSandboxAdapter(
1730
+ options
1731
+ );
1732
+ case SandboxProvider.Modal:
1733
+ return new ModalSandboxAdapter(
1734
+ options
1735
+ );
1736
+ case SandboxProvider.Daytona:
1737
+ return new DaytonaSandboxAdapter(
1738
+ options
1739
+ );
1740
+ case SandboxProvider.Vercel:
1741
+ return new VercelSandboxAdapter(
1742
+ options
1743
+ );
1744
+ case SandboxProvider.E2B:
1745
+ return new E2bSandboxAdapter(
1746
+ options
1747
+ );
1748
+ default:
1749
+ throw new UnsupportedProviderError("sandbox", provider);
1750
+ }
1751
+ }
1752
+ var Sandbox = class {
1753
+ constructor(providerName, options) {
1754
+ this.providerName = providerName;
1755
+ this.options = options;
1756
+ this.adapter = createSandboxAdapter(providerName, options);
1757
+ }
1758
+ providerName;
1759
+ options;
1760
+ adapter;
1761
+ get provider() {
1762
+ return this.providerName;
1763
+ }
1764
+ get optionsSnapshot() {
1765
+ return this.options;
1766
+ }
1767
+ get id() {
1768
+ return this.adapter.id;
1769
+ }
1770
+ get raw() {
1771
+ return this.adapter.raw;
1772
+ }
1773
+ async openPort(port) {
1774
+ await this.adapter.openPort(port);
1775
+ return this;
1776
+ }
1777
+ setSecret(name, value) {
1778
+ this.adapter.setSecret(name, value);
1779
+ return this;
1780
+ }
1781
+ setSecrets(values) {
1782
+ this.adapter.setSecrets(values);
1783
+ return this;
1784
+ }
1785
+ async gitClone(options) {
1786
+ return this.adapter.gitClone(options);
1787
+ }
1788
+ async run(command, options) {
1789
+ return this.adapter.run(command, options);
1790
+ }
1791
+ async runAsync(command, options) {
1792
+ return this.adapter.runAsync(command, options);
1793
+ }
1794
+ async list(options) {
1795
+ return this.adapter.list(options);
1796
+ }
1797
+ async snapshot() {
1798
+ return this.adapter.snapshot();
1799
+ }
1800
+ async stop() {
1801
+ return this.adapter.stop();
1802
+ }
1803
+ async delete() {
1804
+ return this.adapter.delete();
1805
+ }
1806
+ async getPreviewLink(port) {
1807
+ return this.adapter.getPreviewLink(port);
1808
+ }
1809
+ get previewHeaders() {
1810
+ return this.adapter.previewHeaders;
1811
+ }
1812
+ async uploadFile(content, targetPath) {
1813
+ return this.adapter.uploadFile(content, targetPath);
1814
+ }
1815
+ async downloadFile(sourcePath) {
1816
+ return this.adapter.downloadFile(sourcePath);
1817
+ }
1818
+ };
1819
+
1820
+ export {
1821
+ buildGitCloneCommand,
1822
+ SandboxAdapter,
1823
+ Sandbox
1824
+ };