@tangle-network/sandbox 0.10.4 → 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.
@@ -1,5 +1,6 @@
1
- import { a as NotFoundError, d as TimeoutError, f as ValidationError, i as NetworkError, p as parseErrorResponse, t as AuthError } from "./errors-DSz87Rkk.js";
2
- import { d as exportTraceBundle, g as parseSSEStream, l as normalizeConnection, m as encodePromptForWire, n as normalizeStartupDiagnostics, t as SandboxInstance } from "./sandbox-Be9WAqSg.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
@@ -100,7 +101,7 @@ const DEFAULT_MACHINE_LIFETIME_SECONDS = 3600;
100
101
  const DEFAULT_MAX_CONCURRENT_CREATES = 4;
101
102
  const MAX_CONCURRENT_ARTIFACT_READS = 8;
102
103
  const DEFAULT_ARTIFACT_ROOT = "/workspace";
103
- const DEFAULT_MAX_ARTIFACT_BYTES = 25 * 1024 * 1024;
104
+ const DEFAULT_MAX_ARTIFACT_BYTES = 10 * 1024 * 1024;
104
105
  const MAX_MACHINE_ID_LENGTH = 64;
105
106
  const machineIdPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
106
107
  function createFleetId() {
@@ -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
@@ -997,6 +1325,41 @@ const DEFAULT_TIMEOUT_MS = 3e4;
997
1325
  */
998
1326
  const DEFAULT_CREATE_TIMEOUT_MS = 12e4;
999
1327
  const DEFAULT_GPU_CREATE_TIMEOUT_MS = 900 * 1e3;
1328
+ const CREATE_POST_MAX_ATTEMPTS = 5;
1329
+ const CREATE_RETRY_BASE_DELAY_MS = 250;
1330
+ const CREATE_RETRY_MAX_DELAY_MS = 4e3;
1331
+ const RETRYABLE_CREATE_STATUS_CODES = new Set([
1332
+ 408,
1333
+ 425,
1334
+ 429,
1335
+ 500,
1336
+ 502,
1337
+ 503,
1338
+ 504
1339
+ ]);
1340
+ function isRetryableCreateError(error) {
1341
+ if (error instanceof NetworkError) return true;
1342
+ return error instanceof SandboxError && error.status !== void 0 && RETRYABLE_CREATE_STATUS_CODES.has(error.status);
1343
+ }
1344
+ function createRetryDelayMs(error, failedAttempt) {
1345
+ if (error instanceof SandboxError && error.retryAfterMs !== void 0) return error.retryAfterMs;
1346
+ return Math.min(CREATE_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, failedAttempt - 1), CREATE_RETRY_MAX_DELAY_MS);
1347
+ }
1348
+ async function waitForCreateRetry(delayMs, signal) {
1349
+ if (signal.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("Sandbox create aborted");
1350
+ if (delayMs <= 0) return;
1351
+ await new Promise((resolve, reject) => {
1352
+ const timeoutId = setTimeout(() => {
1353
+ signal.removeEventListener("abort", onAbort);
1354
+ resolve();
1355
+ }, delayMs);
1356
+ const onAbort = () => {
1357
+ clearTimeout(timeoutId);
1358
+ reject(signal.reason ?? /* @__PURE__ */ new Error("Sandbox create aborted"));
1359
+ };
1360
+ signal.addEventListener("abort", onAbort, { once: true });
1361
+ });
1362
+ }
1000
1363
  /**
1001
1364
  * Extra grace the SDK's client-side batch deadline gets on top of
1002
1365
  * the server-side `timeoutMs`. The server's clock starts when the
@@ -1291,65 +1654,98 @@ var SandboxClient = class {
1291
1654
  ...backend,
1292
1655
  type: backend.type ?? "opencode"
1293
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
+ }
1294
1673
  const resources = normalizeSandboxResources(options?.resources, options?.driver);
1295
1674
  const postDeadline = AbortSignal.timeout(timeoutMs);
1296
1675
  const postSignal = requestOptions?.signal ? AbortSignal.any([requestOptions.signal, postDeadline]) : postDeadline;
1676
+ const createRequestBody = JSON.stringify({
1677
+ name: options?.name,
1678
+ ...resolvedEnvironment !== void 0 ? {
1679
+ environment: resolvedEnvironment,
1680
+ image: resolvedEnvironment
1681
+ } : {},
1682
+ git,
1683
+ tools: options?.tools,
1684
+ bare: options?.bare,
1685
+ ...options?.publicEdge === false ? { publicEdge: false } : {},
1686
+ driver: options?.driver,
1687
+ backend: requestBackend,
1688
+ permissions: options?.permissions,
1689
+ env: options?.env,
1690
+ resources,
1691
+ gpuLease: options?.gpuLease,
1692
+ sshEnabled: options?.sshEnabled,
1693
+ sshPublicKeys: options?.sshPublicKeys ?? (options?.sshPublicKey ? [options.sshPublicKey] : void 0),
1694
+ sshKeyIds: options?.sshKeyIds,
1695
+ webTerminalEnabled: options?.webTerminalEnabled,
1696
+ maxLifetimeSeconds: options?.maxLifetimeSeconds,
1697
+ idleTimeoutSeconds: options?.idleTimeoutSeconds,
1698
+ storage: options?.storage,
1699
+ ephemeral: options?.ephemeral,
1700
+ fromSnapshot: options?.fromSnapshot,
1701
+ fromSandboxId: options?.fromSandboxId,
1702
+ templateId: options?.templateId,
1703
+ publicTemplateId: options?.publicTemplateId,
1704
+ publicTemplateVersionId: options?.publicTemplateVersionId,
1705
+ secrets: options?.secrets,
1706
+ confidential: options?.confidential,
1707
+ metadata: options?.metadata,
1708
+ teamId: options?.teamId,
1709
+ capabilities: options?.capabilities,
1710
+ privacy: options?.privacy,
1711
+ egressPolicy: options?.egressPolicy,
1712
+ idempotencyKey
1713
+ });
1714
+ const createTimeoutError = () => new TimeoutError(timeoutMs, `Sandbox create did not complete within ${timeoutMs}ms. The provision may still be running server-side; retry create() with idempotencyKey="${idempotencyKey}" to join it.`);
1715
+ const waitBeforeRetry = async (error, failedAttempt) => {
1716
+ try {
1717
+ await waitForCreateRetry(createRetryDelayMs(error, failedAttempt), postSignal);
1718
+ } catch (waitError) {
1719
+ if (requestOptions?.signal?.aborted) throw waitError;
1720
+ throw createTimeoutError();
1721
+ }
1722
+ };
1297
1723
  let response;
1298
- try {
1299
- response = await this.fetch("/v1/sandboxes", {
1300
- method: "POST",
1301
- headers: { "Idempotency-Key": idempotencyKey },
1302
- signal: postSignal,
1303
- body: JSON.stringify({
1304
- name: options?.name,
1305
- ...resolvedEnvironment !== void 0 ? {
1306
- environment: resolvedEnvironment,
1307
- image: resolvedEnvironment
1308
- } : {},
1309
- git,
1310
- tools: options?.tools,
1311
- bare: options?.bare,
1312
- ...options?.publicEdge === false ? { publicEdge: false } : {},
1313
- driver: options?.driver,
1314
- backend: resolvedBackend,
1315
- permissions: options?.permissions,
1316
- env: options?.env,
1317
- resources,
1318
- gpuLease: options?.gpuLease,
1319
- sshEnabled: options?.sshEnabled,
1320
- sshPublicKeys: options?.sshPublicKeys ?? (options?.sshPublicKey ? [options.sshPublicKey] : void 0),
1321
- sshKeyIds: options?.sshKeyIds,
1322
- webTerminalEnabled: options?.webTerminalEnabled,
1323
- maxLifetimeSeconds: options?.maxLifetimeSeconds,
1324
- idleTimeoutSeconds: options?.idleTimeoutSeconds,
1325
- storage: options?.storage,
1326
- ephemeral: options?.ephemeral,
1327
- fromSnapshot: options?.fromSnapshot,
1328
- fromSandboxId: options?.fromSandboxId,
1329
- templateId: options?.templateId,
1330
- publicTemplateId: options?.publicTemplateId,
1331
- publicTemplateVersionId: options?.publicTemplateVersionId,
1332
- secrets: options?.secrets,
1333
- confidential: options?.confidential,
1334
- metadata: options?.metadata,
1335
- teamId: options?.teamId,
1336
- capabilities: options?.capabilities,
1337
- privacy: options?.privacy,
1338
- egressPolicy: options?.egressPolicy,
1339
- idempotencyKey
1340
- })
1341
- });
1342
- } catch (err) {
1343
- if (requestOptions?.signal?.aborted) throw err;
1344
- if (err instanceof TimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) throw new TimeoutError(timeoutMs, `Sandbox create did not complete within ${timeoutMs}ms. The provision may still be running server-side; retry create() with idempotencyKey="${idempotencyKey}" to join it.`);
1345
- throw err;
1346
- }
1347
- if (!response.ok) {
1724
+ for (let attempt = 1; attempt <= CREATE_POST_MAX_ATTEMPTS; attempt++) {
1725
+ try {
1726
+ response = await this.fetch("/v1/sandboxes", {
1727
+ method: "POST",
1728
+ headers: { "Idempotency-Key": idempotencyKey },
1729
+ signal: postSignal,
1730
+ body: createRequestBody
1731
+ });
1732
+ } catch (error) {
1733
+ if (requestOptions?.signal?.aborted) throw error;
1734
+ if (postDeadline.aborted || Date.now() >= deadline || error instanceof TimeoutError || error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) throw createTimeoutError();
1735
+ if (attempt >= CREATE_POST_MAX_ATTEMPTS || !isRetryableCreateError(error)) throw error;
1736
+ await waitBeforeRetry(error, attempt);
1737
+ continue;
1738
+ }
1739
+ if (response.ok) break;
1348
1740
  const body = await response.text();
1349
- throw parseErrorResponse(response.status, body, void 0, response.headers);
1741
+ const error = parseErrorResponse(response.status, body, void 0, response.headers);
1742
+ if (attempt >= CREATE_POST_MAX_ATTEMPTS || !isRetryableCreateError(error)) throw error;
1743
+ await waitBeforeRetry(error, attempt);
1744
+ response = void 0;
1350
1745
  }
1746
+ if (!response) throw createTimeoutError();
1351
1747
  const data = await response.json();
1352
- const instance = new SandboxInstance(this, this.parseInfo(data), resolvedBackend);
1748
+ const instance = new SandboxInstance(this, this.parseInfo(data), requestBackend);
1353
1749
  if (instance.status === "provisioning" || instance.status === "pending") {
1354
1750
  const remainingMs = Math.max(0, deadline - Date.now());
1355
1751
  await instance.waitFor("running", {
@@ -1357,6 +1753,28 @@ var SandboxClient = class {
1357
1753
  signal: requestOptions?.signal
1358
1754
  });
1359
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
+ }
1360
1778
  return instance;
1361
1779
  }
1362
1780
  /**
@@ -2246,4 +2664,4 @@ var TeamsClient = class {
2246
2664
  }
2247
2665
  };
2248
2666
  //#endregion
2249
- 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 };
@@ -1,2 +1,2 @@
1
- import { a as CollaborationClient, i as parseCollaborationDocumentId, n as buildCollaborationDocumentId, r as normalizeCollaborationPath, t as CollaborationFileBridge } from "../collaboration-QZXrXdIb.js";
1
+ import { a as CollaborationClient, i as parseCollaborationDocumentId, n as buildCollaborationDocumentId, r as normalizeCollaborationPath, t as CollaborationFileBridge } from "../collaboration-CcZE2xMl.js";
2
2
  export { CollaborationClient, CollaborationFileBridge, buildCollaborationDocumentId, normalizeCollaborationPath, parseCollaborationDocumentId };
@@ -1,4 +1,4 @@
1
- import { d as TimeoutError, i as NetworkError, p as parseErrorResponse } from "./errors-DSz87Rkk.js";
1
+ import { d as TimeoutError, i as NetworkError, p as parseErrorResponse } from "./errors-wd266B9Q.js";
2
2
  //#region src/collaboration/client.ts
3
3
  const DEFAULT_TIMEOUT_MS = 3e4;
4
4
  function normalizeBaseUrl(url) {
package/dist/core.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { $t as NetworkConfig, Bn as SandboxClientConfig, Or as SandboxStatus, R as CreateSandboxOptions, _r as SandboxInfo, an as PreviewLinkManager, et as ExecOptions, in as PreviewLinkInfo, tt as ExecResult } from "./types-BvSoEHz8.js";
2
- import { n as SandboxInstance } from "./sandbox-BtNWbfd3.js";
3
- import { i as SandboxClient } from "./client-B8i6qrQr.js";
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--P_nbLzM.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
+ 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
- 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-DSz87Rkk.js";
2
- import { t as SandboxInstance } from "./sandbox-Be9WAqSg.js";
3
- import { n as SandboxClient } from "./client-DTpHjPjc.js";
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-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 };
@@ -90,7 +90,7 @@ declare class PartialFailureError extends SandboxError {
90
90
  declare class ValidationError extends SandboxError {
91
91
  /** Field-level validation errors */
92
92
  readonly fields?: Record<string, string>;
93
- constructor(message: string, fields?: Record<string, string>, metadata?: SandboxErrorMetadata);
93
+ constructor(message: string, fields?: Record<string, string>, metadata?: SandboxErrorMetadata, code?: string);
94
94
  }
95
95
  /**
96
96
  * The sandbox is not in a valid state for the requested operation.
@@ -109,8 +109,8 @@ var PartialFailureError = class extends SandboxError {
109
109
  var ValidationError = class extends SandboxError {
110
110
  /** Field-level validation errors */
111
111
  fields;
112
- constructor(message, fields, metadata) {
113
- super(message, "VALIDATION_ERROR", 400, metadata);
112
+ constructor(message, fields, metadata, code = "VALIDATION_ERROR") {
113
+ super(message, code, 400, metadata);
114
114
  this.name = "ValidationError";
115
115
  this.fields = fields;
116
116
  }
@@ -255,7 +255,7 @@ function parseErrorResponse(status, body, context, headers) {
255
255
  if (isPartialFailureCode(code)) return new PartialFailureError(message, code, status, metadata, readFailures(data, errorObj));
256
256
  if (code === "EGRESS_PROXY_RECOVERY_REQUIRED") return new EgressProxyRecoveryError(message, metadata);
257
257
  switch (status) {
258
- case 400: return new ValidationError(message, data.fields, metadata);
258
+ case 400: return new ValidationError(message, data.fields, metadata, code);
259
259
  case 401: return new AuthError(message, metadata);
260
260
  case 404: return new NotFoundError(data.resourceType || "Resource", data.resourceId || "unknown", metadata);
261
261
  case 408: return new TimeoutError(data.timeoutMs || 3e4, message, metadata);
@@ -1,5 +1,5 @@
1
- import { R as CreateSandboxOptions } from "./types-BvSoEHz8.js";
2
- import { n as SandboxInstance, t as HttpClient } from "./sandbox-BtNWbfd3.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
  /**