agentbox-sdk 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -18
- package/dist/{Sandbox-DTprxRZf.d.ts → Sandbox-CByFJI8X.d.ts} +102 -6
- package/dist/agents/index.d.ts +74 -5
- package/dist/agents/index.js +14 -5
- package/dist/chunk-4MBB6QHD.js +4658 -0
- package/dist/chunk-GOFJNFAD.js +18 -0
- package/dist/{chunk-HMBWQSVN.js → chunk-INMA52FV.js} +83 -23
- package/dist/{chunk-QRQFQTGH.js → chunk-LPKKT6YT.js} +742 -50
- package/dist/chunk-ZOWBRUQR.js +476 -0
- package/dist/cli.js +16 -8
- package/dist/enums.d.ts +25 -0
- package/dist/enums.js +8 -0
- package/dist/events/index.d.ts +86 -2
- package/dist/events/index.js +4 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +18 -5
- package/dist/sandboxes/index.d.ts +58 -4
- package/dist/sandboxes/index.js +7 -3
- package/dist/{types-BwcoN0n-.d.ts → types-B3N-Qo2q.d.ts} +147 -6
- package/images/browser-agent.mjs +3 -3
- package/package.json +13 -3
- package/dist/chunk-7FLLQJ6J.js +0 -185
- package/dist/chunk-BW43ESRM.js +0 -4381
- /package/dist/{chunk-JFDP556Q.js → chunk-NSJM57Z4.js} +0 -0
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AsyncQueue,
|
|
3
3
|
UnsupportedProviderError,
|
|
4
|
+
collectAllAgentReservedPorts,
|
|
5
|
+
debugSandbox,
|
|
4
6
|
pipeReadableStream,
|
|
5
7
|
readNodeStream,
|
|
6
8
|
readStreamAsText,
|
|
7
|
-
sleep
|
|
8
|
-
|
|
9
|
+
sleep,
|
|
10
|
+
suppressUnhandledRejection,
|
|
11
|
+
time
|
|
12
|
+
} from "./chunk-INMA52FV.js";
|
|
9
13
|
import {
|
|
10
14
|
shellQuote,
|
|
11
15
|
toShellCommand
|
|
12
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-NSJM57Z4.js";
|
|
17
|
+
import {
|
|
18
|
+
SandboxProvider
|
|
19
|
+
} from "./chunk-GOFJNFAD.js";
|
|
13
20
|
|
|
14
21
|
// src/sandboxes/git.ts
|
|
15
22
|
function encodeExtraHeader(name, value) {
|
|
@@ -55,30 +62,122 @@ var SandboxAdapter = class {
|
|
|
55
62
|
baseEnv;
|
|
56
63
|
provisioned = false;
|
|
57
64
|
provisioning;
|
|
65
|
+
/**
|
|
66
|
+
* Whether `provision()` warm-attached to a pre-existing tagged sandbox
|
|
67
|
+
* (true) or had to create a fresh one (false). Set by adapter
|
|
68
|
+
* `provision()` implementations. Stays `false` until `findOrProvision()`
|
|
69
|
+
* has resolved.
|
|
70
|
+
*/
|
|
71
|
+
wasFoundFlag = false;
|
|
58
72
|
constructor(options) {
|
|
59
73
|
this.options = options;
|
|
60
74
|
this.baseEnv = { ...options.env ?? {} };
|
|
61
75
|
}
|
|
62
|
-
async
|
|
76
|
+
async uploadFile(_content, _targetPath) {
|
|
77
|
+
void _content;
|
|
78
|
+
void _targetPath;
|
|
79
|
+
throw new Error(
|
|
80
|
+
`uploadFile is not supported by the ${this.provider} provider.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
async downloadFile(_sourcePath) {
|
|
84
|
+
void _sourcePath;
|
|
85
|
+
throw new Error(
|
|
86
|
+
`downloadFile is not supported by the ${this.provider} provider.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Upload a tarball of files into the sandbox and execute a command in
|
|
91
|
+
* the same round-trip. Used by setup paths that would otherwise need one
|
|
92
|
+
* sandbox RPC per file plus another to run the install script — Modal-
|
|
93
|
+
* style providers pay ~700ms per RPC, so collapsing N+1 calls into one
|
|
94
|
+
* is the single biggest win on cold setup.
|
|
95
|
+
*
|
|
96
|
+
* Default implementation falls back to `uploadFile` per entry + a final
|
|
97
|
+
* `run`. Providers that support stdin streaming (Modal) override this to
|
|
98
|
+
* do the upload + extract + exec in a single sandbox `exec` call.
|
|
99
|
+
*/
|
|
100
|
+
async uploadAndRun(files, command, options) {
|
|
101
|
+
this.requireProvisioned();
|
|
102
|
+
for (const entry of files) {
|
|
103
|
+
const content = typeof entry.content === "string" ? Buffer.from(entry.content, "utf8") : entry.content;
|
|
104
|
+
await this.uploadFile(content, entry.path);
|
|
105
|
+
}
|
|
106
|
+
if (files.length > 0) {
|
|
107
|
+
const chmodCmd = files.filter((entry) => entry.mode && (entry.mode & 73) !== 0).map(
|
|
108
|
+
(entry) => `chmod ${entry.mode.toString(8)} ${shellQuote(entry.path)}`
|
|
109
|
+
);
|
|
110
|
+
if (chmodCmd.length > 0) {
|
|
111
|
+
await this.run(chmodCmd.join(" && "), options);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return this.run(command, options);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Public hook that callers must invoke before they touch the sandbox
|
|
118
|
+
* (running commands, cloning repos, uploading files, opening preview
|
|
119
|
+
* links, …). It either attaches to an existing tagged sandbox or creates
|
|
120
|
+
* a new one. The result is cached so repeated calls are cheap.
|
|
121
|
+
*
|
|
122
|
+
* Provisioning is no longer triggered implicitly by `run`, `runAsync`,
|
|
123
|
+
* `gitClone`, `uploadAndRun`, etc. Those methods now throw a clear error
|
|
124
|
+
* when the adapter has not been provisioned yet, which makes the
|
|
125
|
+
* lifecycle explicit and gives callers control over when the
|
|
126
|
+
* (potentially slow) sandbox attach / create happens.
|
|
127
|
+
*/
|
|
128
|
+
async findOrProvision() {
|
|
63
129
|
if (this.provisioned) {
|
|
64
130
|
return;
|
|
65
131
|
}
|
|
66
132
|
if (!this.provisioning) {
|
|
67
|
-
this.provisioning = (
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
133
|
+
this.provisioning = time(
|
|
134
|
+
debugSandbox,
|
|
135
|
+
`provision [${this.provider}] (find-or-create)`,
|
|
136
|
+
async () => {
|
|
137
|
+
await this.provision();
|
|
138
|
+
this.provisioned = true;
|
|
139
|
+
}
|
|
140
|
+
).finally(() => {
|
|
71
141
|
this.provisioning = void 0;
|
|
72
142
|
});
|
|
73
143
|
}
|
|
74
144
|
await this.provisioning;
|
|
75
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Throw a consistent error when a method that needs a provisioned
|
|
148
|
+
* sandbox is called before `findOrProvision()`. Provider adapters call
|
|
149
|
+
* this at the top of `run`, `runAsync`, `uploadFile`, etc.
|
|
150
|
+
*/
|
|
151
|
+
requireProvisioned() {
|
|
152
|
+
if (!this.provisioned) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Sandbox (${this.provider}) is not provisioned. Call \`sandbox.findOrProvision()\` once before running commands, cloning repos, or uploading files.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
76
158
|
get tags() {
|
|
77
159
|
return { ...this.options.tags ?? {} };
|
|
78
160
|
}
|
|
79
161
|
get workingDir() {
|
|
80
162
|
return this.options.workingDir ?? "/workspace";
|
|
81
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether `findOrProvision()` warm-attached to a pre-existing tagged
|
|
166
|
+
* sandbox (`true`) or created a fresh one (`false`). Useful to skip
|
|
167
|
+
* idempotent setup that the previous run already performed (e.g.
|
|
168
|
+
* `agent.setup()`). Always `false` before `findOrProvision()` resolves.
|
|
169
|
+
*/
|
|
170
|
+
get wasFound() {
|
|
171
|
+
return this.wasFoundFlag;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Headers that callers should attach to HTTP / WebSocket requests they make
|
|
175
|
+
* against this sandbox's preview URL. Default is empty; providers like
|
|
176
|
+
* Vercel override this to inject Deployment Protection bypass tokens.
|
|
177
|
+
*/
|
|
178
|
+
get previewHeaders() {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
82
181
|
getMergedEnv(extra) {
|
|
83
182
|
return {
|
|
84
183
|
...this.baseEnv,
|
|
@@ -93,7 +192,7 @@ var SandboxAdapter = class {
|
|
|
93
192
|
Object.assign(this.secrets, values);
|
|
94
193
|
}
|
|
95
194
|
async gitClone(options) {
|
|
96
|
-
|
|
195
|
+
this.requireProvisioned();
|
|
97
196
|
return this.run(buildGitCloneCommand(options), {
|
|
98
197
|
cwd: this.workingDir,
|
|
99
198
|
env: this.getMergedEnv()
|
|
@@ -127,7 +226,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
127
226
|
});
|
|
128
227
|
}
|
|
129
228
|
get provider() {
|
|
130
|
-
return
|
|
229
|
+
return SandboxProvider.Daytona;
|
|
131
230
|
}
|
|
132
231
|
get raw() {
|
|
133
232
|
return {
|
|
@@ -143,6 +242,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
143
242
|
if (existing) {
|
|
144
243
|
this.sandbox = existing;
|
|
145
244
|
await existing.start();
|
|
245
|
+
this.wasFoundFlag = true;
|
|
146
246
|
return;
|
|
147
247
|
}
|
|
148
248
|
const labels = this.getLabels();
|
|
@@ -171,6 +271,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
171
271
|
autoDeleteInterval
|
|
172
272
|
};
|
|
173
273
|
const sandbox = await this.client.create({
|
|
274
|
+
...this.options.provider?.createParams,
|
|
174
275
|
...createBase,
|
|
175
276
|
snapshot: image
|
|
176
277
|
});
|
|
@@ -178,7 +279,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
178
279
|
this.sandbox = sandbox;
|
|
179
280
|
}
|
|
180
281
|
async run(command, options) {
|
|
181
|
-
|
|
282
|
+
this.requireProvisioned();
|
|
182
283
|
const sandbox = this.requireSandbox();
|
|
183
284
|
const result = await sandbox.process.executeCommand(
|
|
184
285
|
toShellCommand(command),
|
|
@@ -196,7 +297,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
196
297
|
};
|
|
197
298
|
}
|
|
198
299
|
async runAsync(command, options) {
|
|
199
|
-
|
|
300
|
+
this.requireProvisioned();
|
|
200
301
|
const sandbox = this.requireSandbox();
|
|
201
302
|
const sessionId = `agentbox-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
202
303
|
await sandbox.process.createSession(sessionId);
|
|
@@ -288,9 +389,17 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
288
389
|
queue.fail(error);
|
|
289
390
|
throw error;
|
|
290
391
|
});
|
|
392
|
+
suppressUnhandledRejection(completion);
|
|
291
393
|
return {
|
|
292
394
|
id: commandId,
|
|
293
395
|
raw: { sessionId, commandId },
|
|
396
|
+
write: async (input) => {
|
|
397
|
+
await sandbox.process.sendSessionCommandInput(
|
|
398
|
+
sessionId,
|
|
399
|
+
commandId,
|
|
400
|
+
input
|
|
401
|
+
);
|
|
402
|
+
},
|
|
294
403
|
wait: () => completion,
|
|
295
404
|
kill: async () => {
|
|
296
405
|
killed = true;
|
|
@@ -331,15 +440,26 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
331
440
|
this.sandbox = void 0;
|
|
332
441
|
}
|
|
333
442
|
async openPort(port) {
|
|
334
|
-
|
|
443
|
+
this.requireProvisioned();
|
|
335
444
|
await this.requireSandbox().getPreviewLink(port);
|
|
336
445
|
}
|
|
337
446
|
async getPreviewLink(port) {
|
|
338
|
-
|
|
447
|
+
this.requireProvisioned();
|
|
339
448
|
const sandbox = this.requireSandbox();
|
|
340
449
|
const preview = await sandbox.getPreviewLink(port);
|
|
341
450
|
return preview.url;
|
|
342
451
|
}
|
|
452
|
+
async uploadFile(content, targetPath) {
|
|
453
|
+
this.requireProvisioned();
|
|
454
|
+
const sandbox = this.requireSandbox();
|
|
455
|
+
const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
456
|
+
await sandbox.fs.uploadFile(buffer, targetPath);
|
|
457
|
+
}
|
|
458
|
+
async downloadFile(sourcePath) {
|
|
459
|
+
this.requireProvisioned();
|
|
460
|
+
const sandbox = this.requireSandbox();
|
|
461
|
+
return sandbox.fs.downloadFile(sourcePath);
|
|
462
|
+
}
|
|
343
463
|
getLabels() {
|
|
344
464
|
return {
|
|
345
465
|
"agentbox.provider": this.provider,
|
|
@@ -382,7 +502,7 @@ async function loadE2bModule() {
|
|
|
382
502
|
var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
383
503
|
sandbox;
|
|
384
504
|
get provider() {
|
|
385
|
-
return
|
|
505
|
+
return SandboxProvider.E2B;
|
|
386
506
|
}
|
|
387
507
|
get raw() {
|
|
388
508
|
return {
|
|
@@ -397,6 +517,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
397
517
|
const existing = await this.findMatchingSandbox();
|
|
398
518
|
if (existing) {
|
|
399
519
|
this.sandbox = existing;
|
|
520
|
+
this.wasFoundFlag = true;
|
|
400
521
|
return;
|
|
401
522
|
}
|
|
402
523
|
const template = resolveSandboxImage(this.options.image);
|
|
@@ -422,7 +543,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
422
543
|
});
|
|
423
544
|
}
|
|
424
545
|
async run(command, options) {
|
|
425
|
-
|
|
546
|
+
this.requireProvisioned();
|
|
426
547
|
const sandbox = this.requireSandbox();
|
|
427
548
|
const { CommandExitError } = await loadE2bModule();
|
|
428
549
|
if (options?.pty) {
|
|
@@ -450,7 +571,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
450
571
|
}
|
|
451
572
|
}
|
|
452
573
|
async runAsync(command, options) {
|
|
453
|
-
|
|
574
|
+
this.requireProvisioned();
|
|
454
575
|
const sandbox = this.requireSandbox();
|
|
455
576
|
const { CommandExitError } = await loadE2bModule();
|
|
456
577
|
const queue = new AsyncQueue();
|
|
@@ -520,6 +641,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
520
641
|
queue.fail(error);
|
|
521
642
|
throw error;
|
|
522
643
|
});
|
|
644
|
+
suppressUnhandledRejection(completion);
|
|
523
645
|
return {
|
|
524
646
|
id: String(handle.pid),
|
|
525
647
|
raw: handle,
|
|
@@ -585,6 +707,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
585
707
|
queue.fail(error);
|
|
586
708
|
throw error;
|
|
587
709
|
});
|
|
710
|
+
suppressUnhandledRejection(completion);
|
|
588
711
|
return {
|
|
589
712
|
id: String(handle.pid),
|
|
590
713
|
raw: handle,
|
|
@@ -624,7 +747,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
624
747
|
return sandboxes;
|
|
625
748
|
}
|
|
626
749
|
async snapshot() {
|
|
627
|
-
|
|
750
|
+
this.requireProvisioned();
|
|
628
751
|
const sandbox = this.requireSandbox();
|
|
629
752
|
const snapshot = await sandbox.createSnapshot();
|
|
630
753
|
return snapshot.snapshotId;
|
|
@@ -644,11 +767,27 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
644
767
|
void _port;
|
|
645
768
|
}
|
|
646
769
|
async getPreviewLink(port) {
|
|
647
|
-
|
|
770
|
+
this.requireProvisioned();
|
|
648
771
|
const sandbox = this.requireSandbox();
|
|
649
772
|
const host = sandbox.getHost(port);
|
|
650
773
|
return host.startsWith("localhost:") ? `http://${host}` : `https://${host}`;
|
|
651
774
|
}
|
|
775
|
+
async uploadFile(content, targetPath) {
|
|
776
|
+
this.requireProvisioned();
|
|
777
|
+
const sandbox = this.requireSandbox();
|
|
778
|
+
if (typeof content === "string") {
|
|
779
|
+
await sandbox.files.write(targetPath, content);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const exactBytes = Uint8Array.from(content);
|
|
783
|
+
await sandbox.files.write(targetPath, new Blob([exactBytes]));
|
|
784
|
+
}
|
|
785
|
+
async downloadFile(sourcePath) {
|
|
786
|
+
this.requireProvisioned();
|
|
787
|
+
const sandbox = this.requireSandbox();
|
|
788
|
+
const bytes = await sandbox.files.read(sourcePath, { format: "bytes" });
|
|
789
|
+
return Buffer.from(bytes);
|
|
790
|
+
}
|
|
652
791
|
async findMatchingSandbox() {
|
|
653
792
|
const { Sandbox: E2bSandbox } = await loadE2bModule();
|
|
654
793
|
const matches = await this.list();
|
|
@@ -684,7 +823,14 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
684
823
|
return {
|
|
685
824
|
cwd: options?.cwd ?? this.workingDir,
|
|
686
825
|
envs: this.getMergedEnv(options?.env),
|
|
687
|
-
|
|
826
|
+
// E2B is the only provider whose underlying SDK applies its own
|
|
827
|
+
// per-command timeout (60_000 ms) when nothing is specified.
|
|
828
|
+
// local-docker / modal / daytona pass through `undefined` and
|
|
829
|
+
// let the caller decide, so we do the same here by disabling
|
|
830
|
+
// E2B's default with `0`. Callers that want a wall-clock cap
|
|
831
|
+
// pass `timeoutMs` explicitly; everyone else relies on the
|
|
832
|
+
// sandbox lifecycle / their own AbortController.
|
|
833
|
+
timeoutMs: options?.timeoutMs ?? 0
|
|
688
834
|
};
|
|
689
835
|
}
|
|
690
836
|
resolveTimeoutConfig() {
|
|
@@ -762,7 +908,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
762
908
|
client = new Docker();
|
|
763
909
|
container;
|
|
764
910
|
get provider() {
|
|
765
|
-
return
|
|
911
|
+
return SandboxProvider.LocalDocker;
|
|
766
912
|
}
|
|
767
913
|
get raw() {
|
|
768
914
|
return {
|
|
@@ -789,11 +935,11 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
789
935
|
const env = Object.entries(this.getMergedEnv()).map(
|
|
790
936
|
([key, value]) => `${key}=${value}`
|
|
791
937
|
);
|
|
792
|
-
const publishedPorts = this.
|
|
938
|
+
const publishedPorts = this.resolveDefaultPublishedPorts();
|
|
793
939
|
const portBindings = publishedPorts.length > 0 ? Object.fromEntries(
|
|
794
940
|
publishedPorts.map((port) => [
|
|
795
941
|
`${port}/tcp`,
|
|
796
|
-
[{ HostIp: "127.0.0.1", HostPort:
|
|
942
|
+
[{ HostIp: "127.0.0.1", HostPort: "" }]
|
|
797
943
|
])
|
|
798
944
|
) : void 0;
|
|
799
945
|
const exposedPorts = publishedPorts.length > 0 ? Object.fromEntries(publishedPorts.map((port) => [`${port}/tcp`, {}])) : void 0;
|
|
@@ -821,7 +967,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
821
967
|
await container.start();
|
|
822
968
|
}
|
|
823
969
|
async run(command, options) {
|
|
824
|
-
|
|
970
|
+
this.requireProvisioned();
|
|
825
971
|
const container = this.requireContainer();
|
|
826
972
|
const exec = await container.exec({
|
|
827
973
|
AttachStdout: true,
|
|
@@ -864,7 +1010,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
864
1010
|
};
|
|
865
1011
|
}
|
|
866
1012
|
async runAsync(command, options) {
|
|
867
|
-
|
|
1013
|
+
this.requireProvisioned();
|
|
868
1014
|
const container = this.requireContainer();
|
|
869
1015
|
const exec = await container.exec({
|
|
870
1016
|
AttachStdin: true,
|
|
@@ -921,6 +1067,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
921
1067
|
queue.fail(error);
|
|
922
1068
|
throw error;
|
|
923
1069
|
});
|
|
1070
|
+
suppressUnhandledRejection(completion);
|
|
924
1071
|
return {
|
|
925
1072
|
id: exec.id,
|
|
926
1073
|
raw: { exec, stream },
|
|
@@ -1017,15 +1164,26 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1017
1164
|
if (networkMode === "host") {
|
|
1018
1165
|
return `http://127.0.0.1:${port}`;
|
|
1019
1166
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1167
|
+
const declared = this.resolveDefaultPublishedPorts();
|
|
1168
|
+
if (!declared.includes(port)) {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
`Port ${port} is not reachable from the host. Use local-docker provider.networkMode="host" or provider.publishedPorts to expose it.`
|
|
1171
|
+
);
|
|
1022
1172
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1173
|
+
const container = this.requireContainer();
|
|
1174
|
+
const inspect = await container.inspect();
|
|
1175
|
+
const portsMap = inspect.NetworkSettings?.Ports ?? {};
|
|
1176
|
+
const bindings = portsMap[`${port}/tcp`];
|
|
1177
|
+
const hostPort = Array.isArray(bindings) ? bindings.find((binding) => binding?.HostPort)?.HostPort : void 0;
|
|
1178
|
+
if (!hostPort) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`Port ${port} is not bound on the local-docker container. Make sure it is listed in provider.publishedPorts (or covered by AGENT_RESERVED_PORTS) before findOrProvision().`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return `http://127.0.0.1:${hostPort}`;
|
|
1026
1184
|
}
|
|
1027
1185
|
async uploadFile(content, targetPath) {
|
|
1028
|
-
|
|
1186
|
+
this.requireProvisioned();
|
|
1029
1187
|
const container = this.requireContainer();
|
|
1030
1188
|
const pack = tar.pack();
|
|
1031
1189
|
const body = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
@@ -1034,7 +1192,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1034
1192
|
await container.putArchive(pack, { path: "/" });
|
|
1035
1193
|
}
|
|
1036
1194
|
async downloadFile(sourcePath) {
|
|
1037
|
-
|
|
1195
|
+
this.requireProvisioned();
|
|
1038
1196
|
const container = this.requireContainer();
|
|
1039
1197
|
const archive = await container.getArchive({ path: sourcePath });
|
|
1040
1198
|
const chunks = [];
|
|
@@ -1049,6 +1207,32 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1049
1207
|
}
|
|
1050
1208
|
return this.container;
|
|
1051
1209
|
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Local-docker requires ports to be declared at container creation
|
|
1212
|
+
* time (the `PortBindings` host config can't be amended on a running
|
|
1213
|
+
* container). To make `openPort` work predictably across providers,
|
|
1214
|
+
* we pre-publish all well-known agent-harness ports on every
|
|
1215
|
+
* local-docker sandbox we create — mirroring the Modal adapter's
|
|
1216
|
+
* `resolveDefaultUnencryptedPorts` behavior. Any explicit
|
|
1217
|
+
* `provider.publishedPorts` is honored AND merged with the reserved
|
|
1218
|
+
* set, so callers don't have to remember to list the agent's
|
|
1219
|
+
* default port for things like the claude-code SDK relay.
|
|
1220
|
+
*
|
|
1221
|
+
* Network-mode "host" containers don't use port bindings at all, so
|
|
1222
|
+
* we skip the merge in that case.
|
|
1223
|
+
*/
|
|
1224
|
+
resolveDefaultPublishedPorts() {
|
|
1225
|
+
if (this.options.provider?.networkMode === "host") {
|
|
1226
|
+
return [];
|
|
1227
|
+
}
|
|
1228
|
+
const declared = this.options.provider?.publishedPorts;
|
|
1229
|
+
const reserved = collectAllAgentReservedPorts();
|
|
1230
|
+
const merged = new Set(declared ?? []);
|
|
1231
|
+
for (const port of reserved) {
|
|
1232
|
+
merged.add(port);
|
|
1233
|
+
}
|
|
1234
|
+
return Array.from(merged);
|
|
1235
|
+
}
|
|
1052
1236
|
getLabels() {
|
|
1053
1237
|
return {
|
|
1054
1238
|
"agentbox.provider": this.provider,
|
|
@@ -1101,10 +1285,46 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1101
1285
|
|
|
1102
1286
|
// src/sandboxes/providers/modal.ts
|
|
1103
1287
|
import { ModalClient } from "modal";
|
|
1288
|
+
|
|
1289
|
+
// src/sandboxes/tarball.ts
|
|
1290
|
+
import tar2 from "tar-stream";
|
|
1291
|
+
async function buildTarball(entries) {
|
|
1292
|
+
const pack = tar2.pack();
|
|
1293
|
+
const chunks = [];
|
|
1294
|
+
pack.on("data", (chunk) => {
|
|
1295
|
+
chunks.push(chunk);
|
|
1296
|
+
});
|
|
1297
|
+
const finished2 = new Promise((resolve, reject) => {
|
|
1298
|
+
pack.on("end", () => resolve());
|
|
1299
|
+
pack.on("error", (error) => reject(error));
|
|
1300
|
+
});
|
|
1301
|
+
for (const entry of entries) {
|
|
1302
|
+
const content = typeof entry.content === "string" ? Buffer.from(entry.content, "utf8") : entry.content;
|
|
1303
|
+
pack.entry(
|
|
1304
|
+
{
|
|
1305
|
+
name: entry.path.replace(/^\/+/, ""),
|
|
1306
|
+
mode: entry.mode ?? 420,
|
|
1307
|
+
size: content.length,
|
|
1308
|
+
mtime: /* @__PURE__ */ new Date(0),
|
|
1309
|
+
type: "file"
|
|
1310
|
+
},
|
|
1311
|
+
content
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
pack.finalize();
|
|
1315
|
+
await finished2;
|
|
1316
|
+
return Buffer.concat(chunks);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/sandboxes/providers/modal.ts
|
|
1104
1320
|
var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
1105
1321
|
client;
|
|
1106
1322
|
sandbox;
|
|
1107
1323
|
clientClosed = false;
|
|
1324
|
+
// Cached tunnel map. Populated on the first `getPreviewLink` call after
|
|
1325
|
+
// provision; reused on every subsequent call so the agent runtime path
|
|
1326
|
+
// doesn't re-issue the Modal RPC for each per-run tunnel lookup.
|
|
1327
|
+
tunnelsPromise;
|
|
1108
1328
|
constructor(options) {
|
|
1109
1329
|
super(options);
|
|
1110
1330
|
this.client = new ModalClient({
|
|
@@ -1115,7 +1335,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1115
1335
|
});
|
|
1116
1336
|
}
|
|
1117
1337
|
get provider() {
|
|
1118
|
-
return
|
|
1338
|
+
return SandboxProvider.Modal;
|
|
1119
1339
|
}
|
|
1120
1340
|
get raw() {
|
|
1121
1341
|
return {
|
|
@@ -1130,6 +1350,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1130
1350
|
const existing = await this.findMatchingSandbox();
|
|
1131
1351
|
if (existing) {
|
|
1132
1352
|
this.sandbox = existing;
|
|
1353
|
+
this.wasFoundFlag = true;
|
|
1133
1354
|
return;
|
|
1134
1355
|
}
|
|
1135
1356
|
const appName = this.options.provider?.appName ?? "agentbox";
|
|
@@ -1139,7 +1360,9 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1139
1360
|
});
|
|
1140
1361
|
const image = await this.resolveModalImage();
|
|
1141
1362
|
const resources = resolveSandboxResources(this.options.resources);
|
|
1363
|
+
const unencryptedPorts = this.resolveDefaultUnencryptedPorts();
|
|
1142
1364
|
const sandbox = await this.client.sandboxes.create(app, image, {
|
|
1365
|
+
...this.options.provider?.createParams,
|
|
1143
1366
|
cpu: resources?.cpu,
|
|
1144
1367
|
memoryMiB: resources?.memoryMiB,
|
|
1145
1368
|
timeoutMs: this.options.autoStopMs,
|
|
@@ -1148,17 +1371,39 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1148
1371
|
command: this.options.provider?.command ?? ["sleep", "infinity"],
|
|
1149
1372
|
env: this.getMergedEnv(),
|
|
1150
1373
|
encryptedPorts: this.options.provider?.encryptedPorts,
|
|
1151
|
-
unencryptedPorts
|
|
1374
|
+
unencryptedPorts,
|
|
1152
1375
|
verbose: this.options.provider?.verbose
|
|
1153
1376
|
});
|
|
1154
1377
|
await sandbox.setTags(this.getTags());
|
|
1155
1378
|
this.sandbox = sandbox;
|
|
1156
1379
|
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Modal requires ports to be declared at sandbox creation time — a running
|
|
1382
|
+
* sandbox cannot gain new tunnels. To make `openPort` work predictably
|
|
1383
|
+
* across providers, we pre-declare all well-known agent-harness ports on
|
|
1384
|
+
* every Modal sandbox we create, unless the caller has explicitly pinned
|
|
1385
|
+
* them to a specific (possibly empty) list.
|
|
1386
|
+
*/
|
|
1387
|
+
resolveDefaultUnencryptedPorts() {
|
|
1388
|
+
const declared = this.options.provider?.unencryptedPorts;
|
|
1389
|
+
const encrypted = new Set(this.options.provider?.encryptedPorts ?? []);
|
|
1390
|
+
const reserved = collectAllAgentReservedPorts().filter(
|
|
1391
|
+
(port) => !encrypted.has(port)
|
|
1392
|
+
);
|
|
1393
|
+
if (declared === void 0) {
|
|
1394
|
+
return reserved.length > 0 ? reserved : void 0;
|
|
1395
|
+
}
|
|
1396
|
+
const merged = new Set(declared);
|
|
1397
|
+
for (const port of reserved) {
|
|
1398
|
+
merged.add(port);
|
|
1399
|
+
}
|
|
1400
|
+
return Array.from(merged);
|
|
1401
|
+
}
|
|
1157
1402
|
async run(command, options) {
|
|
1158
|
-
|
|
1403
|
+
this.requireProvisioned();
|
|
1159
1404
|
const sandbox = this.requireSandbox();
|
|
1160
1405
|
const process2 = await sandbox.exec(
|
|
1161
|
-
["/bin/sh", "-
|
|
1406
|
+
["/bin/sh", "-c", toShellCommand(command)],
|
|
1162
1407
|
{
|
|
1163
1408
|
workdir: options?.cwd ?? this.workingDir,
|
|
1164
1409
|
timeoutMs: options?.timeoutMs,
|
|
@@ -1181,10 +1426,10 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1181
1426
|
};
|
|
1182
1427
|
}
|
|
1183
1428
|
async runAsync(command, options) {
|
|
1184
|
-
|
|
1429
|
+
this.requireProvisioned();
|
|
1185
1430
|
const sandbox = this.requireSandbox();
|
|
1186
1431
|
const process2 = await sandbox.exec(
|
|
1187
|
-
["/bin/sh", "-
|
|
1432
|
+
["/bin/sh", "-c", toShellCommand(command)],
|
|
1188
1433
|
{
|
|
1189
1434
|
workdir: options?.cwd ?? this.workingDir,
|
|
1190
1435
|
timeoutMs: options?.timeoutMs,
|
|
@@ -1230,6 +1475,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1230
1475
|
queue.fail(error);
|
|
1231
1476
|
throw error;
|
|
1232
1477
|
});
|
|
1478
|
+
suppressUnhandledRejection(completion);
|
|
1233
1479
|
return {
|
|
1234
1480
|
id: `${sandbox.sandboxId}:${Date.now()}`,
|
|
1235
1481
|
raw: process2,
|
|
@@ -1246,6 +1492,49 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1246
1492
|
[Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
|
|
1247
1493
|
};
|
|
1248
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Upload `files` as a tarball piped through stdin to a single in-sandbox
|
|
1497
|
+
* `tar -x` invocation, then exec `command` — all in one Modal `exec`
|
|
1498
|
+
* round-trip. This collapses the typical "N writeArtifact RPCs +
|
|
1499
|
+
* runCommand" setup pattern (~25 RPCs on cold paths, ~6s wall) into a
|
|
1500
|
+
* single ~1s call dominated by the actual install work.
|
|
1501
|
+
*/
|
|
1502
|
+
async uploadAndRun(files, command, options) {
|
|
1503
|
+
this.requireProvisioned();
|
|
1504
|
+
const sandbox = this.requireSandbox();
|
|
1505
|
+
const tar3 = await buildTarball(files);
|
|
1506
|
+
const wrapped = `set -e
|
|
1507
|
+
tar -xf - -C /
|
|
1508
|
+
${command}`;
|
|
1509
|
+
const process2 = await sandbox.exec(["/bin/sh", "-c", wrapped], {
|
|
1510
|
+
workdir: options?.cwd ?? this.workingDir,
|
|
1511
|
+
timeoutMs: options?.timeoutMs,
|
|
1512
|
+
env: this.getMergedEnv(options?.env),
|
|
1513
|
+
mode: "binary"
|
|
1514
|
+
});
|
|
1515
|
+
const writer = process2.stdin.getWriter();
|
|
1516
|
+
try {
|
|
1517
|
+
await writer.write(tar3);
|
|
1518
|
+
await writer.close();
|
|
1519
|
+
} finally {
|
|
1520
|
+
try {
|
|
1521
|
+
writer.releaseLock();
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1526
|
+
readStreamAsText(process2.stdout),
|
|
1527
|
+
readStreamAsText(process2.stderr),
|
|
1528
|
+
process2.wait()
|
|
1529
|
+
]);
|
|
1530
|
+
return {
|
|
1531
|
+
exitCode,
|
|
1532
|
+
stdout,
|
|
1533
|
+
stderr,
|
|
1534
|
+
combinedOutput: `${stdout}${stderr}`,
|
|
1535
|
+
raw: process2
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1249
1538
|
async list(options) {
|
|
1250
1539
|
const sandboxes = [];
|
|
1251
1540
|
for await (const sandbox of this.client.sandboxes.list({
|
|
@@ -1262,7 +1551,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1262
1551
|
return sandboxes;
|
|
1263
1552
|
}
|
|
1264
1553
|
async snapshot() {
|
|
1265
|
-
|
|
1554
|
+
this.requireProvisioned();
|
|
1266
1555
|
const sandbox = this.requireSandbox();
|
|
1267
1556
|
const image = await sandbox.snapshotFilesystem();
|
|
1268
1557
|
return image.imageId;
|
|
@@ -1274,6 +1563,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1274
1563
|
}
|
|
1275
1564
|
await sandbox.terminate();
|
|
1276
1565
|
this.sandbox = void 0;
|
|
1566
|
+
this.tunnelsPromise = void 0;
|
|
1277
1567
|
}
|
|
1278
1568
|
async delete() {
|
|
1279
1569
|
await this.stop();
|
|
@@ -1284,12 +1574,44 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1284
1574
|
if (provider.encryptedPorts?.includes(port)) {
|
|
1285
1575
|
return;
|
|
1286
1576
|
}
|
|
1287
|
-
|
|
1577
|
+
const alreadyDeclared = provider.unencryptedPorts?.includes(port) ?? false;
|
|
1578
|
+
if (!alreadyDeclared) {
|
|
1579
|
+
provider.unencryptedPorts = [...provider.unencryptedPorts ?? [], port];
|
|
1580
|
+
}
|
|
1581
|
+
if (!this.sandbox) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (alreadyDeclared) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
try {
|
|
1588
|
+
if (!this.tunnelsPromise) {
|
|
1589
|
+
this.tunnelsPromise = this.sandbox.tunnels();
|
|
1590
|
+
}
|
|
1591
|
+
const tunnels = await this.tunnelsPromise;
|
|
1592
|
+
if (tunnels[port]) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
} catch {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
throw new Error(
|
|
1599
|
+
`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.`
|
|
1600
|
+
);
|
|
1288
1601
|
}
|
|
1289
1602
|
async getPreviewLink(port) {
|
|
1290
|
-
|
|
1603
|
+
this.requireProvisioned();
|
|
1291
1604
|
const sandbox = this.requireSandbox();
|
|
1292
|
-
|
|
1605
|
+
if (!this.tunnelsPromise) {
|
|
1606
|
+
this.tunnelsPromise = sandbox.tunnels();
|
|
1607
|
+
}
|
|
1608
|
+
let tunnels;
|
|
1609
|
+
try {
|
|
1610
|
+
tunnels = await this.tunnelsPromise;
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
this.tunnelsPromise = void 0;
|
|
1613
|
+
throw error;
|
|
1614
|
+
}
|
|
1293
1615
|
const tunnel = tunnels[port];
|
|
1294
1616
|
if (!tunnel) {
|
|
1295
1617
|
throw new Error(`Modal sandbox does not expose port ${port}.`);
|
|
@@ -1347,22 +1669,335 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1347
1669
|
}
|
|
1348
1670
|
};
|
|
1349
1671
|
|
|
1672
|
+
// src/sandboxes/providers/vercel.ts
|
|
1673
|
+
import { Sandbox as VercelSandbox } from "@vercel/sandbox";
|
|
1674
|
+
function pickFirstTag(tags) {
|
|
1675
|
+
if (!tags) return void 0;
|
|
1676
|
+
const entries = Object.entries(tags);
|
|
1677
|
+
if (entries.length === 0) return void 0;
|
|
1678
|
+
const [key, value] = entries[0];
|
|
1679
|
+
return { [key]: value };
|
|
1680
|
+
}
|
|
1681
|
+
function matchesAllTags(candidateTags, required) {
|
|
1682
|
+
return Object.entries(required).every(
|
|
1683
|
+
([key, value]) => candidateTags?.[key] === value
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
function describeVercelApiError(error, action) {
|
|
1687
|
+
if (error && typeof error === "object" && "response" in error && error.response instanceof Response) {
|
|
1688
|
+
const apiError = error;
|
|
1689
|
+
const status = apiError.response.status;
|
|
1690
|
+
const body = apiError.json !== void 0 ? JSON.stringify(apiError.json) : apiError.text ?? "";
|
|
1691
|
+
return new Error(
|
|
1692
|
+
`Vercel ${action} failed with HTTP ${status}: ${body || apiError.message}`
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1696
|
+
}
|
|
1697
|
+
async function wrapVercelApiError(action, fn) {
|
|
1698
|
+
try {
|
|
1699
|
+
return await fn();
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
throw describeVercelApiError(error, action);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
function buildTimeoutSignal(timeoutMs) {
|
|
1705
|
+
if (!timeoutMs || timeoutMs <= 0) return void 0;
|
|
1706
|
+
return AbortSignal.timeout(timeoutMs);
|
|
1707
|
+
}
|
|
1708
|
+
function getCredentials(options) {
|
|
1709
|
+
const token = options.provider?.token ?? process.env.VERCEL_TOKEN;
|
|
1710
|
+
const teamId = options.provider?.teamId ?? process.env.VERCEL_TEAM_ID;
|
|
1711
|
+
const projectId = options.provider?.projectId ?? process.env.VERCEL_PROJECT_ID;
|
|
1712
|
+
if (token && teamId && projectId) {
|
|
1713
|
+
return { token, teamId, projectId };
|
|
1714
|
+
}
|
|
1715
|
+
return {};
|
|
1716
|
+
}
|
|
1717
|
+
var VercelSandboxAdapter = class extends SandboxAdapter {
|
|
1718
|
+
sandbox;
|
|
1719
|
+
get provider() {
|
|
1720
|
+
return SandboxProvider.Vercel;
|
|
1721
|
+
}
|
|
1722
|
+
get raw() {
|
|
1723
|
+
return { sandbox: this.sandbox };
|
|
1724
|
+
}
|
|
1725
|
+
get id() {
|
|
1726
|
+
return this.sandbox?.name;
|
|
1727
|
+
}
|
|
1728
|
+
get workingDir() {
|
|
1729
|
+
return this.options.workingDir ?? "/vercel/sandbox";
|
|
1730
|
+
}
|
|
1731
|
+
get previewHeaders() {
|
|
1732
|
+
const token = this.options.provider?.protectionBypass;
|
|
1733
|
+
return token ? { "x-vercel-protection-bypass": token } : {};
|
|
1734
|
+
}
|
|
1735
|
+
async provision() {
|
|
1736
|
+
const existing = await this.findExistingSandbox();
|
|
1737
|
+
if (existing) {
|
|
1738
|
+
this.sandbox = existing;
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const credentials = getCredentials(this.options);
|
|
1742
|
+
const provider = this.options.provider;
|
|
1743
|
+
const snapshotId = provider?.snapshotId;
|
|
1744
|
+
const timeout = provider?.timeoutMs ?? 12e4;
|
|
1745
|
+
const runtime = provider?.runtime ?? "node24";
|
|
1746
|
+
const resources = resolveSandboxResources(this.options.resources);
|
|
1747
|
+
const vcpus = resources?.cpu ? { resources: { vcpus: resources.cpu } } : {};
|
|
1748
|
+
const base = {
|
|
1749
|
+
...credentials,
|
|
1750
|
+
timeout,
|
|
1751
|
+
env: this.getMergedEnv(),
|
|
1752
|
+
...vcpus,
|
|
1753
|
+
tags: this.getTags(),
|
|
1754
|
+
...provider?.ports?.length ? { ports: provider.ports } : {}
|
|
1755
|
+
};
|
|
1756
|
+
const sandbox = await wrapVercelApiError("create sandbox", () => {
|
|
1757
|
+
if (snapshotId) {
|
|
1758
|
+
return VercelSandbox.create({
|
|
1759
|
+
...base,
|
|
1760
|
+
source: { type: "snapshot", snapshotId }
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
if (provider?.gitSource) {
|
|
1764
|
+
const git = provider.gitSource;
|
|
1765
|
+
const source = {
|
|
1766
|
+
type: "git",
|
|
1767
|
+
url: git.url,
|
|
1768
|
+
depth: git.depth,
|
|
1769
|
+
revision: git.revision,
|
|
1770
|
+
...git.username && git.password ? { username: git.username, password: git.password } : {}
|
|
1771
|
+
};
|
|
1772
|
+
return VercelSandbox.create({ ...base, runtime, source });
|
|
1773
|
+
}
|
|
1774
|
+
return VercelSandbox.create({ ...base, runtime });
|
|
1775
|
+
});
|
|
1776
|
+
this.sandbox = sandbox;
|
|
1777
|
+
if (this.workingDir !== "/vercel/sandbox") {
|
|
1778
|
+
await wrapVercelApiError(
|
|
1779
|
+
"create working directory",
|
|
1780
|
+
() => sandbox.runCommand({
|
|
1781
|
+
cmd: "mkdir",
|
|
1782
|
+
args: ["-p", this.workingDir],
|
|
1783
|
+
sudo: true
|
|
1784
|
+
})
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async run(command, options) {
|
|
1789
|
+
this.requireProvisioned();
|
|
1790
|
+
const sandbox = this.requireSandbox();
|
|
1791
|
+
const signal = buildTimeoutSignal(options?.timeoutMs);
|
|
1792
|
+
const result = await wrapVercelApiError(
|
|
1793
|
+
"run command",
|
|
1794
|
+
() => sandbox.runCommand({
|
|
1795
|
+
cmd: "sh",
|
|
1796
|
+
args: ["-lc", toShellCommand(command)],
|
|
1797
|
+
cwd: options?.cwd ?? this.workingDir,
|
|
1798
|
+
env: this.getMergedEnv(options?.env),
|
|
1799
|
+
...signal ? { signal } : {}
|
|
1800
|
+
})
|
|
1801
|
+
);
|
|
1802
|
+
const [stdout, stderr] = await Promise.all([
|
|
1803
|
+
result.stdout(),
|
|
1804
|
+
result.stderr()
|
|
1805
|
+
]);
|
|
1806
|
+
return {
|
|
1807
|
+
exitCode: result.exitCode,
|
|
1808
|
+
stdout,
|
|
1809
|
+
stderr,
|
|
1810
|
+
combinedOutput: `${stdout}${stderr}`,
|
|
1811
|
+
raw: result
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
async runAsync(command, options) {
|
|
1815
|
+
this.requireProvisioned();
|
|
1816
|
+
const sandbox = this.requireSandbox();
|
|
1817
|
+
const signal = buildTimeoutSignal(options?.timeoutMs);
|
|
1818
|
+
const cmd = await wrapVercelApiError(
|
|
1819
|
+
"start async command",
|
|
1820
|
+
() => sandbox.runCommand({
|
|
1821
|
+
cmd: "sh",
|
|
1822
|
+
args: ["-lc", toShellCommand(command)],
|
|
1823
|
+
cwd: options?.cwd ?? this.workingDir,
|
|
1824
|
+
env: this.getMergedEnv(options?.env),
|
|
1825
|
+
detached: true,
|
|
1826
|
+
...signal ? { signal } : {}
|
|
1827
|
+
})
|
|
1828
|
+
);
|
|
1829
|
+
const queue = new AsyncQueue();
|
|
1830
|
+
let stdout = "";
|
|
1831
|
+
let stderr = "";
|
|
1832
|
+
const completion = (async () => {
|
|
1833
|
+
for await (const log of cmd.logs()) {
|
|
1834
|
+
if (log.stream === "stdout") {
|
|
1835
|
+
stdout += log.data;
|
|
1836
|
+
queue.push({
|
|
1837
|
+
type: "stdout",
|
|
1838
|
+
chunk: log.data,
|
|
1839
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1840
|
+
});
|
|
1841
|
+
} else {
|
|
1842
|
+
stderr += log.data;
|
|
1843
|
+
queue.push({
|
|
1844
|
+
type: "stderr",
|
|
1845
|
+
chunk: log.data,
|
|
1846
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
const finished2 = await cmd.wait();
|
|
1851
|
+
const exitCode = finished2.exitCode;
|
|
1852
|
+
queue.push({
|
|
1853
|
+
type: "exit",
|
|
1854
|
+
exitCode,
|
|
1855
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1856
|
+
});
|
|
1857
|
+
queue.finish();
|
|
1858
|
+
return {
|
|
1859
|
+
exitCode,
|
|
1860
|
+
stdout,
|
|
1861
|
+
stderr,
|
|
1862
|
+
combinedOutput: `${stdout}${stderr}`,
|
|
1863
|
+
raw: finished2
|
|
1864
|
+
};
|
|
1865
|
+
})().catch((error) => {
|
|
1866
|
+
queue.fail(error);
|
|
1867
|
+
throw error;
|
|
1868
|
+
});
|
|
1869
|
+
suppressUnhandledRejection(completion);
|
|
1870
|
+
return {
|
|
1871
|
+
id: cmd.cmdId,
|
|
1872
|
+
raw: cmd,
|
|
1873
|
+
wait: () => completion,
|
|
1874
|
+
kill: async () => {
|
|
1875
|
+
await cmd.kill();
|
|
1876
|
+
},
|
|
1877
|
+
[Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
async list(options) {
|
|
1881
|
+
const filterTags = options?.tags ?? this.getTags();
|
|
1882
|
+
const sandboxes = await this.listSandboxesByTags(filterTags);
|
|
1883
|
+
return sandboxes.map(
|
|
1884
|
+
(s) => ({
|
|
1885
|
+
provider: this.provider,
|
|
1886
|
+
id: s.name,
|
|
1887
|
+
state: s.status,
|
|
1888
|
+
tags: s.tags ?? {},
|
|
1889
|
+
createdAt: new Date(s.createdAt).toISOString(),
|
|
1890
|
+
raw: s
|
|
1891
|
+
})
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
async snapshot() {
|
|
1895
|
+
this.requireProvisioned();
|
|
1896
|
+
const sandbox = this.requireSandbox();
|
|
1897
|
+
const snap = await wrapVercelApiError(
|
|
1898
|
+
"snapshot sandbox",
|
|
1899
|
+
() => sandbox.snapshot()
|
|
1900
|
+
);
|
|
1901
|
+
return snap.snapshotId;
|
|
1902
|
+
}
|
|
1903
|
+
async stop() {
|
|
1904
|
+
const sandbox = this.sandbox;
|
|
1905
|
+
if (!sandbox) {
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
await wrapVercelApiError("stop sandbox", () => sandbox.stop());
|
|
1909
|
+
this.sandbox = void 0;
|
|
1910
|
+
}
|
|
1911
|
+
async delete() {
|
|
1912
|
+
await this.stop();
|
|
1913
|
+
}
|
|
1914
|
+
async openPort(_port) {
|
|
1915
|
+
void _port;
|
|
1916
|
+
}
|
|
1917
|
+
async getPreviewLink(port) {
|
|
1918
|
+
this.requireProvisioned();
|
|
1919
|
+
const sandbox = this.requireSandbox();
|
|
1920
|
+
return sandbox.domain(port);
|
|
1921
|
+
}
|
|
1922
|
+
async uploadFile(content, targetPath) {
|
|
1923
|
+
this.requireProvisioned();
|
|
1924
|
+
const sandbox = this.requireSandbox();
|
|
1925
|
+
const data = typeof content === "string" ? content : new Uint8Array(content);
|
|
1926
|
+
await sandbox.writeFiles([{ path: targetPath, content: data }]);
|
|
1927
|
+
}
|
|
1928
|
+
async downloadFile(sourcePath) {
|
|
1929
|
+
this.requireProvisioned();
|
|
1930
|
+
const sandbox = this.requireSandbox();
|
|
1931
|
+
const result = await sandbox.readFileToBuffer({ path: sourcePath });
|
|
1932
|
+
if (!result) {
|
|
1933
|
+
throw new Error(`File not found in Vercel sandbox: ${sourcePath}`);
|
|
1934
|
+
}
|
|
1935
|
+
return result;
|
|
1936
|
+
}
|
|
1937
|
+
getTags() {
|
|
1938
|
+
return {
|
|
1939
|
+
"agentbox.provider": this.provider,
|
|
1940
|
+
...this.options.tags ?? {}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
async listSandboxesByTags(tags) {
|
|
1944
|
+
const credentials = getCredentials(this.options);
|
|
1945
|
+
const result = await wrapVercelApiError(
|
|
1946
|
+
"list sandboxes",
|
|
1947
|
+
() => VercelSandbox.list({
|
|
1948
|
+
...credentials,
|
|
1949
|
+
tags: pickFirstTag(tags)
|
|
1950
|
+
})
|
|
1951
|
+
);
|
|
1952
|
+
return result.sandboxes.filter(
|
|
1953
|
+
(s) => matchesAllTags(s.tags, tags)
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
async findExistingSandbox() {
|
|
1957
|
+
const credentials = getCredentials(this.options);
|
|
1958
|
+
const sandboxes = await this.listSandboxesByTags(this.getTags());
|
|
1959
|
+
const match = sandboxes.find((s) => s.status === "running");
|
|
1960
|
+
if (!match) {
|
|
1961
|
+
return void 0;
|
|
1962
|
+
}
|
|
1963
|
+
return wrapVercelApiError(
|
|
1964
|
+
"get sandbox",
|
|
1965
|
+
() => VercelSandbox.get({ ...credentials, name: match.name })
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
requireSandbox() {
|
|
1969
|
+
if (!this.sandbox) {
|
|
1970
|
+
throw new Error("Vercel sandbox has not been provisioned.");
|
|
1971
|
+
}
|
|
1972
|
+
return this.sandbox;
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
|
|
1350
1976
|
// src/sandboxes/Sandbox.ts
|
|
1977
|
+
function shortLabel(command) {
|
|
1978
|
+
const oneLine = Array.isArray(command) ? command.join(" ") : command;
|
|
1979
|
+
const cleaned = oneLine.replace(/\s+/g, " ").trim();
|
|
1980
|
+
return cleaned.length > 80 ? `${cleaned.slice(0, 80)}\u2026` : cleaned;
|
|
1981
|
+
}
|
|
1351
1982
|
function createSandboxAdapter(provider, options) {
|
|
1352
1983
|
switch (provider) {
|
|
1353
|
-
case
|
|
1984
|
+
case SandboxProvider.LocalDocker:
|
|
1354
1985
|
return new LocalDockerSandboxAdapter(
|
|
1355
1986
|
options
|
|
1356
1987
|
);
|
|
1357
|
-
case
|
|
1988
|
+
case SandboxProvider.Modal:
|
|
1358
1989
|
return new ModalSandboxAdapter(
|
|
1359
1990
|
options
|
|
1360
1991
|
);
|
|
1361
|
-
case
|
|
1992
|
+
case SandboxProvider.Daytona:
|
|
1362
1993
|
return new DaytonaSandboxAdapter(
|
|
1363
1994
|
options
|
|
1364
1995
|
);
|
|
1365
|
-
case
|
|
1996
|
+
case SandboxProvider.Vercel:
|
|
1997
|
+
return new VercelSandboxAdapter(
|
|
1998
|
+
options
|
|
1999
|
+
);
|
|
2000
|
+
case SandboxProvider.E2B:
|
|
1366
2001
|
return new E2bSandboxAdapter(
|
|
1367
2002
|
options
|
|
1368
2003
|
);
|
|
@@ -1391,8 +2026,35 @@ var Sandbox = class {
|
|
|
1391
2026
|
get raw() {
|
|
1392
2027
|
return this.adapter.raw;
|
|
1393
2028
|
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Whether `findOrProvision()` warm-attached to a pre-existing tagged
|
|
2031
|
+
* sandbox (`true`) or created a fresh one (`false`). Useful to skip
|
|
2032
|
+
* idempotent setup that the previous run already performed (e.g.
|
|
2033
|
+
* `agent.setup()`). Always `false` before `findOrProvision()` resolves.
|
|
2034
|
+
*/
|
|
2035
|
+
get wasFound() {
|
|
2036
|
+
return this.adapter.wasFound;
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Attach to an existing tagged sandbox or create a new one. Must be
|
|
2040
|
+
* called before `run`, `runAsync`, `gitClone`, `uploadAndRun`,
|
|
2041
|
+
* `getPreviewLink`, etc. Repeated calls are cheap (the result is
|
|
2042
|
+
* cached internally).
|
|
2043
|
+
*/
|
|
2044
|
+
async findOrProvision() {
|
|
2045
|
+
await time(
|
|
2046
|
+
debugSandbox,
|
|
2047
|
+
`findOrProvision [${this.provider}]`,
|
|
2048
|
+
() => this.adapter.findOrProvision()
|
|
2049
|
+
);
|
|
2050
|
+
return this;
|
|
2051
|
+
}
|
|
1394
2052
|
async openPort(port) {
|
|
1395
|
-
await
|
|
2053
|
+
await time(
|
|
2054
|
+
debugSandbox,
|
|
2055
|
+
`openPort [${this.provider}] :${port}`,
|
|
2056
|
+
() => this.adapter.openPort(port)
|
|
2057
|
+
);
|
|
1396
2058
|
return this;
|
|
1397
2059
|
}
|
|
1398
2060
|
setSecret(name, value) {
|
|
@@ -1407,10 +2069,19 @@ var Sandbox = class {
|
|
|
1407
2069
|
return this.adapter.gitClone(options);
|
|
1408
2070
|
}
|
|
1409
2071
|
async run(command, options) {
|
|
1410
|
-
return
|
|
2072
|
+
return time(
|
|
2073
|
+
debugSandbox,
|
|
2074
|
+
`run [${this.provider}] ${shortLabel(command)}`,
|
|
2075
|
+
() => this.adapter.run(command, options),
|
|
2076
|
+
(result) => ({ exit: result.exitCode })
|
|
2077
|
+
);
|
|
1411
2078
|
}
|
|
1412
2079
|
async runAsync(command, options) {
|
|
1413
|
-
return
|
|
2080
|
+
return time(
|
|
2081
|
+
debugSandbox,
|
|
2082
|
+
`runAsync [${this.provider}] ${shortLabel(command)}`,
|
|
2083
|
+
() => this.adapter.runAsync(command, options)
|
|
2084
|
+
);
|
|
1414
2085
|
}
|
|
1415
2086
|
async list(options) {
|
|
1416
2087
|
return this.adapter.list(options);
|
|
@@ -1425,7 +2096,28 @@ var Sandbox = class {
|
|
|
1425
2096
|
return this.adapter.delete();
|
|
1426
2097
|
}
|
|
1427
2098
|
async getPreviewLink(port) {
|
|
1428
|
-
return
|
|
2099
|
+
return time(
|
|
2100
|
+
debugSandbox,
|
|
2101
|
+
`getPreviewLink [${this.provider}] :${port}`,
|
|
2102
|
+
() => this.adapter.getPreviewLink(port)
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
get previewHeaders() {
|
|
2106
|
+
return this.adapter.previewHeaders;
|
|
2107
|
+
}
|
|
2108
|
+
async uploadFile(content, targetPath) {
|
|
2109
|
+
return this.adapter.uploadFile(content, targetPath);
|
|
2110
|
+
}
|
|
2111
|
+
async downloadFile(sourcePath) {
|
|
2112
|
+
return this.adapter.downloadFile(sourcePath);
|
|
2113
|
+
}
|
|
2114
|
+
async uploadAndRun(files, command, options) {
|
|
2115
|
+
return time(
|
|
2116
|
+
debugSandbox,
|
|
2117
|
+
`uploadAndRun [${this.provider}] ${shortLabel(command)}`,
|
|
2118
|
+
() => this.adapter.uploadAndRun(files, command, options),
|
|
2119
|
+
(result) => ({ exit: result.exitCode, files: files.length })
|
|
2120
|
+
);
|
|
1429
2121
|
}
|
|
1430
2122
|
};
|
|
1431
2123
|
|