@tangle-network/sandbox 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -0
- package/README.md +71 -0
- package/dist/agent/index.d.ts +433 -0
- package/dist/agent/index.js +1 -0
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -271
- package/dist/client-CygjzF3v.js +1 -0
- package/dist/{errors-BI75IXOM.d.ts → client-DM2pIli7.d.ts} +2 -129
- package/dist/collaboration/index.d.ts +1 -1
- package/dist/collaboration/index.js +1 -2
- package/dist/collaboration-CRyb5e8F.js +1 -201
- package/dist/core.d.ts +3 -2
- package/dist/core.js +1 -4
- package/dist/errors-1Se5ATyZ.d.ts +128 -0
- package/dist/errors-CljiGR__.js +1 -262
- package/dist/{index-DhNGZ0h4.d.ts → index-CTj81tF9.d.ts} +1 -1
- package/dist/index.d.ts +256 -7
- package/dist/index.js +1 -825
- package/dist/openai/index.d.ts +21 -6
- package/dist/openai/index.js +1 -1721
- package/dist/{sandbox-aBpWqler.d.ts → sandbox-CBmfYqMQ.d.ts} +291 -117
- package/dist/sandbox-DTup2jzz.js +1 -0
- package/dist/session-gateway/index.js +1 -667
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +1 -2
- package/dist/tangle-CnYnTRi6.js +1 -0
- package/package.json +50 -78
- package/dist/client-Uve6A5C6.js +0 -2280
- package/dist/platform-integrations.d.ts +0 -2
- package/dist/platform-integrations.js +0 -2
- package/dist/sandbox-ksXTNlo-.js +0 -3394
- package/dist/tangle-DQ05paN7.js +0 -826
- /package/dist/{index-Dpj1oB5i.d.ts → index-D-2pH_70.d.ts} +0 -0
- /package/dist/{index-CCsA3S0D.d.ts → index-D7bwmNs8.d.ts} +0 -0
package/dist/sandbox-ksXTNlo-.js
DELETED
|
@@ -1,3394 +0,0 @@
|
|
|
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 };
|