@tangle-network/sandbox 0.10.5 → 0.11.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/README.md +109 -0
- package/dist/agent/index.d.ts +3 -3
- package/dist/{client-CtdFU_nx.d.ts → client-B2YYIsdw.d.ts} +7 -3
- package/dist/{client-Dv93EI90.js → client-C2yRj_dB.js} +481 -63
- package/dist/collaboration/index.js +1 -1
- package/dist/{collaboration-QZXrXdIb.js → collaboration-DDnuTcZd.js} +1 -1
- package/dist/core.d.ts +4 -4
- package/dist/core.js +3 -3
- package/dist/{errors-DSz87Rkk.js → errors-Cbs78OlF.js} +6 -6
- package/dist/{errors--P_nbLzM.d.ts → errors-ntvaf0_F.d.ts} +2 -2
- package/dist/{index-DF36P3Ew.d.ts → index-V2vjBfHU.d.ts} +2 -2
- package/dist/index.d.ts +32 -17
- package/dist/index.js +9 -285
- package/dist/intelligence/index.js +1 -1
- package/dist/runtime-api-kukaWPtF.js +678 -0
- package/dist/runtime.d.ts +49 -6
- package/dist/runtime.js +7 -5
- package/dist/{sandbox-tksuLdrT.d.ts → sandbox-BSsCz6nP.d.ts} +34 -12
- package/dist/{sandbox-Vh7Aog8b.js → sandbox-r2Lxbw8z.js} +208 -124
- package/dist/tangle/index.d.ts +1 -1
- package/dist/tangle/index.js +1 -1
- package/dist/{tangle-Bqbf3dA8.js → tangle-051CtV2b.js} +2 -2
- package/dist/{types-BGGfKALh.d.ts → types-C4dFf5eL.d.ts} +140 -40
- package/package.json +4 -3
- package/dist/runtime-api-CvAUrnHc.js +0 -448
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, l as ServerError, p as parseErrorResponse, s as QuotaError } from "./errors-Cbs78OlF.js";
|
|
2
|
+
//#region src/backend-config.ts
|
|
3
|
+
const LEGACY_QUESTION_ALIAS_KEY = ["question", "Channel"].join("");
|
|
4
|
+
function parseModelString(model) {
|
|
5
|
+
const parts = model.split("/");
|
|
6
|
+
if (parts.length >= 2) return {
|
|
7
|
+
provider: parts[0],
|
|
8
|
+
model: parts.slice(1).join("/")
|
|
9
|
+
};
|
|
10
|
+
return { model };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Normalize runtime backend config for the wire format.
|
|
14
|
+
*
|
|
15
|
+
* Canonical profile objects are already accepted by the sidecar and are
|
|
16
|
+
* forwarded without translation.
|
|
17
|
+
*/
|
|
18
|
+
function normalizeRuntimeBackendConfig(backend, options = {}) {
|
|
19
|
+
if (!backend && !options.model) return void 0;
|
|
20
|
+
if (backend && LEGACY_QUESTION_ALIAS_KEY in backend) throw new Error(`[sandbox-sdk] backend.${LEGACY_QUESTION_ALIAS_KEY} was removed. Use backend.interactions.question instead.`);
|
|
21
|
+
const callerInlineProfile = backend?.inlineProfile;
|
|
22
|
+
if (callerInlineProfile !== void 0) console.warn("[sandbox-sdk] backend.inlineProfile is deprecated. Use backend.profile (AgentProfile) instead.");
|
|
23
|
+
return {
|
|
24
|
+
...backend?.type ? { type: backend.type } : {},
|
|
25
|
+
...backend?.profile !== void 0 ? { profile: backend.profile } : {},
|
|
26
|
+
...callerInlineProfile !== void 0 ? { inlineProfile: callerInlineProfile } : {},
|
|
27
|
+
...backend?.model ? { model: backend.model } : options.model ? { model: parseModelString(options.model) } : {},
|
|
28
|
+
...backend?.server ? { server: backend.server } : {},
|
|
29
|
+
...backend?.interactions !== void 0 ? { interactions: backend.interactions } : {},
|
|
30
|
+
...backend?.metadata !== void 0 ? { metadata: backend.metadata } : {}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated Canonical profile objects can be sent directly as
|
|
35
|
+
* `backend.profile`; this compatibility export now preserves that object.
|
|
36
|
+
*/
|
|
37
|
+
function serializeForSidecar(profile) {
|
|
38
|
+
return profile;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/lib/wire-encoding.ts
|
|
42
|
+
/**
|
|
43
|
+
* Encode a prompt text part for the wire. Server decodes at the route
|
|
44
|
+
* boundary; the LLM only ever sees the original UTF-8. Done
|
|
45
|
+
* unconditionally because readable bodies false-positive on ingress WAF
|
|
46
|
+
* rules that pattern-match shell-injection-shaped substrings (which
|
|
47
|
+
* legitimate prompts routinely contain — e.g. Step-0 dev-server
|
|
48
|
+
* bootstrap).
|
|
49
|
+
*/
|
|
50
|
+
function encodeTextForWire(text) {
|
|
51
|
+
if (typeof Buffer !== "undefined") return Buffer.from(text, "utf8").toString("base64");
|
|
52
|
+
if (typeof btoa === "function") return btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (_, h) => String.fromCharCode(Number.parseInt(h, 16))));
|
|
53
|
+
throw new Error("encodeTextForWire: no base64 encoder available (Buffer and btoa both undefined)");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Convert a caller-supplied prompt (string or parts array) into the
|
|
57
|
+
* wire-format parts array. Every text part is base64-encoded; non-text
|
|
58
|
+
* parts pass through. A bare string is wrapped in a single text part.
|
|
59
|
+
*/
|
|
60
|
+
function encodePromptForWire(message) {
|
|
61
|
+
return (typeof message === "string" ? [{
|
|
62
|
+
type: "text",
|
|
63
|
+
text: message
|
|
64
|
+
}] : message).map((part) => part.type === "text" ? {
|
|
65
|
+
type: "text",
|
|
66
|
+
text: encodeTextForWire(part.text)
|
|
67
|
+
} : part);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/lib/abort-signal.ts
|
|
71
|
+
function combineAbortSignals(signals) {
|
|
72
|
+
return AbortSignal.any(signals);
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region ../../../packages/runtime-contracts/dist/file-limits.js
|
|
76
|
+
const MEBIBYTE$1 = 1024 * 1024;
|
|
77
|
+
10 * MEBIBYTE$1;
|
|
78
|
+
const FILE_DECODED_WRITE_MAX_BYTES = 5 * MEBIBYTE$1;
|
|
79
|
+
5 * MEBIBYTE$1;
|
|
80
|
+
10 * MEBIBYTE$1;
|
|
81
|
+
5 * MEBIBYTE$1;
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region ../../../packages/runtime-contracts/dist/request-limits.js
|
|
84
|
+
/**
|
|
85
|
+
* Raw WIRE/TRANSPORT caps: request-byte limits checked while the body
|
|
86
|
+
* streams in (bodyLimit middleware, Content-Length, encoded proxy bodies),
|
|
87
|
+
* before anything is parsed or decoded. Rejections against these carry
|
|
88
|
+
* code `PAYLOAD_TOO_LARGE` (`payload-too-large.ts`) — the remedy is to
|
|
89
|
+
* split the delivery. Decoded/parsed-content caps (code `FILE_TOO_LARGE`)
|
|
90
|
+
* live in `file-io.ts`.
|
|
91
|
+
*/
|
|
92
|
+
const MEBIBYTE = 1024 * 1024;
|
|
93
|
+
/**
|
|
94
|
+
* Gateway (CF Worker) proxy cap on any request body it forwards — both
|
|
95
|
+
* pass-through requests and route-constructed (`options.body`) proxy
|
|
96
|
+
* payloads. Workers buffer forwarded bodies (streaming upstream stalls),
|
|
97
|
+
* so this bounds Worker memory per request.
|
|
98
|
+
*/
|
|
99
|
+
const SANDBOX_PROXY_REQUEST_MAX_BYTES = MEBIBYTE;
|
|
100
|
+
32 * MEBIBYTE;
|
|
101
|
+
/**
|
|
102
|
+
* Max assembled size of one chunked-upload session — the ceiling of the
|
|
103
|
+
* sanctioned >1 MiB delivery path. Parts stage in sidecar memory until
|
|
104
|
+
* commit, so this bounds per-session memory, not the destination
|
|
105
|
+
* filesystem.
|
|
106
|
+
*/
|
|
107
|
+
const FILE_UPLOAD_SESSION_MAX_BYTES = 64 * MEBIBYTE;
|
|
108
|
+
128 * MEBIBYTE;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region ../../../packages/runtime-contracts/dist/size-limit-errors.js
|
|
111
|
+
/** Canonical error codes for wire-size and decoded-content rejections. */
|
|
112
|
+
const PAYLOAD_TOO_LARGE_CODE = "PAYLOAD_TOO_LARGE";
|
|
113
|
+
const FILE_TOO_LARGE_CODE = "FILE_TOO_LARGE";
|
|
114
|
+
function isRecord(value) {
|
|
115
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
function isPositiveInteger(value) {
|
|
118
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
119
|
+
}
|
|
120
|
+
function isSizeLimitErrorBodyFor(value, code) {
|
|
121
|
+
if (!isRecord(value) || value.success !== false || !isRecord(value.error)) return false;
|
|
122
|
+
const error = value.error;
|
|
123
|
+
return error.code === code && typeof error.message === "string" && typeof error.surface === "string" && isPositiveInteger(error.limitBytes) && (error.actualBytes === void 0 || isPositiveInteger(error.actualBytes));
|
|
124
|
+
}
|
|
125
|
+
function isPayloadTooLargeErrorBody(value) {
|
|
126
|
+
return isSizeLimitErrorBodyFor(value, PAYLOAD_TOO_LARGE_CODE);
|
|
127
|
+
}
|
|
128
|
+
function isFileTooLargeErrorBody(value) {
|
|
129
|
+
return isSizeLimitErrorBodyFor(value, FILE_TOO_LARGE_CODE);
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/lib/chunked-upload.ts
|
|
133
|
+
/**
|
|
134
|
+
* Chunked upload — the sanctioned SDK path for delivering payloads larger
|
|
135
|
+
* than the sandbox API's single-request body caps. Splits the payload into
|
|
136
|
+
* server-sized parts against a `/fs/upload-session` session, PUTs each part
|
|
137
|
+
* with transient retry, then commits with a whole-payload SHA-256 so the
|
|
138
|
+
* runtime can verify nothing was dropped or reordered in transit.
|
|
139
|
+
*
|
|
140
|
+
* Browser/Worker-safe: no `node:` imports, no `Buffer`. Input normalizes to
|
|
141
|
+
* a `Uint8Array` and hashes via Web Crypto (`crypto.subtle`), available in
|
|
142
|
+
* browsers, Workers, and Node 18+ — the same environments this module must
|
|
143
|
+
* run in unmodified.
|
|
144
|
+
*/
|
|
145
|
+
const DEFAULT_MAX_RETRIES = 4;
|
|
146
|
+
const RETRY_BASE_MS = 250;
|
|
147
|
+
const RETRY_MAX_MS = 2e3;
|
|
148
|
+
const delay = (ms, signal) => new Promise((resolve, reject) => {
|
|
149
|
+
if (signal?.aborted) {
|
|
150
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
signal?.removeEventListener("abort", onAbort);
|
|
155
|
+
resolve();
|
|
156
|
+
}, ms);
|
|
157
|
+
function onAbort() {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
|
|
160
|
+
}
|
|
161
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
162
|
+
});
|
|
163
|
+
/** Transient part-upload failures worth retrying: rate-limit/budget (429),
|
|
164
|
+
* 5xx, transport (network), and request timeouts — mirrors
|
|
165
|
+
* `isTransientWriteError` in `sandbox.ts`. A non-transient error (bad path,
|
|
166
|
+
* auth, checksum, containment) fails loud immediately. */
|
|
167
|
+
function isTransientUploadError(err) {
|
|
168
|
+
return err instanceof QuotaError || err instanceof ServerError || err instanceof NetworkError || err instanceof TimeoutError;
|
|
169
|
+
}
|
|
170
|
+
function isChecksumMismatch(err) {
|
|
171
|
+
return err instanceof SandboxError && err.code === "CHECKSUM_MISMATCH";
|
|
172
|
+
}
|
|
173
|
+
/** Cheap size probe used for the pre-flight cap check, read directly off
|
|
174
|
+
* the input shape (`Blob.size` / `ArrayBuffer.byteLength` / view
|
|
175
|
+
* `byteLength`) so an oversized `Blob` never has its bytes materialized
|
|
176
|
+
* just to be rejected. Strings have no cheap length in UTF-8, so they're
|
|
177
|
+
* encoded once here (and once more in {@link normalizeToBytes} — an
|
|
178
|
+
* acceptable cost given strings are never the multi-MB case this path
|
|
179
|
+
* exists for). */
|
|
180
|
+
function sizeOfInput(data) {
|
|
181
|
+
if (typeof data === "string") return new TextEncoder().encode(data).byteLength;
|
|
182
|
+
if (data instanceof ArrayBuffer) return data.byteLength;
|
|
183
|
+
if (ArrayBuffer.isView(data)) return data.byteLength;
|
|
184
|
+
return data.size;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Normalizes to a `Uint8Array` backed by a plain (never shared)
|
|
188
|
+
* `ArrayBuffer`. A caller-supplied view may be backed by a
|
|
189
|
+
* `SharedArrayBuffer`, which `crypto.subtle.digest` and `fetch`'s
|
|
190
|
+
* `BodyInit` both reject at the type level (and some engines reject at
|
|
191
|
+
* runtime) — copying here keeps every downstream call on one concrete,
|
|
192
|
+
* always-accepted buffer type.
|
|
193
|
+
*/
|
|
194
|
+
async function normalizeToBytes(data) {
|
|
195
|
+
if (typeof data === "string") return new TextEncoder().encode(data);
|
|
196
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data.slice(0));
|
|
197
|
+
if (ArrayBuffer.isView(data)) {
|
|
198
|
+
const copy = new Uint8Array(data.byteLength);
|
|
199
|
+
copy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
200
|
+
return copy;
|
|
201
|
+
}
|
|
202
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
203
|
+
}
|
|
204
|
+
function webCryptoSubtle() {
|
|
205
|
+
const subtle = globalThis.crypto?.subtle;
|
|
206
|
+
if (!subtle) throw new Error("chunked upload requires Web Crypto (crypto.subtle) for SHA-256 hashing — available in browsers, Workers, and Node 18+");
|
|
207
|
+
return subtle;
|
|
208
|
+
}
|
|
209
|
+
async function sha256Hex(bytes) {
|
|
210
|
+
const digest = await webCryptoSubtle().digest("SHA-256", bytes);
|
|
211
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Canonical size-cap bodies (413 `PAYLOAD_TOO_LARGE` / `FILE_TOO_LARGE`)
|
|
215
|
+
* carry `surface`/`limitBytes`/`actualBytes` that the generic error parser
|
|
216
|
+
* drops; fold them into the message before delegating to
|
|
217
|
+
* `parseErrorResponse` for the rest (status→class mapping, retryAfterMs,
|
|
218
|
+
* origin).
|
|
219
|
+
*/
|
|
220
|
+
async function parseUploadError(response) {
|
|
221
|
+
const bodyText = await response.text();
|
|
222
|
+
let parsedBody;
|
|
223
|
+
try {
|
|
224
|
+
parsedBody = JSON.parse(bodyText);
|
|
225
|
+
} catch {
|
|
226
|
+
parsedBody = void 0;
|
|
227
|
+
}
|
|
228
|
+
if (isPayloadTooLargeErrorBody(parsedBody) || isFileTooLargeErrorBody(parsedBody)) {
|
|
229
|
+
const { code, message, surface, limitBytes, actualBytes } = parsedBody.error;
|
|
230
|
+
const detail = typeof actualBytes === "number" ? `${message} (${surface}: ${actualBytes} of ${limitBytes} bytes)` : `${message} (${surface}: limit ${limitBytes} bytes)`;
|
|
231
|
+
return parseErrorResponse(response.status, JSON.stringify({
|
|
232
|
+
code,
|
|
233
|
+
message: detail
|
|
234
|
+
}), void 0, response.headers);
|
|
235
|
+
}
|
|
236
|
+
return parseErrorResponse(response.status, bodyText, void 0, response.headers);
|
|
237
|
+
}
|
|
238
|
+
async function initUploadSession(transport, remotePath, totalBytes, sha256, mode, signal) {
|
|
239
|
+
signal?.throwIfAborted();
|
|
240
|
+
const response = await transport.fetch("/fs/upload-session", {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
path: remotePath,
|
|
245
|
+
mode,
|
|
246
|
+
totalBytes,
|
|
247
|
+
sha256
|
|
248
|
+
}),
|
|
249
|
+
signal
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) throw await parseUploadError(response);
|
|
252
|
+
return (await response.json()).data;
|
|
253
|
+
}
|
|
254
|
+
async function putPart(transport, uploadId, index, chunk, options) {
|
|
255
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
256
|
+
for (let attempt = 0;; attempt++) {
|
|
257
|
+
options.signal?.throwIfAborted();
|
|
258
|
+
try {
|
|
259
|
+
const response = await transport.fetch(`/fs/upload-session/${encodeURIComponent(uploadId)}/parts/${index}`, {
|
|
260
|
+
method: "PUT",
|
|
261
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
262
|
+
body: chunk,
|
|
263
|
+
signal: options.signal
|
|
264
|
+
});
|
|
265
|
+
if (!response.ok) throw await parseUploadError(response);
|
|
266
|
+
return;
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (isTransientUploadError(err) && attempt < maxRetries) {
|
|
269
|
+
const retryAfterMs = err instanceof SandboxError ? err.retryAfterMs : void 0;
|
|
270
|
+
const backoff = Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_MAX_MS);
|
|
271
|
+
await delay(retryAfterMs ?? backoff, options.signal);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function partCount(totalBytes, partSize) {
|
|
279
|
+
return Math.ceil(totalBytes / partSize);
|
|
280
|
+
}
|
|
281
|
+
async function uploadAllParts(transport, uploadId, bytes, partSize, totalParts, options) {
|
|
282
|
+
const totalBytes = bytes.byteLength;
|
|
283
|
+
const paceMs = options.paceMs ?? 0;
|
|
284
|
+
let uploadedBytes = 0;
|
|
285
|
+
let started = false;
|
|
286
|
+
for (let index = 0; index < totalParts; index++) {
|
|
287
|
+
if (started && paceMs > 0) await delay(paceMs, options.signal);
|
|
288
|
+
started = true;
|
|
289
|
+
const start = index * partSize;
|
|
290
|
+
const end = Math.min(start + partSize, totalBytes);
|
|
291
|
+
await putPart(transport, uploadId, index, bytes.subarray(start, end), options);
|
|
292
|
+
uploadedBytes += end - start;
|
|
293
|
+
options.onProgress?.({
|
|
294
|
+
bytesUploaded: uploadedBytes,
|
|
295
|
+
totalBytes,
|
|
296
|
+
percentage: totalBytes === 0 ? 100 : Math.round(uploadedBytes / totalBytes * 100)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async function commitSession(transport, uploadId, totalParts, sha256, signal) {
|
|
301
|
+
signal?.throwIfAborted();
|
|
302
|
+
const response = await transport.fetch(`/fs/upload-session/${encodeURIComponent(uploadId)}/commit`, {
|
|
303
|
+
method: "POST",
|
|
304
|
+
headers: { "Content-Type": "application/json" },
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
totalParts,
|
|
307
|
+
sha256
|
|
308
|
+
}),
|
|
309
|
+
signal
|
|
310
|
+
});
|
|
311
|
+
if (!response.ok) throw await parseUploadError(response);
|
|
312
|
+
return (await response.json()).data;
|
|
313
|
+
}
|
|
314
|
+
/** Best-effort abort of an in-progress session — never masks the real
|
|
315
|
+
* failure that triggered it, and never itself throws. An orphaned session
|
|
316
|
+
* also self-expires server-side (`expiresAt`), so a failed DELETE here is
|
|
317
|
+
* not a leak, just deferred cleanup. */
|
|
318
|
+
async function abortSession(transport, uploadId) {
|
|
319
|
+
try {
|
|
320
|
+
await transport.fetch(`/fs/upload-session/${encodeURIComponent(uploadId)}`, { method: "DELETE" });
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Upload arbitrary binary or text data to the sandbox via the chunked
|
|
325
|
+
* upload-session protocol, verifying integrity with a whole-payload
|
|
326
|
+
* SHA-256 at commit. Browser/Worker-safe and works identically over the
|
|
327
|
+
* gateway proxy or a direct sidecar connection — see
|
|
328
|
+
* {@link FileSystem.uploadData}.
|
|
329
|
+
*
|
|
330
|
+
* @param transport - Fetch-shaped adapter scoped to the sandbox runtime
|
|
331
|
+
* (paths are relative to it, e.g. `/fs/upload-session`).
|
|
332
|
+
* @param remotePath - Destination path in the sandbox.
|
|
333
|
+
* @param data - Payload to upload. Strings encode as UTF-8.
|
|
334
|
+
* @param options - See {@link ChunkedUploadOptions}.
|
|
335
|
+
* @throws {SandboxError} `PAYLOAD_TOO_LARGE` immediately (zero requests) if
|
|
336
|
+
* the payload exceeds {@link FILE_UPLOAD_SESSION_MAX_BYTES}; otherwise the
|
|
337
|
+
* typed error from whichever step (init/part/commit) failed after retries
|
|
338
|
+
* are exhausted.
|
|
339
|
+
*/
|
|
340
|
+
async function uploadChunked(transport, remotePath, data, options = {}) {
|
|
341
|
+
const estimatedSize = sizeOfInput(data);
|
|
342
|
+
if (estimatedSize > 67108864) throw new SandboxError(`Payload for ${remotePath} is ${estimatedSize} bytes, over the ${FILE_UPLOAD_SESSION_MAX_BYTES} byte chunked-upload session cap`, "PAYLOAD_TOO_LARGE", 413);
|
|
343
|
+
options.signal?.throwIfAborted();
|
|
344
|
+
const bytes = await normalizeToBytes(data);
|
|
345
|
+
options.signal?.throwIfAborted();
|
|
346
|
+
const sha256 = await sha256Hex(bytes);
|
|
347
|
+
const init = await initUploadSession(transport, remotePath, bytes.byteLength, sha256, options.mode, options.signal);
|
|
348
|
+
const totalParts = partCount(bytes.byteLength, init.partSize);
|
|
349
|
+
try {
|
|
350
|
+
await uploadAllParts(transport, init.uploadId, bytes, init.partSize, totalParts, options);
|
|
351
|
+
try {
|
|
352
|
+
return await commitSession(transport, init.uploadId, totalParts, sha256, options.signal);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (!isChecksumMismatch(err)) throw err;
|
|
355
|
+
await uploadAllParts(transport, init.uploadId, bytes, init.partSize, totalParts, options);
|
|
356
|
+
return await commitSession(transport, init.uploadId, totalParts, sha256, options.signal);
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
await abortSession(transport, init.uploadId);
|
|
360
|
+
throw err;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/runtime-api.ts
|
|
365
|
+
/** Internal typed client for routes served by a sandbox runtime. */
|
|
366
|
+
var SandboxRuntimeApi = class {
|
|
367
|
+
terminals;
|
|
368
|
+
constructor(transport) {
|
|
369
|
+
this.transport = transport;
|
|
370
|
+
this.terminals = {
|
|
371
|
+
create: (options, request) => this.createTerminal(options, request),
|
|
372
|
+
input: (terminalId, data, request) => this.writeTerminal(terminalId, data, request),
|
|
373
|
+
list: () => this.listTerminals(),
|
|
374
|
+
get: (terminalId) => this.getTerminal(terminalId)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async health() {
|
|
378
|
+
await this.transport.ensureRunning();
|
|
379
|
+
return this.json("/health", { method: "GET" });
|
|
380
|
+
}
|
|
381
|
+
async ports() {
|
|
382
|
+
await this.transport.ensureRunning();
|
|
383
|
+
return (await this.json("/ports", { method: "GET" })).data?.ports ?? [];
|
|
384
|
+
}
|
|
385
|
+
async profiles() {
|
|
386
|
+
await this.transport.ensureRunning();
|
|
387
|
+
return this.json("/profiles", { method: "GET" });
|
|
388
|
+
}
|
|
389
|
+
async readFiles(paths, options = {}) {
|
|
390
|
+
if (paths.length === 0) return {
|
|
391
|
+
files: [],
|
|
392
|
+
errors: [],
|
|
393
|
+
stats: {
|
|
394
|
+
requested: 0,
|
|
395
|
+
succeeded: 0,
|
|
396
|
+
failed: 0
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
await this.transport.ensureRunning();
|
|
400
|
+
const files = [];
|
|
401
|
+
const errors = [];
|
|
402
|
+
let requested = 0;
|
|
403
|
+
let succeeded = 0;
|
|
404
|
+
let failed = 0;
|
|
405
|
+
for (let offset = 0; offset < paths.length; offset += 100) {
|
|
406
|
+
const chunk = paths.slice(offset, offset + 100);
|
|
407
|
+
const params = new URLSearchParams();
|
|
408
|
+
if (options.sessionId) params.set("sessionId", options.sessionId);
|
|
409
|
+
const query = params.toString();
|
|
410
|
+
const payload = await this.json(`/files/read-batch${query ? `?${query}` : ""}`, {
|
|
411
|
+
method: "POST",
|
|
412
|
+
body: JSON.stringify({
|
|
413
|
+
paths: chunk,
|
|
414
|
+
encoding: options.encoding ?? "utf8"
|
|
415
|
+
})
|
|
416
|
+
});
|
|
417
|
+
for (const [path, result] of Object.entries(payload.data.files)) if (result.success) files.push({
|
|
418
|
+
path,
|
|
419
|
+
content: result.content,
|
|
420
|
+
encoding: result.encoding,
|
|
421
|
+
hash: result.hash,
|
|
422
|
+
size: result.size,
|
|
423
|
+
mtime: result.mtime
|
|
424
|
+
});
|
|
425
|
+
else errors.push({
|
|
426
|
+
path,
|
|
427
|
+
error: result.error,
|
|
428
|
+
code: result.code
|
|
429
|
+
});
|
|
430
|
+
const results = Object.values(payload.data.files);
|
|
431
|
+
requested += payload.data.stats?.requested ?? chunk.length;
|
|
432
|
+
succeeded += payload.data.stats?.succeeded ?? results.filter((entry) => entry.success).length;
|
|
433
|
+
failed += payload.data.stats?.failed ?? results.filter((entry) => !entry.success).length;
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
files,
|
|
437
|
+
errors,
|
|
438
|
+
stats: {
|
|
439
|
+
requested,
|
|
440
|
+
succeeded,
|
|
441
|
+
failed
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async writeFile(path, content, options = {}) {
|
|
446
|
+
await this.transport.ensureRunning();
|
|
447
|
+
const params = new URLSearchParams();
|
|
448
|
+
if (options.sessionId) params.set("sessionId", options.sessionId);
|
|
449
|
+
const query = params.toString();
|
|
450
|
+
return (await this.json(`/files/write${query ? `?${query}` : ""}`, {
|
|
451
|
+
method: "POST",
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
path,
|
|
454
|
+
content,
|
|
455
|
+
...options.encoding !== void 0 ? { encoding: options.encoding } : {},
|
|
456
|
+
...options.mode !== void 0 ? { mode: options.mode } : {}
|
|
457
|
+
})
|
|
458
|
+
})).data;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Upload in-memory binary or text data to the sandbox over the chunked
|
|
462
|
+
* upload-session protocol — the browser/Worker-safe sanctioned path for
|
|
463
|
+
* payloads over ~1 MiB, sized to clear the gateway proxy's per-request cap
|
|
464
|
+
* (parts stay under {@link FILE_UPLOAD_PART_MAX_BYTES}). Binary-safe:
|
|
465
|
+
* accepts a `Blob`, `ArrayBuffer`, `ArrayBufferView`, or UTF-8 `string`.
|
|
466
|
+
* Delegates to the shared `uploadChunked` engine in `lib/chunked-upload.js`,
|
|
467
|
+
* wired to this client's `transport.fetch` so requests carry the same
|
|
468
|
+
* scoped-token `Authorization` header and base URL as every other method
|
|
469
|
+
* on this class (e.g. {@link SandboxRuntimeApi.writeFile}) — works
|
|
470
|
+
* identically over the gateway proxy or a direct sidecar connection.
|
|
471
|
+
*
|
|
472
|
+
* @throws {SandboxError} `PAYLOAD_TOO_LARGE` immediately, before any
|
|
473
|
+
* request, if the payload exceeds the chunked-upload session cap.
|
|
474
|
+
*/
|
|
475
|
+
async uploadData(remotePath, data, options) {
|
|
476
|
+
await this.transport.ensureRunning();
|
|
477
|
+
return uploadChunked({ fetch: (path, init) => this.transport.fetch(path, init) }, remotePath, data, options);
|
|
478
|
+
}
|
|
479
|
+
async deleteFile(path, options = {}) {
|
|
480
|
+
await this.transport.ensureRunning();
|
|
481
|
+
const params = new URLSearchParams();
|
|
482
|
+
if (options.sessionId) params.set("sessionId", options.sessionId);
|
|
483
|
+
const query = params.toString();
|
|
484
|
+
await this.request(`/files/delete${query ? `?${query}` : ""}`, {
|
|
485
|
+
method: "POST",
|
|
486
|
+
body: JSON.stringify({ path })
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async renameFile(sourcePath, destinationPath, options = {}) {
|
|
490
|
+
await this.transport.ensureRunning();
|
|
491
|
+
const params = new URLSearchParams();
|
|
492
|
+
if (options.sessionId) params.set("sessionId", options.sessionId);
|
|
493
|
+
const query = params.toString();
|
|
494
|
+
return (await this.json(`/files/rename${query ? `?${query}` : ""}`, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
body: JSON.stringify({
|
|
497
|
+
sourcePath,
|
|
498
|
+
destinationPath
|
|
499
|
+
})
|
|
500
|
+
})).data;
|
|
501
|
+
}
|
|
502
|
+
async fileTree(path, options = {}) {
|
|
503
|
+
if (options.maxDepth !== void 0 && (!Number.isInteger(options.maxDepth) || options.maxDepth < 1 || options.maxDepth > 20)) throw new ValidationError("maxDepth must be an integer between 1 and 20");
|
|
504
|
+
await this.transport.ensureRunning();
|
|
505
|
+
const params = new URLSearchParams({ path });
|
|
506
|
+
if (options.maxDepth !== void 0) params.set("maxDepth", String(options.maxDepth));
|
|
507
|
+
if (options.sessionId) params.set("sessionId", options.sessionId);
|
|
508
|
+
return (await this.json(`/files/tree?${params}`, { method: "GET" })).data;
|
|
509
|
+
}
|
|
510
|
+
async createSession(options) {
|
|
511
|
+
await this.transport.ensureRunning();
|
|
512
|
+
const info = normalizeSessionInfo(await this.json("/agents/sessions", {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body: JSON.stringify({
|
|
515
|
+
...options.sessionId !== void 0 ? { sessionId: options.sessionId } : {},
|
|
516
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
517
|
+
...options.parentId !== void 0 ? { parentID: options.parentId } : {},
|
|
518
|
+
backend: normalizeRuntimeBackendConfig(options.backend)
|
|
519
|
+
})
|
|
520
|
+
}));
|
|
521
|
+
if (!info) throw new ServerError("Session creation response missing session id", 502, {
|
|
522
|
+
endpoint: "/agents/sessions",
|
|
523
|
+
origin: "runtime"
|
|
524
|
+
});
|
|
525
|
+
return info;
|
|
526
|
+
}
|
|
527
|
+
async messages(id, options = {}) {
|
|
528
|
+
await this.transport.ensureRunning();
|
|
529
|
+
const params = new URLSearchParams({ sessionId: id });
|
|
530
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
531
|
+
if (options.offset !== void 0) params.set("offset", String(options.offset));
|
|
532
|
+
if (options.since !== void 0) params.set("since", String(options.since));
|
|
533
|
+
const query = params.toString();
|
|
534
|
+
return (await this.json(`/messages${query ? `?${query}` : ""}`, { method: "GET" })).map((message) => ({
|
|
535
|
+
id: message.info.id,
|
|
536
|
+
role: message.info.role,
|
|
537
|
+
timestamp: message.info.timestamp,
|
|
538
|
+
parts: message.parts,
|
|
539
|
+
metadata: message.info.metadata
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
async createTaskSession(options) {
|
|
543
|
+
await this.transport.ensureRunning();
|
|
544
|
+
const info = await this.json("/tasks", {
|
|
545
|
+
method: "POST",
|
|
546
|
+
body: JSON.stringify({
|
|
547
|
+
sessionId: options.sessionId,
|
|
548
|
+
backend: normalizeRuntimeBackendConfig(options.backend),
|
|
549
|
+
title: options.title,
|
|
550
|
+
...options.parentSessionId !== void 0 ? { parentSessionId: options.parentSessionId } : {},
|
|
551
|
+
...options.profile !== void 0 ? { profile: options.profile } : {},
|
|
552
|
+
...options.isolateFileWrites !== void 0 ? { isolateFileWrites: options.isolateFileWrites } : {}
|
|
553
|
+
})
|
|
554
|
+
});
|
|
555
|
+
if (!info.id) throw new ServerError("Task session creation response missing session id", 502, {
|
|
556
|
+
endpoint: "/tasks",
|
|
557
|
+
origin: "runtime"
|
|
558
|
+
});
|
|
559
|
+
return info;
|
|
560
|
+
}
|
|
561
|
+
async sendSessionMessage(id, request, options = {}) {
|
|
562
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs <= 0) throw new ValidationError("timeoutMs must be greater than zero");
|
|
563
|
+
await this.transport.ensureRunning();
|
|
564
|
+
const headers = new Headers({ "x-no-retry": "true" });
|
|
565
|
+
if (options.credentials?.apiKey) headers.set("X-Backend-Api-Key", options.credentials.apiKey);
|
|
566
|
+
if (options.credentials?.baseUrl) headers.set("X-Backend-Base-Url", options.credentials.baseUrl);
|
|
567
|
+
const timeoutSignal = options.timeoutMs !== void 0 ? AbortSignal.timeout(options.timeoutMs) : void 0;
|
|
568
|
+
const signal = options.signal && timeoutSignal ? combineAbortSignals([options.signal, timeoutSignal]) : options.signal ?? timeoutSignal;
|
|
569
|
+
return this.json(`/agents/sessions/${encodeURIComponent(id)}/messages`, {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers,
|
|
572
|
+
signal,
|
|
573
|
+
body: JSON.stringify({
|
|
574
|
+
...request.messageId !== void 0 ? { messageID: request.messageId } : {},
|
|
575
|
+
parts: request.parts.map((part) => part.type === "text" ? {
|
|
576
|
+
...part,
|
|
577
|
+
text: encodeTextForWire(part.text)
|
|
578
|
+
} : part),
|
|
579
|
+
...request.system !== void 0 ? { system: request.system } : {},
|
|
580
|
+
...request.agent !== void 0 ? { agent: request.agent } : {},
|
|
581
|
+
...request.model !== void 0 ? { model: {
|
|
582
|
+
providerID: request.model.providerId,
|
|
583
|
+
modelID: request.model.modelId
|
|
584
|
+
} } : {},
|
|
585
|
+
...request.reasoningEffort !== void 0 ? { reasoningEffort: request.reasoningEffort } : {},
|
|
586
|
+
...request.turnId !== void 0 ? { turnId: request.turnId } : {},
|
|
587
|
+
...request.interactions !== void 0 ? { interactions: request.interactions } : {}
|
|
588
|
+
})
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async deleteSession(id) {
|
|
592
|
+
await this.transport.ensureRunning();
|
|
593
|
+
await this.request(`/agents/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
594
|
+
}
|
|
595
|
+
async taskSessionChanges(id) {
|
|
596
|
+
await this.transport.ensureRunning();
|
|
597
|
+
return this.json(`/tasks/${encodeURIComponent(id)}/changes`, { method: "GET" });
|
|
598
|
+
}
|
|
599
|
+
async commitTaskSession(id, options = {}) {
|
|
600
|
+
await this.transport.ensureRunning();
|
|
601
|
+
return this.json(`/tasks/${encodeURIComponent(id)}/changes`, {
|
|
602
|
+
method: "POST",
|
|
603
|
+
body: JSON.stringify({
|
|
604
|
+
...options.message !== void 0 ? { message: options.message } : {},
|
|
605
|
+
...options.files !== void 0 ? { files: options.files } : {}
|
|
606
|
+
})
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async createTerminal(options = {}, request = {}) {
|
|
610
|
+
await this.transport.ensureRunning();
|
|
611
|
+
const params = new URLSearchParams();
|
|
612
|
+
if (request.sessionId) params.set("sessionId", request.sessionId);
|
|
613
|
+
const query = params.toString();
|
|
614
|
+
return (await this.json(`/terminals${query ? `?${query}` : ""}`, {
|
|
615
|
+
method: "POST",
|
|
616
|
+
body: JSON.stringify(options)
|
|
617
|
+
})).data;
|
|
618
|
+
}
|
|
619
|
+
async writeTerminal(terminalId, data, request = {}) {
|
|
620
|
+
await this.transport.ensureRunning();
|
|
621
|
+
const params = new URLSearchParams();
|
|
622
|
+
if (request.sessionId) params.set("sessionId", request.sessionId);
|
|
623
|
+
const query = params.toString();
|
|
624
|
+
await this.request(`/terminals/${encodeURIComponent(terminalId)}/input${query ? `?${query}` : ""}`, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
body: JSON.stringify({ data })
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
async listTerminals() {
|
|
630
|
+
await this.transport.ensureRunning();
|
|
631
|
+
return (await this.json("/terminals", { method: "GET" })).data ?? [];
|
|
632
|
+
}
|
|
633
|
+
async getTerminal(terminalId) {
|
|
634
|
+
await this.transport.ensureRunning();
|
|
635
|
+
const response = await this.transport.fetch(`/terminals/${encodeURIComponent(terminalId)}`, { method: "GET" });
|
|
636
|
+
if (response.status === 404) return null;
|
|
637
|
+
await this.assertOk(response);
|
|
638
|
+
return (await response.json()).data;
|
|
639
|
+
}
|
|
640
|
+
async json(path, init) {
|
|
641
|
+
return await (await this.request(path, init)).json();
|
|
642
|
+
}
|
|
643
|
+
async request(path, init) {
|
|
644
|
+
const response = await this.transport.fetch(path, init);
|
|
645
|
+
await this.assertOk(response);
|
|
646
|
+
return response;
|
|
647
|
+
}
|
|
648
|
+
async assertOk(response) {
|
|
649
|
+
if (response.ok) return;
|
|
650
|
+
const body = await response.text();
|
|
651
|
+
throw parseErrorResponse(response.status, body, void 0, response.headers);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
function normalizeSessionInfo(raw) {
|
|
655
|
+
const id = raw.id;
|
|
656
|
+
if (typeof id !== "string") return null;
|
|
657
|
+
const status = raw.status || "running";
|
|
658
|
+
return {
|
|
659
|
+
id,
|
|
660
|
+
parentId: typeof raw.parentID === "string" ? raw.parentID : typeof raw.parentId === "string" ? raw.parentId : void 0,
|
|
661
|
+
status: [
|
|
662
|
+
"queued",
|
|
663
|
+
"running",
|
|
664
|
+
"completed",
|
|
665
|
+
"failed",
|
|
666
|
+
"cancelled"
|
|
667
|
+
].includes(status) ? status : "running",
|
|
668
|
+
backend: typeof raw.backendType === "string" ? raw.backendType : typeof raw.backend === "string" ? raw.backend : void 0,
|
|
669
|
+
model: typeof raw.model === "string" ? raw.model : void 0,
|
|
670
|
+
promptCount: typeof raw.promptCount === "number" ? raw.promptCount : void 0,
|
|
671
|
+
createdAt: raw.createdAt ? new Date(raw.createdAt) : void 0,
|
|
672
|
+
startedAt: raw.startedAt ? new Date(raw.startedAt) : void 0,
|
|
673
|
+
endedAt: raw.endedAt ? new Date(raw.endedAt) : void 0,
|
|
674
|
+
raw
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
//#endregion
|
|
678
|
+
export { FILE_DECODED_WRITE_MAX_BYTES as a, encodeTextForWire as c, SANDBOX_PROXY_REQUEST_MAX_BYTES as i, normalizeRuntimeBackendConfig as l, normalizeSessionInfo as n, combineAbortSignals as o, uploadChunked as r, encodePromptForWire as s, SandboxRuntimeApi as t, serializeForSidecar as u };
|