@tangle-network/sandbox 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3394 @@
1
+ import { c as ServerError, f as parseErrorResponse, l as StateError, r as NetworkError, t as AuthError, u as TimeoutError } from "./errors-CljiGR__.js";
2
+ //#region src/lib/sse-parser.ts
3
+ /**
4
+ * Parse a streaming SSE response body into an async iterable of
5
+ * structured events.
6
+ *
7
+ * @throws `TypeError` when the response has no body.
8
+ * @throws `DOMException` with `name === "AbortError"` on cancellation.
9
+ */
10
+ async function* parseSSEStream(body, options) {
11
+ if (!body) throw new TypeError("SSE stream has no response body");
12
+ const reader = body.getReader();
13
+ const decoder = new TextDecoder();
14
+ const { signal } = options ?? {};
15
+ let buffer = "";
16
+ let currentEvent = "";
17
+ let dataLines = [];
18
+ let currentId = "";
19
+ const flush = function* () {
20
+ if (dataLines.length === 0) {
21
+ currentEvent = "";
22
+ currentId = "";
23
+ return;
24
+ }
25
+ const effectiveEvent = currentEvent || "message";
26
+ const joined = dataLines.join("\n");
27
+ try {
28
+ yield {
29
+ type: effectiveEvent,
30
+ data: JSON.parse(joined),
31
+ id: currentId || void 0
32
+ };
33
+ } catch {
34
+ console.warn(`[sandbox-sdk] Malformed SSE JSON for event "${effectiveEvent}": ${joined.slice(0, 200)}`);
35
+ }
36
+ currentEvent = "";
37
+ dataLines = [];
38
+ currentId = "";
39
+ };
40
+ const processLine = (line) => {
41
+ if (line.startsWith("event:")) currentEvent = line.slice(6).trim();
42
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
43
+ else if (line.startsWith("id:")) currentId = line.slice(3).trim();
44
+ };
45
+ try {
46
+ while (true) {
47
+ if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
48
+ const { done, value } = await reader.read();
49
+ if (done) {
50
+ if (signal?.aborted) throw new DOMException("SSE stream aborted", "AbortError");
51
+ break;
52
+ }
53
+ buffer += decoder.decode(value, { stream: true });
54
+ const lines = buffer.split(/\r?\n/);
55
+ buffer = lines.pop() ?? "";
56
+ for (const line of lines) if (line === "") yield* flush();
57
+ else processLine(line);
58
+ }
59
+ buffer += decoder.decode();
60
+ if (buffer.length > 0) processLine(buffer);
61
+ yield* flush();
62
+ } finally {
63
+ reader.releaseLock();
64
+ }
65
+ }
66
+ //#endregion
67
+ //#region src/lib/wire-encoding.ts
68
+ /**
69
+ * Encode a prompt text part for the wire. Server decodes at the route
70
+ * boundary; the LLM only ever sees the original UTF-8. Done
71
+ * unconditionally because readable bodies false-positive on ingress WAF
72
+ * rules that pattern-match shell-injection-shaped substrings (which
73
+ * legitimate prompts routinely contain — e.g. Step-0 dev-server
74
+ * bootstrap).
75
+ */
76
+ function encodeTextForWire(text) {
77
+ if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
78
+ if (typeof btoa === "function") return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, h) => String.fromCharCode(Number.parseInt(h, 16))));
79
+ throw new Error("encodeTextForWire: no base64 encoder available (Buffer and btoa both undefined)");
80
+ }
81
+ /**
82
+ * Convert a caller-supplied prompt (string or parts array) into the
83
+ * wire-format parts array. Every text part is base64-encoded; non-text
84
+ * parts pass through. A bare string is wrapped in a single text part.
85
+ */
86
+ function encodePromptForWire(message) {
87
+ return (typeof message === "string" ? [{
88
+ type: "text",
89
+ text: message
90
+ }] : message).map((part) => part.type === "text" ? {
91
+ type: "text",
92
+ text: encodeTextForWire(part.text)
93
+ } : part);
94
+ }
95
+ //#endregion
96
+ //#region src/trace-exporter.ts
97
+ function buildTraceExportPayload(bundle, format = "tangle", serviceName = "tangle-sandbox") {
98
+ if (format === "tangle") return bundle;
99
+ if (format === "otel-json") return toOtelJson(bundle, serviceName);
100
+ throw new Error(`Unsupported trace export format: ${String(format)}`);
101
+ }
102
+ async function exportTraceBundle(bundle, sink) {
103
+ if (!sink.url) throw new Error("Trace export requires sink.url");
104
+ const fetchImpl = sink.fetch ?? globalThis.fetch;
105
+ if (!fetchImpl) throw new Error("Trace export requires fetch");
106
+ const controller = new AbortController();
107
+ const timeout = setTimeout(() => controller.abort(), sink.timeoutMs ?? 3e4);
108
+ try {
109
+ const response = await fetchImpl(sink.url, {
110
+ method: "POST",
111
+ headers: {
112
+ "content-type": "application/json",
113
+ ...sink.headers
114
+ },
115
+ body: JSON.stringify(buildTraceExportPayload(bundle, sink.format ?? "tangle", sink.serviceName)),
116
+ signal: controller.signal
117
+ });
118
+ const body = await response.text();
119
+ if (!response.ok) throw new Error(`Trace export failed with ${response.status}: ${body.slice(0, 500)}`);
120
+ return {
121
+ status: response.status,
122
+ ok: true,
123
+ body
124
+ };
125
+ } finally {
126
+ clearTimeout(timeout);
127
+ }
128
+ }
129
+ function toOtelJson(bundle, serviceName = "tangle-sandbox") {
130
+ const traceId = hexId(bundle.trace.traceId, 32);
131
+ const rootSpanId = hexId(`${bundle.trace.traceId}:root`, 16);
132
+ const rootStart = unixNano(rootTimestamp(bundle));
133
+ const rootEnd = rootStart + BigInt(Math.round(traceDurationMs(bundle.trace) * 1e6));
134
+ const rootSpan = {
135
+ traceId,
136
+ spanId: rootSpanId,
137
+ name: traceRootName(bundle),
138
+ kind: 1,
139
+ startTimeUnixNano: rootStart.toString(),
140
+ endTimeUnixNano: rootEnd.toString(),
141
+ attributes: [
142
+ attr("tangle.subject.id", traceSubjectId(bundle)),
143
+ attr("tangle.trace.id", bundle.trace.traceId),
144
+ attr("tangle.trace.schema_version", bundle.trace.schemaVersion),
145
+ ...Object.entries(bundle.trace.timings ?? {}).map(([key, value]) => attr(`tangle.timing.${snakeCase(key)}`, value)),
146
+ ...Object.entries(bundle.trace.criticalPath ?? {}).map(([key, value]) => attr(`tangle.critical_path.${snakeCase(key)}`, value))
147
+ ],
148
+ ...traceHasError(bundle) ? otelErrorStatus("Trace contains failure evidence") : {}
149
+ };
150
+ return { resourceSpans: [{
151
+ resource: { attributes: [
152
+ attr("service.name", serviceName),
153
+ attr("tangle.subject.id", traceSubjectId(bundle)),
154
+ attr("tangle.trace.schema_version", bundle.trace.schemaVersion),
155
+ ...bundle.intelligence ? [
156
+ attr("tangle.intelligence.schema_version", bundle.intelligence.schemaVersion),
157
+ attr("tangle.intelligence.billable", false),
158
+ attr("tangle.intelligence.cost_usd", 0)
159
+ ] : []
160
+ ] },
161
+ scopeSpans: [{
162
+ scope: {
163
+ name: "@tangle-network/sandbox",
164
+ version: "trace.v1"
165
+ },
166
+ spans: [rootSpan, ...bundle.trace.events.map((event, index) => {
167
+ const start = unixNano(event.timestamp);
168
+ const durationMs = Math.max(0, event.durationMs ?? 0);
169
+ const end = start + BigInt(Math.round(durationMs * 1e6));
170
+ return {
171
+ traceId,
172
+ spanId: hexId(`${bundle.trace.traceId}:${index}`, 16),
173
+ parentSpanId: rootSpanId,
174
+ name: event.type,
175
+ kind: 1,
176
+ startTimeUnixNano: start.toString(),
177
+ endTimeUnixNano: end.toString(),
178
+ attributes: [
179
+ attr("tangle.subject.id", eventSubjectId(event)),
180
+ ..."machineId" in event && event.machineId ? [attr("tangle.fleet.machine_id", event.machineId)] : [],
181
+ ...Object.entries(event.attributes).map(([key, value]) => attr(key, value))
182
+ ],
183
+ ...eventHasError(event) ? otelErrorStatus("Event contains failure evidence") : {}
184
+ };
185
+ })]
186
+ }]
187
+ }] };
188
+ }
189
+ function otelTraceIdForTangleTrace(traceId) {
190
+ return hexId(traceId, 32);
191
+ }
192
+ function traceSubjectId(bundle) {
193
+ return "fleetId" in bundle.trace ? bundle.trace.fleetId : bundle.trace.sandboxId;
194
+ }
195
+ function eventSubjectId(event) {
196
+ return "fleetId" in event ? event.fleetId : event.sandboxId;
197
+ }
198
+ function attr(key, value) {
199
+ if (typeof value === "boolean") return {
200
+ key,
201
+ value: { boolValue: value }
202
+ };
203
+ if (typeof value === "number" && Number.isFinite(value)) return Number.isInteger(value) ? {
204
+ key,
205
+ value: { intValue: String(value) }
206
+ } : {
207
+ key,
208
+ value: { doubleValue: value }
209
+ };
210
+ if (typeof value === "string") return {
211
+ key,
212
+ value: { stringValue: value }
213
+ };
214
+ if (Array.isArray(value)) return {
215
+ key,
216
+ value: { arrayValue: { values: value.map((item) => attrValue(item)) } }
217
+ };
218
+ if (value && typeof value === "object") return {
219
+ key,
220
+ value: { kvlistValue: { values: Object.entries(value).map(([entryKey, entryValue]) => ({
221
+ key: entryKey,
222
+ value: attrValue(entryValue)
223
+ })) } }
224
+ };
225
+ return {
226
+ key,
227
+ value: { stringValue: JSON.stringify(value ?? null) }
228
+ };
229
+ }
230
+ function attrValue(value) {
231
+ if (typeof value === "boolean") return { boolValue: value };
232
+ if (typeof value === "number" && Number.isFinite(value)) return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
233
+ if (typeof value === "string") return { stringValue: value };
234
+ if (Array.isArray(value)) return { arrayValue: { values: value.map(attrValue) } };
235
+ if (value && typeof value === "object") return { kvlistValue: { values: Object.entries(value).map(([key, item]) => ({
236
+ key,
237
+ value: attrValue(item)
238
+ })) } };
239
+ return { stringValue: JSON.stringify(value ?? null) };
240
+ }
241
+ function unixNano(timestamp) {
242
+ const millis = Date.parse(timestamp);
243
+ return BigInt(Number.isFinite(millis) ? millis : 0) * 1000000n;
244
+ }
245
+ function hexId(input, length) {
246
+ const bytes = new TextEncoder().encode(input);
247
+ const hashes = [
248
+ 2166136261,
249
+ 2654435769,
250
+ 2246822507,
251
+ 3266489909
252
+ ];
253
+ for (const byte of bytes) for (let lane = 0; lane < hashes.length; lane += 1) {
254
+ hashes[lane] ^= byte + lane;
255
+ hashes[lane] = Math.imul(hashes[lane], 16777619);
256
+ }
257
+ let out = hashes.map((hash) => (hash >>> 0).toString(16).padStart(8, "0")).join("");
258
+ while (out.length < length) {
259
+ const next = hexId(`${input}:${out.length}`, 32);
260
+ out += next;
261
+ }
262
+ return out.slice(0, length);
263
+ }
264
+ function traceRootName(bundle) {
265
+ return "fleetId" in bundle.trace ? "tangle.fleet.trace" : "tangle.sandbox.trace";
266
+ }
267
+ function rootTimestamp(bundle) {
268
+ return bundle.trace.events[0]?.timestamp ?? bundle.trace.exportedAt;
269
+ }
270
+ function traceDurationMs(trace) {
271
+ return Math.max(0, trace.criticalPath?.durationMs ?? 0);
272
+ }
273
+ function traceHasError(bundle) {
274
+ return bundle.trace.events.some(eventHasError);
275
+ }
276
+ function eventHasError(event) {
277
+ const status = event.attributes.status;
278
+ const error = event.attributes.error;
279
+ return event.type.includes("fail") || status === "failed" || status === "error" || Boolean(error);
280
+ }
281
+ function otelErrorStatus(message) {
282
+ return { status: {
283
+ code: 2,
284
+ message
285
+ } };
286
+ }
287
+ function snakeCase(value) {
288
+ return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
289
+ }
290
+ //#endregion
291
+ //#region src/normalize.ts
292
+ /**
293
+ * Normalize a raw `connection` payload from the Sandbox API into the SDK's
294
+ * canonical `SandboxConnection` shape.
295
+ *
296
+ * The current server returns the runtime URL as `connection.sidecarUrl`;
297
+ * the SDK exposes it as `connection.runtimeUrl`. This helper reads either
298
+ * field name so a future server-side rename needs no client change.
299
+ *
300
+ * Returns `undefined` when the raw payload has no usable runtime URL, so
301
+ * callers using `normalizeConnection(data.connection) ?? fallback` can
302
+ * preserve a previously-known connection during transient server states
303
+ * (e.g. a partial payload that only carries an auth token). Without this
304
+ * guard, a partial `{ authToken: "..." }` response would overwrite a
305
+ * previously-valid connection with an object missing its URL.
306
+ */
307
+ function normalizeConnection(raw) {
308
+ if (!raw || typeof raw !== "object") return void 0;
309
+ const c = raw;
310
+ const runtimeUrl = c.runtimeUrl ?? c.sidecarUrl;
311
+ if (!runtimeUrl) return void 0;
312
+ return {
313
+ runtimeUrl,
314
+ authToken: c.authToken,
315
+ authTokenExpiresAt: c.authTokenExpiresAt,
316
+ ssh: normalizeSshCredentials(c.ssh),
317
+ webTerminalUrl: c.webTerminalUrl
318
+ };
319
+ }
320
+ /**
321
+ * Narrow a raw `ssh` payload into a `SSHCredentials` object. Returns
322
+ * `undefined` if the payload is missing required fields so callers that
323
+ * dereference `connection.ssh.proxyCommand` don't surprise-crash on partial
324
+ * server responses.
325
+ */
326
+ function normalizeSshCredentials(raw) {
327
+ if (!raw || typeof raw !== "object") return void 0;
328
+ const s = raw;
329
+ if (typeof s.port !== "number" || typeof s.username !== "string" || typeof s.proxyCommand !== "string" || s.proxyCommand.length === 0) return;
330
+ return {
331
+ username: s.username,
332
+ port: s.port,
333
+ proxyCommand: s.proxyCommand
334
+ };
335
+ }
336
+ //#endregion
337
+ //#region src/backend-config.ts
338
+ function parseModelString(model) {
339
+ const parts = model.split("/");
340
+ if (parts.length >= 2) return {
341
+ provider: parts[0],
342
+ model: parts.slice(1).join("/")
343
+ };
344
+ return { model };
345
+ }
346
+ /**
347
+ * Normalize runtime backend config for the wire format.
348
+ *
349
+ * Callers set `backend.profile` (named string or portable AgentProfile).
350
+ * This function generates the native `inlineProfile` shim automatically.
351
+ */
352
+ function normalizeRuntimeBackendConfig(backend, options = {}) {
353
+ if (!backend && !options.model) return void 0;
354
+ const portableProfile = backend?.profile && typeof backend.profile !== "string" ? backend.profile : void 0;
355
+ const inlineProfile = portableProfile ? toBackendProfile(portableProfile) : void 0;
356
+ const callerInlineProfile = backend?.inlineProfile;
357
+ if (callerInlineProfile && !inlineProfile) console.warn("[sandbox-sdk] backend.inlineProfile is deprecated. Use backend.profile (AgentPortableProfile) instead.");
358
+ return {
359
+ ...backend?.type ? { type: backend.type } : {},
360
+ ...backend?.profile !== void 0 ? { profile: backend.profile } : {},
361
+ ...inlineProfile ? { inlineProfile } : callerInlineProfile ? { inlineProfile: callerInlineProfile } : {},
362
+ ...backend?.model ? { model: backend.model } : options.model ? { model: parseModelString(options.model) } : {},
363
+ ...backend?.server ? { server: backend.server } : {}
364
+ };
365
+ }
366
+ function toBackendProfile(profile) {
367
+ return {
368
+ name: profile.name,
369
+ model: profile.model?.default,
370
+ ...profile.prompt?.systemPrompt ? { systemPrompt: profile.prompt.systemPrompt } : {},
371
+ ...profile.tools && Object.keys(profile.tools).length > 0 ? { tools: profile.tools } : {},
372
+ ...profile.prompt?.instructions?.length ? { instructions: profile.prompt.instructions } : {},
373
+ ...profile.permissions && Object.keys(profile.permissions).length > 0 ? { permission: Object.fromEntries(Object.entries(profile.permissions).map(([key, value]) => [key === "network" ? "webfetch" : key, value])) } : {},
374
+ ...profile.mcp ? { mcp: Object.fromEntries(Object.entries(profile.mcp).map(([name, config]) => [name, {
375
+ ...config.transport || config.url ? { type: config.transport === "stdio" ? "local" : config.transport === "sse" ? "remote" : config.transport } : {},
376
+ command: config.command ?? "",
377
+ ...config.args ? { args: config.args } : {},
378
+ ...config.env ? { env: config.env } : {},
379
+ ...config.cwd ? { cwd: config.cwd } : {},
380
+ ...config.url ? { url: config.url } : {},
381
+ ...config.headers ? { headers: config.headers } : {}
382
+ }])) } : {},
383
+ ...profile.subagents ? { subagents: profile.subagents } : {},
384
+ ...profile.resources ? { resources: profile.resources } : {},
385
+ ...profile.hooks ? { hooks: profile.hooks } : {},
386
+ ...profile.modes ? { modes: profile.modes } : {},
387
+ ...profile.extensions ? { extensions: profile.extensions } : {}
388
+ };
389
+ }
390
+ //#endregion
391
+ //#region src/session.ts
392
+ /**
393
+ * A single agent session inside a sandbox. Created via
394
+ * `box.session(id)` — does not hit the network until a method is called.
395
+ */
396
+ var SandboxSession = class {
397
+ /**
398
+ * @internal SDK-internal constructor — apps should call `box.session(id)`.
399
+ */
400
+ constructor(box, id) {
401
+ this.box = box;
402
+ this.id = id;
403
+ }
404
+ /**
405
+ * Fetch the current session state from the sandbox. Includes status,
406
+ * model, prompt count, token usage if known, and timing metadata.
407
+ *
408
+ * Throws on transport error; returns `null` if the session id is not
409
+ * known to the sandbox (e.g. it ended and was reaped, or the id is
410
+ * invalid).
411
+ */
412
+ async status() {
413
+ return this.box._sessionStatus(this.id);
414
+ }
415
+ /**
416
+ * Stream events from this session as they arrive. With no `since`,
417
+ * starts at the live tail; with `since`, replays from that event id
418
+ * forward — useful for reconnect-after-disconnect flows.
419
+ *
420
+ * The async iterator terminates when the session reaches a terminal
421
+ * state (`completed`, `failed`, `cancelled`) and the corresponding
422
+ * terminal event has been yielded, OR when the caller's signal aborts.
423
+ */
424
+ events(opts) {
425
+ return this.box._sessionEvents(this.id, opts);
426
+ }
427
+ /**
428
+ * Await the session's terminal result. Polls status + drains events
429
+ * until the session reaches a terminal state, then returns the
430
+ * aggregated `PromptResult`.
431
+ *
432
+ * Use this to wait for a session that was started by another caller
433
+ * (e.g. `dispatchPrompt`).
434
+ */
435
+ async result() {
436
+ return this.box._sessionResult(this.id);
437
+ }
438
+ /**
439
+ * Continue this session with an additional prompt. Equivalent to
440
+ * `box.prompt(message, { ...opts, sessionId: this.id })` but reads
441
+ * naturally on a Session reference.
442
+ */
443
+ async prompt(message, opts) {
444
+ return this.box.prompt(message, {
445
+ ...opts,
446
+ sessionId: this.id
447
+ });
448
+ }
449
+ /**
450
+ * Cancel the session. Best-effort: an in-flight LLM call may still
451
+ * complete one more token before the abort takes effect. Idempotent —
452
+ * cancelling a completed session is a no-op.
453
+ */
454
+ async cancel() {
455
+ return this.box._sessionCancel(this.id);
456
+ }
457
+ };
458
+ //#endregion
459
+ //#region src/sandbox.ts
460
+ /**
461
+ * Sandbox Instance
462
+ *
463
+ * Represents a single sandbox and provides methods to interact with it.
464
+ */
465
+ function normalizeProvisionStep(raw) {
466
+ if (raw === "sidecar-ready") return "runtime-ready";
467
+ return raw;
468
+ }
469
+ /**
470
+ * Replace bearer-secret fields on a `SandboxConnection` with a sentinel
471
+ * before serialization. The serialized shape is preserved (so external
472
+ * tooling that switches on `connection.authToken !== undefined` keeps
473
+ * working), but the actual token value never leaves the process.
474
+ */
475
+ function redactConnectionSecrets(connection) {
476
+ if (!connection) return connection;
477
+ if (connection.authToken === void 0) return connection;
478
+ return {
479
+ ...connection,
480
+ authToken: "[REDACTED]"
481
+ };
482
+ }
483
+ function normalizePreviewLink(raw) {
484
+ const sandboxId = raw.sandboxId ?? raw.sidecarId ?? "";
485
+ return {
486
+ previewId: raw.previewId,
487
+ sandboxId,
488
+ port: raw.port,
489
+ protocol: raw.protocol,
490
+ hostname: raw.hostname,
491
+ url: raw.url,
492
+ status: raw.status,
493
+ lastSyncAt: raw.lastSyncAt,
494
+ createdAt: raw.createdAt,
495
+ updatedAt: raw.updatedAt,
496
+ metadata: raw.metadata
497
+ };
498
+ }
499
+ const SNAPSHOT_RESTORE_POLL_INTERVAL_MS = 1e3;
500
+ const SNAPSHOT_RESTORE_TIMEOUT_MS = 600 * 1e3;
501
+ const SNAPSHOT_VISIBILITY_POLL_INTERVAL_MS = 1e3;
502
+ const SNAPSHOT_VISIBILITY_TIMEOUT_MS = 120 * 1e3;
503
+ function toSnapshotResult(data) {
504
+ return {
505
+ snapshotId: data.snapshotId ?? data.id,
506
+ createdAt: new Date(data.createdAt),
507
+ sizeBytes: data.sizeBytes,
508
+ tags: data.tags ?? []
509
+ };
510
+ }
511
+ function isTransientSnapshotVisibilityError(error) {
512
+ const message = error instanceof Error ? error.message : String(error);
513
+ return message.includes("SNAPSHOT_NOT_FOUND") || message.includes("Failed to list snapshot") || message.includes("Storage agent error (404)");
514
+ }
515
+ function getSandboxEventText(event) {
516
+ if (event.type === "result") return event.data.finalText ?? void 0;
517
+ if (event.type !== "message.part.updated") return;
518
+ const part = event.data.part;
519
+ if (part?.type !== "text") return;
520
+ return part.text ?? part.content ?? void 0;
521
+ }
522
+ /**
523
+ * A sandbox instance with methods for interaction.
524
+ */
525
+ var SandboxInstance = class SandboxInstance {
526
+ client;
527
+ info;
528
+ defaultRuntimeBackend;
529
+ constructor(client, info, defaultRuntimeBackend) {
530
+ this.client = client;
531
+ this.info = info;
532
+ this.defaultRuntimeBackend = defaultRuntimeBackend;
533
+ }
534
+ /** Unique sandbox identifier */
535
+ get id() {
536
+ return this.info.id;
537
+ }
538
+ /** Human-readable name */
539
+ get name() {
540
+ return this.info.name;
541
+ }
542
+ /** Current status */
543
+ get status() {
544
+ return this.info.status;
545
+ }
546
+ /** Connection information */
547
+ get connection() {
548
+ return this.info.connection;
549
+ }
550
+ /** Custom metadata */
551
+ get metadata() {
552
+ return this.info.metadata;
553
+ }
554
+ /** When the sandbox was created */
555
+ get createdAt() {
556
+ return this.info.createdAt;
557
+ }
558
+ /** When the sandbox started running */
559
+ get startedAt() {
560
+ return this.info.startedAt;
561
+ }
562
+ /** Last activity timestamp */
563
+ get lastActivityAt() {
564
+ return this.info.lastActivityAt;
565
+ }
566
+ /** When the sandbox will expire */
567
+ get expiresAt() {
568
+ return this.info.expiresAt;
569
+ }
570
+ /** Error message if status is 'failed' */
571
+ get error() {
572
+ return this.info.error;
573
+ }
574
+ /** Web terminal URL for browser-based access */
575
+ get url() {
576
+ return this.info.connection?.webTerminalUrl;
577
+ }
578
+ /**
579
+ * Serialize to the public sandbox shape for logs and structured
580
+ * output. Secrets in `connection` (currently `authToken`) are
581
+ * redacted so that `JSON.stringify(box)` is safe to ship to log
582
+ * sinks. Use {@link toDebugJSON} when the bearer is required (e.g.
583
+ * one-off CLI commands that print credentials to the user).
584
+ */
585
+ toJSON() {
586
+ return {
587
+ id: this.id,
588
+ name: this.name,
589
+ status: this.status,
590
+ connection: redactConnectionSecrets(this.connection),
591
+ metadata: this.metadata,
592
+ createdAt: this.createdAt,
593
+ startedAt: this.startedAt,
594
+ lastActivityAt: this.lastActivityAt,
595
+ expiresAt: this.expiresAt,
596
+ error: this.error
597
+ };
598
+ }
599
+ /**
600
+ * Serialize the sandbox **including secrets** when `includeSecrets`
601
+ * is true. The default behavior matches {@link toJSON} and redacts
602
+ * `connection.authToken`.
603
+ *
604
+ * Use only when the caller has an explicit need for the bearer
605
+ * (e.g. presenting it once to the human operator). Never wire the
606
+ * result of `toDebugJSON({ includeSecrets: true })` into a structured
607
+ * logger — the bearer will land in any log sink consuming that output.
608
+ */
609
+ toDebugJSON(options = {}) {
610
+ if (options.includeSecrets !== true) return this.toJSON();
611
+ return {
612
+ ...this.toJSON(),
613
+ connection: this.connection
614
+ };
615
+ }
616
+ /**
617
+ * Create an advanced direct-runtime view of this sandbox.
618
+ *
619
+ * Runtime methods on the returned instance talk to the sandbox runtime
620
+ * directly using `connection.runtimeUrl` and `connection.authToken`.
621
+ * Lifecycle methods still go through the parent SDK client.
622
+ */
623
+ direct() {
624
+ if (!this.connection?.runtimeUrl) throw new StateError("Sandbox has no direct runtime URL", this.status, "running");
625
+ let directInstance;
626
+ directInstance = new SandboxInstance(new DirectRuntimeHttpClient(this.client, this.id, () => directInstance.connection, async () => {
627
+ await directInstance.refresh();
628
+ const next = directInstance.connection?.authToken;
629
+ const client = this.client;
630
+ if (next && typeof client._emitTokenRefresh === "function") client._emitTokenRefresh(this.id, next);
631
+ }), { ...this.info }, this.defaultRuntimeBackend);
632
+ return directInstance;
633
+ }
634
+ /**
635
+ * Get an MCP endpoint for this sandbox. Returns a paste-able config
636
+ * for any MCP-capable host (Claude Desktop, Cursor, claude-code,
637
+ * codex, opencode, …) plus a freshly-minted, capability-scoped JWT.
638
+ *
639
+ * The token is short-lived and limited to the requested capabilities
640
+ * — it cannot be used against admin endpoints (`/exec`, `/files`,
641
+ * etc.) on the sandbox. Call `getMcpEndpoint()` again to rotate.
642
+ *
643
+ * Requires the sandbox to have been created with `capabilities`
644
+ * including the requested capability (default: `computer_use`).
645
+ *
646
+ * @example
647
+ * ```typescript
648
+ * const ep = await box.getMcpEndpoint();
649
+ * // Save ep.config to your IDE's mcp.json — that's it.
650
+ * fs.writeFileSync("mcp.json", JSON.stringify(ep.config, null, 2));
651
+ * ```
652
+ */
653
+ async getMcpEndpoint(options) {
654
+ const params = new URLSearchParams();
655
+ if (options?.capabilities && options.capabilities.length > 0) params.set("capabilities", options.capabilities.join(","));
656
+ if (options?.serverName) params.set("serverName", options.serverName);
657
+ if (options?.ttlMinutes) params.set("ttlMinutes", String(options.ttlMinutes));
658
+ const qs = params.toString();
659
+ const path = `/v1/sandboxes/${this.id}/mcp${qs ? `?${qs}` : ""}`;
660
+ const response = await this.client.fetch(path);
661
+ if (!response.ok) {
662
+ const body = await response.text();
663
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
664
+ }
665
+ return await response.json();
666
+ }
667
+ /**
668
+ * Refresh sandbox information from the server.
669
+ */
670
+ async refresh() {
671
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}`);
672
+ if (!response.ok) {
673
+ const body = await response.text();
674
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
675
+ }
676
+ const data = await response.json();
677
+ this.info = this.parseInfo(data);
678
+ }
679
+ /**
680
+ * Fetch fresh TEE attestation evidence for this sandbox.
681
+ *
682
+ * When `attestationNonce` is supplied, the runtime must return evidence bound
683
+ * to that challenge or fail closed if the selected TEE backend cannot support
684
+ * nonce-bound report data.
685
+ */
686
+ async getTeeAttestation(options) {
687
+ await this.ensureRunning();
688
+ const hasNonce = Boolean(options?.attestationNonce);
689
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/tee/attestation`, {
690
+ method: hasNonce ? "POST" : "GET",
691
+ body: hasNonce ? JSON.stringify({ attestation_nonce: options?.attestationNonce }) : void 0
692
+ });
693
+ if (!response.ok) {
694
+ const body = await response.text();
695
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
696
+ }
697
+ const data = await response.json();
698
+ if (hasNonce) data.attestationNonce = options?.attestationNonce;
699
+ this.info = {
700
+ ...this.info,
701
+ metadata: {
702
+ ...this.info.metadata ?? {},
703
+ teeAttestationJson: JSON.stringify(data.attestation),
704
+ ...data.attestationNonce ? { attestationNonce: data.attestationNonce } : {}
705
+ }
706
+ };
707
+ return data;
708
+ }
709
+ /**
710
+ * Fetch the TEE-bound public key used for sealed-secret encryption.
711
+ *
712
+ * The returned key includes an attestation report. Verify that report before
713
+ * encrypting secrets to the key.
714
+ */
715
+ async getTeePublicKey() {
716
+ await this.ensureRunning();
717
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/tee/public-key`, { method: "GET" });
718
+ if (!response.ok) {
719
+ const body = await response.text();
720
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
721
+ }
722
+ const data = await response.json();
723
+ this.info = {
724
+ ...this.info,
725
+ metadata: {
726
+ ...this.info.metadata ?? {},
727
+ teePublicKeyJson: JSON.stringify(data.public_key)
728
+ }
729
+ };
730
+ return data;
731
+ }
732
+ /**
733
+ * Bootstrap a real-time collaboration session for a file.
734
+ * Returns the WebSocket URL and auth token needed to connect a
735
+ * Hocuspocus/Yjs provider for live multi-user editing.
736
+ *
737
+ * @example
738
+ * ```typescript
739
+ * const collab = await box.collaborate("src/index.ts")
740
+ * // Use collab.transport.websocketUrl + collab.transport.token
741
+ * // with @hocuspocus/provider to connect
742
+ * ```
743
+ */
744
+ async collaborate(path, options) {
745
+ const response = await this.client.fetch("/v1/collaboration/bootstrap", {
746
+ method: "POST",
747
+ headers: { "Content-Type": "application/json" },
748
+ body: JSON.stringify({
749
+ sandboxId: this.id,
750
+ path,
751
+ access: options?.access ?? "write"
752
+ })
753
+ });
754
+ if (!response.ok) {
755
+ const body = await response.text();
756
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
757
+ }
758
+ return await response.json();
759
+ }
760
+ /**
761
+ * Refresh a collaboration token for an existing document session.
762
+ */
763
+ /**
764
+ * Refresh a collaboration token. Access level is preserved from the original
765
+ * token — cannot be escalated (read stays read, write stays write).
766
+ */
767
+ async refreshCollaborationToken(documentId, currentToken) {
768
+ const response = await this.client.fetch("/v1/collaboration/token", {
769
+ method: "POST",
770
+ headers: { "Content-Type": "application/json" },
771
+ body: JSON.stringify({
772
+ sandboxId: this.id,
773
+ documentId,
774
+ currentToken
775
+ })
776
+ });
777
+ if (!response.ok) {
778
+ const body = await response.text();
779
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
780
+ }
781
+ return await response.json();
782
+ }
783
+ /**
784
+ * Get SSH credentials for connecting to the sandbox.
785
+ * Throws if SSH is not enabled or sandbox is not running.
786
+ */
787
+ async ssh() {
788
+ await this.ensureRunning();
789
+ if (!this.connection?.ssh) throw new StateError("SSH is not enabled for this sandbox", this.status, "running");
790
+ return this.connection.ssh;
791
+ }
792
+ async sshCommand() {
793
+ const ssh = await this.ssh();
794
+ const authToken = this.client.getApiKey?.();
795
+ if (!authToken) throw new AuthError("SSH command requires client API key access to populate proxy auth");
796
+ const nullKnownHostsFile = process.platform === "win32" ? "NUL" : "/dev/null";
797
+ return {
798
+ command: `ssh -o ProxyCommand="${ssh.proxyCommand}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=${nullKnownHostsFile} -o GlobalKnownHostsFile=${nullKnownHostsFile} -o LogLevel=ERROR -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o TCPKeepAlive=yes ${ssh.username}@localhost -p ${ssh.port}`,
799
+ env: { TANGLE_SSH_PROXY_AUTH_TOKEN: authToken }
800
+ };
801
+ }
802
+ /**
803
+ * Execute a command in the sandbox.
804
+ */
805
+ async exec(command, options) {
806
+ await this.ensureRunning();
807
+ const headers = {};
808
+ if (options?.sessionId) headers["X-Session-Id"] = options.sessionId;
809
+ const response = await this.runtimeFetch("/terminals/commands", {
810
+ method: "POST",
811
+ headers,
812
+ body: JSON.stringify({
813
+ command,
814
+ cwd: options?.cwd,
815
+ env: options?.env,
816
+ timeout: options?.timeoutMs
817
+ })
818
+ });
819
+ if (!response.ok) {
820
+ const body = await response.text();
821
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
822
+ }
823
+ const data = await response.json();
824
+ const result = data.result ?? data;
825
+ return {
826
+ exitCode: result.exitCode ?? 0,
827
+ stdout: result.stdout ?? "",
828
+ stderr: result.stderr ?? ""
829
+ };
830
+ }
831
+ /**
832
+ * Read a file from the sandbox.
833
+ *
834
+ * @param path - Path to the file. Relative paths resolve from the workspace root.
835
+ * Absolute paths (e.g., `/tmp/output.json`) access the container filesystem directly.
836
+ * @returns File content as string
837
+ *
838
+ * @example
839
+ * ```typescript
840
+ * const content = await box.read("src/index.ts");
841
+ * const report = await box.read("/output/report.json");
842
+ * ```
843
+ */
844
+ async read(path, options) {
845
+ await this.ensureRunning();
846
+ const headers = {};
847
+ if (options?.sessionId) headers["X-Session-Id"] = options.sessionId;
848
+ const response = await this.runtimeFetch("/files/read", {
849
+ method: "POST",
850
+ headers,
851
+ body: JSON.stringify({ path })
852
+ });
853
+ if (!response.ok) {
854
+ const body = await response.text();
855
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
856
+ }
857
+ return (await response.json()).data?.content ?? "";
858
+ }
859
+ /**
860
+ * Write content to a file in the sandbox.
861
+ *
862
+ * @param path - Path to the file. Relative paths resolve from the workspace root.
863
+ * Absolute paths (e.g., `/tmp/cases.json`) write to the container filesystem directly.
864
+ * @param content - Content to write
865
+ *
866
+ * @example
867
+ * ```typescript
868
+ * await box.write("src/fix.ts", "export const fix = () => {}");
869
+ * await box.write("/tmp/config.json", JSON.stringify(config));
870
+ * ```
871
+ */
872
+ async write(path, content) {
873
+ await this.ensureRunning();
874
+ const response = await this.runtimeFetch("/files/write", {
875
+ method: "POST",
876
+ body: JSON.stringify({
877
+ path,
878
+ content
879
+ })
880
+ });
881
+ if (!response.ok) {
882
+ const body = await response.text();
883
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
884
+ }
885
+ }
886
+ /**
887
+ * Send a prompt to the agent running in the sandbox.
888
+ * Returns the complete response after the agent finishes.
889
+ */
890
+ async prompt(message, options) {
891
+ await this.ensureRunning();
892
+ const startTime = Date.now();
893
+ let responseText;
894
+ let error;
895
+ let traceId;
896
+ let usage;
897
+ const controller = new AbortController();
898
+ try {
899
+ const signal = options?.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
900
+ for await (const event of this.streamPrompt(message, {
901
+ ...options,
902
+ signal
903
+ })) {
904
+ const eventText = getSandboxEventText(event);
905
+ if (eventText !== void 0) responseText = eventText;
906
+ if (event.type === "result") {
907
+ const tokenUsage = event.data.tokenUsage;
908
+ if (tokenUsage) usage = {
909
+ inputTokens: tokenUsage.inputTokens ?? 0,
910
+ outputTokens: tokenUsage.outputTokens ?? 0
911
+ };
912
+ }
913
+ if (event.type === "trace.id") traceId = event.data.traceId;
914
+ if (event.type === "error") error = event.data.message;
915
+ }
916
+ return {
917
+ success: !error,
918
+ response: responseText,
919
+ error,
920
+ traceId,
921
+ durationMs: Date.now() - startTime,
922
+ usage
923
+ };
924
+ } catch (err) {
925
+ return {
926
+ success: false,
927
+ error: err instanceof Error ? err.message : String(err),
928
+ durationMs: Date.now() - startTime
929
+ };
930
+ } finally {
931
+ controller.abort();
932
+ }
933
+ }
934
+ /**
935
+ * Stream events from an agent prompt.
936
+ * Use this for real-time updates during agent execution.
937
+ *
938
+ * Automatically reconnects via the runtime event replay endpoint if the
939
+ * SSE stream drops before a terminal event (`result` or `done`) is received.
940
+ * Reconnection is transparent — replayed events that were already yielded
941
+ * (based on event ID tracking) are deduplicated.
942
+ */
943
+ async *streamPrompt(message, options) {
944
+ await this.ensureRunning();
945
+ const parts = encodePromptForWire(message);
946
+ const response = await this.runtimeFetch("/agents/run/stream", {
947
+ method: "POST",
948
+ signal: options?.signal,
949
+ headers: {
950
+ ...options?.executionId ? { "X-Execution-ID": options.executionId } : {},
951
+ ...options?.lastEventId ? { "Last-Event-ID": options.lastEventId } : {}
952
+ },
953
+ body: JSON.stringify({
954
+ identifier: "default",
955
+ parts,
956
+ sessionId: options?.sessionId,
957
+ metadata: options?.context,
958
+ backend: normalizeRuntimeBackendConfig(options?.backend ?? this.defaultRuntimeBackend, { model: options?.model })
959
+ })
960
+ });
961
+ if (!response.ok) {
962
+ const body = await response.text();
963
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
964
+ }
965
+ let lastEventId = options?.lastEventId;
966
+ let executionId = options?.executionId;
967
+ let receivedTerminal = false;
968
+ const MAX_RECONNECT_ATTEMPTS = 3;
969
+ const RECONNECT_DELAY_MS = 2e3;
970
+ for await (const event of this.parseSSEStream(response, options?.signal)) {
971
+ if (event.id) lastEventId = event.id;
972
+ if (event.type === "execution.started" && event.data?.executionId) executionId = event.data.executionId;
973
+ if (event.type === "result" || event.type === "done") receivedTerminal = true;
974
+ yield event;
975
+ }
976
+ if (receivedTerminal || options?.signal?.aborted) return;
977
+ if (!executionId) {
978
+ console.warn("[sandbox-sdk] Stream dropped before execution.started, cannot reconnect");
979
+ return;
980
+ }
981
+ for (let attempt = 1; attempt <= MAX_RECONNECT_ATTEMPTS; attempt++) {
982
+ if (options?.signal?.aborted) return;
983
+ console.warn(`[sandbox-sdk] Stream dropped without terminal event, reconnecting (attempt ${attempt}/${MAX_RECONNECT_ATTEMPTS})`);
984
+ await new Promise((resolve) => setTimeout(resolve, RECONNECT_DELAY_MS));
985
+ if (options?.signal?.aborted) return;
986
+ try {
987
+ const queryParams = lastEventId ? `?lastEventId=${encodeURIComponent(lastEventId)}&format=sse` : "?format=sse";
988
+ const replayResponse = await this.runtimeFetch(`/agents/run/${encodeURIComponent(executionId)}/events${queryParams}`, {
989
+ method: "GET",
990
+ signal: options?.signal
991
+ });
992
+ if (!replayResponse.ok) {
993
+ console.warn(`[sandbox-sdk] Replay endpoint returned ${replayResponse.status}, attempt ${attempt}`);
994
+ continue;
995
+ }
996
+ for await (const event of this.parseSSEStream(replayResponse, options?.signal)) {
997
+ if (event.type === "history.replay.start" || event.type === "history.replay.end") continue;
998
+ if (event.id) lastEventId = event.id;
999
+ if (event.type === "result" || event.type === "done") receivedTerminal = true;
1000
+ yield event;
1001
+ }
1002
+ if (receivedTerminal || options?.signal?.aborted) return;
1003
+ } catch (err) {
1004
+ console.warn(`[sandbox-sdk] Reconnection attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
1005
+ }
1006
+ }
1007
+ if (!receivedTerminal) console.warn(`[sandbox-sdk] Exhausted ${MAX_RECONNECT_ATTEMPTS} reconnection attempts without receiving terminal event`);
1008
+ }
1009
+ /**
1010
+ * Stream sandbox lifecycle and activity events.
1011
+ */
1012
+ async *events(options) {
1013
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/events`, { signal: options?.signal });
1014
+ if (!response.ok) {
1015
+ const body = await response.text();
1016
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1017
+ }
1018
+ for await (const event of this.parseSSEStream(response, options?.signal)) {
1019
+ if (options?.eventTypes && !options.eventTypes.includes(event.type)) continue;
1020
+ yield event;
1021
+ }
1022
+ }
1023
+ async trace(options = {}) {
1024
+ const query = new URLSearchParams();
1025
+ if (options.includeIntelligence === true) query.set("includeIntelligence", "true");
1026
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/trace${query.size ? `?${query}` : ""}`);
1027
+ if (!response.ok) {
1028
+ const body = await response.text();
1029
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1030
+ }
1031
+ return await response.json();
1032
+ }
1033
+ async intelligence() {
1034
+ const intelligence = (await this.trace({ includeIntelligence: true })).intelligence;
1035
+ if (!intelligence) throw new Error("Sandbox intelligence was not returned");
1036
+ return intelligence;
1037
+ }
1038
+ async createIntelligenceReport(options = {}) {
1039
+ const { window, compareTo, ...rest } = options;
1040
+ const response = await this.client.fetch("/v1/intelligence/reports", {
1041
+ method: "POST",
1042
+ body: JSON.stringify({
1043
+ subject: {
1044
+ type: "sandbox",
1045
+ id: this.id,
1046
+ window,
1047
+ compareTo
1048
+ },
1049
+ ...rest
1050
+ })
1051
+ });
1052
+ const body = await response.text();
1053
+ if (!response.ok) throw parseErrorResponse(response.status, body, void 0, response.headers);
1054
+ return JSON.parse(body).report;
1055
+ }
1056
+ async createAgenticIntelligenceReport(options) {
1057
+ return this.createIntelligenceReport({
1058
+ mode: "agentic",
1059
+ budget: {
1060
+ billTo: "customer",
1061
+ maxUsd: options.maxUsd
1062
+ },
1063
+ metadata: options.metadata
1064
+ });
1065
+ }
1066
+ async exportTrace(sink) {
1067
+ return exportTraceBundle(await this.trace(), sink);
1068
+ }
1069
+ /**
1070
+ * Stream real-time provisioning progress events.
1071
+ *
1072
+ * Connects to the SSE events stream and yields typed `ProvisionEvent` objects
1073
+ * for each provisioning step. The generator completes when provisioning
1074
+ * finishes (success or failure) and returns the terminal result.
1075
+ *
1076
+ * @returns AsyncGenerator of ProvisionEvent, with a ProvisionResult return value
1077
+ *
1078
+ * @example
1079
+ * ```typescript
1080
+ * const box = await client.create({ image: "ethereum" });
1081
+ *
1082
+ * const stream = box.watchProvisioning();
1083
+ * for await (const event of stream) {
1084
+ * console.log(`[${event.step}] ${event.status} — ${event.message}`);
1085
+ * }
1086
+ * // stream.return value contains the terminal result
1087
+ * ```
1088
+ */
1089
+ async *watchProvisioning(options) {
1090
+ const abortController = new AbortController();
1091
+ const signal = options?.signal;
1092
+ if (signal) if (signal.aborted) abortController.abort();
1093
+ else signal.addEventListener("abort", () => abortController.abort(), { once: true });
1094
+ let result;
1095
+ try {
1096
+ for await (const event of this.events({
1097
+ signal: abortController.signal,
1098
+ eventTypes: [
1099
+ "provision_progress",
1100
+ "provision_complete",
1101
+ "provision_failed"
1102
+ ]
1103
+ })) if (event.type === "provision_progress") {
1104
+ const d = event.data;
1105
+ yield {
1106
+ step: normalizeProvisionStep(d.step),
1107
+ status: d.status,
1108
+ message: d.message,
1109
+ percent: d.progress,
1110
+ detail: d.detail,
1111
+ timestamp: d.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
1112
+ };
1113
+ } else if (event.type === "provision_complete") {
1114
+ result = {
1115
+ type: "complete",
1116
+ containerId: event.data.containerId,
1117
+ timestamp: event.data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
1118
+ };
1119
+ break;
1120
+ } else if (event.type === "provision_failed") {
1121
+ result = {
1122
+ type: "failed",
1123
+ error: event.data.error ?? "Provisioning failed",
1124
+ timestamp: event.data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
1125
+ };
1126
+ break;
1127
+ }
1128
+ } finally {
1129
+ abortController.abort();
1130
+ }
1131
+ try {
1132
+ await this.refresh();
1133
+ } catch {}
1134
+ return result;
1135
+ }
1136
+ /**
1137
+ * Run an agentic task until completion.
1138
+ *
1139
+ * Unlike prompt(), task() is designed for autonomous agent work:
1140
+ * - The agent works until it completes the task or hits an error
1141
+ * - Session state is maintained for context continuity
1142
+ * - Token usage is aggregated across the execution
1143
+ *
1144
+ * Note: The agent (OpenCode/Claude) handles multi-turn execution internally.
1145
+ * Most tasks complete in a single call. The maxTurns option is for edge cases
1146
+ * where the agent explicitly signals it needs additional input.
1147
+ *
1148
+ * @param prompt - Task description for the agent
1149
+ * @param options - Task options
1150
+ * @returns Task result with response and execution metadata
1151
+ */
1152
+ async task(prompt, options) {
1153
+ const startTime = Date.now();
1154
+ const sessionId = options?.sessionId ?? `task-${crypto.randomUUID()}`;
1155
+ let responseText;
1156
+ let error;
1157
+ let traceId;
1158
+ let usage;
1159
+ try {
1160
+ for await (const event of this.streamPrompt(prompt, {
1161
+ ...options,
1162
+ sessionId
1163
+ })) {
1164
+ const eventText = getSandboxEventText(event);
1165
+ if (eventText !== void 0) responseText = eventText;
1166
+ if (event.type === "result") {
1167
+ const tokenUsage = event.data.tokenUsage;
1168
+ if (tokenUsage) usage = usage ? {
1169
+ inputTokens: usage.inputTokens + (tokenUsage.inputTokens ?? 0),
1170
+ outputTokens: usage.outputTokens + (tokenUsage.outputTokens ?? 0)
1171
+ } : {
1172
+ inputTokens: tokenUsage.inputTokens ?? 0,
1173
+ outputTokens: tokenUsage.outputTokens ?? 0
1174
+ };
1175
+ }
1176
+ if (event.type === "trace.id") traceId = event.data.traceId;
1177
+ if (event.type === "error") error = event.data.message;
1178
+ }
1179
+ return {
1180
+ success: !error,
1181
+ response: responseText,
1182
+ error,
1183
+ traceId,
1184
+ durationMs: Date.now() - startTime,
1185
+ usage,
1186
+ turnsUsed: 1,
1187
+ sessionId
1188
+ };
1189
+ } catch (err) {
1190
+ return {
1191
+ success: false,
1192
+ error: err instanceof Error ? err.message : String(err),
1193
+ durationMs: Date.now() - startTime,
1194
+ turnsUsed: 1,
1195
+ sessionId
1196
+ };
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Stream events from a task execution.
1201
+ *
1202
+ * Use this for real-time updates as the agent works:
1203
+ * - Tool calls and results
1204
+ * - Thinking/reasoning steps
1205
+ * - File operations
1206
+ * - Final response
1207
+ *
1208
+ * @param prompt - Task description for the agent
1209
+ * @param options - Task options
1210
+ */
1211
+ async *streamTask(prompt, options) {
1212
+ const sessionId = options?.sessionId ?? `task-${crypto.randomUUID()}`;
1213
+ yield {
1214
+ type: "task.start",
1215
+ data: {
1216
+ sessionId,
1217
+ prompt
1218
+ }
1219
+ };
1220
+ for await (const event of this.streamPrompt(prompt, {
1221
+ ...options,
1222
+ sessionId
1223
+ })) yield event;
1224
+ yield {
1225
+ type: "task.complete",
1226
+ data: { sessionId }
1227
+ };
1228
+ }
1229
+ /**
1230
+ * Search for text patterns in files using ripgrep.
1231
+ *
1232
+ * This is a first-class code search capability, not a shell wrapper.
1233
+ * Ripgrep is pre-installed in all managed sandboxes.
1234
+ *
1235
+ * @param pattern - Regular expression pattern to search for
1236
+ * @param options - Search options
1237
+ * @returns Async iterator of search matches
1238
+ *
1239
+ * @example Search for task-marker comments
1240
+ * ```typescript
1241
+ * for await (const match of box.search("TASK:", { glob: "**\/*.ts" })) {
1242
+ * console.log(`${match.path}:${match.line}: ${match.text}`);
1243
+ * }
1244
+ * ```
1245
+ *
1246
+ * @example Collect all matches
1247
+ * ```typescript
1248
+ * const matches = [];
1249
+ * for await (const match of box.search("function.*async")) {
1250
+ * matches.push(match);
1251
+ * }
1252
+ * ```
1253
+ */
1254
+ async *search(pattern, options) {
1255
+ await this.ensureRunning();
1256
+ const response = await this.runtimeFetch("/search", {
1257
+ method: "POST",
1258
+ body: JSON.stringify({
1259
+ pattern,
1260
+ glob: options?.glob,
1261
+ cwd: options?.cwd,
1262
+ maxResults: options?.maxResults,
1263
+ ignoreCase: options?.ignoreCase,
1264
+ context: options?.context
1265
+ })
1266
+ });
1267
+ if (!response.ok) {
1268
+ const body = await response.text();
1269
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1270
+ }
1271
+ const matches = (await response.json()).matches ?? [];
1272
+ for (const match of matches) yield {
1273
+ path: match.path,
1274
+ line: match.line,
1275
+ column: match.column ?? 1,
1276
+ text: match.text,
1277
+ before: match.before,
1278
+ after: match.after
1279
+ };
1280
+ }
1281
+ /**
1282
+ * Git capability object for repository operations.
1283
+ *
1284
+ * All git operations are executed in the sandbox workspace.
1285
+ *
1286
+ * @example Check status and commit
1287
+ * ```typescript
1288
+ * const status = await box.git.status();
1289
+ * if (status.isDirty) {
1290
+ * await box.git.add(["."]);
1291
+ * await box.git.commit("Update files");
1292
+ * await box.git.push();
1293
+ * }
1294
+ * ```
1295
+ */
1296
+ get git() {
1297
+ return {
1298
+ status: () => this.gitStatus(),
1299
+ log: (limit) => this.gitLog(limit),
1300
+ diff: (ref) => this.gitDiff(ref),
1301
+ add: (paths) => this.gitAdd(paths),
1302
+ commit: (message, options) => this.gitCommit(message, options),
1303
+ push: (options) => this.gitPush(options),
1304
+ pull: (options) => this.gitPull(options),
1305
+ branches: () => this.gitBranches(),
1306
+ checkout: (ref, options) => this.gitCheckout(ref, options)
1307
+ };
1308
+ }
1309
+ async gitStatus() {
1310
+ await this.ensureRunning();
1311
+ const response = await this.runtimeFetch("/git/status", { method: "GET" });
1312
+ if (!response.ok) {
1313
+ const body = await response.text();
1314
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1315
+ }
1316
+ const data = await response.json();
1317
+ return {
1318
+ branch: data.branch ?? "main",
1319
+ head: data.head ?? "",
1320
+ isDirty: data.isDirty ?? false,
1321
+ ahead: data.ahead ?? 0,
1322
+ behind: data.behind ?? 0,
1323
+ staged: data.staged ?? [],
1324
+ modified: data.modified ?? [],
1325
+ untracked: data.untracked ?? []
1326
+ };
1327
+ }
1328
+ async gitLog(limit = 10) {
1329
+ await this.ensureRunning();
1330
+ const response = await this.runtimeFetch(`/git/log?limit=${limit}`, { method: "GET" });
1331
+ if (!response.ok) {
1332
+ const body = await response.text();
1333
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1334
+ }
1335
+ return ((await response.json()).commits ?? []).map((c) => ({
1336
+ sha: c.sha,
1337
+ shortSha: c.sha.slice(0, 7),
1338
+ message: c.message,
1339
+ author: c.author,
1340
+ email: c.email,
1341
+ date: new Date(c.date)
1342
+ }));
1343
+ }
1344
+ async gitDiff(ref) {
1345
+ await this.ensureRunning();
1346
+ const url = ref ? `/git/diff?ref=${encodeURIComponent(ref)}` : "/git/diff";
1347
+ const response = await this.runtimeFetch(url, { method: "GET" });
1348
+ if (!response.ok) {
1349
+ const body = await response.text();
1350
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1351
+ }
1352
+ const data = await response.json();
1353
+ return {
1354
+ files: data.files ?? [],
1355
+ additions: data.additions ?? 0,
1356
+ deletions: data.deletions ?? 0,
1357
+ raw: data.raw ?? ""
1358
+ };
1359
+ }
1360
+ async gitAdd(paths) {
1361
+ await this.ensureRunning();
1362
+ const response = await this.runtimeFetch("/git/add", {
1363
+ method: "POST",
1364
+ body: JSON.stringify({ paths })
1365
+ });
1366
+ if (!response.ok) {
1367
+ const body = await response.text();
1368
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1369
+ }
1370
+ }
1371
+ async gitCommit(message, options) {
1372
+ await this.ensureRunning();
1373
+ const response = await this.runtimeFetch("/git/commit", {
1374
+ method: "POST",
1375
+ body: JSON.stringify({
1376
+ message,
1377
+ amend: options?.amend
1378
+ })
1379
+ });
1380
+ if (!response.ok) {
1381
+ const body = await response.text();
1382
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1383
+ }
1384
+ const data = await response.json();
1385
+ return {
1386
+ sha: data.sha,
1387
+ shortSha: data.sha.slice(0, 7),
1388
+ message: data.message,
1389
+ author: data.author,
1390
+ email: data.email,
1391
+ date: new Date(data.date)
1392
+ };
1393
+ }
1394
+ async gitPush(options) {
1395
+ await this.ensureRunning();
1396
+ const response = await this.runtimeFetch("/git/push", {
1397
+ method: "POST",
1398
+ body: JSON.stringify({ force: options?.force })
1399
+ });
1400
+ if (!response.ok) {
1401
+ const body = await response.text();
1402
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1403
+ }
1404
+ }
1405
+ async gitPull(options) {
1406
+ await this.ensureRunning();
1407
+ const response = await this.runtimeFetch("/git/pull", {
1408
+ method: "POST",
1409
+ body: JSON.stringify({ rebase: options?.rebase })
1410
+ });
1411
+ if (!response.ok) {
1412
+ const body = await response.text();
1413
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1414
+ }
1415
+ }
1416
+ async gitBranches() {
1417
+ await this.ensureRunning();
1418
+ const response = await this.runtimeFetch("/git/branches", { method: "GET" });
1419
+ if (!response.ok) {
1420
+ const body = await response.text();
1421
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1422
+ }
1423
+ return (await response.json()).branches ?? [];
1424
+ }
1425
+ async gitCheckout(ref, options) {
1426
+ await this.ensureRunning();
1427
+ const response = await this.runtimeFetch("/git/checkout", {
1428
+ method: "POST",
1429
+ body: JSON.stringify({
1430
+ ref,
1431
+ create: options?.create
1432
+ })
1433
+ });
1434
+ if (!response.ok) {
1435
+ const body = await response.text();
1436
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1437
+ }
1438
+ }
1439
+ /**
1440
+ * Tools capability object for managing language runtimes.
1441
+ *
1442
+ * Uses mise (polyglot version manager) to install and manage tools.
1443
+ *
1444
+ * @example Install and use Node.js
1445
+ * ```typescript
1446
+ * await box.tools.install("node", "22");
1447
+ * await box.tools.use("node", "22");
1448
+ * const list = await box.tools.list();
1449
+ * ```
1450
+ */
1451
+ get tools() {
1452
+ return {
1453
+ install: (tool, version) => this.toolsInstall(tool, version),
1454
+ use: (tool, version) => this.toolsUse(tool, version),
1455
+ list: () => this.toolsList(),
1456
+ run: (tool, args) => this.toolsRun(tool, args)
1457
+ };
1458
+ }
1459
+ /**
1460
+ * File system operations beyond basic read/write:
1461
+ * - Binary upload/download
1462
+ * - Directory ops (uploadDir, downloadDir, list, mkdir)
1463
+ * - Metadata (stat, exists)
1464
+ * - Progress reporting for large files
1465
+ *
1466
+ * @example Upload and download
1467
+ * ```typescript
1468
+ * await box.fs.upload("./model.bin", "/workspace/models/model.bin");
1469
+ * await box.fs.download("/workspace/results.zip", "./results.zip");
1470
+ * ```
1471
+ *
1472
+ * @example Directory operations
1473
+ * ```typescript
1474
+ * await box.fs.uploadDir("./project", "/workspace/project");
1475
+ * const files = await box.fs.list("/workspace");
1476
+ * ```
1477
+ *
1478
+ * @example File management
1479
+ * ```typescript
1480
+ * if (await box.fs.exists("/workspace/config.json")) {
1481
+ * const info = await box.fs.stat("/workspace/config.json");
1482
+ * console.log(`Size: ${info.size}`);
1483
+ * }
1484
+ * await box.fs.mkdir("/workspace/output", { recursive: true });
1485
+ * await box.fs.delete("/workspace/temp", { recursive: true });
1486
+ * ```
1487
+ */
1488
+ get fs() {
1489
+ return {
1490
+ read: (path) => this.read(path),
1491
+ write: (path, content) => this.write(path, content),
1492
+ search: (query, options) => this.search(query, options),
1493
+ upload: (localPath, remotePath, options) => this.fsUpload(localPath, remotePath, options),
1494
+ download: (remotePath, localPath, options) => this.fsDownload(remotePath, localPath, options),
1495
+ uploadDir: (localDir, remoteDir) => this.fsUploadDir(localDir, remoteDir),
1496
+ downloadDir: (remoteDir, localDir) => this.fsDownloadDir(remoteDir, localDir),
1497
+ list: (path, options) => this.fsList(path, options),
1498
+ stat: (path) => this.fsStat(path),
1499
+ delete: (path, options) => this.fsDelete(path, options),
1500
+ mkdir: (path, options) => this.fsMkdir(path, options),
1501
+ exists: (path) => this.fsExists(path)
1502
+ };
1503
+ }
1504
+ async fsUpload(localPath, remotePath, options) {
1505
+ await this.ensureRunning();
1506
+ const fs = await import("node:fs/promises");
1507
+ const path = await import("node:path");
1508
+ const fileBuffer = await fs.readFile(localPath);
1509
+ const fileName = path.basename(localPath);
1510
+ if (options?.onProgress) options.onProgress({
1511
+ bytesUploaded: 0,
1512
+ totalBytes: fileBuffer.length,
1513
+ percentage: 0
1514
+ });
1515
+ const formData = new FormData();
1516
+ formData.append("file", new Blob([new Uint8Array(fileBuffer)]), fileName);
1517
+ formData.append("path", remotePath);
1518
+ const response = await this.runtimeFetch("/fs/upload", {
1519
+ method: "POST",
1520
+ body: formData,
1521
+ headers: {}
1522
+ });
1523
+ if (!response.ok) {
1524
+ const body = await response.text();
1525
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1526
+ }
1527
+ if (options?.onProgress) options.onProgress({
1528
+ bytesUploaded: fileBuffer.length,
1529
+ totalBytes: fileBuffer.length,
1530
+ percentage: 100
1531
+ });
1532
+ }
1533
+ async fsDownload(remotePath, localPath, options) {
1534
+ await this.ensureRunning();
1535
+ const fs = await import("node:fs/promises");
1536
+ const path = await import("node:path");
1537
+ await fs.mkdir(path.dirname(localPath), { recursive: true });
1538
+ const response = await this.runtimeFetch(`/fs/download${remotePath}`, { method: "GET" });
1539
+ if (!response.ok) {
1540
+ const body = await response.text();
1541
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1542
+ }
1543
+ const contentLength = response.headers.get("content-length");
1544
+ const totalBytes = contentLength ? Number.parseInt(contentLength, 10) : 0;
1545
+ if (options?.onProgress && totalBytes > 0) options.onProgress({
1546
+ bytesDownloaded: 0,
1547
+ totalBytes,
1548
+ percentage: 0
1549
+ });
1550
+ const buffer = await response.arrayBuffer();
1551
+ await fs.writeFile(localPath, Buffer.from(buffer));
1552
+ if (options?.onProgress) options.onProgress({
1553
+ bytesDownloaded: buffer.byteLength,
1554
+ totalBytes: buffer.byteLength,
1555
+ percentage: 100
1556
+ });
1557
+ }
1558
+ async fsUploadDir(localDir, remoteDir) {
1559
+ await this.ensureRunning();
1560
+ const fs = await import("node:fs/promises");
1561
+ const path = await import("node:path");
1562
+ const { execSync } = await import("node:child_process");
1563
+ const tempTarPath = path.join(await fs.mkdtemp(path.join((await import("node:os")).tmpdir(), "upload-")), "archive.tar.gz");
1564
+ try {
1565
+ execSync(`tar -czf "${tempTarPath}" -C "${localDir}" .`, { stdio: "pipe" });
1566
+ const tarBuffer = await fs.readFile(tempTarPath);
1567
+ const formData = new FormData();
1568
+ formData.append("archive", new Blob([new Uint8Array(tarBuffer)]), "archive.tar.gz");
1569
+ formData.append("path", remoteDir);
1570
+ const response = await this.runtimeFetch("/fs/upload-dir", {
1571
+ method: "POST",
1572
+ body: formData,
1573
+ headers: {}
1574
+ });
1575
+ if (!response.ok) {
1576
+ const body = await response.text();
1577
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1578
+ }
1579
+ } finally {
1580
+ await fs.unlink(tempTarPath).catch(() => {});
1581
+ }
1582
+ }
1583
+ async fsDownloadDir(remoteDir, localDir) {
1584
+ await this.ensureRunning();
1585
+ const fs = await import("node:fs/promises");
1586
+ const path = await import("node:path");
1587
+ const { execSync } = await import("node:child_process");
1588
+ const response = await this.runtimeFetch(`/fs/download-dir${remoteDir}`, { method: "GET" });
1589
+ if (!response.ok) {
1590
+ const body = await response.text();
1591
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1592
+ }
1593
+ const tempDir = await fs.mkdtemp(path.join((await import("node:os")).tmpdir(), "download-"));
1594
+ const tempTarPath = path.join(tempDir, "archive.tar.gz");
1595
+ try {
1596
+ const buffer = await response.arrayBuffer();
1597
+ await fs.writeFile(tempTarPath, Buffer.from(buffer));
1598
+ await fs.mkdir(localDir, { recursive: true });
1599
+ execSync(`tar -xzf "${tempTarPath}" -C "${localDir}"`, { stdio: "pipe" });
1600
+ } finally {
1601
+ await fs.rm(tempDir, { recursive: true }).catch(() => {});
1602
+ }
1603
+ }
1604
+ async fsList(path, options) {
1605
+ await this.ensureRunning();
1606
+ const params = new URLSearchParams();
1607
+ if (options?.all) params.set("all", "true");
1608
+ if (options?.long) params.set("long", "true");
1609
+ const query = params.toString();
1610
+ const url = query ? `/fs/list${path}?${query}` : `/fs/list${path}`;
1611
+ const response = await this.runtimeFetch(url, { method: "GET" });
1612
+ if (!response.ok) {
1613
+ const body = await response.text();
1614
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1615
+ }
1616
+ return ((await response.json()).data?.entries ?? []).map((e) => ({
1617
+ name: e.name,
1618
+ path: e.path,
1619
+ size: e.size,
1620
+ isDir: e.isDir,
1621
+ isFile: e.isFile,
1622
+ isSymlink: e.isSymlink,
1623
+ permissions: e.permissions,
1624
+ owner: e.owner,
1625
+ group: e.group,
1626
+ modTime: new Date(e.modTime),
1627
+ accessTime: new Date(e.accessTime)
1628
+ }));
1629
+ }
1630
+ async fsStat(path) {
1631
+ await this.ensureRunning();
1632
+ const response = await this.runtimeFetch(`/fs/stat${path}`, { method: "GET" });
1633
+ if (!response.ok) {
1634
+ const body = await response.text();
1635
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1636
+ }
1637
+ const e = (await response.json()).data;
1638
+ return {
1639
+ name: e.name,
1640
+ path: e.path,
1641
+ size: e.size,
1642
+ isDir: e.isDir,
1643
+ isFile: e.isFile,
1644
+ isSymlink: e.isSymlink,
1645
+ permissions: e.permissions,
1646
+ owner: e.owner,
1647
+ group: e.group,
1648
+ modTime: new Date(e.modTime),
1649
+ accessTime: new Date(e.accessTime)
1650
+ };
1651
+ }
1652
+ async fsDelete(path, options) {
1653
+ await this.ensureRunning();
1654
+ const params = new URLSearchParams();
1655
+ if (options?.recursive) params.set("recursive", "true");
1656
+ const query = params.toString();
1657
+ const url = query ? `/fs${path}?${query}` : `/fs${path}`;
1658
+ const response = await this.runtimeFetch(url, { method: "DELETE" });
1659
+ if (!response.ok) {
1660
+ const body = await response.text();
1661
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1662
+ }
1663
+ }
1664
+ async fsMkdir(path, options) {
1665
+ await this.ensureRunning();
1666
+ const params = new URLSearchParams();
1667
+ if (options?.recursive) params.set("recursive", "true");
1668
+ const query = params.toString();
1669
+ const url = query ? `/fs/mkdir${path}?${query}` : `/fs/mkdir${path}`;
1670
+ const response = await this.runtimeFetch(url, { method: "POST" });
1671
+ if (!response.ok) {
1672
+ const body = await response.text();
1673
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1674
+ }
1675
+ }
1676
+ async fsExists(path) {
1677
+ await this.ensureRunning();
1678
+ const response = await this.runtimeFetch(`/fs/exists${path}`, { method: "GET" });
1679
+ if (!response.ok) {
1680
+ const body = await response.text();
1681
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1682
+ }
1683
+ return (await response.json()).data?.exists === true;
1684
+ }
1685
+ /**
1686
+ * Permissions manager for multi-user access control.
1687
+ *
1688
+ * @example List users
1689
+ * ```typescript
1690
+ * const users = await box.permissions.list();
1691
+ * for (const user of users) {
1692
+ * console.log(`${user.username}: ${user.role}`);
1693
+ * }
1694
+ * ```
1695
+ *
1696
+ * @example Add a developer
1697
+ * ```typescript
1698
+ * await box.permissions.add({
1699
+ * userId: "user_abc",
1700
+ * role: "developer",
1701
+ * sshKeys: ["ssh-ed25519 AAAA..."],
1702
+ * });
1703
+ * ```
1704
+ */
1705
+ get permissions() {
1706
+ return {
1707
+ list: () => this.permissionsList(),
1708
+ get: (userId) => this.permissionsGet(userId),
1709
+ add: (options) => this.permissionsAdd(options),
1710
+ update: (userId, options) => this.permissionsUpdate(userId, options),
1711
+ remove: (userId, options) => this.permissionsRemove(userId, options),
1712
+ setAccessPolicies: (userId, rules) => this.permissionsSetAccessPolicies(userId, rules),
1713
+ getAccessPolicies: (userId) => this.permissionsGetAccessPolicies(userId),
1714
+ checkAccess: (userId, path, action) => this.permissionsCheckAccess(userId, path, action)
1715
+ };
1716
+ }
1717
+ async permissionsList() {
1718
+ await this.ensureRunning();
1719
+ const response = await this.runtimeFetch("/permissions/users", { method: "GET" });
1720
+ if (!response.ok) {
1721
+ const body = await response.text();
1722
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1723
+ }
1724
+ return ((await response.json()).users ?? []).map((u) => ({
1725
+ userId: u.userId,
1726
+ username: u.username,
1727
+ homeDir: u.homeDir,
1728
+ role: u.role,
1729
+ sshKeys: u.sshKeys ?? [],
1730
+ directoryPermissions: u.directoryPermissions,
1731
+ accessPolicies: u.accessPolicies,
1732
+ createdAt: new Date(u.createdAt)
1733
+ }));
1734
+ }
1735
+ async permissionsGet(userId) {
1736
+ await this.ensureRunning();
1737
+ const response = await this.runtimeFetch(`/permissions/users/${encodeURIComponent(userId)}`, { method: "GET" });
1738
+ if (response.status === 404) return null;
1739
+ if (!response.ok) {
1740
+ const body = await response.text();
1741
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1742
+ }
1743
+ const u = await response.json();
1744
+ return {
1745
+ userId: u.userId,
1746
+ username: u.username,
1747
+ homeDir: u.homeDir,
1748
+ role: u.role,
1749
+ sshKeys: u.sshKeys ?? [],
1750
+ directoryPermissions: u.directoryPermissions,
1751
+ accessPolicies: u.accessPolicies,
1752
+ createdAt: new Date(u.createdAt)
1753
+ };
1754
+ }
1755
+ async permissionsAdd(options) {
1756
+ await this.ensureRunning();
1757
+ const response = await this.runtimeFetch("/permissions/users", {
1758
+ method: "POST",
1759
+ body: JSON.stringify(options)
1760
+ });
1761
+ if (!response.ok) {
1762
+ const body = await response.text();
1763
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1764
+ }
1765
+ const u = await response.json();
1766
+ return {
1767
+ userId: u.userId,
1768
+ username: u.username,
1769
+ homeDir: u.homeDir,
1770
+ role: u.role,
1771
+ sshKeys: u.sshKeys ?? [],
1772
+ directoryPermissions: u.directoryPermissions,
1773
+ accessPolicies: u.accessPolicies,
1774
+ createdAt: new Date(u.createdAt)
1775
+ };
1776
+ }
1777
+ async permissionsUpdate(userId, options) {
1778
+ await this.ensureRunning();
1779
+ const response = await this.runtimeFetch(`/permissions/users/${encodeURIComponent(userId)}`, {
1780
+ method: "PATCH",
1781
+ body: JSON.stringify(options)
1782
+ });
1783
+ if (!response.ok) {
1784
+ const body = await response.text();
1785
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1786
+ }
1787
+ const u = await response.json();
1788
+ return {
1789
+ userId: u.userId,
1790
+ username: u.username,
1791
+ homeDir: u.homeDir,
1792
+ role: u.role,
1793
+ sshKeys: u.sshKeys ?? [],
1794
+ directoryPermissions: u.directoryPermissions,
1795
+ accessPolicies: u.accessPolicies,
1796
+ createdAt: new Date(u.createdAt)
1797
+ };
1798
+ }
1799
+ async permissionsRemove(userId, options) {
1800
+ await this.ensureRunning();
1801
+ const params = new URLSearchParams();
1802
+ if (options?.preserveHomeDir) params.set("preserveHomeDir", "true");
1803
+ const query = params.toString();
1804
+ const path = query ? `/permissions/users/${encodeURIComponent(userId)}?${query}` : `/permissions/users/${encodeURIComponent(userId)}`;
1805
+ const response = await this.runtimeFetch(path, { method: "DELETE" });
1806
+ if (!response.ok) {
1807
+ const body = await response.text();
1808
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1809
+ }
1810
+ }
1811
+ async permissionsSetAccessPolicies(userId, rules) {
1812
+ await this.ensureRunning();
1813
+ const response = await this.runtimeFetch(`/permissions/users/${encodeURIComponent(userId)}/policies`, {
1814
+ method: "PUT",
1815
+ body: JSON.stringify({ rules })
1816
+ });
1817
+ if (!response.ok) {
1818
+ const body = await response.text();
1819
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1820
+ }
1821
+ }
1822
+ async permissionsGetAccessPolicies(userId) {
1823
+ await this.ensureRunning();
1824
+ const response = await this.runtimeFetch(`/permissions/users/${encodeURIComponent(userId)}/policies`, { method: "GET" });
1825
+ if (!response.ok) {
1826
+ const body = await response.text();
1827
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1828
+ }
1829
+ return (await response.json()).rules ?? [];
1830
+ }
1831
+ async permissionsCheckAccess(userId, path, action) {
1832
+ await this.ensureRunning();
1833
+ const response = await this.runtimeFetch(`/permissions/users/${encodeURIComponent(userId)}/check`, {
1834
+ method: "POST",
1835
+ body: JSON.stringify({
1836
+ path,
1837
+ action
1838
+ })
1839
+ });
1840
+ if (!response.ok) {
1841
+ const body = await response.text();
1842
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1843
+ }
1844
+ return (await response.json()).allowed === true;
1845
+ }
1846
+ /**
1847
+ * Backend manager for runtime agent configuration.
1848
+ *
1849
+ * @example Check backend status
1850
+ * ```typescript
1851
+ * const status = await box.backend.status();
1852
+ * console.log(`Backend: ${status.type}, Status: ${status.status}`);
1853
+ * ```
1854
+ *
1855
+ * @example Add MCP server at runtime
1856
+ * ```typescript
1857
+ * await box.backend.addMcp("web-search", {
1858
+ * command: "npx",
1859
+ * args: ["-y", "@anthropic/web-search"],
1860
+ * });
1861
+ * ```
1862
+ *
1863
+ * @example Read provider-native Cursor metadata
1864
+ * ```typescript
1865
+ * const models = await box.backend.models();
1866
+ * const agents = await box.backend.agents({ limit: 20 });
1867
+ * const runs = await box.backend.runs(agents.items[0].agentId);
1868
+ * ```
1869
+ */
1870
+ get backend() {
1871
+ return {
1872
+ status: () => this.backendStatus(),
1873
+ capabilities: () => this.backendCapabilities(),
1874
+ addMcp: (name, config) => this.backendAddMcp(name, config),
1875
+ getMcpStatus: () => this.backendGetMcpStatus(),
1876
+ updateConfig: (config) => this.backendUpdateConfig(config),
1877
+ account: () => this.backendAccount(),
1878
+ models: () => this.backendModels(),
1879
+ repositories: () => this.backendRepositories(),
1880
+ agents: (options) => this.backendAgents(options),
1881
+ agent: (agentId) => this.backendAgent(agentId),
1882
+ archiveAgent: (agentId) => this.backendArchiveAgent(agentId),
1883
+ unarchiveAgent: (agentId) => this.backendUnarchiveAgent(agentId),
1884
+ deleteAgent: (agentId) => this.backendDeleteAgent(agentId),
1885
+ runs: (agentId, options) => this.backendRuns(agentId, options),
1886
+ run: (runId, options) => this.backendRun(runId, options),
1887
+ agentMessages: (agentId, options) => this.backendAgentMessages(agentId, options),
1888
+ artifacts: (sessionId) => this.backendArtifacts(sessionId),
1889
+ downloadArtifact: (sessionId, path) => this.backendDownloadArtifact(sessionId, path),
1890
+ restart: () => this.backendRestart()
1891
+ };
1892
+ }
1893
+ async backendStatus() {
1894
+ await this.ensureRunning();
1895
+ const response = await this.runtimeFetch("/backend/status", { method: "GET" });
1896
+ if (!response.ok) {
1897
+ const body = await response.text();
1898
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1899
+ }
1900
+ return await response.json();
1901
+ }
1902
+ async backendCapabilities() {
1903
+ await this.ensureRunning();
1904
+ const response = await this.runtimeFetch("/backend/capabilities", { method: "GET" });
1905
+ if (!response.ok) {
1906
+ const body = await response.text();
1907
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1908
+ }
1909
+ return await response.json();
1910
+ }
1911
+ async backendAddMcp(name, config) {
1912
+ await this.ensureRunning();
1913
+ const response = await this.runtimeFetch("/backend/mcp", {
1914
+ method: "POST",
1915
+ body: JSON.stringify({
1916
+ name,
1917
+ config
1918
+ })
1919
+ });
1920
+ if (!response.ok) {
1921
+ const body = await response.text();
1922
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1923
+ }
1924
+ }
1925
+ async backendGetMcpStatus() {
1926
+ await this.ensureRunning();
1927
+ const response = await this.runtimeFetch("/backend/mcp", { method: "GET" });
1928
+ if (!response.ok) {
1929
+ const body = await response.text();
1930
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1931
+ }
1932
+ return (await response.json()).servers ?? {};
1933
+ }
1934
+ async backendUpdateConfig(config) {
1935
+ await this.ensureRunning();
1936
+ const response = await this.runtimeFetch("/backend/config", {
1937
+ method: "PATCH",
1938
+ body: JSON.stringify(config)
1939
+ });
1940
+ if (!response.ok) {
1941
+ const body = await response.text();
1942
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1943
+ }
1944
+ }
1945
+ async backendControlData(path) {
1946
+ await this.ensureRunning();
1947
+ const response = await this.runtimeFetch(path, { method: "GET" });
1948
+ if (!response.ok) {
1949
+ const body = await response.text();
1950
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1951
+ }
1952
+ const payload = await response.json();
1953
+ if (!payload || typeof payload !== "object" || !("data" in payload)) throw new ServerError("Backend control response missing data", 502, {
1954
+ endpoint: path,
1955
+ origin: "runtime"
1956
+ });
1957
+ return payload.data;
1958
+ }
1959
+ async backendControlAction(path, method) {
1960
+ await this.ensureRunning();
1961
+ const response = await this.runtimeFetch(path, { method });
1962
+ if (!response.ok) {
1963
+ const body = await response.text();
1964
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
1965
+ }
1966
+ }
1967
+ backendListSearch(options) {
1968
+ const search = new URLSearchParams();
1969
+ if (options?.limit !== void 0) search.set("limit", String(options.limit));
1970
+ if (options?.cursor) search.set("cursor", options.cursor);
1971
+ const query = search.toString();
1972
+ return query ? `?${query}` : "";
1973
+ }
1974
+ async backendAccount() {
1975
+ return this.backendControlData("/config/account");
1976
+ }
1977
+ async backendModels() {
1978
+ return this.backendControlData("/config/models");
1979
+ }
1980
+ async backendRepositories() {
1981
+ return this.backendControlData("/config/repositories");
1982
+ }
1983
+ async backendAgents(options) {
1984
+ return this.backendControlData(`/config/backend-agents${this.backendListSearch(options)}`);
1985
+ }
1986
+ async backendAgent(agentId) {
1987
+ return this.backendControlData(`/config/backend-agents/${encodeURIComponent(agentId)}`);
1988
+ }
1989
+ async backendArchiveAgent(agentId) {
1990
+ await this.backendControlAction(`/config/backend-agents/${encodeURIComponent(agentId)}/archive`, "POST");
1991
+ }
1992
+ async backendUnarchiveAgent(agentId) {
1993
+ await this.backendControlAction(`/config/backend-agents/${encodeURIComponent(agentId)}/unarchive`, "POST");
1994
+ }
1995
+ async backendDeleteAgent(agentId) {
1996
+ await this.backendControlAction(`/config/backend-agents/${encodeURIComponent(agentId)}`, "DELETE");
1997
+ }
1998
+ async backendRuns(agentId, options) {
1999
+ return this.backendControlData(`/config/backend-agents/${encodeURIComponent(agentId)}/runs${this.backendListSearch(options)}`);
2000
+ }
2001
+ async backendRun(runId, options) {
2002
+ const search = new URLSearchParams();
2003
+ if (options?.agentId) search.set("agentId", options.agentId);
2004
+ const query = search.toString();
2005
+ return this.backendControlData(`/config/backend-runs/${encodeURIComponent(runId)}${query ? `?${query}` : ""}`);
2006
+ }
2007
+ async backendAgentMessages(agentId, options) {
2008
+ return this.backendControlData(`/config/backend-agents/${encodeURIComponent(agentId)}/messages${this.backendListSearch(options)}`);
2009
+ }
2010
+ async backendArtifacts(sessionId) {
2011
+ return this.backendControlData(`/config/artifacts/${encodeURIComponent(sessionId)}`);
2012
+ }
2013
+ async backendDownloadArtifact(sessionId, path) {
2014
+ await this.ensureRunning();
2015
+ const search = new URLSearchParams({ path });
2016
+ const response = await this.runtimeFetch(`/config/artifacts/${encodeURIComponent(sessionId)}/download?${search.toString()}`, { method: "GET" });
2017
+ if (!response.ok) {
2018
+ const body = await response.text();
2019
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2020
+ }
2021
+ const payload = await response.json();
2022
+ if (!payload || typeof payload.base64 !== "string") throw new ServerError("Backend artifact download response missing base64", 502, {
2023
+ endpoint: `/config/artifacts/${encodeURIComponent(sessionId)}/download`,
2024
+ origin: "runtime"
2025
+ });
2026
+ return Uint8Array.from(Buffer.from(payload.base64, "base64"));
2027
+ }
2028
+ async backendRestart() {
2029
+ await this.ensureRunning();
2030
+ const response = await this.runtimeFetch("/backend/restart", { method: "POST" });
2031
+ if (!response.ok) {
2032
+ const body = await response.text();
2033
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2034
+ }
2035
+ }
2036
+ /**
2037
+ * Process manager for spawning and controlling processes.
2038
+ *
2039
+ * Provides non-blocking process execution with real-time log streaming,
2040
+ * ideal for long-running tasks like ML training or dev servers.
2041
+ *
2042
+ * @example Non-blocking process
2043
+ * ```typescript
2044
+ * const proc = await box.process.spawn("python train.py", {
2045
+ * cwd: "/workspace",
2046
+ * env: { "CUDA_VISIBLE_DEVICES": "0" }
2047
+ * });
2048
+ *
2049
+ * // Stream logs
2050
+ * for await (const entry of proc.logs()) {
2051
+ * console.log(`[${entry.type}] ${entry.data}`);
2052
+ * }
2053
+ *
2054
+ * // Check status
2055
+ * const status = await proc.status();
2056
+ * console.log(`Running: ${status.running}`);
2057
+ *
2058
+ * // Kill if needed
2059
+ * await proc.kill();
2060
+ * ```
2061
+ *
2062
+ * @example Run Python code directly
2063
+ * ```typescript
2064
+ * const result = await box.process.runCode(`
2065
+ * import numpy as np
2066
+ * print(np.random.rand(10).mean())
2067
+ * `);
2068
+ * console.log(result.stdout);
2069
+ * ```
2070
+ */
2071
+ get process() {
2072
+ return {
2073
+ spawn: (command, options) => this.processSpawn(command, options),
2074
+ runCode: (code, options) => this.processRunCode(code, options),
2075
+ list: () => this.processList(),
2076
+ get: (pid) => this.processGet(pid)
2077
+ };
2078
+ }
2079
+ async processSpawn(command, options) {
2080
+ await this.ensureRunning();
2081
+ const response = await this.runtimeFetch("/process/spawn", {
2082
+ method: "POST",
2083
+ body: JSON.stringify({
2084
+ command,
2085
+ cwd: options?.cwd,
2086
+ env: options?.env,
2087
+ timeoutMs: options?.timeoutMs,
2088
+ blocking: false
2089
+ })
2090
+ });
2091
+ if (!response.ok) {
2092
+ const body = await response.text();
2093
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2094
+ }
2095
+ const data = await response.json();
2096
+ return this.createProcessHandle(data.data.pid, command);
2097
+ }
2098
+ async processRunCode(code, options) {
2099
+ await this.ensureRunning();
2100
+ const response = await this.runtimeFetch("/process/run-code", {
2101
+ method: "POST",
2102
+ body: JSON.stringify({
2103
+ code,
2104
+ cwd: options?.cwd,
2105
+ env: options?.env,
2106
+ timeoutMs: options?.timeoutMs,
2107
+ blocking: true
2108
+ })
2109
+ });
2110
+ if (!response.ok) {
2111
+ const body = await response.text();
2112
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2113
+ }
2114
+ return (await response.json()).data;
2115
+ }
2116
+ async processList() {
2117
+ await this.ensureRunning();
2118
+ const response = await this.runtimeFetch("/process", { method: "GET" });
2119
+ if (!response.ok) {
2120
+ const body = await response.text();
2121
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2122
+ }
2123
+ return ((await response.json()).data ?? []).map((p) => ({
2124
+ pid: p.pid,
2125
+ command: p.command,
2126
+ cwd: p.cwd,
2127
+ running: p.running,
2128
+ exitCode: p.exitCode,
2129
+ exitSignal: p.exitSignal,
2130
+ startedAt: new Date(p.startedAt),
2131
+ exitedAt: p.exitedAt ? new Date(p.exitedAt) : void 0
2132
+ }));
2133
+ }
2134
+ async processGet(pid) {
2135
+ await this.ensureRunning();
2136
+ const response = await this.runtimeFetch(`/process/${pid}`, { method: "GET" });
2137
+ if (response.status === 404) return null;
2138
+ if (!response.ok) {
2139
+ const body = await response.text();
2140
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2141
+ }
2142
+ const data = await response.json();
2143
+ return this.createProcessHandle(pid, data.data.command);
2144
+ }
2145
+ createProcessHandle(pid, command) {
2146
+ const self = this;
2147
+ return {
2148
+ pid,
2149
+ command,
2150
+ async status() {
2151
+ const response = await self.runtimeFetch(`/process/${pid}`, { method: "GET" });
2152
+ if (!response.ok) {
2153
+ const body = await response.text();
2154
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2155
+ }
2156
+ const p = (await response.json()).data;
2157
+ return {
2158
+ pid: p.pid,
2159
+ command: p.command,
2160
+ cwd: p.cwd,
2161
+ running: p.running,
2162
+ exitCode: p.exitCode,
2163
+ exitSignal: p.exitSignal,
2164
+ startedAt: new Date(p.startedAt),
2165
+ exitedAt: p.exitedAt ? new Date(p.exitedAt) : void 0
2166
+ };
2167
+ },
2168
+ async wait() {
2169
+ while (true) {
2170
+ const s = await this.status();
2171
+ if (!s.running) return s.exitCode;
2172
+ await new Promise((resolve) => setTimeout(resolve, 500));
2173
+ }
2174
+ },
2175
+ async kill(signal = "SIGTERM") {
2176
+ const response = await self.runtimeFetch(`/process/${pid}`, {
2177
+ method: "DELETE",
2178
+ body: JSON.stringify({ signal })
2179
+ });
2180
+ if (!response.ok && response.status !== 404) {
2181
+ const body = await response.text();
2182
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2183
+ }
2184
+ },
2185
+ async *logs() {
2186
+ const response = await self.runtimeFetch(`/process/${pid}/logs`, { method: "GET" });
2187
+ if (!response.ok) {
2188
+ const body = await response.text();
2189
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2190
+ }
2191
+ yield* self.parseProcessLogStream(response);
2192
+ },
2193
+ async *stdout() {
2194
+ for await (const entry of this.logs()) if (entry.type === "stdout") yield entry.data;
2195
+ },
2196
+ async *stderr() {
2197
+ for await (const entry of this.logs()) if (entry.type === "stderr") yield entry.data;
2198
+ }
2199
+ };
2200
+ }
2201
+ async *parseProcessLogStream(response) {
2202
+ const reader = response.body?.getReader();
2203
+ if (!reader) throw new NetworkError("No response body");
2204
+ const decoder = new TextDecoder();
2205
+ let buffer = "";
2206
+ let currentEvent = "";
2207
+ let currentData = "";
2208
+ try {
2209
+ while (true) {
2210
+ const { done, value } = await reader.read();
2211
+ if (done) break;
2212
+ buffer += decoder.decode(value, { stream: true });
2213
+ const lines = buffer.split("\n");
2214
+ buffer = lines.pop() ?? "";
2215
+ for (const line of lines) if (line.startsWith("event:")) currentEvent = line.slice(6).trim();
2216
+ else if (line.startsWith("data:")) currentData = line.slice(5).trim();
2217
+ else if (line === "" && currentEvent && currentData) {
2218
+ if (currentEvent === "stdout" || currentEvent === "stderr") try {
2219
+ const parsed = JSON.parse(currentData);
2220
+ yield {
2221
+ type: currentEvent,
2222
+ data: parsed.data,
2223
+ timestamp: parsed.timestamp
2224
+ };
2225
+ } catch {}
2226
+ else if (currentEvent === "exit") return;
2227
+ currentEvent = "";
2228
+ currentData = "";
2229
+ }
2230
+ }
2231
+ } finally {
2232
+ reader.releaseLock();
2233
+ }
2234
+ }
2235
+ /**
2236
+ * Network manager for runtime network configuration.
2237
+ *
2238
+ * @example Update network restrictions
2239
+ * ```typescript
2240
+ * // Block all outbound traffic
2241
+ * await box.network.update({ blockOutbound: true });
2242
+ *
2243
+ * // Or switch to allowlist mode
2244
+ * await box.network.update({
2245
+ * allowList: ["192.168.1.0/24", "8.8.8.8/32"]
2246
+ * });
2247
+ * ```
2248
+ *
2249
+ * @example Expose ports dynamically
2250
+ * ```typescript
2251
+ * const url = await box.network.exposePort(8000);
2252
+ * console.log(`Service available at: ${url}`);
2253
+ * ```
2254
+ */
2255
+ get network() {
2256
+ return {
2257
+ update: (config) => this.networkUpdate(config),
2258
+ exposePort: (port) => this.networkExposePort(port),
2259
+ listUrls: () => this.networkListUrls(),
2260
+ getConfig: () => this.networkGetConfig()
2261
+ };
2262
+ }
2263
+ async networkUpdate(config) {
2264
+ if (config.blockOutbound !== void 0 && config.allowList !== void 0) {
2265
+ if (config.blockOutbound && config.allowList.length > 0) throw new Error("blockOutbound and allowList are mutually exclusive");
2266
+ }
2267
+ if (config.allowList) {
2268
+ if (config.allowList.length > 10) throw new Error("allowList cannot exceed 10 entries");
2269
+ for (const cidr of config.allowList) if (!this.isValidCidr(cidr)) throw new Error(`Invalid CIDR format: ${cidr}`);
2270
+ }
2271
+ const response = await this.runtimeFetch("/network", {
2272
+ method: "PATCH",
2273
+ body: JSON.stringify(config)
2274
+ });
2275
+ if (!response.ok) {
2276
+ const body = await response.text();
2277
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2278
+ }
2279
+ }
2280
+ async networkExposePort(port) {
2281
+ if (port < 1 || port > 65535) throw new Error("Port must be between 1 and 65535");
2282
+ const response = await this.runtimeFetch("/network/expose", {
2283
+ method: "POST",
2284
+ body: JSON.stringify({ port })
2285
+ });
2286
+ if (!response.ok) {
2287
+ const body = await response.text();
2288
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2289
+ }
2290
+ return (await response.json()).url;
2291
+ }
2292
+ async networkListUrls() {
2293
+ const response = await this.runtimeFetch("/network/urls");
2294
+ if (!response.ok) {
2295
+ const body = await response.text();
2296
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2297
+ }
2298
+ return (await response.json()).urls ?? {};
2299
+ }
2300
+ async networkGetConfig() {
2301
+ const response = await this.runtimeFetch("/network");
2302
+ if (!response.ok) {
2303
+ const body = await response.text();
2304
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2305
+ }
2306
+ return await response.json();
2307
+ }
2308
+ /**
2309
+ * Validate CIDR notation (IPv4 and IPv6)
2310
+ */
2311
+ isValidCidr(cidr) {
2312
+ const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\/(?:[0-9]|[12]\d|3[0-2])$/;
2313
+ const ipv6Pattern = /^([a-fA-F0-9:]+)\/(\d{1,3})$/;
2314
+ if (ipv4Pattern.test(cidr)) return true;
2315
+ const ipv6Match = cidr.match(ipv6Pattern);
2316
+ if (ipv6Match) {
2317
+ const prefix = Number.parseInt(ipv6Match[2], 10);
2318
+ if (prefix >= 0 && prefix <= 128) {
2319
+ const ipPart = ipv6Match[1];
2320
+ const groups = ipPart.split(":");
2321
+ if (groups.length <= 8 && ipPart.includes(":")) return groups.every((g) => g === "" || /^[a-fA-F0-9]{1,4}$/.test(g));
2322
+ }
2323
+ }
2324
+ return false;
2325
+ }
2326
+ /**
2327
+ * Preview link management.
2328
+ *
2329
+ * Create publicly accessible HTTPS URLs for TCP ports inside the sandbox.
2330
+ *
2331
+ * @example
2332
+ * ```typescript
2333
+ * const link = await box.previewLinks.create(3000);
2334
+ * console.log(link.url);
2335
+ *
2336
+ * const links = await box.previewLinks.list();
2337
+ * await box.previewLinks.remove(link.previewId);
2338
+ * ```
2339
+ */
2340
+ get previewLinks() {
2341
+ return {
2342
+ create: (port, options) => this.previewLinkCreate(port, options),
2343
+ list: () => this.previewLinkList(),
2344
+ remove: (previewId) => this.previewLinkRemove(previewId)
2345
+ };
2346
+ }
2347
+ async previewLinkCreate(port, options) {
2348
+ if (port < 1 || port > 65535) throw new Error("Port must be between 1 and 65535");
2349
+ const response = await this.runtimeFetch("/preview-links", {
2350
+ method: "POST",
2351
+ body: JSON.stringify({
2352
+ port,
2353
+ protocol: options?.protocol ?? "tcp",
2354
+ metadata: options?.metadata
2355
+ })
2356
+ });
2357
+ if (!response.ok) {
2358
+ const body = await response.text();
2359
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2360
+ }
2361
+ return normalizePreviewLink((await response.json()).previewLink);
2362
+ }
2363
+ async previewLinkList() {
2364
+ const response = await this.runtimeFetch("/preview-links");
2365
+ if (!response.ok) {
2366
+ const body = await response.text();
2367
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2368
+ }
2369
+ return ((await response.json()).previewLinks ?? []).map(normalizePreviewLink);
2370
+ }
2371
+ async previewLinkRemove(previewId) {
2372
+ const response = await this.runtimeFetch(`/preview-links/${encodeURIComponent(previewId)}`, { method: "DELETE" });
2373
+ if (!response.ok) {
2374
+ const body = await response.text();
2375
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2376
+ }
2377
+ }
2378
+ /**
2379
+ * Get information about the infrastructure driver for this sandbox.
2380
+ *
2381
+ * @example
2382
+ * ```typescript
2383
+ * const info = await box.getDriverInfo();
2384
+ * console.log(`Driver: ${info.type}, CRIU: ${info.capabilities.criu}`);
2385
+ * ```
2386
+ */
2387
+ async getDriverInfo() {
2388
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/driver`);
2389
+ if (!response.ok) {
2390
+ const body = await response.text();
2391
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2392
+ }
2393
+ return await response.json();
2394
+ }
2395
+ async toolsInstall(tool, version) {
2396
+ await this.ensureRunning();
2397
+ const response = await this.runtimeFetch("/tools/install", {
2398
+ method: "POST",
2399
+ body: JSON.stringify({
2400
+ tool,
2401
+ version
2402
+ })
2403
+ });
2404
+ if (!response.ok) {
2405
+ const body = await response.text();
2406
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2407
+ }
2408
+ }
2409
+ async toolsUse(tool, version) {
2410
+ await this.ensureRunning();
2411
+ const response = await this.runtimeFetch("/tools/use", {
2412
+ method: "POST",
2413
+ body: JSON.stringify({
2414
+ tool,
2415
+ version
2416
+ })
2417
+ });
2418
+ if (!response.ok) {
2419
+ const body = await response.text();
2420
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2421
+ }
2422
+ }
2423
+ async toolsList() {
2424
+ await this.ensureRunning();
2425
+ const response = await this.runtimeFetch("/tools", { method: "GET" });
2426
+ if (!response.ok) {
2427
+ const body = await response.text();
2428
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2429
+ }
2430
+ return (await response.json()).tools ?? [];
2431
+ }
2432
+ async toolsRun(tool, args) {
2433
+ await this.ensureRunning();
2434
+ const response = await this.runtimeFetch("/tools/run", {
2435
+ method: "POST",
2436
+ body: JSON.stringify({
2437
+ tool,
2438
+ args
2439
+ })
2440
+ });
2441
+ if (!response.ok) {
2442
+ const body = await response.text();
2443
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2444
+ }
2445
+ const data = await response.json();
2446
+ return {
2447
+ exitCode: data.exitCode ?? 0,
2448
+ stdout: data.stdout ?? "",
2449
+ stderr: data.stderr ?? ""
2450
+ };
2451
+ }
2452
+ /**
2453
+ * Create a snapshot of the sandbox state.
2454
+ * Snapshots can be used to save workspace state for later restoration.
2455
+ *
2456
+ * If `storage` is provided (BYOS3), the snapshot is created directly
2457
+ * directly on the sandbox and uploaded to customer-provided S3 storage.
2458
+ *
2459
+ * @param options - Snapshot options (tags, paths, storage)
2460
+ * @returns Snapshot result with ID and metadata
2461
+ *
2462
+ * @example Standard snapshot (our storage)
2463
+ * ```typescript
2464
+ * const snap = await box.snapshot({
2465
+ * tags: ["v1.0", "stable"],
2466
+ * });
2467
+ * console.log(`Snapshot: ${snap.snapshotId}`);
2468
+ * ```
2469
+ *
2470
+ * @example BYOS3 snapshot (customer storage)
2471
+ * ```typescript
2472
+ * const snap = await box.snapshot({
2473
+ * tags: ["production"],
2474
+ * storage: {
2475
+ * type: "s3",
2476
+ * bucket: "my-snapshots",
2477
+ * credentials: { accessKeyId: "...", secretAccessKey: "..." },
2478
+ * },
2479
+ * });
2480
+ * ```
2481
+ */
2482
+ async snapshot(options) {
2483
+ await this.ensureRunning();
2484
+ if (options?.storage) {
2485
+ const response = await this.runtimeFetch("/snapshots", {
2486
+ method: "POST",
2487
+ body: JSON.stringify({
2488
+ projectId: this.id,
2489
+ storage: options.storage,
2490
+ tags: options.tags,
2491
+ paths: options.paths
2492
+ })
2493
+ });
2494
+ if (!response.ok) {
2495
+ const body = await response.text();
2496
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2497
+ }
2498
+ const data = await response.json();
2499
+ return {
2500
+ snapshotId: data.snapshot?.id ?? data.snapshotId,
2501
+ createdAt: new Date(data.snapshot?.createdAt ?? data.createdAt ?? Date.now()),
2502
+ sizeBytes: data.snapshot?.sizeBytes ?? data.sizeBytes,
2503
+ tags: data.snapshot?.tags ?? data.tags ?? []
2504
+ };
2505
+ }
2506
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/snapshots`, {
2507
+ method: "POST",
2508
+ body: JSON.stringify({
2509
+ tags: options?.tags,
2510
+ paths: options?.paths
2511
+ })
2512
+ });
2513
+ if (!response.ok) {
2514
+ const body = await response.text();
2515
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2516
+ }
2517
+ const data = await response.json();
2518
+ const snapshotId = data.snapshotId ?? data.id;
2519
+ if (typeof snapshotId !== "string" || snapshotId.length === 0) throw new ServerError(`Snapshot create returned 200 but body had no snapshot ID: ${JSON.stringify(data).slice(0, 200)}`, response.status);
2520
+ const snapshot = {
2521
+ snapshotId,
2522
+ createdAt: new Date(data.createdAt ?? Date.now()),
2523
+ sizeBytes: data.sizeBytes,
2524
+ tags: data.tags ?? []
2525
+ };
2526
+ await this.waitForSnapshotVisible(snapshot.snapshotId, {
2527
+ createdAt: snapshot.createdAt,
2528
+ sizeBytes: snapshot.sizeBytes,
2529
+ tags: snapshot.tags
2530
+ });
2531
+ return snapshot;
2532
+ }
2533
+ /**
2534
+ * List all snapshots for this sandbox.
2535
+ *
2536
+ * If `storage` is provided (BYOS3), lists snapshots from customer-provided
2537
+ * S3 storage.
2538
+ *
2539
+ * @param storage - Optional customer storage config for BYOS3
2540
+ * @returns Array of snapshot metadata
2541
+ *
2542
+ * @example List from our storage
2543
+ * ```typescript
2544
+ * const snapshots = await box.listSnapshots();
2545
+ * for (const snap of snapshots) {
2546
+ * console.log(`${snap.snapshotId}: ${snap.createdAt}`);
2547
+ * }
2548
+ * ```
2549
+ *
2550
+ * @example List from customer S3 (BYOS3)
2551
+ * ```typescript
2552
+ * const snapshots = await box.listSnapshots({
2553
+ * type: "s3",
2554
+ * bucket: "my-snapshots",
2555
+ * credentials: { accessKeyId: "...", secretAccessKey: "..." },
2556
+ * });
2557
+ * ```
2558
+ */
2559
+ async listSnapshots(storage) {
2560
+ if (storage) {
2561
+ await this.ensureRunning();
2562
+ const response = await this.runtimeFetch("/snapshots/list", {
2563
+ method: "POST",
2564
+ body: JSON.stringify({
2565
+ projectId: this.id,
2566
+ storage
2567
+ })
2568
+ });
2569
+ if (!response.ok) {
2570
+ const body = await response.text();
2571
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2572
+ }
2573
+ return ((await response.json()).snapshots ?? []).map((s) => ({
2574
+ snapshotId: s.id ?? s.snapshotId,
2575
+ sandboxId: this.id,
2576
+ createdAt: new Date(s.createdAt),
2577
+ tags: s.tags ?? [],
2578
+ paths: [],
2579
+ sizeBytes: s.sizeBytes
2580
+ }));
2581
+ }
2582
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/snapshots`);
2583
+ if (!response.ok) {
2584
+ const body = await response.text();
2585
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2586
+ }
2587
+ const data = await response.json();
2588
+ return (Array.isArray(data) ? data : data.snapshots ?? []).map((s) => ({
2589
+ snapshotId: s.snapshotId ?? s.id,
2590
+ sandboxId: s.sandboxId ?? s.projectRef ?? this.id,
2591
+ createdAt: new Date(s.createdAt),
2592
+ tags: s.tags ?? [],
2593
+ paths: s.paths ?? [],
2594
+ sizeBytes: s.sizeBytes
2595
+ }));
2596
+ }
2597
+ async revertToSnapshot(snapshotId) {
2598
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/snapshots/${encodeURIComponent(snapshotId)}/restore`, { method: "POST" });
2599
+ if (!response.ok) {
2600
+ const body = await response.text();
2601
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2602
+ }
2603
+ const data = await response.json();
2604
+ if (response.status === 202) return this.waitForSnapshotRestore(snapshotId, data);
2605
+ return toSnapshotResult(data);
2606
+ }
2607
+ async waitForSnapshotRestore(snapshotId, accepted) {
2608
+ const deadline = Date.now() + SNAPSHOT_RESTORE_TIMEOUT_MS;
2609
+ while (Date.now() < deadline) {
2610
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/snapshots/jobs/${encodeURIComponent(accepted.jobId)}`);
2611
+ if (!response.ok) {
2612
+ const body = await response.text();
2613
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2614
+ }
2615
+ const status = await response.json();
2616
+ if (status.status === "failed" || status.result?.success === false) throw new ServerError(status.result?.error ?? `Snapshot restore job ${accepted.jobId} failed`);
2617
+ if (status.status === "completed") return await this.waitForSnapshotVisible(snapshotId, status.completedAt ? { createdAt: new Date(status.completedAt) } : void 0);
2618
+ await this.sleep(SNAPSHOT_RESTORE_POLL_INTERVAL_MS);
2619
+ }
2620
+ throw new TimeoutError(SNAPSHOT_RESTORE_TIMEOUT_MS);
2621
+ }
2622
+ async waitForSnapshotVisible(snapshotId, fallback) {
2623
+ const deadline = Date.now() + SNAPSHOT_VISIBILITY_TIMEOUT_MS;
2624
+ let lastError;
2625
+ while (Date.now() < deadline) {
2626
+ try {
2627
+ const snapshot = (await this.listSnapshots()).find((item) => item.snapshotId === snapshotId);
2628
+ if (snapshot) return {
2629
+ snapshotId: snapshot.snapshotId,
2630
+ createdAt: snapshot.createdAt,
2631
+ sizeBytes: snapshot.sizeBytes,
2632
+ tags: snapshot.tags
2633
+ };
2634
+ lastError = /* @__PURE__ */ new Error(`Snapshot ${snapshotId} not yet visible in list`);
2635
+ } catch (error) {
2636
+ if (!isTransientSnapshotVisibilityError(error)) throw error;
2637
+ lastError = error;
2638
+ }
2639
+ await this.sleep(SNAPSHOT_VISIBILITY_POLL_INTERVAL_MS);
2640
+ }
2641
+ if (fallback) return {
2642
+ snapshotId,
2643
+ createdAt: fallback.createdAt ?? /* @__PURE__ */ new Date(),
2644
+ sizeBytes: fallback.sizeBytes,
2645
+ tags: fallback.tags ?? []
2646
+ };
2647
+ throw lastError instanceof Error ? lastError : new TimeoutError(SNAPSHOT_VISIBILITY_TIMEOUT_MS);
2648
+ }
2649
+ async deleteSnapshot(snapshotId) {
2650
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/snapshots/${encodeURIComponent(snapshotId)}`, { method: "DELETE" });
2651
+ if (!response.ok) {
2652
+ const body = await response.text();
2653
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2654
+ }
2655
+ }
2656
+ /**
2657
+ * Restore from the latest snapshot in customer-provided storage.
2658
+ * Only available when using BYOS3 (calls the runtime directly).
2659
+ *
2660
+ * @param storage - Customer storage config (required)
2661
+ * @param destinationPath - Optional path to restore to
2662
+ * @returns Snapshot info if restored, null if no snapshot found
2663
+ *
2664
+ * @example Restore from customer S3
2665
+ * ```typescript
2666
+ * const result = await box.restoreFromStorage({
2667
+ * type: "s3",
2668
+ * bucket: "my-snapshots",
2669
+ * credentials: { accessKeyId: "...", secretAccessKey: "..." },
2670
+ * });
2671
+ *
2672
+ * if (result) {
2673
+ * console.log(`Restored from ${result.snapshotId}`);
2674
+ * } else {
2675
+ * console.log("No snapshot found");
2676
+ * }
2677
+ * ```
2678
+ */
2679
+ async restoreFromStorage(storage, options) {
2680
+ if (!storage) throw new Error("Storage config is required for restoreFromStorage");
2681
+ await this.ensureRunning();
2682
+ const response = await this.runtimeFetch("/snapshots/restore", {
2683
+ method: "POST",
2684
+ body: JSON.stringify({
2685
+ projectId: this.id,
2686
+ storage,
2687
+ destinationPath: options?.destinationPath,
2688
+ snapshotId: options?.snapshotId
2689
+ })
2690
+ });
2691
+ if (response.status === 404) return null;
2692
+ if (!response.ok) {
2693
+ const body = await response.text();
2694
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2695
+ }
2696
+ const data = await response.json();
2697
+ if (!data.snapshot) return null;
2698
+ return {
2699
+ snapshotId: data.snapshot.id,
2700
+ createdAt: new Date(data.snapshot.createdAt),
2701
+ sizeBytes: data.snapshot.sizeBytes,
2702
+ tags: data.snapshot.tags ?? []
2703
+ };
2704
+ }
2705
+ /**
2706
+ * Create a CRIU checkpoint of the sandbox's memory state.
2707
+ *
2708
+ * Checkpoints capture the complete memory state of the running sandbox,
2709
+ * enabling true pause/resume and fork operations. Unlike snapshots which
2710
+ * only preserve filesystem state, checkpoints preserve process memory,
2711
+ * open file descriptors, and execution state.
2712
+ *
2713
+ * **Requirements:** CRIU must be available on the host. Check availability
2714
+ * with `client.criuStatus()` before calling.
2715
+ *
2716
+ * **Note:** By default, checkpoint stops the sandbox. Use `leaveRunning: true`
2717
+ * to keep it running (creates a copy-on-write checkpoint).
2718
+ *
2719
+ * @param options - Checkpoint options
2720
+ * @returns Checkpoint result with ID and metadata
2721
+ *
2722
+ * @example Basic checkpoint (stops sandbox)
2723
+ * ```typescript
2724
+ * const checkpoint = await box.checkpoint();
2725
+ * console.log(`Checkpoint: ${checkpoint.checkpointId}`);
2726
+ * // Sandbox is now stopped, resume with box.resume()
2727
+ * ```
2728
+ *
2729
+ * @example Checkpoint without stopping
2730
+ * ```typescript
2731
+ * const checkpoint = await box.checkpoint({
2732
+ * tags: ["before-deploy"],
2733
+ * leaveRunning: true,
2734
+ * });
2735
+ * // Sandbox continues running
2736
+ * ```
2737
+ */
2738
+ async checkpoint(options) {
2739
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints`, {
2740
+ method: "POST",
2741
+ body: JSON.stringify({
2742
+ tags: options?.tags,
2743
+ leaveRunning: options?.leaveRunning,
2744
+ includeSnapshot: options?.includeSnapshot
2745
+ })
2746
+ });
2747
+ if (!response.ok) {
2748
+ const body = await response.text();
2749
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2750
+ }
2751
+ const data = await response.json();
2752
+ return {
2753
+ checkpointId: data.checkpointId ?? data.id,
2754
+ createdAt: new Date(data.createdAt ?? ""),
2755
+ sizeBytes: data.sizeBytes,
2756
+ tags: data.tags ?? []
2757
+ };
2758
+ }
2759
+ /**
2760
+ * List all checkpoints for this sandbox.
2761
+ *
2762
+ * @returns Array of checkpoint metadata
2763
+ *
2764
+ * @example
2765
+ * ```typescript
2766
+ * const checkpoints = await box.listCheckpoints();
2767
+ * for (const cp of checkpoints) {
2768
+ * console.log(`${cp.checkpointId}: ${cp.createdAt}`);
2769
+ * }
2770
+ * ```
2771
+ */
2772
+ async listCheckpoints() {
2773
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints`);
2774
+ if (!response.ok) {
2775
+ const body = await response.text();
2776
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2777
+ }
2778
+ const data = await response.json();
2779
+ return (Array.isArray(data) ? data : data.checkpoints ?? []).map((cp) => this.parseCheckpointInfo(cp));
2780
+ }
2781
+ /**
2782
+ * Delete a checkpoint.
2783
+ *
2784
+ * @param checkpointId - ID of the checkpoint to delete
2785
+ */
2786
+ async deleteCheckpoint(checkpointId) {
2787
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/checkpoints/${encodeURIComponent(checkpointId)}`, { method: "DELETE" });
2788
+ if (!response.ok) {
2789
+ const body = await response.text();
2790
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2791
+ }
2792
+ }
2793
+ /**
2794
+ * Fork a new sandbox from a checkpoint.
2795
+ *
2796
+ * Creates a new sandbox with the same memory state as this sandbox
2797
+ * at the time of the checkpoint. The fork has a new identity but
2798
+ * preserves the execution state.
2799
+ *
2800
+ * **Use cases:**
2801
+ * - Branch workflows: Create parallel execution paths
2802
+ * - A/B testing: Run same state with different configurations
2803
+ * - Debugging: Fork at a specific point to investigate
2804
+ *
2805
+ * @param checkpointId - ID of the checkpoint to fork from
2806
+ * @param options - Fork configuration
2807
+ * @returns The new sandbox instance
2808
+ *
2809
+ * @example Basic fork
2810
+ * ```typescript
2811
+ * const checkpoint = await box.checkpoint({ leaveRunning: true });
2812
+ * const forked = await box.fork(checkpoint.checkpointId);
2813
+ * // forked has same memory state as box at checkpoint time
2814
+ * ```
2815
+ *
2816
+ * @example Fork with custom config
2817
+ * ```typescript
2818
+ * const forked = await box.fork(checkpointId, {
2819
+ * name: "experiment-branch",
2820
+ * env: { EXPERIMENT: "true" },
2821
+ * });
2822
+ * ```
2823
+ */
2824
+ async fork(checkpointId, options) {
2825
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/fork`, {
2826
+ method: "POST",
2827
+ body: JSON.stringify({
2828
+ checkpointId,
2829
+ name: options?.name,
2830
+ env: options?.env,
2831
+ resources: options?.resources,
2832
+ metadata: options?.metadata
2833
+ })
2834
+ });
2835
+ if (!response.ok) {
2836
+ const body = await response.text();
2837
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2838
+ }
2839
+ const data = await response.json();
2840
+ return new SandboxInstance(this.client, this.parseInfo(data.sandbox ?? data), this.defaultRuntimeBackend);
2841
+ }
2842
+ /**
2843
+ * Stop the sandbox (keeps state for resume).
2844
+ */
2845
+ async stop() {
2846
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/stop`, { method: "POST" });
2847
+ if (!response.ok) {
2848
+ const body = await response.text();
2849
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2850
+ }
2851
+ await this.refresh();
2852
+ }
2853
+ /**
2854
+ * Resume a stopped sandbox.
2855
+ */
2856
+ async resume() {
2857
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}/resume`, { method: "POST" });
2858
+ if (!response.ok) {
2859
+ const body = await response.text();
2860
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2861
+ }
2862
+ await this.refresh();
2863
+ }
2864
+ /**
2865
+ * Delete the sandbox permanently.
2866
+ */
2867
+ async delete() {
2868
+ const response = await this.client.fetch(`/v1/sandboxes/${this.id}`, { method: "DELETE" });
2869
+ if (!response.ok) {
2870
+ const body = await response.text();
2871
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
2872
+ }
2873
+ }
2874
+ /**
2875
+ * keepAlive is intentionally unavailable until the API exposes timeout updates.
2876
+ * @param seconds - Reserved for future support
2877
+ */
2878
+ async keepAlive(_seconds = 3600) {
2879
+ throw new Error("Sandbox keepAlive() is not supported by the current API. Set idleTimeoutSeconds when creating the sandbox instead.");
2880
+ }
2881
+ /**
2882
+ * Upload a local directory to the sandbox via tar.
2883
+ * @param localPath - Local directory path to upload
2884
+ * @param remotePath - Destination path in the sandbox (default: /home/user)
2885
+ */
2886
+ async uploadDirectory(localPath, remotePath = "/home/user") {
2887
+ const { stat } = await import("node:fs/promises");
2888
+ const { resolve } = await import("node:path");
2889
+ const { spawn } = await import("node:child_process");
2890
+ const absPath = resolve(localPath);
2891
+ if (!(await stat(absPath).catch(() => null))?.isDirectory()) throw new Error(`Local path is not a directory: ${localPath}`);
2892
+ if (!remotePath || remotePath.trim() === "") throw new Error("Remote path cannot be empty");
2893
+ const tarBase64 = (await new Promise((resolveTar, rejectTar) => {
2894
+ const tar = spawn("tar", [
2895
+ "-cf",
2896
+ "-",
2897
+ "--exclude=node_modules",
2898
+ "--exclude=.git",
2899
+ "-C",
2900
+ absPath,
2901
+ "."
2902
+ ], { stdio: [
2903
+ "ignore",
2904
+ "pipe",
2905
+ "pipe"
2906
+ ] });
2907
+ const chunks = [];
2908
+ const stderrChunks = [];
2909
+ let totalSize = 0;
2910
+ let settled = false;
2911
+ const maxTarBytes = 100 * 1024 * 1024;
2912
+ const rejectOnce = (error) => {
2913
+ if (settled) return;
2914
+ settled = true;
2915
+ rejectTar(error);
2916
+ };
2917
+ tar.stdout.on("data", (chunk) => {
2918
+ totalSize += chunk.length;
2919
+ if (totalSize > maxTarBytes) {
2920
+ tar.kill("SIGKILL");
2921
+ rejectOnce(/* @__PURE__ */ new Error("uploadDirectory archive exceeds 100MB limit"));
2922
+ return;
2923
+ }
2924
+ chunks.push(chunk);
2925
+ });
2926
+ tar.stderr.on("data", (chunk) => {
2927
+ stderrChunks.push(chunk);
2928
+ });
2929
+ tar.on("error", (error) => {
2930
+ rejectOnce(/* @__PURE__ */ new Error(`Failed to create tar archive: ${error.message}`));
2931
+ });
2932
+ tar.on("close", (code) => {
2933
+ if (settled) return;
2934
+ if (code !== 0) {
2935
+ const stderr = Buffer.concat(stderrChunks).toString().trim();
2936
+ rejectOnce(/* @__PURE__ */ new Error(`tar failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`));
2937
+ return;
2938
+ }
2939
+ settled = true;
2940
+ resolveTar(Buffer.concat(chunks));
2941
+ });
2942
+ })).toString("base64");
2943
+ const quotedRemotePath = quoteForShell(remotePath);
2944
+ await this.exec(`mkdir -p ${quotedRemotePath}`);
2945
+ await this.exec(`printf %s ${quoteForShell(tarBase64)} | base64 -d | tar -xf - -C ${quotedRemotePath}`);
2946
+ }
2947
+ /**
2948
+ * Wait for the sandbox to reach a specific status.
2949
+ *
2950
+ * When `onProgress` is provided and the sandbox is still provisioning,
2951
+ * uses SSE events for real-time progress instead of polling. Falls back
2952
+ * to polling if the SSE connection fails or is unavailable.
2953
+ *
2954
+ * @example Basic wait
2955
+ * ```typescript
2956
+ * await box.waitFor("running");
2957
+ * ```
2958
+ *
2959
+ * @example Wait with progress tracking
2960
+ * ```typescript
2961
+ * await box.waitFor("running", {
2962
+ * onProgress: (event) => {
2963
+ * console.log(`[${event.step}] ${event.status} — ${event.message}`);
2964
+ * if (event.percent !== undefined) {
2965
+ * updateProgressBar(event.percent);
2966
+ * }
2967
+ * },
2968
+ * });
2969
+ * ```
2970
+ */
2971
+ async waitFor(status, options) {
2972
+ const statuses = Array.isArray(status) ? status : [status];
2973
+ const timeoutMs = options?.timeoutMs ?? 12e4;
2974
+ const pollIntervalMs = options?.pollIntervalMs ?? 2e3;
2975
+ if (options?.onProgress && (this.status === "provisioning" || this.status === "pending")) try {
2976
+ await this.waitForWithSSE(statuses, timeoutMs, options.onProgress, options.signal);
2977
+ return;
2978
+ } catch (err) {
2979
+ if (err instanceof StateError || err instanceof TimeoutError) throw err;
2980
+ }
2981
+ const startTime = Date.now();
2982
+ while (true) {
2983
+ if (options?.signal?.aborted) throw new TimeoutError(0, "Aborted");
2984
+ await this.refresh();
2985
+ if (statuses.includes(this.status)) return;
2986
+ if (this.status === "failed") throw new StateError(this.error ?? "Sandbox failed", this.status);
2987
+ if (Date.now() - startTime > timeoutMs) throw new TimeoutError(timeoutMs, `Timed out waiting for sandbox to reach ${statuses.join(" or ")}`);
2988
+ await this.sleep(pollIntervalMs);
2989
+ }
2990
+ }
2991
+ /**
2992
+ * SSE-based wait implementation. Subscribes to provisioning events and
2993
+ * delivers progress callbacks while waiting for a target status.
2994
+ */
2995
+ async waitForWithSSE(statuses, timeoutMs, onProgress, signal) {
2996
+ const abortController = new AbortController();
2997
+ const timeout = setTimeout(() => abortController.abort(), timeoutMs);
2998
+ if (signal) {
2999
+ if (signal.aborted) {
3000
+ clearTimeout(timeout);
3001
+ throw new TimeoutError(0, "Aborted");
3002
+ }
3003
+ signal.addEventListener("abort", () => abortController.abort(), { once: true });
3004
+ }
3005
+ try {
3006
+ for await (const event of this.events({
3007
+ signal: abortController.signal,
3008
+ eventTypes: [
3009
+ "provision_progress",
3010
+ "provision_complete",
3011
+ "provision_failed",
3012
+ "status_change"
3013
+ ]
3014
+ })) if (event.type === "provision_progress") {
3015
+ const d = event.data;
3016
+ onProgress({
3017
+ step: normalizeProvisionStep(d.step),
3018
+ status: d.status,
3019
+ message: d.message,
3020
+ percent: d.progress,
3021
+ detail: d.detail,
3022
+ timestamp: d.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
3023
+ });
3024
+ } else if (event.type === "provision_failed") {
3025
+ await this.refresh();
3026
+ throw new StateError(event.data.error ?? "Provisioning failed", this.status);
3027
+ } else if (event.type === "provision_complete" || event.type === "status_change") {
3028
+ await this.refresh();
3029
+ if (statuses.includes(this.status)) return;
3030
+ if (this.status === "failed") throw new StateError(this.error ?? "Sandbox failed", this.status);
3031
+ }
3032
+ await this.refresh();
3033
+ if (statuses.includes(this.status)) return;
3034
+ if (this.status === "failed") throw new StateError(this.error ?? "Sandbox failed", this.status);
3035
+ throw new TimeoutError(timeoutMs, `SSE stream ended before sandbox reached ${statuses.join(" or ")}`);
3036
+ } finally {
3037
+ clearTimeout(timeout);
3038
+ abortController.abort();
3039
+ }
3040
+ }
3041
+ parseCheckpointInfo(cp) {
3042
+ return {
3043
+ checkpointId: cp.checkpointId ?? cp.id,
3044
+ sandboxId: cp.sandboxId ?? cp.projectRef ?? this.id,
3045
+ createdAt: new Date(cp.createdAt),
3046
+ tags: cp.tags ?? [],
3047
+ sizeBytes: cp.sizeBytes,
3048
+ hasMemoryState: cp.hasMemoryState ?? true,
3049
+ hasFilesystemSnapshot: cp.hasFilesystemSnapshot ?? false
3050
+ };
3051
+ }
3052
+ async ensureRunning() {
3053
+ await this.refresh();
3054
+ if (this.status !== "running") throw new StateError(`Sandbox is not running (status: ${this.status})`, this.status, "running");
3055
+ }
3056
+ async runtimeFetch(path, options) {
3057
+ const url = `/v1/sandboxes/${encodeURIComponent(this.id)}/runtime${path}`;
3058
+ try {
3059
+ return options ? await this.client.fetch(url, options) : await this.client.fetch(url);
3060
+ } catch (err) {
3061
+ throw new NetworkError(`Failed to connect to sandbox: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : void 0, {
3062
+ endpoint: path,
3063
+ origin: "runtime"
3064
+ });
3065
+ }
3066
+ }
3067
+ /**
3068
+ * Delegates to the shared `parseSSEStream` in `lib/sse-parser.ts`
3069
+ * — one canonical SSE implementation for the whole SDK. The
3070
+ * shared parser throws `AbortError` on cancellation (so consumers
3071
+ * can distinguish cancel from clean EOF), but agent/task streaming
3072
+ * callers of this method historically relied on a silent-return
3073
+ * abort: they check `options?.signal?.aborted` AFTER the loop and
3074
+ * decide whether to reconnect. The try/catch below preserves that
3075
+ * legacy contract by swallowing AbortError at the delegate layer.
3076
+ */
3077
+ async *parseSSEStream(response, signal) {
3078
+ if (!response.body) throw new NetworkError("No response body");
3079
+ try {
3080
+ for await (const event of parseSSEStream(response.body, { signal })) yield event;
3081
+ } catch (err) {
3082
+ if (err instanceof Error && err.name === "AbortError") return;
3083
+ throw err;
3084
+ }
3085
+ }
3086
+ /**
3087
+ * Associate a session ID with a user ID so the Sandbox API can route
3088
+ * subsequent WebSocket connections to this sandbox.
3089
+ *
3090
+ * @param opts - Session and user identifiers
3091
+ *
3092
+ * @example
3093
+ * ```typescript
3094
+ * await box.registerSessionMapping({
3095
+ * sessionId: "sess_abc",
3096
+ * userId: "user_123",
3097
+ * });
3098
+ * ```
3099
+ */
3100
+ async registerSessionMapping(opts) {
3101
+ const response = await this.client.fetch(`/v1/session/${encodeURIComponent(opts.sessionId)}/mapping`, {
3102
+ method: "PUT",
3103
+ body: JSON.stringify({
3104
+ userId: opts.userId,
3105
+ sandboxId: this.id
3106
+ })
3107
+ });
3108
+ if (!response.ok) {
3109
+ const body = await response.text();
3110
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3111
+ }
3112
+ }
3113
+ parseInfo(data) {
3114
+ return {
3115
+ id: data.id ?? this.id,
3116
+ name: data.name,
3117
+ status: data.status ?? "pending",
3118
+ connection: normalizeConnection(data.connection) ?? this.info.connection,
3119
+ metadata: data.metadata,
3120
+ createdAt: data.createdAt ? new Date(data.createdAt) : this.info.createdAt,
3121
+ startedAt: data.startedAt ? new Date(data.startedAt) : void 0,
3122
+ lastActivityAt: data.lastActivityAt ? new Date(data.lastActivityAt) : void 0,
3123
+ expiresAt: data.expiresAt ? new Date(data.expiresAt) : void 0,
3124
+ error: data.error
3125
+ };
3126
+ }
3127
+ sleep(ms) {
3128
+ return new Promise((resolve) => setTimeout(resolve, ms));
3129
+ }
3130
+ /**
3131
+ * Get a session reference bound to this sandbox. Lazy: does not hit the
3132
+ * network until you call a method on the returned `SandboxSession`.
3133
+ * Use {@link sessions} to discover existing session ids.
3134
+ */
3135
+ session(id) {
3136
+ return new SandboxSession(this, id);
3137
+ }
3138
+ /**
3139
+ * List sessions on this sandbox, optionally filtering by status. Returns
3140
+ * `SandboxSession` instances paired with their last-known
3141
+ * {@link SessionInfo} so callers can avoid an extra round-trip per
3142
+ * session for status.
3143
+ */
3144
+ async sessions(opts) {
3145
+ await this.ensureRunning();
3146
+ const search = new URLSearchParams();
3147
+ if (opts?.backend) search.set("backend", opts.backend);
3148
+ const qs = search.toString();
3149
+ const response = await this.runtimeFetch(`/agents/sessions${qs ? `?${qs}` : ""}`);
3150
+ if (!response.ok) {
3151
+ const body = await response.text();
3152
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3153
+ }
3154
+ const list = await response.json();
3155
+ const out = [];
3156
+ for (const raw of list) {
3157
+ const info = normalizeSessionInfo(raw);
3158
+ if (!info) continue;
3159
+ if (opts?.status && info.status !== opts.status) continue;
3160
+ out.push({
3161
+ session: this.session(info.id),
3162
+ info
3163
+ });
3164
+ }
3165
+ return out;
3166
+ }
3167
+ /**
3168
+ * Dispatch a prompt and return immediately with the session id (Issue
3169
+ * #913 Gap 2). The sandbox keeps running the prompt after this call
3170
+ * returns; reconnect via `box.session(id).events()` or wait for
3171
+ * completion with `box.session(id).result()`.
3172
+ *
3173
+ * Idempotent on `opts.sessionId`: re-dispatching with the same id when
3174
+ * the session is already running is a lookup, not a re-create. This
3175
+ * lets queue retries and reconnect-after-Worker-restart be safe by
3176
+ * construction.
3177
+ */
3178
+ async dispatchPrompt(message, opts) {
3179
+ await this.ensureRunning();
3180
+ if (opts?.sessionId) {
3181
+ const existing = await this._sessionStatus(opts.sessionId);
3182
+ if (existing) return {
3183
+ sessionId: existing.id,
3184
+ status: existing.status,
3185
+ alreadyExisted: true
3186
+ };
3187
+ }
3188
+ const ctrl = new AbortController();
3189
+ const stream = this.streamPrompt(message, {
3190
+ ...opts,
3191
+ signal: ctrl.signal
3192
+ });
3193
+ let resolvedSessionId = opts?.sessionId;
3194
+ try {
3195
+ for await (const event of stream) {
3196
+ const id = event.data?.sessionId;
3197
+ if (typeof id === "string" && id.length > 0) {
3198
+ resolvedSessionId = id;
3199
+ break;
3200
+ }
3201
+ }
3202
+ } finally {
3203
+ ctrl.abort();
3204
+ }
3205
+ if (!resolvedSessionId) throw new ServerError("dispatchPrompt did not receive a session id from the sandbox");
3206
+ return {
3207
+ sessionId: resolvedSessionId,
3208
+ status: "running",
3209
+ alreadyExisted: false
3210
+ };
3211
+ }
3212
+ /**
3213
+ * Mint a scoped, time-bounded JWT for direct browser access to this
3214
+ * sandbox (Issue #913 Gap 1). Authority is the caller's
3215
+ * `TANGLE_API_KEY` (sk-tan-*) — the Sandbox API mints the token;
3216
+ * signing secrets stay server-side.
3217
+ *
3218
+ * Use this to give a browser direct read access to the sandbox without
3219
+ * leaking the full bearer (`box.connection.authToken`). The returned
3220
+ * token verifies against the same sidecar middleware that already
3221
+ * gates ProductTokenIssuer-issued JWTs — no new sidecar surface.
3222
+ */
3223
+ async mintScopedToken(opts) {
3224
+ const response = await this.client.fetch(`/v1/sandboxes/${encodeURIComponent(this.id)}/scoped-token`, {
3225
+ method: "POST",
3226
+ headers: { "Content-Type": "application/json" },
3227
+ body: JSON.stringify({
3228
+ scope: opts.scope,
3229
+ ...opts.sessionId ? { sessionId: opts.sessionId } : {},
3230
+ ...opts.ttlMinutes ? { ttlMinutes: opts.ttlMinutes } : {}
3231
+ })
3232
+ });
3233
+ if (!response.ok) {
3234
+ const body = await response.text();
3235
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3236
+ }
3237
+ const data = await response.json();
3238
+ return {
3239
+ token: data.token,
3240
+ expiresAt: /* @__PURE__ */ new Date(data.expiresAt * 1e3),
3241
+ scope: data.scope
3242
+ };
3243
+ }
3244
+ /** @internal — invoked by SandboxSession.status(). */
3245
+ async _sessionStatus(id) {
3246
+ await this.ensureRunning();
3247
+ const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}`);
3248
+ if (response.status === 404) return null;
3249
+ if (!response.ok) {
3250
+ const body = await response.text();
3251
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3252
+ }
3253
+ return normalizeSessionInfo(await response.json());
3254
+ }
3255
+ /** @internal — invoked by SandboxSession.events(). */
3256
+ async *_sessionEvents(id, opts) {
3257
+ await this.ensureRunning();
3258
+ const search = new URLSearchParams();
3259
+ search.set("sessionId", id);
3260
+ if (opts?.since) search.set("since", opts.since);
3261
+ const response = await this.runtimeFetch(`/agents/events/?${search.toString()}`, { signal: opts?.signal });
3262
+ if (!response.ok) {
3263
+ const body = await response.text();
3264
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3265
+ }
3266
+ yield* this.parseSSEStream(response, opts?.signal);
3267
+ }
3268
+ /** @internal — invoked by SandboxSession.result(). */
3269
+ async _sessionResult(id) {
3270
+ const startTime = Date.now();
3271
+ let response;
3272
+ let error;
3273
+ let traceId;
3274
+ let usage;
3275
+ for await (const event of this._sessionEvents(id)) {
3276
+ const text = getSandboxEventText(event);
3277
+ if (text !== void 0) response = text;
3278
+ if (event.type === "result") {
3279
+ const tu = event.data.tokenUsage;
3280
+ if (tu) usage = {
3281
+ inputTokens: tu.inputTokens ?? 0,
3282
+ outputTokens: tu.outputTokens ?? 0
3283
+ };
3284
+ }
3285
+ if (event.type === "trace.id") traceId = event.data.traceId;
3286
+ if (event.type === "error") error = event.data.message;
3287
+ if (event.type === "done" || event.type === "result") break;
3288
+ }
3289
+ return {
3290
+ success: !error,
3291
+ response,
3292
+ error,
3293
+ traceId,
3294
+ durationMs: Date.now() - startTime,
3295
+ usage
3296
+ };
3297
+ }
3298
+ /** @internal — invoked by SandboxSession.cancel(). */
3299
+ async _sessionCancel(id) {
3300
+ await this.ensureRunning();
3301
+ const response = await this.runtimeFetch(`/agents/sessions/${encodeURIComponent(id)}/abort`, { method: "POST" });
3302
+ if (!response.ok && response.status !== 404) {
3303
+ const body = await response.text();
3304
+ throw parseErrorResponse(response.status, body, void 0, response.headers);
3305
+ }
3306
+ }
3307
+ };
3308
+ function normalizeSessionInfo(raw) {
3309
+ const id = raw.id;
3310
+ if (typeof id !== "string") return null;
3311
+ const status = raw.status || "running";
3312
+ return {
3313
+ id,
3314
+ status: [
3315
+ "queued",
3316
+ "running",
3317
+ "completed",
3318
+ "failed",
3319
+ "cancelled"
3320
+ ].includes(status) ? status : "running",
3321
+ backend: typeof raw.backend === "string" ? raw.backend : void 0,
3322
+ model: typeof raw.model === "string" ? raw.model : void 0,
3323
+ promptCount: typeof raw.promptCount === "number" ? raw.promptCount : void 0,
3324
+ createdAt: raw.createdAt ? new Date(raw.createdAt) : void 0,
3325
+ startedAt: raw.startedAt ? new Date(raw.startedAt) : void 0,
3326
+ endedAt: raw.endedAt ? new Date(raw.endedAt) : void 0,
3327
+ raw
3328
+ };
3329
+ }
3330
+ var DirectRuntimeHttpClient = class {
3331
+ baseClient;
3332
+ sandboxId;
3333
+ getConnection;
3334
+ onUnauthorized;
3335
+ constructor(baseClient, sandboxId, getConnection, onUnauthorized) {
3336
+ this.baseClient = baseClient;
3337
+ this.sandboxId = sandboxId;
3338
+ this.getConnection = getConnection;
3339
+ this.onUnauthorized = onUnauthorized;
3340
+ }
3341
+ async fetch(path, options) {
3342
+ const runtimePrefix = `/v1/sandboxes/${encodeURIComponent(this.sandboxId)}/runtime`;
3343
+ if (!path.startsWith(runtimePrefix)) return this.baseClient.fetch(path, options);
3344
+ const runtimePath = path.slice(runtimePrefix.length);
3345
+ if (runtimePath === "/network" || runtimePath.startsWith("/network/") || runtimePath === "/preview-links" || runtimePath.startsWith("/preview-links/")) return this.baseClient.fetch(path, options);
3346
+ const connection = this.getConnection();
3347
+ const runtimeUrl = connection?.runtimeUrl;
3348
+ if (!runtimeUrl) throw new NetworkError("Sandbox has no direct runtime URL");
3349
+ const targetUrl = `${runtimeUrl.replace(/\/$/, "")}${runtimePath}`;
3350
+ const headers = new Headers(options?.headers);
3351
+ if (connection?.authToken) headers.set("Authorization", `Bearer ${connection.authToken}`);
3352
+ const isFormDataBody = typeof FormData !== "undefined" && options?.body instanceof FormData;
3353
+ if (!headers.has("Content-Type") && !isFormDataBody) headers.set("Content-Type", "application/json");
3354
+ const init = {
3355
+ ...options,
3356
+ headers
3357
+ };
3358
+ if (options?.body && typeof options.body === "object" && !isFormDataBody) init.duplex = "half";
3359
+ let response = await fetch(targetUrl, init);
3360
+ if (response.status === 401 && this.onUnauthorized && !headers.has("x-no-retry")) {
3361
+ await response.arrayBuffer().catch(() => void 0);
3362
+ try {
3363
+ await this.onUnauthorized();
3364
+ } catch {
3365
+ return new Response("", {
3366
+ status: 401,
3367
+ statusText: response.statusText
3368
+ });
3369
+ }
3370
+ const refreshedConnection = this.getConnection();
3371
+ if (refreshedConnection?.authToken) {
3372
+ headers.set("Authorization", `Bearer ${refreshedConnection.authToken}`);
3373
+ response = await fetch(targetUrl, {
3374
+ ...init,
3375
+ headers
3376
+ });
3377
+ }
3378
+ }
3379
+ const responseHeaders = new Headers(response.headers);
3380
+ responseHeaders.set("x-tangle-request-path", path);
3381
+ if (options?.method) responseHeaders.set("x-tangle-request-method", options.method);
3382
+ return new Response(response.body, {
3383
+ status: response.status,
3384
+ statusText: response.statusText,
3385
+ headers: responseHeaders
3386
+ });
3387
+ }
3388
+ };
3389
+ function quoteForShell(value) {
3390
+ if (value.includes("\0")) throw new Error("Shell argument contains NUL byte");
3391
+ return `'${value.replace(/'/g, "'\\''")}'`;
3392
+ }
3393
+ //#endregion
3394
+ export { exportTraceBundle as a, encodePromptForWire as c, buildTraceExportPayload as i, parseSSEStream as l, SandboxSession as n, otelTraceIdForTangleTrace as o, normalizeConnection as r, toOtelJson as s, SandboxInstance as t };