agentbox-sdk 0.1.1 → 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 +43 -4
- package/dist/{Sandbox-BQX-sWzs.d.ts → Sandbox-CByFJI8X.d.ts} +56 -3
- package/dist/agents/index.d.ts +54 -5
- package/dist/agents/index.js +4 -4
- package/dist/{chunk-G27423WX.js → chunk-4MBB6QHD.js} +2118 -1825
- package/dist/{chunk-2NKMDGYH.js → chunk-GOFJNFAD.js} +1 -1
- package/dist/{chunk-O7HCJXKW.js → chunk-INMA52FV.js} +56 -23
- package/dist/{chunk-X7AWPYDK.js → chunk-LPKKT6YT.js} +351 -47
- package/dist/chunk-ZOWBRUQR.js +476 -0
- package/dist/cli.js +1 -1
- package/dist/enums.d.ts +1 -1
- package/dist/enums.js +1 -1
- package/dist/events/index.d.ts +85 -3
- package/dist/events/index.js +4 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +7 -5
- package/dist/sandboxes/index.d.ts +48 -4
- package/dist/sandboxes/index.js +3 -3
- package/dist/{types-Et22oPap.d.ts → types-B3N-Qo2q.d.ts} +145 -5
- package/images/browser-agent.mjs +3 -3
- package/package.json +5 -2
- package/dist/chunk-7FLLQJ6J.js +0 -185
|
@@ -2,19 +2,21 @@ import {
|
|
|
2
2
|
AsyncQueue,
|
|
3
3
|
UnsupportedProviderError,
|
|
4
4
|
collectAllAgentReservedPorts,
|
|
5
|
+
debugSandbox,
|
|
5
6
|
pipeReadableStream,
|
|
6
7
|
readNodeStream,
|
|
7
8
|
readStreamAsText,
|
|
8
9
|
sleep,
|
|
9
|
-
suppressUnhandledRejection
|
|
10
|
-
|
|
10
|
+
suppressUnhandledRejection,
|
|
11
|
+
time
|
|
12
|
+
} from "./chunk-INMA52FV.js";
|
|
11
13
|
import {
|
|
12
14
|
shellQuote,
|
|
13
15
|
toShellCommand
|
|
14
16
|
} from "./chunk-NSJM57Z4.js";
|
|
15
17
|
import {
|
|
16
18
|
SandboxProvider
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-GOFJNFAD.js";
|
|
18
20
|
|
|
19
21
|
// src/sandboxes/git.ts
|
|
20
22
|
function encodeExtraHeader(name, value) {
|
|
@@ -60,6 +62,13 @@ var SandboxAdapter = class {
|
|
|
60
62
|
baseEnv;
|
|
61
63
|
provisioned = false;
|
|
62
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;
|
|
63
72
|
constructor(options) {
|
|
64
73
|
this.options = options;
|
|
65
74
|
this.baseEnv = { ...options.env ?? {} };
|
|
@@ -77,26 +86,90 @@ var SandboxAdapter = class {
|
|
|
77
86
|
`downloadFile is not supported by the ${this.provider} provider.`
|
|
78
87
|
);
|
|
79
88
|
}
|
|
80
|
-
|
|
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() {
|
|
81
129
|
if (this.provisioned) {
|
|
82
130
|
return;
|
|
83
131
|
}
|
|
84
132
|
if (!this.provisioning) {
|
|
85
|
-
this.provisioning = (
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
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(() => {
|
|
89
141
|
this.provisioning = void 0;
|
|
90
142
|
});
|
|
91
143
|
}
|
|
92
144
|
await this.provisioning;
|
|
93
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
|
+
}
|
|
94
158
|
get tags() {
|
|
95
159
|
return { ...this.options.tags ?? {} };
|
|
96
160
|
}
|
|
97
161
|
get workingDir() {
|
|
98
162
|
return this.options.workingDir ?? "/workspace";
|
|
99
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
|
+
}
|
|
100
173
|
/**
|
|
101
174
|
* Headers that callers should attach to HTTP / WebSocket requests they make
|
|
102
175
|
* against this sandbox's preview URL. Default is empty; providers like
|
|
@@ -119,7 +192,7 @@ var SandboxAdapter = class {
|
|
|
119
192
|
Object.assign(this.secrets, values);
|
|
120
193
|
}
|
|
121
194
|
async gitClone(options) {
|
|
122
|
-
|
|
195
|
+
this.requireProvisioned();
|
|
123
196
|
return this.run(buildGitCloneCommand(options), {
|
|
124
197
|
cwd: this.workingDir,
|
|
125
198
|
env: this.getMergedEnv()
|
|
@@ -169,6 +242,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
169
242
|
if (existing) {
|
|
170
243
|
this.sandbox = existing;
|
|
171
244
|
await existing.start();
|
|
245
|
+
this.wasFoundFlag = true;
|
|
172
246
|
return;
|
|
173
247
|
}
|
|
174
248
|
const labels = this.getLabels();
|
|
@@ -197,6 +271,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
197
271
|
autoDeleteInterval
|
|
198
272
|
};
|
|
199
273
|
const sandbox = await this.client.create({
|
|
274
|
+
...this.options.provider?.createParams,
|
|
200
275
|
...createBase,
|
|
201
276
|
snapshot: image
|
|
202
277
|
});
|
|
@@ -204,7 +279,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
204
279
|
this.sandbox = sandbox;
|
|
205
280
|
}
|
|
206
281
|
async run(command, options) {
|
|
207
|
-
|
|
282
|
+
this.requireProvisioned();
|
|
208
283
|
const sandbox = this.requireSandbox();
|
|
209
284
|
const result = await sandbox.process.executeCommand(
|
|
210
285
|
toShellCommand(command),
|
|
@@ -222,7 +297,7 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
222
297
|
};
|
|
223
298
|
}
|
|
224
299
|
async runAsync(command, options) {
|
|
225
|
-
|
|
300
|
+
this.requireProvisioned();
|
|
226
301
|
const sandbox = this.requireSandbox();
|
|
227
302
|
const sessionId = `agentbox-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
228
303
|
await sandbox.process.createSession(sessionId);
|
|
@@ -318,6 +393,13 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
318
393
|
return {
|
|
319
394
|
id: commandId,
|
|
320
395
|
raw: { sessionId, commandId },
|
|
396
|
+
write: async (input) => {
|
|
397
|
+
await sandbox.process.sendSessionCommandInput(
|
|
398
|
+
sessionId,
|
|
399
|
+
commandId,
|
|
400
|
+
input
|
|
401
|
+
);
|
|
402
|
+
},
|
|
321
403
|
wait: () => completion,
|
|
322
404
|
kill: async () => {
|
|
323
405
|
killed = true;
|
|
@@ -358,15 +440,26 @@ var DaytonaSandboxAdapter = class extends SandboxAdapter {
|
|
|
358
440
|
this.sandbox = void 0;
|
|
359
441
|
}
|
|
360
442
|
async openPort(port) {
|
|
361
|
-
|
|
443
|
+
this.requireProvisioned();
|
|
362
444
|
await this.requireSandbox().getPreviewLink(port);
|
|
363
445
|
}
|
|
364
446
|
async getPreviewLink(port) {
|
|
365
|
-
|
|
447
|
+
this.requireProvisioned();
|
|
366
448
|
const sandbox = this.requireSandbox();
|
|
367
449
|
const preview = await sandbox.getPreviewLink(port);
|
|
368
450
|
return preview.url;
|
|
369
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
|
+
}
|
|
370
463
|
getLabels() {
|
|
371
464
|
return {
|
|
372
465
|
"agentbox.provider": this.provider,
|
|
@@ -424,6 +517,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
424
517
|
const existing = await this.findMatchingSandbox();
|
|
425
518
|
if (existing) {
|
|
426
519
|
this.sandbox = existing;
|
|
520
|
+
this.wasFoundFlag = true;
|
|
427
521
|
return;
|
|
428
522
|
}
|
|
429
523
|
const template = resolveSandboxImage(this.options.image);
|
|
@@ -449,7 +543,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
449
543
|
});
|
|
450
544
|
}
|
|
451
545
|
async run(command, options) {
|
|
452
|
-
|
|
546
|
+
this.requireProvisioned();
|
|
453
547
|
const sandbox = this.requireSandbox();
|
|
454
548
|
const { CommandExitError } = await loadE2bModule();
|
|
455
549
|
if (options?.pty) {
|
|
@@ -477,7 +571,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
477
571
|
}
|
|
478
572
|
}
|
|
479
573
|
async runAsync(command, options) {
|
|
480
|
-
|
|
574
|
+
this.requireProvisioned();
|
|
481
575
|
const sandbox = this.requireSandbox();
|
|
482
576
|
const { CommandExitError } = await loadE2bModule();
|
|
483
577
|
const queue = new AsyncQueue();
|
|
@@ -653,7 +747,7 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
653
747
|
return sandboxes;
|
|
654
748
|
}
|
|
655
749
|
async snapshot() {
|
|
656
|
-
|
|
750
|
+
this.requireProvisioned();
|
|
657
751
|
const sandbox = this.requireSandbox();
|
|
658
752
|
const snapshot = await sandbox.createSnapshot();
|
|
659
753
|
return snapshot.snapshotId;
|
|
@@ -673,11 +767,27 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
673
767
|
void _port;
|
|
674
768
|
}
|
|
675
769
|
async getPreviewLink(port) {
|
|
676
|
-
|
|
770
|
+
this.requireProvisioned();
|
|
677
771
|
const sandbox = this.requireSandbox();
|
|
678
772
|
const host = sandbox.getHost(port);
|
|
679
773
|
return host.startsWith("localhost:") ? `http://${host}` : `https://${host}`;
|
|
680
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
|
+
}
|
|
681
791
|
async findMatchingSandbox() {
|
|
682
792
|
const { Sandbox: E2bSandbox } = await loadE2bModule();
|
|
683
793
|
const matches = await this.list();
|
|
@@ -713,7 +823,14 @@ var E2bSandboxAdapter = class extends SandboxAdapter {
|
|
|
713
823
|
return {
|
|
714
824
|
cwd: options?.cwd ?? this.workingDir,
|
|
715
825
|
envs: this.getMergedEnv(options?.env),
|
|
716
|
-
|
|
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
|
|
717
834
|
};
|
|
718
835
|
}
|
|
719
836
|
resolveTimeoutConfig() {
|
|
@@ -818,11 +935,11 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
818
935
|
const env = Object.entries(this.getMergedEnv()).map(
|
|
819
936
|
([key, value]) => `${key}=${value}`
|
|
820
937
|
);
|
|
821
|
-
const publishedPorts = this.
|
|
938
|
+
const publishedPorts = this.resolveDefaultPublishedPorts();
|
|
822
939
|
const portBindings = publishedPorts.length > 0 ? Object.fromEntries(
|
|
823
940
|
publishedPorts.map((port) => [
|
|
824
941
|
`${port}/tcp`,
|
|
825
|
-
[{ HostIp: "127.0.0.1", HostPort:
|
|
942
|
+
[{ HostIp: "127.0.0.1", HostPort: "" }]
|
|
826
943
|
])
|
|
827
944
|
) : void 0;
|
|
828
945
|
const exposedPorts = publishedPorts.length > 0 ? Object.fromEntries(publishedPorts.map((port) => [`${port}/tcp`, {}])) : void 0;
|
|
@@ -850,7 +967,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
850
967
|
await container.start();
|
|
851
968
|
}
|
|
852
969
|
async run(command, options) {
|
|
853
|
-
|
|
970
|
+
this.requireProvisioned();
|
|
854
971
|
const container = this.requireContainer();
|
|
855
972
|
const exec = await container.exec({
|
|
856
973
|
AttachStdout: true,
|
|
@@ -893,7 +1010,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
893
1010
|
};
|
|
894
1011
|
}
|
|
895
1012
|
async runAsync(command, options) {
|
|
896
|
-
|
|
1013
|
+
this.requireProvisioned();
|
|
897
1014
|
const container = this.requireContainer();
|
|
898
1015
|
const exec = await container.exec({
|
|
899
1016
|
AttachStdin: true,
|
|
@@ -1047,15 +1164,26 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1047
1164
|
if (networkMode === "host") {
|
|
1048
1165
|
return `http://127.0.0.1:${port}`;
|
|
1049
1166
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
+
);
|
|
1052
1172
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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}`;
|
|
1056
1184
|
}
|
|
1057
1185
|
async uploadFile(content, targetPath) {
|
|
1058
|
-
|
|
1186
|
+
this.requireProvisioned();
|
|
1059
1187
|
const container = this.requireContainer();
|
|
1060
1188
|
const pack = tar.pack();
|
|
1061
1189
|
const body = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
|
|
@@ -1064,7 +1192,7 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1064
1192
|
await container.putArchive(pack, { path: "/" });
|
|
1065
1193
|
}
|
|
1066
1194
|
async downloadFile(sourcePath) {
|
|
1067
|
-
|
|
1195
|
+
this.requireProvisioned();
|
|
1068
1196
|
const container = this.requireContainer();
|
|
1069
1197
|
const archive = await container.getArchive({ path: sourcePath });
|
|
1070
1198
|
const chunks = [];
|
|
@@ -1079,6 +1207,32 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1079
1207
|
}
|
|
1080
1208
|
return this.container;
|
|
1081
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
|
+
}
|
|
1082
1236
|
getLabels() {
|
|
1083
1237
|
return {
|
|
1084
1238
|
"agentbox.provider": this.provider,
|
|
@@ -1131,10 +1285,46 @@ var LocalDockerSandboxAdapter = class extends SandboxAdapter {
|
|
|
1131
1285
|
|
|
1132
1286
|
// src/sandboxes/providers/modal.ts
|
|
1133
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
|
|
1134
1320
|
var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
1135
1321
|
client;
|
|
1136
1322
|
sandbox;
|
|
1137
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;
|
|
1138
1328
|
constructor(options) {
|
|
1139
1329
|
super(options);
|
|
1140
1330
|
this.client = new ModalClient({
|
|
@@ -1160,6 +1350,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1160
1350
|
const existing = await this.findMatchingSandbox();
|
|
1161
1351
|
if (existing) {
|
|
1162
1352
|
this.sandbox = existing;
|
|
1353
|
+
this.wasFoundFlag = true;
|
|
1163
1354
|
return;
|
|
1164
1355
|
}
|
|
1165
1356
|
const appName = this.options.provider?.appName ?? "agentbox";
|
|
@@ -1171,6 +1362,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1171
1362
|
const resources = resolveSandboxResources(this.options.resources);
|
|
1172
1363
|
const unencryptedPorts = this.resolveDefaultUnencryptedPorts();
|
|
1173
1364
|
const sandbox = await this.client.sandboxes.create(app, image, {
|
|
1365
|
+
...this.options.provider?.createParams,
|
|
1174
1366
|
cpu: resources?.cpu,
|
|
1175
1367
|
memoryMiB: resources?.memoryMiB,
|
|
1176
1368
|
timeoutMs: this.options.autoStopMs,
|
|
@@ -1208,10 +1400,10 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1208
1400
|
return Array.from(merged);
|
|
1209
1401
|
}
|
|
1210
1402
|
async run(command, options) {
|
|
1211
|
-
|
|
1403
|
+
this.requireProvisioned();
|
|
1212
1404
|
const sandbox = this.requireSandbox();
|
|
1213
1405
|
const process2 = await sandbox.exec(
|
|
1214
|
-
["/bin/sh", "-
|
|
1406
|
+
["/bin/sh", "-c", toShellCommand(command)],
|
|
1215
1407
|
{
|
|
1216
1408
|
workdir: options?.cwd ?? this.workingDir,
|
|
1217
1409
|
timeoutMs: options?.timeoutMs,
|
|
@@ -1234,10 +1426,10 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1234
1426
|
};
|
|
1235
1427
|
}
|
|
1236
1428
|
async runAsync(command, options) {
|
|
1237
|
-
|
|
1429
|
+
this.requireProvisioned();
|
|
1238
1430
|
const sandbox = this.requireSandbox();
|
|
1239
1431
|
const process2 = await sandbox.exec(
|
|
1240
|
-
["/bin/sh", "-
|
|
1432
|
+
["/bin/sh", "-c", toShellCommand(command)],
|
|
1241
1433
|
{
|
|
1242
1434
|
workdir: options?.cwd ?? this.workingDir,
|
|
1243
1435
|
timeoutMs: options?.timeoutMs,
|
|
@@ -1300,6 +1492,49 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1300
1492
|
[Symbol.asyncIterator]: () => queue[Symbol.asyncIterator]()
|
|
1301
1493
|
};
|
|
1302
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
|
+
}
|
|
1303
1538
|
async list(options) {
|
|
1304
1539
|
const sandboxes = [];
|
|
1305
1540
|
for await (const sandbox of this.client.sandboxes.list({
|
|
@@ -1316,7 +1551,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1316
1551
|
return sandboxes;
|
|
1317
1552
|
}
|
|
1318
1553
|
async snapshot() {
|
|
1319
|
-
|
|
1554
|
+
this.requireProvisioned();
|
|
1320
1555
|
const sandbox = this.requireSandbox();
|
|
1321
1556
|
const image = await sandbox.snapshotFilesystem();
|
|
1322
1557
|
return image.imageId;
|
|
@@ -1328,6 +1563,7 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1328
1563
|
}
|
|
1329
1564
|
await sandbox.terminate();
|
|
1330
1565
|
this.sandbox = void 0;
|
|
1566
|
+
this.tunnelsPromise = void 0;
|
|
1331
1567
|
}
|
|
1332
1568
|
async delete() {
|
|
1333
1569
|
await this.stop();
|
|
@@ -1345,8 +1581,14 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1345
1581
|
if (!this.sandbox) {
|
|
1346
1582
|
return;
|
|
1347
1583
|
}
|
|
1584
|
+
if (alreadyDeclared) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1348
1587
|
try {
|
|
1349
|
-
|
|
1588
|
+
if (!this.tunnelsPromise) {
|
|
1589
|
+
this.tunnelsPromise = this.sandbox.tunnels();
|
|
1590
|
+
}
|
|
1591
|
+
const tunnels = await this.tunnelsPromise;
|
|
1350
1592
|
if (tunnels[port]) {
|
|
1351
1593
|
return;
|
|
1352
1594
|
}
|
|
@@ -1358,9 +1600,18 @@ var ModalSandboxAdapter = class extends SandboxAdapter {
|
|
|
1358
1600
|
);
|
|
1359
1601
|
}
|
|
1360
1602
|
async getPreviewLink(port) {
|
|
1361
|
-
|
|
1603
|
+
this.requireProvisioned();
|
|
1362
1604
|
const sandbox = this.requireSandbox();
|
|
1363
|
-
|
|
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
|
+
}
|
|
1364
1615
|
const tunnel = tunnels[port];
|
|
1365
1616
|
if (!tunnel) {
|
|
1366
1617
|
throw new Error(`Modal sandbox does not expose port ${port}.`);
|
|
@@ -1535,7 +1786,7 @@ var VercelSandboxAdapter = class extends SandboxAdapter {
|
|
|
1535
1786
|
}
|
|
1536
1787
|
}
|
|
1537
1788
|
async run(command, options) {
|
|
1538
|
-
|
|
1789
|
+
this.requireProvisioned();
|
|
1539
1790
|
const sandbox = this.requireSandbox();
|
|
1540
1791
|
const signal = buildTimeoutSignal(options?.timeoutMs);
|
|
1541
1792
|
const result = await wrapVercelApiError(
|
|
@@ -1561,7 +1812,7 @@ var VercelSandboxAdapter = class extends SandboxAdapter {
|
|
|
1561
1812
|
};
|
|
1562
1813
|
}
|
|
1563
1814
|
async runAsync(command, options) {
|
|
1564
|
-
|
|
1815
|
+
this.requireProvisioned();
|
|
1565
1816
|
const sandbox = this.requireSandbox();
|
|
1566
1817
|
const signal = buildTimeoutSignal(options?.timeoutMs);
|
|
1567
1818
|
const cmd = await wrapVercelApiError(
|
|
@@ -1641,7 +1892,7 @@ var VercelSandboxAdapter = class extends SandboxAdapter {
|
|
|
1641
1892
|
);
|
|
1642
1893
|
}
|
|
1643
1894
|
async snapshot() {
|
|
1644
|
-
|
|
1895
|
+
this.requireProvisioned();
|
|
1645
1896
|
const sandbox = this.requireSandbox();
|
|
1646
1897
|
const snap = await wrapVercelApiError(
|
|
1647
1898
|
"snapshot sandbox",
|
|
@@ -1664,18 +1915,18 @@ var VercelSandboxAdapter = class extends SandboxAdapter {
|
|
|
1664
1915
|
void _port;
|
|
1665
1916
|
}
|
|
1666
1917
|
async getPreviewLink(port) {
|
|
1667
|
-
|
|
1918
|
+
this.requireProvisioned();
|
|
1668
1919
|
const sandbox = this.requireSandbox();
|
|
1669
1920
|
return sandbox.domain(port);
|
|
1670
1921
|
}
|
|
1671
1922
|
async uploadFile(content, targetPath) {
|
|
1672
|
-
|
|
1923
|
+
this.requireProvisioned();
|
|
1673
1924
|
const sandbox = this.requireSandbox();
|
|
1674
1925
|
const data = typeof content === "string" ? content : new Uint8Array(content);
|
|
1675
1926
|
await sandbox.writeFiles([{ path: targetPath, content: data }]);
|
|
1676
1927
|
}
|
|
1677
1928
|
async downloadFile(sourcePath) {
|
|
1678
|
-
|
|
1929
|
+
this.requireProvisioned();
|
|
1679
1930
|
const sandbox = this.requireSandbox();
|
|
1680
1931
|
const result = await sandbox.readFileToBuffer({ path: sourcePath });
|
|
1681
1932
|
if (!result) {
|
|
@@ -1723,6 +1974,11 @@ var VercelSandboxAdapter = class extends SandboxAdapter {
|
|
|
1723
1974
|
};
|
|
1724
1975
|
|
|
1725
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
|
+
}
|
|
1726
1982
|
function createSandboxAdapter(provider, options) {
|
|
1727
1983
|
switch (provider) {
|
|
1728
1984
|
case SandboxProvider.LocalDocker:
|
|
@@ -1770,8 +2026,35 @@ var Sandbox = class {
|
|
|
1770
2026
|
get raw() {
|
|
1771
2027
|
return this.adapter.raw;
|
|
1772
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
|
+
}
|
|
1773
2052
|
async openPort(port) {
|
|
1774
|
-
await
|
|
2053
|
+
await time(
|
|
2054
|
+
debugSandbox,
|
|
2055
|
+
`openPort [${this.provider}] :${port}`,
|
|
2056
|
+
() => this.adapter.openPort(port)
|
|
2057
|
+
);
|
|
1775
2058
|
return this;
|
|
1776
2059
|
}
|
|
1777
2060
|
setSecret(name, value) {
|
|
@@ -1786,10 +2069,19 @@ var Sandbox = class {
|
|
|
1786
2069
|
return this.adapter.gitClone(options);
|
|
1787
2070
|
}
|
|
1788
2071
|
async run(command, options) {
|
|
1789
|
-
return
|
|
2072
|
+
return time(
|
|
2073
|
+
debugSandbox,
|
|
2074
|
+
`run [${this.provider}] ${shortLabel(command)}`,
|
|
2075
|
+
() => this.adapter.run(command, options),
|
|
2076
|
+
(result) => ({ exit: result.exitCode })
|
|
2077
|
+
);
|
|
1790
2078
|
}
|
|
1791
2079
|
async runAsync(command, options) {
|
|
1792
|
-
return
|
|
2080
|
+
return time(
|
|
2081
|
+
debugSandbox,
|
|
2082
|
+
`runAsync [${this.provider}] ${shortLabel(command)}`,
|
|
2083
|
+
() => this.adapter.runAsync(command, options)
|
|
2084
|
+
);
|
|
1793
2085
|
}
|
|
1794
2086
|
async list(options) {
|
|
1795
2087
|
return this.adapter.list(options);
|
|
@@ -1804,7 +2096,11 @@ var Sandbox = class {
|
|
|
1804
2096
|
return this.adapter.delete();
|
|
1805
2097
|
}
|
|
1806
2098
|
async getPreviewLink(port) {
|
|
1807
|
-
return
|
|
2099
|
+
return time(
|
|
2100
|
+
debugSandbox,
|
|
2101
|
+
`getPreviewLink [${this.provider}] :${port}`,
|
|
2102
|
+
() => this.adapter.getPreviewLink(port)
|
|
2103
|
+
);
|
|
1808
2104
|
}
|
|
1809
2105
|
get previewHeaders() {
|
|
1810
2106
|
return this.adapter.previewHeaders;
|
|
@@ -1815,6 +2111,14 @@ var Sandbox = class {
|
|
|
1815
2111
|
async downloadFile(sourcePath) {
|
|
1816
2112
|
return this.adapter.downloadFile(sourcePath);
|
|
1817
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
|
+
);
|
|
2121
|
+
}
|
|
1818
2122
|
};
|
|
1819
2123
|
|
|
1820
2124
|
export {
|