@tangle-network/sandbox 0.10.5-develop.20260713235503.8f2cefc → 0.10.5-develop.20260715115519.fee3d86

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 CHANGED
@@ -222,6 +222,38 @@ A gateway crash before acknowledgement redelivers the same event IDs, which the
222
222
  See `examples/cf-worker-chat.ts`, `examples/browser-streaming-resume.ts`, and
223
223
  `examples/reconnect-from-last-event-id.ts` for end-to-end patterns.
224
224
 
225
+ ### Browser-safe scoped runtime client
226
+
227
+ `@tangle-network/sandbox/runtime` is a browser- and Worker-safe entry point
228
+ (no `node:` imports) for talking directly to one sandbox's runtime with a
229
+ short-lived scoped token — never the account-level Sandbox API key — minted
230
+ server-side via `box.mintScopedToken({ scope: "session-runtime", sessionId })`
231
+ or `box.mintScopedToken({ scope: "read-only" })`.
232
+
233
+ ```typescript
234
+ import { createSandboxRuntimeClient } from "@tangle-network/sandbox/runtime";
235
+
236
+ const runtime = createSandboxRuntimeClient({
237
+ baseUrl: scopedToken.sidecarProxyUrl, // browser-safe proxy base from box.mintScopedToken()
238
+ token: scopedToken.token,
239
+ });
240
+
241
+ await runtime.writeFile("/workspace/notes.md", "# from the browser");
242
+
243
+ // Binary-safe, chunked upload for payloads over ~1 MiB — same engine and
244
+ // size contract as SandboxInstance.fs.uploadData, wired to the scoped token
245
+ const blob = await fileInput.files[0].arrayBuffer();
246
+ await runtime.uploadData("/workspace/uploads/file.bin", blob, {
247
+ onProgress: (p) => console.log(`${p.percentage.toFixed(1)}%`),
248
+ });
249
+ ```
250
+
251
+ Every method on the scoped client — `writeFile`, `readFiles`, `uploadData`,
252
+ terminals, sessions — carries the same `Authorization: Bearer <token>` header
253
+ and base URL prefix, so the request conventions match whether the token
254
+ scope allows the call or not; a denied call fails with the typed
255
+ `SandboxError`, not a silent no-op.
256
+
225
257
  ## Features
226
258
 
227
259
  - **Sandbox Management** - Create, list, stop, resume, and delete sandboxes
@@ -727,6 +759,70 @@ for await (const event of box.events({ signal: controller.signal })) {
727
759
  }
728
760
  ```
729
761
 
762
+ ### File I/O
763
+
764
+ | Method | Environment | Payload | Notes |
765
+ |---|---|---|---|
766
+ | `fs.read(path)` / `fs.write(path, content, options?)` | Node + browser | UTF-8 string, or base64 via `options.encoding` | Single request — see the size contract below for the cap |
767
+ | `fs.writeMany(files, options?)` | Node + browser | Same as `write`; per-file `encoding` | Paced, retry-aware batch through `fs.write`'s endpoint; fails loud on the first file that can't be written |
768
+ | `fs.upload(localPath, remotePath, options?)` | Node only | Local file path | Reads the local file with `node:fs`; not available in a browser tab |
769
+ | `fs.uploadData(remotePath, data, options?)` | Node + browser | `Blob \| ArrayBuffer \| ArrayBufferView \| string` | Browser/Worker-safe, binary-safe — the sanctioned path for payloads over ~1 MiB (see below) |
770
+ | `fs.download(remotePath, localPath, options?)` | Node only | — | Writes to a local path with `node:fs` |
771
+ | `fs.uploadDir` / `fs.downloadDir` | Node only | Local directory path | tar-based directory transfer |
772
+ | `fs.list` / `fs.stat` / `fs.mkdir` / `fs.delete` / `fs.rename` / `fs.exists` | Node + browser | — | Directory listing and file metadata operations |
773
+
774
+ ```typescript
775
+ // Text write/read (UTF-8 by default)
776
+ await box.fs.write("/workspace/config.json", JSON.stringify(config));
777
+ const raw = await box.fs.read("/workspace/config.json");
778
+
779
+ // Base64 for binary content through the same single-shot endpoint
780
+ await box.fs.write("/workspace/icon.png", base64Png, { encoding: "base64" });
781
+
782
+ // Batch-write many small files (paced, retried)
783
+ await box.fs.writeMany([
784
+ { path: "skills/review.md", content: reviewSkill },
785
+ { path: "assets/logo.png", content: base64Logo, encoding: "base64" },
786
+ ]);
787
+
788
+ // Node-only: upload a local file
789
+ await box.fs.upload("./model.bin", "/workspace/models/model.bin");
790
+
791
+ // Browser/Worker-safe: upload in-memory data over ~1 MiB (e.g. an
792
+ // <input type="file"> Blob) — chunked and verified end-to-end
793
+ const blob = await fetch("https://example.com/model.bin").then((r) => r.blob());
794
+ await box.fs.uploadData("/workspace/models/model.bin", blob, {
795
+ onProgress: (p) => console.log(`${p.percentage.toFixed(1)}%`),
796
+ });
797
+
798
+ await box.fs.download("/workspace/results.zip", "./results.zip");
799
+ ```
800
+
801
+ `fs.uploadData` is the sanctioned path once a payload clears the single-shot
802
+ write cap: it splits the payload into server-sized parts against a chunked
803
+ upload-session and verifies the whole payload with a SHA-256 at commit, so
804
+ it's safe for arbitrarily-shaped binary content (video, model weights,
805
+ archives) up to the session cap. It works identically whether the sandbox is
806
+ reached through the gateway proxy or a direct sidecar connection
807
+ (`box.direct()`).
808
+
809
+ #### File size contract
810
+
811
+ | Surface | Cap | Constant | Failure |
812
+ |---|---|---|---|
813
+ | Single-shot decoded write (`fs.write`, `fs.writeMany`) | 5 MiB | `FILE_DECODED_WRITE_MAX_BYTES` | `FILE_TOO_LARGE` |
814
+ | Gateway proxy request (wire, any route) | 1 MiB | `SANDBOX_PROXY_REQUEST_MAX_BYTES` | `PAYLOAD_TOO_LARGE` |
815
+ | Direct-runtime write request (wire, `box.direct()` only) | 32 MiB | `SANDBOX_DIRECT_FILE_REQUEST_MAX_BYTES` | `PAYLOAD_TOO_LARGE` |
816
+ | Multipart upload (`fs.upload`) | 5 MiB | `FILE_MULTIPART_UPLOAD_MAX_BYTES` | `FILE_TOO_LARGE` |
817
+ | Chunked upload part (`fs.uploadData`, internal) | 768 KiB | `FILE_UPLOAD_PART_MAX_BYTES` | — (server-sized, transparent to callers) |
818
+ | Chunked upload session (`fs.uploadData`) | 64 MiB | `FILE_UPLOAD_SESSION_MAX_BYTES` | `PAYLOAD_TOO_LARGE` |
819
+
820
+ All constants are exported from `@tangle-network/runtime-contracts`. Use
821
+ `fs.uploadData` for anything above the single-shot caps. `PAYLOAD_TOO_LARGE`
822
+ means split the delivery (use `fs.uploadData` instead of one request);
823
+ `FILE_TOO_LARGE` means the content itself exceeds what that surface accepts,
824
+ no matter how it's delivered.
825
+
730
826
  ### Snapshots
731
827
 
732
828
  #### `box.snapshot(options?)`
@@ -1,6 +1,6 @@
1
- import { D as CodeLanguage, E as CodeExecutionResult, T as CodeExecutionOptions, k as CodeResultPart } from "../types-yNRVRww-.js";
2
- import { n as SandboxInstance } from "../sandbox-BvNEOnIl.js";
3
- import { i as SandboxClient } from "../client-C-4NcZoa.js";
1
+ import { D as CodeExecutionOptions, O as CodeExecutionResult, j as CodeResultPart, k as CodeLanguage } from "../types-LNLeU9Ub.js";
2
+ import { n as SandboxInstance } from "../sandbox-B9P4tyt4.js";
3
+ import { i as SandboxClient } from "../client-CSaBJ8wD.js";
4
4
  import * as _$_modelcontextprotocol_sdk_server_index_js0 from "@modelcontextprotocol/sdk/server/index.js";
5
5
 
6
6
  //#region src/agent/tools/_specs.d.ts
@@ -1,5 +1,5 @@
1
- import { $n as SandboxFleetInfo, Ci as TokenRefreshHandler, Cn as PublishPublicTemplateOptions, Dn as ReconcileSandboxFleetsOptions, En as ReapExpiredSandboxFleetsResult, F as CreateSandboxFleetOptions, Gr as SecretsManager, Gt as IntelligenceReportWindow, Hn as SandboxClientConfig, Ht as IntelligenceReportBudget, I as CreateSandboxFleetTokenOptions, Jn as SandboxFleetCostEstimate, Kn as SandboxFleetArtifact, L as CreateSandboxFleetWithCoordinatorOptions, N as CreateIntelligenceReportOptions, On as ReconcileSandboxFleetsResult, P as CreateRequestOptions, R as CreateSandboxOptions, Sn as PublicTemplateVersionInfo, Tn as ReapExpiredSandboxFleetsOptions, Ut as IntelligenceReportCompareTo, Vt as IntelligenceReport, Wn as SandboxEnvironment, Xn as SandboxFleetDispatchResponse, Xt as ListSandboxOptions, Yt as ListSandboxFleetOptions, Zn as SandboxFleetDriverCapability, _ as BatchResult, _r as SandboxFleetWorkspaceRestoreResult, _t as FleetExecDispatchOptions, a as AttachSandboxFleetMachineOptions, ar as SandboxFleetManifest, bt as FleetPromptDispatchOptions, et as ExecOptions, g as BatchOptions, gn as PromptResult, gr as SandboxFleetWorkspaceReconcileResult, gt as FleetDispatchStreamOptions, hn as PromptOptions, ht as FleetDispatchResultBufferOptions, ki as UsageInfo, lr as SandboxFleetToken, m as BatchEvent, mn as PromptInputPart, mr as SandboxFleetUsage, mt as FleetDispatchResultBuffer, oi as SshKeysManager, pr as SandboxFleetTraceOptions, pt as FleetDispatchCancelResult, qn as SandboxFleetArtifactSpec, rr as SandboxFleetMachineRecord, sr as SandboxFleetOperationsSummary, tt as ExecResult, ui as SubscriptionInfo, ur as SandboxFleetTraceBundle, v as BatchRunOptions, vr as SandboxFleetWorkspaceSnapshotResult, vt as FleetExecDispatchResult, wn as PublishPublicTemplateVersionOptions, wr as SandboxProfileSummary, x as BatchTask, xn as PublicTemplateInfo, xt as FleetPromptDispatchResult, y as BatchRunRequest, yr as SandboxInfo, yt as FleetMachineId } from "./types-yNRVRww-.js";
2
- import { a as TraceExportSink, i as TraceExportResult, n as SandboxInstance, t as HttpClient } from "./sandbox-BvNEOnIl.js";
1
+ import { $n as SandboxFleetDriverCapability, An as ReconcileSandboxFleetsResult, B as CreateSandboxOptions, Cn as PublicTemplateInfo, Ct as FleetPromptDispatchResult, Dn as ReapExpiredSandboxFleetsOptions, En as PublishPublicTemplateVersionOptions, Er as SandboxProfileSummary, F as CreateIntelligenceReportOptions, Gt as IntelligenceReportCompareTo, I as CreateRequestOptions, Jn as SandboxFleetArtifact, Kn as SandboxEnvironment, L as CreateSandboxFleetOptions, On as ReapExpiredSandboxFleetsResult, Qn as SandboxFleetDispatchResponse, Qt as ListSandboxOptions, R as CreateSandboxFleetTokenOptions, St as FleetPromptDispatchOptions, Ti as TokenRefreshHandler, Tn as PublishPublicTemplateOptions, Ut as IntelligenceReport, Wn as SandboxClientConfig, Wt as IntelligenceReportBudget, Xn as SandboxFleetCostEstimate, Yn as SandboxFleetArtifactSpec, Zt as ListSandboxFleetOptions, _ as BatchResult, _n as PromptOptions, _t as FleetDispatchResultBufferOptions, a as AttachSandboxFleetMachineOptions, ar as SandboxFleetMachineRecord, br as SandboxFleetWorkspaceSnapshotResult, bt as FleetExecDispatchResult, ci as SshKeysManager, dr as SandboxFleetToken, fi as SubscriptionInfo, fr as SandboxFleetTraceBundle, g as BatchOptions, gn as PromptInputPart, gr as SandboxFleetUsage, gt as FleetDispatchResultBuffer, hr as SandboxFleetTraceOptions, ht as FleetDispatchCancelResult, ji as UsageInfo, kn as ReconcileSandboxFleetsOptions, lr as SandboxFleetOperationsSummary, m as BatchEvent, nt as ExecOptions, qr as SecretsManager, qt as IntelligenceReportWindow, rt as ExecResult, sr as SandboxFleetManifest, tr as SandboxFleetInfo, v as BatchRunOptions, vn as PromptResult, vr as SandboxFleetWorkspaceReconcileResult, vt as FleetDispatchStreamOptions, wn as PublicTemplateVersionInfo, x as BatchTask, xr as SandboxInfo, xt as FleetMachineId, y as BatchRunRequest, yr as SandboxFleetWorkspaceRestoreResult, yt as FleetExecDispatchOptions, z as CreateSandboxFleetWithCoordinatorOptions } from "./types-LNLeU9Ub.js";
2
+ import { a as TraceExportSink, i as TraceExportResult, n as SandboxInstance, t as HttpClient } from "./sandbox-B9P4tyt4.js";
3
3
 
4
4
  //#region src/lib/sse-parser.d.ts
5
5
  /**
@@ -1,5 +1,6 @@
1
- import { a as NotFoundError, c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, p as parseErrorResponse, t as AuthError } from "./errors-wd266B9Q.js";
2
- import { d as exportTraceBundle, g as parseSSEStream, l as normalizeConnection, m as encodePromptForWire, n as normalizeStartupDiagnostics, t as SandboxInstance } from "./sandbox-CIHssZCH.js";
1
+ import { a as NotFoundError, c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, o as PartialFailureError, p as parseErrorResponse, t as AuthError } from "./errors-wd266B9Q.js";
2
+ import { d as exportTraceBundle, g as parseSSEStream, h as encodeTextForWire, l as normalizeConnection, m as encodePromptForWire, n as normalizeStartupDiagnostics, t as SandboxInstance } from "./sandbox-KiKLRbR_.js";
3
+ import { FILE_DECODED_WRITE_MAX_BYTES, SANDBOX_PROXY_REQUEST_MAX_BYTES } from "@tangle-network/runtime-contracts";
3
4
  //#region src/resources.ts
4
5
  /**
5
6
  * Compute tiers, ordered smallest → largest. Defined HERE (not imported from
@@ -946,6 +947,338 @@ function getMachineRole(info) {
946
947
  return value === "coordinator" || value === "worker" ? value : void 0;
947
948
  }
948
949
  //#endregion
950
+ //#region src/profile-file-mounts.ts
951
+ const PROFILE_FILE_MOUNT_B64_CHUNK_CHARS = 3e3;
952
+ const PROFILE_FILE_MOUNT_MAX_RETRIES = 4;
953
+ const PROFILE_FILE_MOUNT_RETRY_BASE_MS = 250;
954
+ const PROFILE_FILE_MOUNT_RETRY_MAX_MS = 2e3;
955
+ const PROFILE_FILE_MOUNT_EXEC_TIMEOUT_MS = 3e4;
956
+ const PROFILE_FILE_MOUNT_PACE_MS = 150;
957
+ const PROFILE_BIN_DIR_RE = /(^|\/)(s?bin)\//;
958
+ /**
959
+ * Wire budget for a single-shot `POST /files/write` of a mount.
960
+ * Materialization runs over whatever connection the box has — on the
961
+ * default create() path that is the GATEWAY, whose wire cap
962
+ * ({@link SANDBOX_PROXY_REQUEST_MAX_BYTES}, 1 MiB) binds long before the
963
+ * sidecar's 5 MiB decoded cap. The routing decision measures the ACTUAL
964
+ * serialized content (JSON escaping inflates unevenly — a NUL byte
965
+ * becomes the six characters `\u0000`, a backslash doubles — so no fixed
966
+ * inflation factor is safe), leaving headroom for the request envelope
967
+ * (path up to 4 KiB, mode, encoding, braces). Anything that doesn't fit
968
+ * rides chunked upload, whose parts are sized for the gateway.
969
+ */
970
+ const PROFILE_FILE_WRITE_WIRE_BUDGET_BYTES = SANDBOX_PROXY_REQUEST_MAX_BYTES - 8 * 1024;
971
+ function fitsSingleShotWrite(content, contentBytes) {
972
+ if (contentBytes > FILE_DECODED_WRITE_MAX_BYTES) return false;
973
+ if (contentBytes * 6 <= PROFILE_FILE_WRITE_WIRE_BUDGET_BYTES) return true;
974
+ return new TextEncoder().encode(JSON.stringify(content)).byteLength <= PROFILE_FILE_WRITE_WIRE_BUDGET_BYTES;
975
+ }
976
+ const DEFAULT_PROFILE_FILE_API_ABSOLUTE_PREFIXES = [
977
+ ["/home", "agent"].join("/"),
978
+ ["/home", "user"].join("/"),
979
+ "/tmp",
980
+ "/output"
981
+ ];
982
+ function inlineContent(mount) {
983
+ const resource = mount.resource;
984
+ if (resource.kind !== "inline") throw new Error(`materializeProfileFileMounts: ${mount.path} requires an inline resource`);
985
+ if (typeof resource.content !== "string") throw new Error(`materializeProfileFileMounts: ${mount.path} inline resource requires string content`);
986
+ return resource.content;
987
+ }
988
+ /**
989
+ * Synchronous pre-flight validation of deferred inline mounts: the same
990
+ * content/path rules {@link materializeProfileFileMounts} enforces, runnable
991
+ * BEFORE a sandbox is provisioned. create() calls this ahead of its POST so
992
+ * client-detectable bad input (traversal paths, control characters,
993
+ * non-string content) fails without paying for a sandbox that would only be
994
+ * orphaned by the post-provision throw.
995
+ */
996
+ function validateDeferredProfileFileMounts(files) {
997
+ for (const mount of files) {
998
+ inlineContent(mount);
999
+ validateProfileFilePath(mount.path);
1000
+ }
1001
+ }
1002
+ function splitInlineProfileFileMounts(profile) {
1003
+ const files = profile.resources?.files ?? [];
1004
+ const deferredFiles = [];
1005
+ const keptFiles = [];
1006
+ for (const mount of files) if (mount.resource.kind === "inline") deferredFiles.push(mount);
1007
+ else keptFiles.push(mount);
1008
+ if (deferredFiles.length === 0) return {
1009
+ leanProfile: profile,
1010
+ deferredFiles
1011
+ };
1012
+ return {
1013
+ leanProfile: {
1014
+ ...profile,
1015
+ resources: {
1016
+ ...profile.resources ?? {},
1017
+ files: keptFiles
1018
+ }
1019
+ },
1020
+ deferredFiles
1021
+ };
1022
+ }
1023
+ function shellSingleQuote(value) {
1024
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1025
+ }
1026
+ function shellPath(path) {
1027
+ if (path === "~") return "\"$HOME\"";
1028
+ if (path.startsWith("~/")) {
1029
+ const rest = path.slice(2);
1030
+ return rest ? `"$HOME"/${shellSingleQuote(rest)}` : "\"$HOME\"";
1031
+ }
1032
+ return shellSingleQuote(path);
1033
+ }
1034
+ function validateProfileFilePath(path) {
1035
+ if (!path) throw new Error("materializeProfileFileMounts: file path is required");
1036
+ if (path.endsWith("/")) throw new Error(`materializeProfileFileMounts: refusing directory-shaped file path ${path}`);
1037
+ if (hasControlCharacter(path)) throw new Error(`materializeProfileFileMounts: refusing unsafe file path ${JSON.stringify(path)}`);
1038
+ if (path === "~" || /^~[^/]/.test(path)) throw new Error(`materializeProfileFileMounts: refusing unsupported home path ${path}`);
1039
+ if (hasUnsafeFileApiSegment(path)) throw new Error(`materializeProfileFileMounts: refusing unsafe file path ${path}`);
1040
+ }
1041
+ function hasUnsafeFileApiSegment(path) {
1042
+ return path.split("/").some((segment) => segment === "." || segment === ".." || segment === ".sidecar");
1043
+ }
1044
+ function hasControlCharacter(value) {
1045
+ for (let i = 0; i < value.length; i++) {
1046
+ const code = value.charCodeAt(i);
1047
+ if (code < 32 || code === 127) return true;
1048
+ }
1049
+ return false;
1050
+ }
1051
+ function normalizeAbsolutePrefixes(prefixes) {
1052
+ return prefixes.map((prefix) => {
1053
+ const normalized = prefix.trim().replace(/\/+$/, "");
1054
+ if (!normalized.startsWith("/") || hasControlCharacter(normalized)) throw new Error(`materializeProfileFileMounts: fileApiAbsolutePrefixes must contain absolute paths; received ${JSON.stringify(prefix)}`);
1055
+ if (hasUnsafeFileApiSegment(normalized)) throw new Error(`materializeProfileFileMounts: fileApiAbsolutePrefixes contains unsafe path ${JSON.stringify(prefix)}`);
1056
+ return normalized;
1057
+ });
1058
+ }
1059
+ function isAllowedAbsoluteFileApiPath(path, prefixes) {
1060
+ for (const prefix of prefixes) if (path === prefix || path.startsWith(`${prefix}/`)) return true;
1061
+ return false;
1062
+ }
1063
+ function fileApiTarget(mount, absolutePrefixes) {
1064
+ if (mount.path.startsWith("~/")) return null;
1065
+ if (mount.path.startsWith("/")) return isAllowedAbsoluteFileApiPath(mount.path, absolutePrefixes) ? mount.path : null;
1066
+ if (mount.path.startsWith("~")) return null;
1067
+ if (mount.path.length === 0) return null;
1068
+ return mount.path;
1069
+ }
1070
+ function isExecutableProfileFile(mount) {
1071
+ return mount.executable ?? PROFILE_BIN_DIR_RE.test(mount.path);
1072
+ }
1073
+ function profileFileMode(mount) {
1074
+ return isExecutableProfileFile(mount) ? 493 : void 0;
1075
+ }
1076
+ function fileApiSupportsMode(box) {
1077
+ return box.fs?.supportsWriteMode === true;
1078
+ }
1079
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1080
+ var ProfileFileMountExecTimeoutError = class extends Error {
1081
+ constructor(timeoutMs) {
1082
+ super(`exec exceeded ${timeoutMs}ms`);
1083
+ this.name = "ProfileFileMountExecTimeoutError";
1084
+ }
1085
+ };
1086
+ function execWithTimeout(box, cmd, timeoutMs) {
1087
+ return new Promise((resolve, reject) => {
1088
+ let settled = false;
1089
+ const timer = setTimeout(() => {
1090
+ if (settled) return;
1091
+ settled = true;
1092
+ reject(new ProfileFileMountExecTimeoutError(timeoutMs));
1093
+ }, timeoutMs);
1094
+ box.exec(cmd, { timeoutMs }).then((res) => {
1095
+ if (settled) return;
1096
+ settled = true;
1097
+ clearTimeout(timer);
1098
+ resolve(res);
1099
+ }, (err) => {
1100
+ if (settled) return;
1101
+ settled = true;
1102
+ clearTimeout(timer);
1103
+ reject(err);
1104
+ });
1105
+ });
1106
+ }
1107
+ const TRANSIENT_EXEC_STATUS_CODES = new Set([
1108
+ 408,
1109
+ 409,
1110
+ 425,
1111
+ 429,
1112
+ 500,
1113
+ 502,
1114
+ 503,
1115
+ 504
1116
+ ]);
1117
+ const TRANSIENT_EXEC_CODE_RE = /^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ECONNABORTED)$/i;
1118
+ const TRANSIENT_EXEC_MESSAGE_RE = /\b(408|409|425|429|500|502|503|504)\b|rate.?limit|too many requests|\bfetch failed\b|network error|connection reset|socket hang up|timed? out|service unavailable|bad gateway|gateway timeout|internal server error|\b(?:sidecar|runtime|exec(?:ution)?|terminal|sandbox|service|command(?:s)?|proxy)\b.{0,80}\bnot ready\b|\bnot ready\b.{0,80}\b(?:sidecar|runtime|exec(?:ution)?|terminal|sandbox|service|command(?:s)?|proxy)\b/i;
1119
+ function errorStatus(err) {
1120
+ const rawStatus = err.status ?? err.statusCode ?? (err.response && typeof err.response === "object" ? err.response.status : void 0);
1121
+ if (typeof rawStatus === "number") return rawStatus;
1122
+ if (typeof rawStatus === "string" && /^\d+$/.test(rawStatus)) return Number(rawStatus);
1123
+ }
1124
+ function retryAfterMs(err, seen = /* @__PURE__ */ new Set()) {
1125
+ if (!err || typeof err !== "object") return void 0;
1126
+ if (seen.has(err)) return void 0;
1127
+ seen.add(err);
1128
+ const e = err;
1129
+ if (typeof e.retryAfterMs === "number") return e.retryAfterMs;
1130
+ return retryAfterMs(e.cause, seen);
1131
+ }
1132
+ function isTransientExecError(err, seen = /* @__PURE__ */ new Set()) {
1133
+ if (!err || typeof err !== "object") return false;
1134
+ if (seen.has(err)) return false;
1135
+ seen.add(err);
1136
+ const e = err;
1137
+ const status = errorStatus(e);
1138
+ if (status !== void 0 && TRANSIENT_EXEC_STATUS_CODES.has(status)) return true;
1139
+ if (typeof e.code === "string") {
1140
+ if (TRANSIENT_EXEC_CODE_RE.test(e.code)) return true;
1141
+ if (/rate.?limit|too.?many.?requests|429|server.?error|service.?unavailable/i.test(e.code)) return true;
1142
+ }
1143
+ if (typeof e.message === "string" && TRANSIENT_EXEC_MESSAGE_RE.test(e.message)) return true;
1144
+ return isTransientExecError(e.cause, seen);
1145
+ }
1146
+ function transientExecError(err) {
1147
+ if (err instanceof ProfileFileMountExecTimeoutError) return { retryable: false };
1148
+ if (isTransientExecError(err)) return {
1149
+ retryable: true,
1150
+ retryAfterMs: retryAfterMs(err)
1151
+ };
1152
+ return { retryable: false };
1153
+ }
1154
+ async function sha256Hex(content) {
1155
+ const bytes = new TextEncoder().encode(content);
1156
+ const webSubtle = globalThis.crypto?.subtle;
1157
+ if (webSubtle) {
1158
+ const digest = await webSubtle.digest("SHA-256", bytes);
1159
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1160
+ }
1161
+ const { createHash } = await import("node:crypto");
1162
+ return createHash("sha256").update(content, "utf8").digest("hex");
1163
+ }
1164
+ async function execProfileFileMount(box, mount, content, options, execState) {
1165
+ const maxRetries = options.maxRetries ?? PROFILE_FILE_MOUNT_MAX_RETRIES;
1166
+ const path = mount.path;
1167
+ const b64 = encodeTextForWire(content);
1168
+ const b64Chunks = [];
1169
+ for (let i = 0; i < b64.length; i += PROFILE_FILE_MOUNT_B64_CHUNK_CHARS) b64Chunks.push(b64.slice(i, i + PROFILE_FILE_MOUNT_B64_CHUNK_CHARS));
1170
+ const expectedSha256 = await sha256Hex(content);
1171
+ const dir = path.replace(/\/[^/]*$/, "");
1172
+ const executable = isExecutableProfileFile(mount);
1173
+ const q = shellPath(path);
1174
+ const qb64 = shellPath(`${path}.b64`);
1175
+ const qtmp = shellPath(`${path}.tmp`);
1176
+ const qpartPrefix = shellPath(`${path}.b64.part.`);
1177
+ const homeGuard = path.startsWith("~/") ? `[ -n "$HOME" ] || { echo ${shellSingleQuote(`materializeProfileFileMounts: HOME is unset for ${path}`)} >&2; exit 1; }; ` : "";
1178
+ const run = async (cmd) => {
1179
+ for (let attempt = 0;; attempt++) {
1180
+ if (execState.started && options.paceMs > 0) await sleep(options.paceMs);
1181
+ execState.started = true;
1182
+ try {
1183
+ return await execWithTimeout(box, cmd, options.execTimeoutMs);
1184
+ } catch (err) {
1185
+ const { retryable, retryAfterMs: retryAfter } = transientExecError(err);
1186
+ if (retryable && attempt < maxRetries) {
1187
+ const backoff = Math.min(PROFILE_FILE_MOUNT_RETRY_BASE_MS * 2 ** attempt, PROFILE_FILE_MOUNT_RETRY_MAX_MS);
1188
+ await sleep(Math.min(retryAfter ?? backoff, PROFILE_FILE_MOUNT_RETRY_MAX_MS));
1189
+ continue;
1190
+ }
1191
+ throw new Error(`materializeProfileFileMounts: exec failed for ${path}`, { cause: err });
1192
+ }
1193
+ }
1194
+ };
1195
+ const step = async (cmd) => {
1196
+ const result = await run(`${homeGuard}${cmd}`);
1197
+ if (result.exitCode !== 0) throw new Error(`materializeProfileFileMounts: failed to write ${path} (exit ${result.exitCode}): ${result.stderr.slice(0, 500)}`);
1198
+ };
1199
+ await step(dir ? `mkdir -p ${shellPath(dir)}` : ":");
1200
+ for (let i = 0; i < b64Chunks.length; i++) {
1201
+ const slice = b64Chunks[i];
1202
+ if (slice === void 0) continue;
1203
+ await step(`printf '%s' '${slice}' > ${shellPath(`${path}.b64.part.${i}`)}`);
1204
+ }
1205
+ const chmod = executable ? `chmod +x ${q} || exit 1; ` : "";
1206
+ const checksumMismatch = shellSingleQuote(`materializeProfileFileMounts: checksum mismatch for ${path}`);
1207
+ await step(`cleanup() { ${`rm -f ${qb64} ${qtmp}; i=0; while [ "$i" -lt ${b64Chunks.length} ]; do rm -f ${qpartPrefix}$i; i=$((i+1)); done`}; }; trap cleanup EXIT; expected='${expectedSha256}'; if [ -f ${q} ] && [ "$(sha256sum ${q} | awk '{print $1}')" = "$expected" ]; then ${chmod}exit 0; fi; : > ${qb64} && i=0; while [ "$i" -lt ${b64Chunks.length} ]; do cat ${qpartPrefix}$i >> ${qb64} || exit 1; i=$((i+1)); done && base64 -d ${qb64} > ${qtmp} && [ "$(sha256sum ${qtmp} | awk '{print $1}')" = "$expected" ] || { echo ${checksumMismatch} >&2; exit 1; }; mv ${qtmp} ${q} && ${executable ? `chmod +x ${q} && ` : ""}[ "$(sha256sum ${q} | awk '{print $1}')" = "$expected" ] || { echo ${checksumMismatch} >&2; exit 1; }`);
1208
+ }
1209
+ async function materializeProfileFileMounts(box, files, options = {}) {
1210
+ const execTimeoutMs = options.execTimeoutMs ?? PROFILE_FILE_MOUNT_EXEC_TIMEOUT_MS;
1211
+ const paceMs = options.paceMs ?? PROFILE_FILE_MOUNT_PACE_MS;
1212
+ const maxRetries = options.maxRetries ?? PROFILE_FILE_MOUNT_MAX_RETRIES;
1213
+ const absolutePrefixes = normalizeAbsolutePrefixes(options.fileApiAbsolutePrefixes ?? DEFAULT_PROFILE_FILE_API_ABSOLUTE_PREFIXES);
1214
+ const fs = box.fs;
1215
+ const fileApiAvailable = typeof fs?.writeMany === "function";
1216
+ const modeAwareFileApi = fileApiSupportsMode(box);
1217
+ const uploadDataFn = fs?.uploadData;
1218
+ const viaFileApi = [];
1219
+ const viaExec = [];
1220
+ const viaUpload = [];
1221
+ for (const mount of files) {
1222
+ const content = inlineContent(mount);
1223
+ validateProfileFilePath(mount.path);
1224
+ const target = fileApiAvailable ? fileApiTarget(mount, absolutePrefixes) : null;
1225
+ const executable = isExecutableProfileFile(mount);
1226
+ if (target !== null && (!executable || modeAwareFileApi)) {
1227
+ const mode = profileFileMode(mount);
1228
+ const contentBytes = new TextEncoder().encode(content).byteLength;
1229
+ if (!fitsSingleShotWrite(content, contentBytes)) {
1230
+ if (typeof uploadDataFn !== "function") throw new Error(`materializeProfileFileMounts: ${mount.path} (${contentBytes} bytes decoded) does not fit a gateway-safe single-shot write once JSON-serialized, and the box does not expose fs.uploadData for chunked delivery`);
1231
+ viaUpload.push({
1232
+ path: target,
1233
+ content,
1234
+ ...mode !== void 0 ? { mode } : {}
1235
+ });
1236
+ continue;
1237
+ }
1238
+ viaFileApi.push({
1239
+ path: target,
1240
+ content,
1241
+ ...mode !== void 0 ? { mode } : {}
1242
+ });
1243
+ } else viaExec.push({
1244
+ mount,
1245
+ content
1246
+ });
1247
+ }
1248
+ if (viaFileApi.length > 0) try {
1249
+ await fs?.writeMany(viaFileApi, {
1250
+ paceMs,
1251
+ maxRetries
1252
+ });
1253
+ } catch (err) {
1254
+ throw new Error("materializeProfileFileMounts: file API batch write failed", { cause: err });
1255
+ }
1256
+ if (viaUpload.length > 0) {
1257
+ if (typeof uploadDataFn !== "function") throw new Error("materializeProfileFileMounts: viaUpload entries exist but fs.uploadData is unavailable");
1258
+ for (const item of viaUpload) try {
1259
+ await uploadDataFn(item.path, item.content, {
1260
+ ...item.mode !== void 0 ? { mode: item.mode } : {},
1261
+ paceMs,
1262
+ maxRetries
1263
+ });
1264
+ } catch (err) {
1265
+ throw new Error(`materializeProfileFileMounts: uploadData failed for ${item.path}`, { cause: err });
1266
+ }
1267
+ }
1268
+ const execState = { started: false };
1269
+ for (const item of viaExec) await execProfileFileMount(box, item.mount, item.content, {
1270
+ execTimeoutMs,
1271
+ paceMs,
1272
+ maxRetries
1273
+ }, execState);
1274
+ return {
1275
+ written: viaFileApi.length + viaExec.length + viaUpload.length,
1276
+ viaFileApi: viaFileApi.length,
1277
+ viaExec: viaExec.length,
1278
+ viaUpload: viaUpload.length
1279
+ };
1280
+ }
1281
+ //#endregion
949
1282
  //#region src/session-events.ts
950
1283
  async function broadcastSessionEvent(client, sessionId, event) {
951
1284
  if (!sessionId.trim()) throw new ValidationError("Session ID is required");
@@ -980,11 +1313,6 @@ async function broadcastSessionAgentEvent(client, sessionId, event) {
980
1313
  }
981
1314
  //#endregion
982
1315
  //#region src/client.ts
983
- /**
984
- * Sandbox Client
985
- *
986
- * Main client class for interacting with the Sandbox API.
987
- */
988
1316
  const DEFAULT_TIMEOUT_MS = 3e4;
989
1317
  /**
990
1318
  * Default time budget for `create()` — the full create-and-reach-running
@@ -1326,6 +1654,22 @@ var SandboxClient = class {
1326
1654
  ...backend,
1327
1655
  type: backend.type ?? "opencode"
1328
1656
  } : void 0;
1657
+ const autoMaterializeProfileFiles = options?.autoMaterializeProfileFiles ?? true;
1658
+ let deferredProfileFiles = [];
1659
+ let profileFilesFailOnError = true;
1660
+ let requestBackend = resolvedBackend;
1661
+ if (autoMaterializeProfileFiles && resolvedBackend?.profile && typeof resolvedBackend.profile === "object") {
1662
+ const split = splitInlineProfileFileMounts(resolvedBackend.profile);
1663
+ deferredProfileFiles = split.deferredFiles;
1664
+ if (deferredProfileFiles.length > 0) {
1665
+ requestBackend = {
1666
+ ...resolvedBackend,
1667
+ profile: split.leanProfile
1668
+ };
1669
+ profileFilesFailOnError = resolvedBackend.profile.resources?.failOnError !== false;
1670
+ if (profileFilesFailOnError) validateDeferredProfileFileMounts(deferredProfileFiles);
1671
+ }
1672
+ }
1329
1673
  const resources = normalizeSandboxResources(options?.resources, options?.driver);
1330
1674
  const postDeadline = AbortSignal.timeout(timeoutMs);
1331
1675
  const postSignal = requestOptions?.signal ? AbortSignal.any([requestOptions.signal, postDeadline]) : postDeadline;
@@ -1340,7 +1684,7 @@ var SandboxClient = class {
1340
1684
  bare: options?.bare,
1341
1685
  ...options?.publicEdge === false ? { publicEdge: false } : {},
1342
1686
  driver: options?.driver,
1343
- backend: resolvedBackend,
1687
+ backend: requestBackend,
1344
1688
  permissions: options?.permissions,
1345
1689
  env: options?.env,
1346
1690
  resources,
@@ -1401,7 +1745,7 @@ var SandboxClient = class {
1401
1745
  }
1402
1746
  if (!response) throw createTimeoutError();
1403
1747
  const data = await response.json();
1404
- const instance = new SandboxInstance(this, this.parseInfo(data), resolvedBackend);
1748
+ const instance = new SandboxInstance(this, this.parseInfo(data), requestBackend);
1405
1749
  if (instance.status === "provisioning" || instance.status === "pending") {
1406
1750
  const remainingMs = Math.max(0, deadline - Date.now());
1407
1751
  await instance.waitFor("running", {
@@ -1409,6 +1753,28 @@ var SandboxClient = class {
1409
1753
  signal: requestOptions?.signal
1410
1754
  });
1411
1755
  }
1756
+ if (deferredProfileFiles.length > 0) {
1757
+ if (instance.status !== "running") {
1758
+ const paths = deferredProfileFiles.map((file) => file.path);
1759
+ const message = `Sandbox ${instance.id} reached status "${instance.status}" before its ${deferredProfileFiles.length} deferred inline profile file mount(s) [${paths.join(", ")}] could be materialized. The sandbox was NOT deleted — inspect it, or recreate with autoMaterializeProfileFiles: false.`;
1760
+ if (!profileFilesFailOnError) {
1761
+ console.warn(`[sandbox.create] ${message}`);
1762
+ return instance;
1763
+ }
1764
+ throw new PartialFailureError(message, "PROFILE_FILES_MATERIALIZE_FAILED", 502, void 0, paths.map((path) => ({ path })));
1765
+ }
1766
+ try {
1767
+ await materializeProfileFileMounts(instance, deferredProfileFiles);
1768
+ } catch (err) {
1769
+ const paths = deferredProfileFiles.map((file) => file.path);
1770
+ const message = `Sandbox ${instance.id} is RUNNING but INCOMPLETELY PROVISIONED: materialization of its ${deferredProfileFiles.length} deferred inline profile file mount(s) failed partway; some or all of [${paths.join(", ")}] may be missing in the sandbox. The sandbox was NOT deleted — inspect it, retry writing the files yourself, or delete it once you've recovered what you need. Cause: ${err instanceof Error ? err.message : String(err)}`;
1771
+ if (!profileFilesFailOnError) {
1772
+ console.warn(`[sandbox.create] ${message}`);
1773
+ return instance;
1774
+ }
1775
+ throw new PartialFailureError(message, "PROFILE_FILES_MATERIALIZE_FAILED", 502, void 0, paths.map((path) => ({ path })));
1776
+ }
1777
+ }
1412
1778
  return instance;
1413
1779
  }
1414
1780
  /**
@@ -2298,4 +2664,4 @@ var TeamsClient = class {
2298
2664
  }
2299
2665
  };
2300
2666
  //#endregion
2301
- export { DEFAULT_SANDBOX_SIZE as a, resolveSandboxResources as c, SandboxFleetClient as i, sandboxResourcesForSize as l, SandboxClient as n, SANDBOX_SIZE_PRESETS as o, SandboxFleet as r, SANDBOX_SIZE_PRESET_NAMES as s, IntelligenceClient as t };
2667
+ export { validateDeferredProfileFileMounts as a, DEFAULT_SANDBOX_SIZE as c, resolveSandboxResources as d, sandboxResourcesForSize as f, splitInlineProfileFileMounts as i, SANDBOX_SIZE_PRESETS as l, SandboxClient as n, SandboxFleet as o, materializeProfileFileMounts as r, SandboxFleetClient as s, IntelligenceClient as t, SANDBOX_SIZE_PRESET_NAMES as u };
package/dist/core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Ar as SandboxStatus, Hn as SandboxClientConfig, R as CreateSandboxOptions, an as PreviewLinkInfo, en as NetworkConfig, et as ExecOptions, on as PreviewLinkManager, tt as ExecResult, yr as SandboxInfo } from "./types-yNRVRww-.js";
2
- import { n as SandboxInstance } from "./sandbox-BvNEOnIl.js";
3
- import { i as SandboxClient } from "./client-C-4NcZoa.js";
1
+ import { B as CreateSandboxOptions, Mr as SandboxStatus, Wn as SandboxClientConfig, cn as PreviewLinkManager, nn as NetworkConfig, nt as ExecOptions, rt as ExecResult, sn as PreviewLinkInfo, xr as SandboxInfo } from "./types-LNLeU9Ub.js";
2
+ import { n as SandboxInstance } from "./sandbox-B9P4tyt4.js";
3
+ import { i as SandboxClient } from "./client-CSaBJ8wD.js";
4
4
  import { a as NotFoundError, c as SandboxError, d as ServerError, f as StateError, i as NetworkError, l as SandboxErrorJson, m as ValidationError, n as CapabilityError, o as PartialFailureError, p as TimeoutError, s as QuotaError, t as AuthError, u as SandboxFailureDetail } from "./errors-CNCz3Ms9.js";
5
5
  export { AuthError, CapabilityError, type CreateSandboxOptions, type ExecOptions, type ExecResult, type NetworkConfig, NetworkError, NotFoundError, PartialFailureError, type PreviewLinkInfo, type PreviewLinkManager, QuotaError, SandboxClient as Sandbox, SandboxClient, type SandboxClientConfig, SandboxError, type SandboxErrorJson, type SandboxFailureDetail, type SandboxInfo, SandboxInstance, type SandboxStatus, ServerError, StateError, TimeoutError, ValidationError };
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { a as NotFoundError, c as SandboxError, d as TimeoutError, f as ValidationError, i as NetworkError, l as ServerError, n as CapabilityError, o as PartialFailureError, s as QuotaError, t as AuthError, u as StateError } from "./errors-wd266B9Q.js";
2
- import { t as SandboxInstance } from "./sandbox-CIHssZCH.js";
3
- import { n as SandboxClient } from "./client-CL_cbjU4.js";
2
+ import { t as SandboxInstance } from "./sandbox-KiKLRbR_.js";
3
+ import { n as SandboxClient } from "./client-_OBj4sNO.js";
4
4
  export { AuthError, CapabilityError, NetworkError, NotFoundError, PartialFailureError, QuotaError, SandboxClient as Sandbox, SandboxClient, SandboxError, SandboxInstance, ServerError, StateError, TimeoutError, ValidationError };
@@ -1,5 +1,5 @@
1
- import { R as CreateSandboxOptions } from "./types-yNRVRww-.js";
2
- import { n as SandboxInstance, t as HttpClient } from "./sandbox-BvNEOnIl.js";
1
+ import { B as CreateSandboxOptions } from "./types-LNLeU9Ub.js";
2
+ import { n as SandboxInstance, t as HttpClient } from "./sandbox-B9P4tyt4.js";
3
3
 
4
4
  //#region src/tangle/abi.d.ts
5
5
  /**