@claxedo/sandbox-manager 0.10.3 → 0.10.4

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,6 +1,6 @@
1
1
  import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./contract";
2
2
  import { type SandboxDriverBilling } from "./driver-billing";
3
- import { sandboxDriverId, type SandboxDriverEnv, type SandboxDriverID } from "@claxedo/sandbox-contract";
3
+ import { sandboxDriverId, type SandboxDriverID } from "@claxedo/sandbox-contract";
4
4
  export { sandboxDriverId };
5
5
  export { sandboxMachineCostUsd, sandboxMachineRate, type SandboxDriverBilling, type SandboxMachineRate } from "./driver-billing";
6
6
  export type SandboxDriverCatalogEntry<ID extends string = SandboxDriverID> = {
@@ -50,4 +50,3 @@ export declare const localBrokeringTestDriverCatalogEntry: {
50
50
  };
51
51
  };
52
52
  export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
53
- export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
@@ -82,6 +82,9 @@ function trimToUndefined(value) {
82
82
  const trimmed = value.trim();
83
83
  return trimmed.length > 0 ? trimmed : void 0;
84
84
  }
85
+ function isEnabledFlag(value) {
86
+ return ["1", "true", "yes", "on"].includes(value?.trim().toLowerCase() ?? "");
87
+ }
85
88
  var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "agent37", "docker"];
86
89
  var sandboxDriverCredentialFields = {
87
90
  modal: [
@@ -112,11 +115,8 @@ var sandboxDriverLabels = {
112
115
  function isSandboxDriverID(input) {
113
116
  return !!input && sandboxDriverIds.includes(input);
114
117
  }
115
- function enabled(input) {
116
- return ["1", "true", "yes", "on"].includes(input?.trim().toLowerCase() ?? "");
117
- }
118
118
  function dockerSandboxDriverEnabled(env = process.env) {
119
- return enabled(env.CLAXEDO_ENABLE_DOCKER_SANDBOX) || enabled(env.CLAXEDO_DEV_DOCKER_SANDBOX);
119
+ return isEnabledFlag(env.CLAXEDO_ENABLE_DOCKER_SANDBOX) || isEnabledFlag(env.CLAXEDO_DEV_DOCKER_SANDBOX);
120
120
  }
121
121
  function sandboxDriverAuthValues(cfg, id, env = process.env) {
122
122
  if (id === "modal") {
@@ -351,11 +351,7 @@ var sandboxDriverCatalog = {
351
351
  }
352
352
  }
353
353
  };
354
- function dockerSandboxDriverEnabled2(env = process.env) {
355
- return dockerSandboxDriverEnabled(env);
356
- }
357
354
  export {
358
- dockerSandboxDriverEnabled2 as dockerSandboxDriverEnabled,
359
355
  localBrokeringTestDriverCatalogEntry,
360
356
  sandboxDriverCatalog,
361
357
  sandboxDriverId,
@@ -298,10 +298,8 @@ function isRecord(value) {
298
298
  function asRecord(value) {
299
299
  return isRecord(value) ? value : void 0;
300
300
  }
301
-
302
- // src/json.ts
303
- function num(input) {
304
- return typeof input === "number" ? input : void 0;
301
+ function asNumber(value) {
302
+ return typeof value === "number" ? value : void 0;
305
303
  }
306
304
 
307
305
  // src/lease-failure.ts
@@ -581,7 +579,7 @@ function driverErrorSignals(err) {
581
579
  return {
582
580
  // `response.status` is the axios shape; `statusCode` is what SDK-native
583
581
  // error classes expose instead.
584
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
582
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
585
583
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
586
584
  };
587
585
  }
@@ -44,7 +44,7 @@ export declare class BoatApiError extends Error {
44
44
  });
45
45
  }
46
46
  export declare function invalid(): never;
47
- export declare function requiredText(value: unknown): string;
47
+ export declare function responseString(value: unknown): string;
48
48
  export type BoatClientOptions = {
49
49
  apiKey: string;
50
50
  baseUrl?: string;
@@ -429,10 +429,8 @@ function isRecord(value) {
429
429
  function asRecord(value) {
430
430
  return isRecord(value) ? value : void 0;
431
431
  }
432
-
433
- // src/json.ts
434
- function num(input) {
435
- return typeof input === "number" ? input : void 0;
432
+ function asNumber(value) {
433
+ return typeof value === "number" ? value : void 0;
436
434
  }
437
435
 
438
436
  // ../claxedo-helpers/dist/deadline.mjs
@@ -497,7 +495,7 @@ function startLimitOf(body) {
497
495
  function invalid() {
498
496
  throw new BoatApiError("Boat API returned an invalid response", "invalid_response");
499
497
  }
500
- function requiredText(value) {
498
+ function responseString(value) {
501
499
  return typeof value === "string" ? value : invalid();
502
500
  }
503
501
  function requiredFlag(value) {
@@ -511,7 +509,7 @@ function isSandboxState(value) {
511
509
  }
512
510
  function sandbox(body) {
513
511
  const value = asRecord(body.sandbox) ?? invalid();
514
- const id = requiredText(value.id);
512
+ const id = responseString(value.id);
515
513
  if (!/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(id) || !isSandboxState(value.state)) invalid();
516
514
  return { id, state: value.state, hydrated: value.hydrated === true };
517
515
  }
@@ -634,8 +632,8 @@ function createBoatClient(options) {
634
632
  const body = await api(`${path(id)}/commands`, ["command.finished"], { method: "POST", body: input, timeoutMs });
635
633
  return {
636
634
  success: requiredFlag(body.success),
637
- stdout: requiredText(body.stdout),
638
- stderr: requiredText(body.stderr),
635
+ stdout: responseString(body.stdout),
636
+ stderr: responseString(body.stderr),
639
637
  exitCode: commandExitCode(body.exitCode),
640
638
  timedOut: requiredFlag(body.timedOut)
641
639
  };
@@ -653,7 +651,7 @@ function driverErrorSignals(err) {
653
651
  return {
654
652
  // `response.status` is the axios shape; `statusCode` is what SDK-native
655
653
  // error classes expose instead.
656
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
654
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
657
655
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
658
656
  };
659
657
  }
@@ -766,6 +764,13 @@ function machineBootScripts(boot) {
766
764
  };
767
765
  }
768
766
 
767
+ // ../claxedo-helpers/dist/index.mjs
768
+ function sleep(ms) {
769
+ return new Promise((resolve) => {
770
+ setTimeout(resolve, ms);
771
+ });
772
+ }
773
+
769
774
  // src/drivers/boat.ts
770
775
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
771
776
  var DEFAULT_WORKSPACE_DIR = "/workspace";
@@ -800,9 +805,6 @@ function archivedOrThrow(err) {
800
805
  if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
801
806
  hostGoneOrThrow(err);
802
807
  }
803
- function sleep(ms) {
804
- return new Promise((resolve) => setTimeout(resolve, ms));
805
- }
806
808
  function scrubbedTail(text, secrets) {
807
809
  return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
808
810
  }
@@ -1108,13 +1110,13 @@ var STATUSES = ["saving", "ready", "failed"];
1108
1110
  function namedSnapshot(value) {
1109
1111
  const snapshot = asRecord(value) ?? invalid();
1110
1112
  const status = STATUSES.find((known) => known === snapshot.status) ?? invalid();
1111
- return { name: requiredText(snapshot.name), status, createdAt: requiredText(snapshot.createdAt) };
1113
+ return { name: responseString(snapshot.name), status, createdAt: responseString(snapshot.createdAt) };
1112
1114
  }
1113
1115
  function share(value) {
1114
1116
  const entry = asRecord(value) ?? invalid();
1115
- const status = requiredText(entry.status);
1117
+ const status = responseString(entry.status);
1116
1118
  if (status !== "active" && status !== "revoked") invalid();
1117
- return { id: requiredText(entry.id), status, createdAt: requiredText(entry.createdAt) };
1119
+ return { id: responseString(entry.id), status, createdAt: responseString(entry.createdAt) };
1118
1120
  }
1119
1121
  function list(value, item) {
1120
1122
  return Array.isArray(value) ? value.map(item) : invalid();
@@ -1145,7 +1147,7 @@ function createBoatNamedSnapshotClient(options) {
1145
1147
  /** Boat shows the code only in this answer and keeps a hash of it. */
1146
1148
  async share(name) {
1147
1149
  const body = await api(`${path(name)}/shares`, ["snapshot.share.created"], { method: "POST" });
1148
- return { shareCode: requiredText(body.shareCode), share: share(body.share) };
1150
+ return { shareCode: responseString(body.shareCode), share: share(body.share) };
1149
1151
  },
1150
1152
  async shares(name) {
1151
1153
  return list((await api(`${path(name)}/shares`, ["snapshot.share.list"])).shares, share);
@@ -1164,9 +1166,6 @@ var DEFAULT_POLL_INTERVAL_MS = 2e3;
1164
1166
  var DEFAULT_HYDRATION_TIMEOUT_MS = 10 * 6e4;
1165
1167
  var DEFAULT_STOP_TIMEOUT_MS = 10 * 6e4;
1166
1168
  var DEFAULT_SAVE_TIMEOUT_MS = 10 * 6e4;
1167
- function sleep2(ms) {
1168
- return new Promise((resolve) => setTimeout(resolve, ms));
1169
- }
1170
1169
  async function boatRuntimeTemplateName(image, prefix = DEFAULT_PREFIX) {
1171
1170
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(image)));
1172
1171
  const name = `${prefix}${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, NAME_HASH_CHARS)}`;
@@ -1188,7 +1187,7 @@ async function pollBoat(label, timeoutMs, intervalMs, read) {
1188
1187
  const value = await read();
1189
1188
  if (value !== void 0) return value;
1190
1189
  if (Date.now() >= deadline) throw new Error(`${label} did not happen within ${Math.round(timeoutMs / 1e3)} s`);
1191
- await sleep2(intervalMs);
1190
+ await sleep(intervalMs);
1192
1191
  }
1193
1192
  }
1194
1193
  async function ensureBoatRuntimeTemplate(options) {
@@ -1236,7 +1235,7 @@ async function buildTemplate(options, name) {
1236
1235
  const lease = sandboxLease({ workspaceId, homeRegion: "auto", driver: "boat", epoch, sandboxId });
1237
1236
  const result = sandboxId ? await driver.resumeHost({ lease, ensure: ensure(epoch) }) : await driver.ensureHost(ensure(epoch));
1238
1237
  if (!("provisioning" in result)) return result;
1239
- await sleep2(result.retryAfterMs);
1238
+ await sleep(result.retryAfterMs);
1240
1239
  }
1241
1240
  };
1242
1241
  const archived = (id) => pollBoat(`Boat ${id} stop`, options.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS, interval, async () => (await sandboxes.get(id)).state === "archived" ? true : void 0);
@@ -397,10 +397,8 @@ function isRecord(value) {
397
397
  function asRecord(value) {
398
398
  return isRecord(value) ? value : void 0;
399
399
  }
400
-
401
- // src/json.ts
402
- function num(input) {
403
- return typeof input === "number" ? input : void 0;
400
+ function asNumber(value) {
401
+ return typeof value === "number" ? value : void 0;
404
402
  }
405
403
 
406
404
  // ../claxedo-helpers/dist/deadline.mjs
@@ -465,7 +463,7 @@ function startLimitOf(body) {
465
463
  function invalid() {
466
464
  throw new BoatApiError("Boat API returned an invalid response", "invalid_response");
467
465
  }
468
- function requiredText(value) {
466
+ function responseString(value) {
469
467
  return typeof value === "string" ? value : invalid();
470
468
  }
471
469
  function requiredFlag(value) {
@@ -479,7 +477,7 @@ function isSandboxState(value) {
479
477
  }
480
478
  function sandbox(body) {
481
479
  const value = asRecord(body.sandbox) ?? invalid();
482
- const id = requiredText(value.id);
480
+ const id = responseString(value.id);
483
481
  if (!/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(id) || !isSandboxState(value.state)) invalid();
484
482
  return { id, state: value.state, hydrated: value.hydrated === true };
485
483
  }
@@ -602,8 +600,8 @@ function createBoatClient(options) {
602
600
  const body = await api(`${path(id)}/commands`, ["command.finished"], { method: "POST", body: input, timeoutMs });
603
601
  return {
604
602
  success: requiredFlag(body.success),
605
- stdout: requiredText(body.stdout),
606
- stderr: requiredText(body.stderr),
603
+ stdout: responseString(body.stdout),
604
+ stderr: responseString(body.stderr),
607
605
  exitCode: commandExitCode(body.exitCode),
608
606
  timedOut: requiredFlag(body.timedOut)
609
607
  };
@@ -621,7 +619,7 @@ function driverErrorSignals(err) {
621
619
  return {
622
620
  // `response.status` is the axios shape; `statusCode` is what SDK-native
623
621
  // error classes expose instead.
624
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
622
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
625
623
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
626
624
  };
627
625
  }
@@ -723,6 +721,13 @@ function machineBootScripts(boot) {
723
721
  };
724
722
  }
725
723
 
724
+ // ../claxedo-helpers/dist/index.mjs
725
+ function sleep(ms) {
726
+ return new Promise((resolve) => {
727
+ setTimeout(resolve, ms);
728
+ });
729
+ }
730
+
726
731
  // src/drivers/boat.ts
727
732
  function formatBoatRuntimeTemplate(template) {
728
733
  return JSON.stringify({ image: template.image, shareCode: template.shareCode });
@@ -773,9 +778,6 @@ function archivedOrThrow(err) {
773
778
  if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
774
779
  hostGoneOrThrow(err);
775
780
  }
776
- function sleep(ms) {
777
- return new Promise((resolve) => setTimeout(resolve, ms));
778
- }
779
781
  function scrubbedTail(text, secrets) {
780
782
  return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
781
783
  }
@@ -254,6 +254,17 @@ var sandboxDriverCatalog = {
254
254
  }
255
255
  };
256
256
 
257
+ // ../claxedo-helpers/dist/deadline.mjs
258
+ async function withDeadlineSignal(timeoutMs, work, parent) {
259
+ const deadline = new AbortController();
260
+ const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
261
+ try {
262
+ return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
263
+ } finally {
264
+ clearTimeout(timer);
265
+ }
266
+ }
267
+
257
268
  // src/egress-policy.ts
258
269
  function brokeredSecretPlaceholder(name) {
259
270
  return `claxedo-broker:${name}`;
@@ -292,10 +303,11 @@ function isRecord(value) {
292
303
  function asRecord(value) {
293
304
  return isRecord(value) ? value : void 0;
294
305
  }
295
-
296
- // src/json.ts
297
- function text(input) {
298
- return typeof input === "string" && input.length > 0 ? input : void 0;
306
+ function isNonEmptyString(value) {
307
+ return typeof value === "string" && value.length > 0;
308
+ }
309
+ function nonEmptyString(value) {
310
+ return isNonEmptyString(value) ? value : void 0;
299
311
  }
300
312
 
301
313
  // src/runtime-env.ts
@@ -387,19 +399,6 @@ var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
387
399
  var DEFAULT_TIMEOUT_MS = 45e3;
388
400
  var DEFAULT_ENSURE_TIMEOUT_MS = 3e5;
389
401
  var DEFAULT_SNAPSHOT_TIMEOUT_MS = 18e4;
390
- function deadlineSignal(timeoutMs) {
391
- const controller = new AbortController();
392
- const timer = setTimeout(
393
- () => controller.abort(new DOMException("The operation timed out", "TimeoutError")),
394
- timeoutMs
395
- );
396
- return {
397
- signal: controller.signal,
398
- cleanup() {
399
- clearTimeout(timer);
400
- }
401
- };
402
- }
403
402
  var RUNTIME_HOME = "/home/claxedo";
404
403
  function sandboxIdFor(workspaceId) {
405
404
  return `claxedo-${workspaceId}`;
@@ -431,19 +430,17 @@ function createCloudflareSandboxDriver(options) {
431
430
  };
432
431
  async function call(sandboxId, action, body, method = "POST") {
433
432
  const url = `${base}/sandbox/${encodeURIComponent(sandboxId)}${action ? `/${action}` : ""}`;
434
- const deadline = deadlineSignal(action === "ensure-runtime" ? ensureTimeoutMs : action === "snapshot" ? snapshotTimeoutMs : timeoutMs);
435
- try {
433
+ const deadlineMs = action === "ensure-runtime" ? ensureTimeoutMs : action === "snapshot" ? snapshotTimeoutMs : timeoutMs;
434
+ return withDeadlineSignal(deadlineMs, async (signal) => {
436
435
  const res = await doFetch(url, {
437
436
  method,
438
437
  headers,
439
438
  ...method === "POST" ? { body: JSON.stringify(body) } : {},
440
- signal: deadline.signal
439
+ signal
441
440
  });
442
441
  const data = asRecord(await res.json().catch(() => ({}))) ?? {};
443
442
  return { status: res.status, data };
444
- } finally {
445
- deadline.cleanup();
446
- }
443
+ });
447
444
  }
448
445
  function workspaceDirectory(input) {
449
446
  return input.workspaceRoot ?? workspaceDir;
@@ -521,16 +518,16 @@ function createCloudflareSandboxDriver(options) {
521
518
  });
522
519
  if (!response) return { provisioning: true, retryAfterMs: 0 };
523
520
  const { status, data } = response;
524
- const runtimeUrl = text(data.url);
521
+ const runtimeUrl = nonEmptyString(data.url);
525
522
  if (status === 503 && data.error === "workspace-runtime did not become ready") {
526
523
  return { provisioning: true, retryAfterMs: 0 };
527
524
  }
528
- if (data.exited === true) throw new SandboxRuntimeBootError(text(data.error) ?? "The workspace runtime exited before it was ready");
525
+ if (data.exited === true) throw new SandboxRuntimeBootError(nonEmptyString(data.error) ?? "The workspace runtime exited before it was ready");
529
526
  if (status === 410 && isSandboxCheckpointUnusableCause(data.checkpointUnusable)) {
530
- throw new SandboxCheckpointUnusableError(data.checkpointUnusable, text(data.error) ?? "This workspace's saved files cannot be restored; choose Start fresh.");
527
+ throw new SandboxCheckpointUnusableError(data.checkpointUnusable, nonEmptyString(data.error) ?? "This workspace's saved files cannot be restored; choose Start fresh.");
531
528
  }
532
529
  if (status >= 400 || !runtimeUrl) {
533
- const reason = `Cloudflare ensure-runtime failed (${status}): ${text(data.error) ?? "no runtime url"}`;
530
+ const reason = `Cloudflare ensure-runtime failed (${status}): ${nonEmptyString(data.error) ?? "no runtime url"}`;
534
531
  throw status === 401 || status === 403 ? new SandboxProviderKeyRefusedError(reason) : new Error(reason);
535
532
  }
536
533
  if (net && asRecord(data.egress)?.contained !== true) {
@@ -574,12 +571,11 @@ function createCloudflareSandboxDriver(options) {
574
571
  //
575
572
  // A missing registry route or R2 binding cannot establish an empty inventory.
576
573
  async list() {
577
- const deadline = deadlineSignal(timeoutMs);
578
- try {
574
+ return withDeadlineSignal(timeoutMs, async (signal) => {
579
575
  const res = await doFetch(`${base}/sandboxes`, {
580
576
  method: "GET",
581
577
  headers,
582
- signal: deadline.signal
578
+ signal
583
579
  });
584
580
  if (res.status === 404 || res.status === 501) {
585
581
  throw new CloudflareSandboxListingUnsupportedError(`Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}); requires the /sandboxes registry route and SANDBOX_REGISTRY binding`);
@@ -588,11 +584,11 @@ function createCloudflareSandboxDriver(options) {
588
584
  if (!data) throw new Error("Invalid Cloudflare sandbox listing envelope");
589
585
  if (data.supported === false) {
590
586
  throw new CloudflareSandboxListingUnsupportedError(
591
- text(data.error) ?? `Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}) \u2014 deploy a Worker with the /sandboxes registry route and an R2 SANDBOX_REGISTRY binding`
587
+ nonEmptyString(data.error) ?? `Cloudflare sandbox Worker cannot enumerate sandboxes (${res.status}) \u2014 deploy a Worker with the /sandboxes registry route and an R2 SANDBOX_REGISTRY binding`
592
588
  );
593
589
  }
594
590
  if (!res.ok) {
595
- throw new Error(`Cloudflare sandbox listing failed (${res.status}): ${text(data.error) ?? "unknown error"}`);
591
+ throw new Error(`Cloudflare sandbox listing failed (${res.status}): ${nonEmptyString(data.error) ?? "unknown error"}`);
596
592
  }
597
593
  return registryEntries(data.sandboxes).flatMap((entry) => {
598
594
  const sandboxId = entry.sandboxId;
@@ -611,9 +607,7 @@ function createCloudflareSandboxDriver(options) {
611
607
  };
612
608
  return [target];
613
609
  });
614
- } finally {
615
- deadline.cleanup();
616
- }
610
+ });
617
611
  },
618
612
  metadata: {
619
613
  driverRunsIn: ["worker"],
@@ -635,14 +629,14 @@ function createCloudflareSandboxDriver(options) {
635
629
  async stop(target) {
636
630
  const { status, data } = await call(target.sandboxId, "stop", { epoch: target.epoch });
637
631
  if (status >= 400 || data.ok !== true) {
638
- throw new Error(`Cloudflare stop failed (${status}) for ${target.sandboxId}: ${text(data.error) ?? "not stopped"}`);
632
+ throw new Error(`Cloudflare stop failed (${status}) for ${target.sandboxId}: ${nonEmptyString(data.error) ?? "not stopped"}`);
639
633
  }
640
634
  },
641
635
  // The sandbox's object outlives its container and keeps the snapshots it took, so a container that is not running is a stopped host, never a gone one.
642
636
  async inspect(target) {
643
637
  const { status, data } = await call(target.sandboxId, "inspect", {});
644
638
  if (status < 200 || status >= 300 || typeof data.running !== "boolean") {
645
- throw new Error(`Cloudflare inspect failed (${status}) for ${target.sandboxId}: ${text(data.error) ?? "no container state"}`);
639
+ throw new Error(`Cloudflare inspect failed (${status}) for ${target.sandboxId}: ${nonEmptyString(data.error) ?? "no container state"}`);
646
640
  }
647
641
  return data.running ? "serving" : "stopped";
648
642
  },
@@ -654,9 +648,9 @@ function createCloudflareSandboxDriver(options) {
654
648
  },
655
649
  async snapshot(target) {
656
650
  const { status, data } = await call(target.sandboxId, "snapshot", {});
657
- const snapshotId = text(data.snapshotId);
651
+ const snapshotId = nonEmptyString(data.snapshotId);
658
652
  if (status >= 400 || !snapshotId) {
659
- throw new Error(`Cloudflare snapshot failed (${status}): ${text(data.error) ?? "no snapshot id"}`);
653
+ throw new Error(`Cloudflare snapshot failed (${status}): ${nonEmptyString(data.error) ?? "no snapshot id"}`);
660
654
  }
661
655
  return { snapshotId };
662
656
  }
@@ -368,10 +368,8 @@ var sandboxDriverCatalog = {
368
368
  function isRecord(value) {
369
369
  return value !== null && typeof value === "object" && !Array.isArray(value);
370
370
  }
371
-
372
- // src/json.ts
373
- function num(input) {
374
- return typeof input === "number" ? input : void 0;
371
+ function asNumber(value) {
372
+ return typeof value === "number" ? value : void 0;
375
373
  }
376
374
 
377
375
  // src/drivers/transient-error.ts
@@ -384,7 +382,7 @@ function driverErrorSignals(err) {
384
382
  return {
385
383
  // `response.status` is the axios shape; `statusCode` is what SDK-native
386
384
  // error classes expose instead.
387
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
385
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
388
386
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
389
387
  };
390
388
  }
@@ -406,6 +404,18 @@ async function withDeadlineSignal(timeoutMs, work, parent) {
406
404
  }
407
405
  }
408
406
 
407
+ // ../claxedo-helpers/dist/index.mjs
408
+ function sleep(ms) {
409
+ return new Promise((resolve) => {
410
+ setTimeout(resolve, ms);
411
+ });
412
+ }
413
+
414
+ // ../claxedo-helpers/dist/env.mjs
415
+ function isEnabledFlag(value) {
416
+ return ["1", "true", "yes", "on"].includes(value?.trim().toLowerCase() ?? "");
417
+ }
418
+
409
419
  // src/drivers/docker.ts
410
420
  var execFileAsync = promisify(execFile);
411
421
  var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
@@ -416,7 +426,7 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
416
426
  var DOCKER_LABEL = "claxedo";
417
427
  var AUTH_STAGE_DIR = "/tmp/claxedo-auth";
418
428
  function dockerSandboxSyncLocalAuth(env = process.env) {
419
- return enabledFlag(env.CLAXEDO_DOCKER_SANDBOX_SYNC_LOCAL_AUTH);
429
+ return isEnabledFlag(env.CLAXEDO_DOCKER_SANDBOX_SYNC_LOCAL_AUTH);
420
430
  }
421
431
  function dockerSandboxAuthHome(env = process.env) {
422
432
  return trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_AUTH_HOME) ?? trimToUndefined(env.HOME) ?? os.homedir();
@@ -453,9 +463,6 @@ function dockerLocalAuthCandidates(home) {
453
463
  }
454
464
  ];
455
465
  }
456
- function enabledFlag(input) {
457
- return ["1", "true", "yes", "on"].includes(input?.trim().toLowerCase() ?? "");
458
- }
459
466
  function nameFor(workspaceId) {
460
467
  const cleanId = workspaceId.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
461
468
  return `claxedo-${cleanId || "workspace"}-${Date.now().toString(36)}`.slice(0, 63);
@@ -467,9 +474,6 @@ async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS, env) {
467
474
  ...env ? { env: { ...process.env, ...env } } : {}
468
475
  });
469
476
  }
470
- function sleep(ms) {
471
- return new Promise((resolve) => setTimeout(resolve, ms));
472
- }
473
477
  async function stageLocalAgentAuth(containerId, input) {
474
478
  if (!input.syncLocalAuth) return;
475
479
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "claxedo-docker-auth-"));
@@ -522,9 +526,6 @@ function labelArgs(input) {
522
526
  return Object.entries(input).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
523
527
  }
524
528
  var TRANSIENT_MARKERS = ["timeout", "temporarily unavailable", "connection refused"];
525
- function transientDriverError(err) {
526
- return isTransientDriverError(err, TRANSIENT_MARKERS);
527
- }
528
529
  function createDockerSandboxDriver(options) {
529
530
  const dockerCommand = options.docker ?? docker;
530
531
  const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
@@ -621,7 +622,7 @@ function createDockerSandboxDriver(options) {
621
622
  "-lc",
622
623
  bootScript(workspaceDirectory(input), runtimeCommand)
623
624
  ], 12e4, env).catch((err) => {
624
- if (transientDriverError(err)) return void 0;
625
+ if (isTransientDriverError(err, TRANSIENT_MARKERS)) return void 0;
625
626
  throw err;
626
627
  });
627
628
  if (!created) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
@@ -635,7 +636,7 @@ function createDockerSandboxDriver(options) {
635
636
  if (!sandboxId) return ensureHost(input.ensure);
636
637
  await dockerCommand(["inspect", "-f", "{{.Id}}", sandboxId], timeoutMs);
637
638
  await dockerCommand(["start", sandboxId], timeoutMs).catch((err) => {
638
- if (transientDriverError(err)) return void 0;
639
+ if (isTransientDriverError(err, TRANSIENT_MARKERS)) return void 0;
639
640
  throw err;
640
641
  });
641
642
  return readyTarget(input.ensure, sandboxId, input.ensure.hostId ?? sandboxId);
@@ -48,6 +48,8 @@ var CLAXEDO_TOOL_NAMES = [
48
48
  "ui_guide"
49
49
  ];
50
50
  var CLAXEDO_TOOL_NAME_SET = new Set(CLAXEDO_TOOL_NAMES);
51
+ var PROMPT_ATTACHMENT_MAX_BYTES = 32 * 1024 * 1024;
52
+ var PROMPT_ATTACHMENT_UPLOAD_MAX_BODY_BYTES = Math.ceil(PROMPT_ATTACHMENT_MAX_BYTES / 3) * 4 + 64 * 1024;
51
53
  var CREDENTIAL_BROKER_ERRORS = {
52
54
  binding_route_required: {
53
55
  fault: "harness",
@@ -347,10 +347,8 @@ function isRecord(value) {
347
347
  function asRecord(value) {
348
348
  return isRecord(value) ? value : void 0;
349
349
  }
350
-
351
- // src/json.ts
352
- function num(input) {
353
- return typeof input === "number" ? input : void 0;
350
+ function asNumber(value) {
351
+ return typeof value === "number" ? value : void 0;
354
352
  }
355
353
 
356
354
  // src/drivers/transient-error.ts
@@ -363,7 +361,7 @@ function driverErrorSignals(err) {
363
361
  return {
364
362
  // `response.status` is the axios shape; `statusCode` is what SDK-native
365
363
  // error classes expose instead.
366
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
364
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
367
365
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
368
366
  };
369
367
  }
@@ -389,9 +387,6 @@ function nameFor(workspaceId) {
389
387
  return `claxedo-${workspaceId}`;
390
388
  }
391
389
  var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline"];
392
- function transientDriverError(err) {
393
- return isTransientDriverError(err, TRANSIENT_MARKERS);
394
- }
395
390
  function createModalSandboxDriver(options) {
396
391
  let defaultClient;
397
392
  function resolveClient() {
@@ -489,7 +484,7 @@ function createModalSandboxDriver(options) {
489
484
  ...network(input)
490
485
  }
491
486
  ).catch((err) => {
492
- if (transientDriverError(err)) return void 0;
487
+ if (isTransientDriverError(err, TRANSIENT_MARKERS)) return void 0;
493
488
  throw err;
494
489
  });
495
490
  if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
@@ -334,10 +334,8 @@ function isRecord(value) {
334
334
  function asRecord(value) {
335
335
  return isRecord(value) ? value : void 0;
336
336
  }
337
-
338
- // src/json.ts
339
- function num(input) {
340
- return typeof input === "number" ? input : void 0;
337
+ function asNumber(value) {
338
+ return typeof value === "number" ? value : void 0;
341
339
  }
342
340
 
343
341
  // src/drivers/transient-error.ts
@@ -350,7 +348,7 @@ function driverErrorSignals(err) {
350
348
  return {
351
349
  // `response.status` is the axios shape; `statusCode` is what SDK-native
352
350
  // error classes expose instead.
353
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
351
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
354
352
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
355
353
  };
356
354
  }
@@ -379,7 +377,8 @@ var HARNESS_RELEASE_AGE_EXCLUSIONS = `printf '%s\\n' ${[
379
377
  "@anthropic-ai/claude-*",
380
378
  "@cursor/sdk*",
381
379
  "@earendil-works/*",
382
- "@openai/codex"
380
+ "@openai/codex",
381
+ "@opencode/*"
383
382
  ].map((name) => `'min-release-age-exclude[]=${name}'`).join(" ")} >> "$HOME/.npmrc"`;
384
383
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
385
384
  var DEFAULT_WORKSPACE_DIR = "/workspace";
@@ -401,9 +400,6 @@ function nameFor(workspaceId) {
401
400
  return `claxedo-${workspaceId}`;
402
401
  }
403
402
  var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
404
- function transientDriverError(err) {
405
- return isTransientDriverError(err, TRANSIENT_MARKERS);
406
- }
407
403
  function network(input) {
408
404
  if (!input.net || input.net.mode === "allow-all") return void 0;
409
405
  const hosts = input.net.hosts ?? [];
@@ -538,7 +534,9 @@ function createVercelSandboxDriver(options) {
538
534
  await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
539
535
  await runSetup(builder, [
540
536
  HARNESS_RELEASE_AGE_EXCLUSIONS,
541
- "npm i -g --min-release-age=2 tsx@4.22.3 opencode-ai@1.15.10 @anthropic-ai/claude-code@2.1.285 @openai/codex@0.159.2",
537
+ "npm i -g --min-release-age=2 tsx@4.22.3 @anthropic-ai/claude-code@2.1.323 @openai/codex@0.165.0",
538
+ "sudo mkdir -p /etc/codex",
539
+ "printf 'check_for_update_on_startup = false\\n' | sudo tee /etc/codex/config.toml > /dev/null",
542
540
  "curl https://cursor.com/install -fsS | bash",
543
541
  "curl -fsSL https://ampcode.com/install.sh | bash",
544
542
  "curl -fsSL https://app.factory.ai/cli | sh",
@@ -548,7 +546,6 @@ function createVercelSandboxDriver(options) {
548
546
  "sudo ln -sf $HOME/.local/bin/amp /usr/local/bin/amp",
549
547
  "sudo ln -sf $HOME/.local/bin/droid /usr/local/bin/droid",
550
548
  "sudo ln -sf $HOME/.local/bin/agy /usr/local/bin/agy",
551
- "command -v opencode",
552
549
  "command -v claude",
553
550
  "command -v codex",
554
551
  "command -v agent",
@@ -631,7 +628,7 @@ function createVercelSandboxDriver(options) {
631
628
  env,
632
629
  ...createPolicy ? { networkPolicy: createPolicy } : {}
633
630
  }).catch((err) => {
634
- if (transientDriverError(err)) return void 0;
631
+ if (isTransientDriverError(err, TRANSIENT_MARKERS)) return void 0;
635
632
  throw err;
636
633
  });
637
634
  if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
package/dist/index.mjs CHANGED
@@ -553,10 +553,8 @@ function sandboxRuntimeBootFailure(error) {
553
553
  return workspaceRuntimeBootReason(diagnostic) ?? diagnostic;
554
554
  }
555
555
  var SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
556
- var SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
557
556
  var CODED_REASONS = /* @__PURE__ */ new Map([
558
557
  [SANDBOX_RUNTIME_UNHEALTHY, "The workspace machine stopped answering."],
559
- [SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."],
560
558
  [SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED, "The update couldn't finish downloading; your workspace is still running. Try again."]
561
559
  ]);
562
560
  function firstLine(text) {
@@ -1543,14 +1541,6 @@ function createSandboxManager(options) {
1543
1541
  ...existing.retryCount >= maxRetryCount ? { exhausted: true } : {}
1544
1542
  };
1545
1543
  }
1546
- if (existing.status === "unavailable" && existing.retryCount >= maxRetryCount && existing.nextRetryAt === void 0 && sandboxProviderRateLimit(existing.lastError) !== "day") {
1547
- return {
1548
- status: "unavailable",
1549
- error: existing.lastError ?? SANDBOX_RETRY_CAP_EXCEEDED,
1550
- epoch: existing.epoch,
1551
- homeRegion: existing.homeRegion
1552
- };
1553
- }
1554
1544
  return void 0;
1555
1545
  }
1556
1546
  function continues(lease) {
@@ -1757,7 +1747,6 @@ export {
1757
1747
  SANDBOX_IMAGE_LABEL,
1758
1748
  SANDBOX_MODEL_PROVIDER_HOSTS,
1759
1749
  SANDBOX_PACKAGE_REGISTRY_HOSTS,
1760
- SANDBOX_RETRY_CAP_EXCEEDED,
1761
1750
  SANDBOX_RUNTIME_UNHEALTHY,
1762
1751
  SANDBOX_START_WINDOWS,
1763
1752
  SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED,
package/dist/json.d.ts CHANGED
@@ -1,4 +1 @@
1
- export { asRecord as record, isRecord } from "@claxedo/helpers/guards";
2
- /** An empty string is absent. */
3
- export declare function text(input: unknown): string | undefined;
4
- export declare function num(input: unknown): number | undefined;
1
+ export { asNumber as num, asRecord as record, isRecord, nonEmptyString as text } from "@claxedo/helpers/guards";
@@ -158,8 +158,6 @@ export declare function runtimeBootDiagnostic(log: string, maxChars: number): st
158
158
  export declare function sandboxRuntimeBootFailure(error: string | undefined): string | undefined;
159
159
  /** The error a lease keeps when the runtime it serves stopped answering its health reports. */
160
160
  export declare const SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
161
- /** The error a capped lease answers when it kept none of its own. */
162
- export declare const SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
163
161
  /**
164
162
  * The one line a person is told for a start step's error: the runtime's own
165
163
  * reason for a boot that failed, the provider's refusal in Claxedo's words,
@@ -1,4 +1,3 @@
1
- import type { SandboxDriverID } from "@claxedo/sandbox-contract";
2
1
  export type WorkspaceRuntimeControlEnv = {
3
2
  relayJwksUrl?: string;
4
3
  relayVerifyPem?: string;
@@ -35,9 +34,3 @@ export declare function workspaceRuntimeSourceEnv(input: {
35
34
  kind: "empty";
36
35
  };
37
36
  }): Record<string, string>;
38
- export declare function workspaceRuntimeServiceExposureEnv(input: {
39
- driver: SandboxDriverID;
40
- source: "driver-service-url";
41
- access: "private" | "public" | "driver-authenticated" | "unknown";
42
- note?: string;
43
- }): Record<string, string>;
@@ -66,22 +66,12 @@ function workspaceRuntimeSourceEnv(input) {
66
66
  ...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
67
67
  };
68
68
  }
69
- function workspaceRuntimeServiceExposureEnv(input) {
70
- return {
71
- ...input.driver === "docker" ? { WORKSPACE_RUNTIME_HOST: "0.0.0.0" } : {},
72
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_SOURCE: input.source,
73
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_ACCESS: input.access,
74
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_DRIVER: input.driver,
75
- ...input.note ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_NOTE: input.note } : {}
76
- };
77
- }
78
69
  export {
79
70
  assertWorkspaceRuntimeIdentityEnv,
80
71
  redactWorkspaceRuntimeBootSecrets,
81
72
  workspaceRuntimeBootEnv,
82
73
  workspaceRuntimeBootSecrets,
83
74
  workspaceRuntimeIdentityEnvConflicts,
84
- workspaceRuntimeServiceExposureEnv,
85
75
  workspaceRuntimeSourceEnv,
86
76
  workspaceRuntimeTargetEnv
87
77
  };
package/package.json CHANGED
@@ -171,7 +171,7 @@
171
171
  "access": "public"
172
172
  },
173
173
  "name": "@claxedo/sandbox-manager",
174
- "version": "0.10.3",
174
+ "version": "0.10.4",
175
175
  "scripts": {
176
176
  "build": "bun scripts/build.ts",
177
177
  "prepublishOnly": "npm run build",
@@ -180,8 +180,8 @@
180
180
  "test": "mkdir -p .artifacts/unit && node ../../script/test-deadline.mjs bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
181
181
  },
182
182
  "dependencies": {
183
- "@claxedo/helpers": "0.2.3",
184
- "@claxedo/sandbox-contract": "0.10.3",
183
+ "@claxedo/helpers": "0.2.4",
184
+ "@claxedo/sandbox-contract": "0.10.4",
185
185
  "@vercel/sandbox": "3.3.0",
186
186
  "modal": "0.10.1"
187
187
  },