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.
@@ -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
- } from "./chunk-O7HCJXKW.js";
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-2NKMDGYH.js";
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
- async ensureProvisioned() {
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 = (async () => {
86
- await this.provision();
87
- this.provisioned = true;
88
- })().finally(() => {
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
443
+ this.requireProvisioned();
362
444
  await this.requireSandbox().getPreviewLink(port);
363
445
  }
364
446
  async getPreviewLink(port) {
365
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- timeoutMs: options?.timeoutMs
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.options.provider?.publishedPorts ?? [];
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: String(port) }]
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- if (this.options.provider?.publishedPorts?.includes(port)) {
1051
- return `http://127.0.0.1:${port}`;
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
- 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
- );
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
1403
+ this.requireProvisioned();
1212
1404
  const sandbox = this.requireSandbox();
1213
1405
  const process2 = await sandbox.exec(
1214
- ["/bin/sh", "-lc", toShellCommand(command)],
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
- await this.ensureProvisioned();
1429
+ this.requireProvisioned();
1238
1430
  const sandbox = this.requireSandbox();
1239
1431
  const process2 = await sandbox.exec(
1240
- ["/bin/sh", "-lc", toShellCommand(command)],
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
- await this.ensureProvisioned();
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
- const tunnels = await this.sandbox.tunnels();
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
- await this.ensureProvisioned();
1603
+ this.requireProvisioned();
1362
1604
  const sandbox = this.requireSandbox();
1363
- const tunnels = await sandbox.tunnels();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
1918
+ this.requireProvisioned();
1668
1919
  const sandbox = this.requireSandbox();
1669
1920
  return sandbox.domain(port);
1670
1921
  }
1671
1922
  async uploadFile(content, targetPath) {
1672
- await this.ensureProvisioned();
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
- await this.ensureProvisioned();
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 this.adapter.openPort(port);
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 this.adapter.run(command, options);
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 this.adapter.runAsync(command, options);
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 this.adapter.getPreviewLink(port);
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 {