@claxedo/sandbox-manager 0.10.2 → 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.
Files changed (37) hide show
  1. package/README.md +67 -29
  2. package/dist/checkpoint-manager.mjs +11 -0
  3. package/dist/contract.d.ts +28 -16
  4. package/dist/driver-billing.d.ts +23 -0
  5. package/dist/driver-catalog.d.ts +1 -2
  6. package/dist/driver-catalog.mjs +61 -11
  7. package/dist/driver-ensure-input.d.ts +3 -2
  8. package/dist/drivers/agent37-client.d.ts +87 -0
  9. package/dist/drivers/agent37.d.ts +25 -0
  10. package/dist/drivers/agent37.mjs +886 -0
  11. package/dist/drivers/boat-client.d.ts +5 -5
  12. package/dist/drivers/boat-machine.d.ts +7 -17
  13. package/dist/drivers/boat-template.mjs +87 -35
  14. package/dist/drivers/boat.d.ts +0 -5
  15. package/dist/drivers/boat.mjs +81 -26
  16. package/dist/drivers/cloudflare.d.ts +5 -9
  17. package/dist/drivers/cloudflare.mjs +97 -43
  18. package/dist/drivers/docker.mjs +70 -19
  19. package/dist/drivers/local-brokering.mjs +90 -18
  20. package/dist/drivers/modal.mjs +56 -11
  21. package/dist/drivers/transient-error.d.ts +0 -1
  22. package/dist/drivers/vercel.mjs +65 -15
  23. package/dist/egress-policy.d.ts +33 -32
  24. package/dist/egress-warnings.d.ts +4 -6
  25. package/dist/hosted-network-policy.d.ts +10 -64
  26. package/dist/hosted-network-policy.mjs +1 -1
  27. package/dist/image-name.d.ts +0 -1
  28. package/dist/index.mjs +103 -78
  29. package/dist/json.d.ts +1 -13
  30. package/dist/lease-failure.d.ts +3 -3
  31. package/dist/lease-types.d.ts +1 -10
  32. package/dist/runtime-env.d.ts +6 -7
  33. package/dist/runtime-env.mjs +5 -10
  34. package/dist/runtime-version.d.ts +4 -0
  35. package/dist/runtime-version.mjs +6 -0
  36. package/dist/start-admission.d.ts +32 -0
  37. package/package.json +12 -4
@@ -18,7 +18,7 @@ export type BoatFetch = (input: string, init?: RequestInit) => Promise<Response>
18
18
  /**
19
19
  * What Boat's 429 `rate_limited` says about the account's start limit: the
20
20
  * window that is used up, its limit, and the starts used of each window. It
21
- * names no time the window reopens (2026-10-15), so none is kept.
21
+ * names no time the window reopens, so none is kept.
22
22
  */
23
23
  export type BoatStartLimit = {
24
24
  window: SandboxStartWindow;
@@ -33,10 +33,10 @@ export declare class BoatApiError extends Error {
33
33
  readonly status?: number | undefined;
34
34
  /** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
35
35
  readonly providerKeyRefused: boolean;
36
- /** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
36
+ /** Create, fork and resume each count as one start (Boat's own refusal). */
37
37
  readonly providerRateLimited?: SandboxStartLimit;
38
38
  readonly startLimit?: BoatStartLimit;
39
- /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
39
+ /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
40
40
  readonly sandboxState?: BoatSandboxState;
41
41
  constructor(message: string, code: string, status?: number | undefined, refusal?: {
42
42
  startLimit?: BoatStartLimit;
@@ -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;
@@ -75,7 +75,7 @@ export declare function createBoatClient(options: BoatClientOptions): {
75
75
  }): Promise<void>;
76
76
  /**
77
77
  * `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
78
- * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated` (2026-10-13).
78
+ * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
79
79
  */
80
80
  update(id: string, input: {
81
81
  ttlSeconds: number | null;
@@ -9,8 +9,8 @@ export declare const REGISTRY_PASSWORD_PATH = ".claxedo-registry-password";
9
9
  * and Boat's ~/.ascii/playbook.json) and removes what this machine's boot made
10
10
  * (the runtime container and Boat's record and image archive of it, the
11
11
  * workspace, the runtime's state, the boot's env). Nothing the runtime wrote
12
- * into the image's trees was found there (Boat, 2026-10-16: the trees held only
13
- * the image's own entries after two boots); what else differs on the machine
12
+ * into the image's trees was found there (the trees held only the image's own
13
+ * entries after two boots); what else differs on the machine
14
14
  * is Boat's own, and a machine created from the template gets its own
15
15
  * machine-id, SSH host keys, hostname and no hosted port.
16
16
  */
@@ -25,7 +25,7 @@ export declare function holdingBootLock(commandTimeoutSeconds: number, script: s
25
25
  * a machine whose trees hold only an older image keeps that image when its
26
26
  * runtime is no older than the oldest the control plane serves, so a wake
27
27
  * after a deploy does not wait 60–100 s on the new image's pull and seed
28
- * (staging, 2026-10-21); the workspace then reads as outdated, and the
28
+ * (staging); the workspace then reads as outdated, and the
29
29
  * person's Restart to update asks for the new image with `keepPreviousImage`
30
30
  * off. A cold boot has no previous image to keep.
31
31
  */
@@ -48,6 +48,10 @@ export type MachineBoot = {
48
48
  export declare function bootedImageFile(staged: StagedFiles): string;
49
49
  /** The image a preparation booted: the last line it prints, since `docker login` prints before it. */
50
50
  export declare function bootedImage(prepareStdout: string): string | undefined;
51
+ /** The last line a preparation prints when its pull gave up on a registry link that kept breaking. */
52
+ export declare const IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
53
+ /** Leads the line a boot that could keep the machine's last image prints when it takes the named one instead, with what it found. */
54
+ export declare const KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
51
55
  /**
52
56
  * The command carries only paths: env arrives through the staged file
53
57
  * (mounted read-only, sourced by the entrypoint at every container start so
@@ -70,20 +74,6 @@ export declare function bootedImage(prepareStdout: string): string | undefined;
70
74
  * recreated after a stop that was not this driver's; then it removes the
71
75
  * image if a seed left it. A failure there leaves nothing serving.
72
76
  */
73
- /**
74
- * `docker pull` that goes on after the connection under it breaks. Docker ends
75
- * a pull at the first reset and keeps none of a layer that was still
76
- * downloading, but keeps every layer it finished, so pulling again continues
77
- * from those. Boat's link to ghcr resets mid-pull (2 of 8 staging updates on
78
- * 22 October, `failed to copy: read tcp …: connection reset by peer`). A pull
79
- * is run again only after such a transport failure and only while each run
80
- * finishes a layer none before it did, so it ends; any other failure, and a
81
- * run that finished nothing new, fails with the pull's own output.
82
- */
83
- /** The last line a preparation prints when its pull gave up on a registry link that kept breaking. */
84
- export declare const IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
85
- /** Leads the line a boot that could keep the machine's last image prints when it takes the named one instead, with what it found. */
86
- export declare const KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
87
77
  export declare function machineBootScripts(boot: MachineBoot): {
88
78
  prepare: string;
89
79
  start: string;
@@ -63,6 +63,7 @@ var sandboxDriverCredentialFields = {
63
63
  { key: "worker_url", label: "Worker URL" }
64
64
  ],
65
65
  boat: [{ key: "api_key", label: "API Key", secret: true }],
66
+ agent37: [{ key: "api_key", label: "API Key", secret: true }],
66
67
  docker: [{ key: "image", label: "Image" }]
67
68
  };
68
69
  var sandboxDriverLabels = {
@@ -70,6 +71,7 @@ var sandboxDriverLabels = {
70
71
  vercel: "Vercel",
71
72
  cloudflare: "Cloudflare",
72
73
  boat: "Boat",
74
+ agent37: "Agent37",
73
75
  docker: "Docker"
74
76
  };
75
77
 
@@ -143,6 +145,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
143
145
  function workspaceRuntimeBootSecrets(env) {
144
146
  return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
145
147
  }
148
+ var REDACTED_VALUE_MIN_LENGTH = 8;
149
+ function redactWorkspaceRuntimeBootSecrets(text, secrets) {
150
+ return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
151
+ }
146
152
  function workspaceRuntimeBootEnv(input) {
147
153
  assertWorkspaceRuntimeIdentityEnv(input.env);
148
154
  const env = {
@@ -194,6 +200,10 @@ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
194
200
  image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
195
201
  };
196
202
  var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
203
+ var SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
204
+ function servedRuntimeVersionTest(file) {
205
+ return `version=$(cat ${file} 2>/dev/null) && [ -n "$version" ] && [ "$(printf '%s\\n%s\\n' ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} "$version" | sort -V | head -n 1)" = ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} ]`;
206
+ }
197
207
 
198
208
  // src/image-name.ts
199
209
  function assertSandboxImageReference(image) {
@@ -214,6 +224,27 @@ var boatBilling = {
214
224
  large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
215
225
  }
216
226
  };
227
+ var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
228
+ var AGENT37_MONTH_HOURS = 730;
229
+ var agent37MachineShapes = {
230
+ small: { cpu: 2, memory: 4, disk: 10 },
231
+ default: { cpu: 4, memory: 8, disk: 20 },
232
+ large: { cpu: 4, memory: 16, disk: 40 }
233
+ };
234
+ function agent37Rate(shape) {
235
+ const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
236
+ return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
237
+ }
238
+ var agent37Billing = {
239
+ source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
240
+ meter: "Agent37 meters an instance per minute at a monthly rate over 730 hours: $0.80 per vCPU, $0.70 per GB of memory and $0.09 per GB of disk while it runs, and the disk alone while it is stopped.",
241
+ rates: {
242
+ small: agent37Rate(agent37MachineShapes.small),
243
+ default: agent37Rate(agent37MachineShapes.default),
244
+ large: agent37Rate(agent37MachineShapes.large)
245
+ },
246
+ excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
247
+ };
217
248
  var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
218
249
  var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
219
250
  var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
@@ -299,8 +330,8 @@ var sandboxDriverCatalog = {
299
330
  hostWatchesIdle: true,
300
331
  targetAccess: "relay",
301
332
  secretBrokering: "native",
302
- // Credential handlers do not restrict unrelated destinations.
303
- egressControl: "none",
333
+ // The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
334
+ egressControl: "hosts",
304
335
  // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
305
336
  persistence: {
306
337
  resume: "replacement-restore",
@@ -338,6 +369,33 @@ var sandboxDriverCatalog = {
338
369
  machineClasses: ["small", "default", "large"]
339
370
  }
340
371
  },
372
+ agent37: {
373
+ id: "agent37",
374
+ label: sandboxDriverLabels.agent37,
375
+ credentialFields: sandboxDriverCredentialFields.agent37,
376
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
377
+ runtimeEnv: "callback",
378
+ preparedImage: true,
379
+ billing: agent37Billing,
380
+ metadata: {
381
+ driverRunsIn: ["worker", "node"],
382
+ hostStopBehavior: "suspends-host",
383
+ hostResumeBehavior: "same-host",
384
+ targetAccess: "relay",
385
+ secretBrokering: "none",
386
+ // An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
387
+ egressControl: "hosts-and-cidrs",
388
+ persistence: {
389
+ resume: "same-sandbox",
390
+ capture: "none",
391
+ clone: false,
392
+ captureSource: "not-applicable",
393
+ retention: "provider-managed",
394
+ restoreMount: "same-resource"
395
+ },
396
+ machineClasses: ["small", "default", "large"]
397
+ }
398
+ },
341
399
  docker: {
342
400
  id: "docker",
343
401
  label: sandboxDriverLabels.docker,
@@ -371,10 +429,8 @@ function isRecord(value) {
371
429
  function asRecord(value) {
372
430
  return isRecord(value) ? value : void 0;
373
431
  }
374
-
375
- // src/json.ts
376
- function num(input) {
377
- return typeof input === "number" ? input : void 0;
432
+ function asNumber(value) {
433
+ return typeof value === "number" ? value : void 0;
378
434
  }
379
435
 
380
436
  // ../claxedo-helpers/dist/deadline.mjs
@@ -410,10 +466,10 @@ var BoatApiError = class extends Error {
410
466
  }
411
467
  /** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
412
468
  providerKeyRefused;
413
- /** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
469
+ /** Create, fork and resume each count as one start (Boat's own refusal). */
414
470
  providerRateLimited;
415
471
  startLimit;
416
- /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
472
+ /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
417
473
  sandboxState;
418
474
  };
419
475
  function startCount(value) {
@@ -439,7 +495,7 @@ function startLimitOf(body) {
439
495
  function invalid() {
440
496
  throw new BoatApiError("Boat API returned an invalid response", "invalid_response");
441
497
  }
442
- function requiredText(value) {
498
+ function responseString(value) {
443
499
  return typeof value === "string" ? value : invalid();
444
500
  }
445
501
  function requiredFlag(value) {
@@ -453,7 +509,7 @@ function isSandboxState(value) {
453
509
  }
454
510
  function sandbox(body) {
455
511
  const value = asRecord(body.sandbox) ?? invalid();
456
- const id = requiredText(value.id);
512
+ const id = responseString(value.id);
457
513
  if (!/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(id) || !isSandboxState(value.state)) invalid();
458
514
  return { id, state: value.state, hydrated: value.hydrated === true };
459
515
  }
@@ -541,7 +597,7 @@ function createBoatClient(options) {
541
597
  },
542
598
  /**
543
599
  * `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
544
- * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated` (2026-10-13).
600
+ * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
545
601
  */
546
602
  async update(id, input) {
547
603
  const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
@@ -576,8 +632,8 @@ function createBoatClient(options) {
576
632
  const body = await api(`${path(id)}/commands`, ["command.finished"], { method: "POST", body: input, timeoutMs });
577
633
  return {
578
634
  success: requiredFlag(body.success),
579
- stdout: requiredText(body.stdout),
580
- stderr: requiredText(body.stderr),
635
+ stdout: responseString(body.stdout),
636
+ stderr: responseString(body.stderr),
581
637
  exitCode: commandExitCode(body.exitCode),
582
638
  timedOut: requiredFlag(body.timedOut)
583
639
  };
@@ -595,7 +651,7 @@ function driverErrorSignals(err) {
595
651
  return {
596
652
  // `response.status` is the axios shape; `statusCode` is what SDK-native
597
653
  // error classes expose instead.
598
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
654
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
599
655
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
600
656
  };
601
657
  }
@@ -658,12 +714,11 @@ function bootedImageFile(staged) {
658
714
  function bootedImage(prepareStdout) {
659
715
  return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
660
716
  }
661
- var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
662
717
  var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
663
718
  var treesFunctions = [
664
719
  `trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
665
720
  `seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
666
- `served() { version=$(cat "$1/${VERSION_FILE}" 2>/dev/null) && [ -n "$version" ] && [ "$(printf '%s\\n%s\\n' ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} "$version" | sort -V | head -n 1)" = ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} ]; }`
721
+ `served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
667
722
  ].join(" && ");
668
723
  var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
669
724
  var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
@@ -685,7 +740,7 @@ function machineBootScripts(boot) {
685
740
  const seedStub = `stub=$(mktemp -d) && docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' ${named} > "$stub/${IMAGE_ENV_FILE}" && workdir=$(docker image inspect --format '{{or .Config.WorkingDir "/"}}' ${named}) && mkdir "$stub/root" ${ROOT_DIRECTORIES.map((directory) => `"$stub/root/${directory}"`).join(" ")} && ${ROOT_LINKS.map((link) => `ln -s usr/${link} "$stub/root/${link}"`).join(" && ")} && chmod 1777 "$stub/root/tmp" && tar --owner=0 --group=0 --numeric-owner -cf "$stub/skeleton.tar" -C "$stub/root" . && docker import --change "WORKDIR $workdir" --change ${shell(`LABEL ${IMAGE_LABEL}=${boot.image}`)} "$stub/skeleton.tar" ${STUB_IMAGE} >/dev/null && docker save -o "$stub/${STUB_IMAGE_ARCHIVE}" ${STUB_IMAGE} && sudo mv "$stub/${IMAGE_ENV_FILE}" "$stub/${STUB_IMAGE_ARCHIVE}" ${trees}/ && rm -rf "$stub"`;
686
741
  const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
687
742
  const keepPrevious = `previous=$(${stubLabel}) && [ -n "$previous" ] && seeded "$(trees_of "$previous")" && served "$(trees_of "$previous")"`;
688
- const keptImageSkipped = `echo "${KEPT_IMAGE_SKIPPED} stub=\${previous:-none} seeded=$(seeded "$(trees_of "$previous")" && echo yes || echo no) version=$(cat "$(trees_of "$previous")/${VERSION_FILE}" 2>/dev/null || echo none)" >&2`;
743
+ const keptImageSkipped = `echo "${KEPT_IMAGE_SKIPPED} stub=\${previous:-none} seeded=$(seeded "$(trees_of "$previous")" && echo yes || echo no) version=$(cat "$(trees_of "$previous")/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}" 2>/dev/null || echo none)" >&2`;
689
744
  const removeImage = `if docker image inspect "$booted" >/dev/null 2>&1; then leftovers=$(docker ps -aq --filter ancestor="$booted") && { [ -z "$leftovers" ] || docker rm -f $leftovers >/dev/null; } && id=$(docker image inspect --format '{{.Id}}' "$booted") && docker rmi "$booted" >/dev/null && sudo rm -f ${BOAT_SAVED_IMAGES}/"\${id#sha256:}".tar; fi`;
690
745
  const prepare = [
691
746
  `chmod 600 ${boot.staged.env}`,
@@ -709,6 +764,13 @@ function machineBootScripts(boot) {
709
764
  };
710
765
  }
711
766
 
767
+ // ../claxedo-helpers/dist/index.mjs
768
+ function sleep(ms) {
769
+ return new Promise((resolve) => {
770
+ setTimeout(resolve, ms);
771
+ });
772
+ }
773
+
712
774
  // src/drivers/boat.ts
713
775
  var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
714
776
  var DEFAULT_WORKSPACE_DIR = "/workspace";
@@ -724,7 +786,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
724
786
  var CONTAINER_STOP_GRACE_SECONDS = 30;
725
787
  var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
726
788
  var STDERR_TAIL_CHARS = 600;
727
- var REDACTED_VALUE_MIN_LENGTH = 8;
728
789
  var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
729
790
  var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
730
791
  var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
@@ -744,14 +805,8 @@ function archivedOrThrow(err) {
744
805
  if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
745
806
  hostGoneOrThrow(err);
746
807
  }
747
- function sleep(ms) {
748
- return new Promise((resolve) => setTimeout(resolve, ms));
749
- }
750
- function scrubbed(text, secrets) {
751
- return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
752
- }
753
808
  function scrubbedTail(text, secrets) {
754
- return scrubbed(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
809
+ return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
755
810
  }
756
811
  var BoatDriverError = class extends Error {
757
812
  constructor(message) {
@@ -861,7 +916,7 @@ function createBoatSandboxDriver(options) {
861
916
  command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
862
917
  timeoutSeconds: 10
863
918
  });
864
- diagnostic = runtimeBootDiagnostic(scrubbed(`${result.stdout}
919
+ diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
865
920
  ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
866
921
  } catch {
867
922
  diagnostic = "container diagnostics unavailable";
@@ -1055,13 +1110,13 @@ var STATUSES = ["saving", "ready", "failed"];
1055
1110
  function namedSnapshot(value) {
1056
1111
  const snapshot = asRecord(value) ?? invalid();
1057
1112
  const status = STATUSES.find((known) => known === snapshot.status) ?? invalid();
1058
- return { name: requiredText(snapshot.name), status, createdAt: requiredText(snapshot.createdAt) };
1113
+ return { name: responseString(snapshot.name), status, createdAt: responseString(snapshot.createdAt) };
1059
1114
  }
1060
1115
  function share(value) {
1061
1116
  const entry = asRecord(value) ?? invalid();
1062
- const status = requiredText(entry.status);
1117
+ const status = responseString(entry.status);
1063
1118
  if (status !== "active" && status !== "revoked") invalid();
1064
- return { id: requiredText(entry.id), status, createdAt: requiredText(entry.createdAt) };
1119
+ return { id: responseString(entry.id), status, createdAt: responseString(entry.createdAt) };
1065
1120
  }
1066
1121
  function list(value, item) {
1067
1122
  return Array.isArray(value) ? value.map(item) : invalid();
@@ -1092,7 +1147,7 @@ function createBoatNamedSnapshotClient(options) {
1092
1147
  /** Boat shows the code only in this answer and keeps a hash of it. */
1093
1148
  async share(name) {
1094
1149
  const body = await api(`${path(name)}/shares`, ["snapshot.share.created"], { method: "POST" });
1095
- return { shareCode: requiredText(body.shareCode), share: share(body.share) };
1150
+ return { shareCode: responseString(body.shareCode), share: share(body.share) };
1096
1151
  },
1097
1152
  async shares(name) {
1098
1153
  return list((await api(`${path(name)}/shares`, ["snapshot.share.list"])).shares, share);
@@ -1111,9 +1166,6 @@ var DEFAULT_POLL_INTERVAL_MS = 2e3;
1111
1166
  var DEFAULT_HYDRATION_TIMEOUT_MS = 10 * 6e4;
1112
1167
  var DEFAULT_STOP_TIMEOUT_MS = 10 * 6e4;
1113
1168
  var DEFAULT_SAVE_TIMEOUT_MS = 10 * 6e4;
1114
- function sleep2(ms) {
1115
- return new Promise((resolve) => setTimeout(resolve, ms));
1116
- }
1117
1169
  async function boatRuntimeTemplateName(image, prefix = DEFAULT_PREFIX) {
1118
1170
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(image)));
1119
1171
  const name = `${prefix}${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, NAME_HASH_CHARS)}`;
@@ -1135,7 +1187,7 @@ async function pollBoat(label, timeoutMs, intervalMs, read) {
1135
1187
  const value = await read();
1136
1188
  if (value !== void 0) return value;
1137
1189
  if (Date.now() >= deadline) throw new Error(`${label} did not happen within ${Math.round(timeoutMs / 1e3)} s`);
1138
- await sleep2(intervalMs);
1190
+ await sleep(intervalMs);
1139
1191
  }
1140
1192
  }
1141
1193
  async function ensureBoatRuntimeTemplate(options) {
@@ -1183,7 +1235,7 @@ async function buildTemplate(options, name) {
1183
1235
  const lease = sandboxLease({ workspaceId, homeRegion: "auto", driver: "boat", epoch, sandboxId });
1184
1236
  const result = sandboxId ? await driver.resumeHost({ lease, ensure: ensure(epoch) }) : await driver.ensureHost(ensure(epoch));
1185
1237
  if (!("provisioning" in result)) return result;
1186
- await sleep2(result.retryAfterMs);
1238
+ await sleep(result.retryAfterMs);
1187
1239
  }
1188
1240
  };
1189
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);
@@ -22,12 +22,7 @@ export type BoatSandboxDriverOptions = {
22
22
  workspaceDir?: string;
23
23
  nativeHarness?: string;
24
24
  controlEnv?: WorkspaceRuntimeControlEnv;
25
- /** Container name used for the runtime container inside the sandbox. */
26
25
  containerName?: string;
27
- /**
28
- * Optional `docker login` before the pull, for a private runtime registry.
29
- * Omit for public images.
30
- */
31
26
  registryAuth?: {
32
27
  server: string;
33
28
  username: string;
@@ -31,6 +31,7 @@ var sandboxDriverCredentialFields = {
31
31
  { key: "worker_url", label: "Worker URL" }
32
32
  ],
33
33
  boat: [{ key: "api_key", label: "API Key", secret: true }],
34
+ agent37: [{ key: "api_key", label: "API Key", secret: true }],
34
35
  docker: [{ key: "image", label: "Image" }]
35
36
  };
36
37
  var sandboxDriverLabels = {
@@ -38,6 +39,7 @@ var sandboxDriverLabels = {
38
39
  vercel: "Vercel",
39
40
  cloudflare: "Cloudflare",
40
41
  boat: "Boat",
42
+ agent37: "Agent37",
41
43
  docker: "Docker"
42
44
  };
43
45
 
@@ -111,6 +113,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
111
113
  function workspaceRuntimeBootSecrets(env) {
112
114
  return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
113
115
  }
116
+ var REDACTED_VALUE_MIN_LENGTH = 8;
117
+ function redactWorkspaceRuntimeBootSecrets(text, secrets) {
118
+ return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
119
+ }
114
120
  function workspaceRuntimeBootEnv(input) {
115
121
  assertWorkspaceRuntimeIdentityEnv(input.env);
116
122
  const env = {
@@ -162,6 +168,10 @@ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
162
168
  image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
163
169
  };
164
170
  var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
171
+ var SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
172
+ function servedRuntimeVersionTest(file) {
173
+ return `version=$(cat ${file} 2>/dev/null) && [ -n "$version" ] && [ "$(printf '%s\\n%s\\n' ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} "$version" | sort -V | head -n 1)" = ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} ]`;
174
+ }
165
175
 
166
176
  // src/image-name.ts
167
177
  function assertSandboxImageReference(image) {
@@ -182,6 +192,27 @@ var boatBilling = {
182
192
  large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
183
193
  }
184
194
  };
195
+ var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
196
+ var AGENT37_MONTH_HOURS = 730;
197
+ var agent37MachineShapes = {
198
+ small: { cpu: 2, memory: 4, disk: 10 },
199
+ default: { cpu: 4, memory: 8, disk: 20 },
200
+ large: { cpu: 4, memory: 16, disk: 40 }
201
+ };
202
+ function agent37Rate(shape) {
203
+ const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
204
+ return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
205
+ }
206
+ var agent37Billing = {
207
+ source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
208
+ meter: "Agent37 meters an instance per minute at a monthly rate over 730 hours: $0.80 per vCPU, $0.70 per GB of memory and $0.09 per GB of disk while it runs, and the disk alone while it is stopped.",
209
+ rates: {
210
+ small: agent37Rate(agent37MachineShapes.small),
211
+ default: agent37Rate(agent37MachineShapes.default),
212
+ large: agent37Rate(agent37MachineShapes.large)
213
+ },
214
+ excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
215
+ };
185
216
  var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
186
217
  var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
187
218
  var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
@@ -267,8 +298,8 @@ var sandboxDriverCatalog = {
267
298
  hostWatchesIdle: true,
268
299
  targetAccess: "relay",
269
300
  secretBrokering: "native",
270
- // Credential handlers do not restrict unrelated destinations.
271
- egressControl: "none",
301
+ // The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
302
+ egressControl: "hosts",
272
303
  // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
273
304
  persistence: {
274
305
  resume: "replacement-restore",
@@ -306,6 +337,33 @@ var sandboxDriverCatalog = {
306
337
  machineClasses: ["small", "default", "large"]
307
338
  }
308
339
  },
340
+ agent37: {
341
+ id: "agent37",
342
+ label: sandboxDriverLabels.agent37,
343
+ credentialFields: sandboxDriverCredentialFields.agent37,
344
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
345
+ runtimeEnv: "callback",
346
+ preparedImage: true,
347
+ billing: agent37Billing,
348
+ metadata: {
349
+ driverRunsIn: ["worker", "node"],
350
+ hostStopBehavior: "suspends-host",
351
+ hostResumeBehavior: "same-host",
352
+ targetAccess: "relay",
353
+ secretBrokering: "none",
354
+ // An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
355
+ egressControl: "hosts-and-cidrs",
356
+ persistence: {
357
+ resume: "same-sandbox",
358
+ capture: "none",
359
+ clone: false,
360
+ captureSource: "not-applicable",
361
+ retention: "provider-managed",
362
+ restoreMount: "same-resource"
363
+ },
364
+ machineClasses: ["small", "default", "large"]
365
+ }
366
+ },
309
367
  docker: {
310
368
  id: "docker",
311
369
  label: sandboxDriverLabels.docker,
@@ -339,10 +397,8 @@ function isRecord(value) {
339
397
  function asRecord(value) {
340
398
  return isRecord(value) ? value : void 0;
341
399
  }
342
-
343
- // src/json.ts
344
- function num(input) {
345
- return typeof input === "number" ? input : void 0;
400
+ function asNumber(value) {
401
+ return typeof value === "number" ? value : void 0;
346
402
  }
347
403
 
348
404
  // ../claxedo-helpers/dist/deadline.mjs
@@ -378,10 +434,10 @@ var BoatApiError = class extends Error {
378
434
  }
379
435
  /** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
380
436
  providerKeyRefused;
381
- /** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
437
+ /** Create, fork and resume each count as one start (Boat's own refusal). */
382
438
  providerRateLimited;
383
439
  startLimit;
384
- /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
440
+ /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
385
441
  sandboxState;
386
442
  };
387
443
  function startCount(value) {
@@ -407,7 +463,7 @@ function startLimitOf(body) {
407
463
  function invalid() {
408
464
  throw new BoatApiError("Boat API returned an invalid response", "invalid_response");
409
465
  }
410
- function requiredText(value) {
466
+ function responseString(value) {
411
467
  return typeof value === "string" ? value : invalid();
412
468
  }
413
469
  function requiredFlag(value) {
@@ -421,7 +477,7 @@ function isSandboxState(value) {
421
477
  }
422
478
  function sandbox(body) {
423
479
  const value = asRecord(body.sandbox) ?? invalid();
424
- const id = requiredText(value.id);
480
+ const id = responseString(value.id);
425
481
  if (!/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(id) || !isSandboxState(value.state)) invalid();
426
482
  return { id, state: value.state, hydrated: value.hydrated === true };
427
483
  }
@@ -509,7 +565,7 @@ function createBoatClient(options) {
509
565
  },
510
566
  /**
511
567
  * `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
512
- * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated` (2026-10-13).
568
+ * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
513
569
  */
514
570
  async update(id, input) {
515
571
  const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
@@ -544,8 +600,8 @@ function createBoatClient(options) {
544
600
  const body = await api(`${path(id)}/commands`, ["command.finished"], { method: "POST", body: input, timeoutMs });
545
601
  return {
546
602
  success: requiredFlag(body.success),
547
- stdout: requiredText(body.stdout),
548
- stderr: requiredText(body.stderr),
603
+ stdout: responseString(body.stdout),
604
+ stderr: responseString(body.stderr),
549
605
  exitCode: commandExitCode(body.exitCode),
550
606
  timedOut: requiredFlag(body.timedOut)
551
607
  };
@@ -563,7 +619,7 @@ function driverErrorSignals(err) {
563
619
  return {
564
620
  // `response.status` is the axios shape; `statusCode` is what SDK-native
565
621
  // error classes expose instead.
566
- status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
622
+ status: asNumber(nested.status) ?? asNumber(shaped.status) ?? asNumber(shaped.statusCode),
567
623
  text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
568
624
  };
569
625
  }
@@ -615,12 +671,11 @@ function bootedImageFile(staged) {
615
671
  function bootedImage(prepareStdout) {
616
672
  return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
617
673
  }
618
- var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
619
674
  var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
620
675
  var treesFunctions = [
621
676
  `trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
622
677
  `seeded() { [ -f "$1/${IMAGE_TREES_COMPLETE_MARKER}" ] && [ -f "$1/${STUB_IMAGE_ARCHIVE}" ] && [ -f "$1/${IMAGE_ENV_FILE}" ]; }`,
623
- `served() { version=$(cat "$1/${VERSION_FILE}" 2>/dev/null) && [ -n "$version" ] && [ "$(printf '%s\\n%s\\n' ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} "$version" | sort -V | head -n 1)" = ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} ]; }`
678
+ `served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
624
679
  ].join(" && ");
625
680
  var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
626
681
  var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
@@ -642,7 +697,7 @@ function machineBootScripts(boot) {
642
697
  const seedStub = `stub=$(mktemp -d) && docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' ${named} > "$stub/${IMAGE_ENV_FILE}" && workdir=$(docker image inspect --format '{{or .Config.WorkingDir "/"}}' ${named}) && mkdir "$stub/root" ${ROOT_DIRECTORIES.map((directory) => `"$stub/root/${directory}"`).join(" ")} && ${ROOT_LINKS.map((link) => `ln -s usr/${link} "$stub/root/${link}"`).join(" && ")} && chmod 1777 "$stub/root/tmp" && tar --owner=0 --group=0 --numeric-owner -cf "$stub/skeleton.tar" -C "$stub/root" . && docker import --change "WORKDIR $workdir" --change ${shell(`LABEL ${IMAGE_LABEL}=${boot.image}`)} "$stub/skeleton.tar" ${STUB_IMAGE} >/dev/null && docker save -o "$stub/${STUB_IMAGE_ARCHIVE}" ${STUB_IMAGE} && sudo mv "$stub/${IMAGE_ENV_FILE}" "$stub/${STUB_IMAGE_ARCHIVE}" ${trees}/ && rm -rf "$stub"`;
643
698
  const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
644
699
  const keepPrevious = `previous=$(${stubLabel}) && [ -n "$previous" ] && seeded "$(trees_of "$previous")" && served "$(trees_of "$previous")"`;
645
- const keptImageSkipped = `echo "${KEPT_IMAGE_SKIPPED} stub=\${previous:-none} seeded=$(seeded "$(trees_of "$previous")" && echo yes || echo no) version=$(cat "$(trees_of "$previous")/${VERSION_FILE}" 2>/dev/null || echo none)" >&2`;
700
+ const keptImageSkipped = `echo "${KEPT_IMAGE_SKIPPED} stub=\${previous:-none} seeded=$(seeded "$(trees_of "$previous")" && echo yes || echo no) version=$(cat "$(trees_of "$previous")/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}" 2>/dev/null || echo none)" >&2`;
646
701
  const removeImage = `if docker image inspect "$booted" >/dev/null 2>&1; then leftovers=$(docker ps -aq --filter ancestor="$booted") && { [ -z "$leftovers" ] || docker rm -f $leftovers >/dev/null; } && id=$(docker image inspect --format '{{.Id}}' "$booted") && docker rmi "$booted" >/dev/null && sudo rm -f ${BOAT_SAVED_IMAGES}/"\${id#sha256:}".tar; fi`;
647
702
  const prepare = [
648
703
  `chmod 600 ${boot.staged.env}`,
@@ -666,6 +721,13 @@ function machineBootScripts(boot) {
666
721
  };
667
722
  }
668
723
 
724
+ // ../claxedo-helpers/dist/index.mjs
725
+ function sleep(ms) {
726
+ return new Promise((resolve) => {
727
+ setTimeout(resolve, ms);
728
+ });
729
+ }
730
+
669
731
  // src/drivers/boat.ts
670
732
  function formatBoatRuntimeTemplate(template) {
671
733
  return JSON.stringify({ image: template.image, shareCode: template.shareCode });
@@ -697,7 +759,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
697
759
  var CONTAINER_STOP_GRACE_SECONDS = 30;
698
760
  var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
699
761
  var STDERR_TAIL_CHARS = 600;
700
- var REDACTED_VALUE_MIN_LENGTH = 8;
701
762
  var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
702
763
  var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
703
764
  var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
@@ -717,14 +778,8 @@ function archivedOrThrow(err) {
717
778
  if (err instanceof BoatApiError && err.code === "stop_failed" && err.sandboxState === "archived") return;
718
779
  hostGoneOrThrow(err);
719
780
  }
720
- function sleep(ms) {
721
- return new Promise((resolve) => setTimeout(resolve, ms));
722
- }
723
- function scrubbed(text, secrets) {
724
- return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
725
- }
726
781
  function scrubbedTail(text, secrets) {
727
- return scrubbed(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
782
+ return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
728
783
  }
729
784
  var BoatDriverError = class extends Error {
730
785
  constructor(message) {
@@ -834,7 +889,7 @@ function createBoatSandboxDriver(options) {
834
889
  command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
835
890
  timeoutSeconds: 10
836
891
  });
837
- diagnostic = runtimeBootDiagnostic(scrubbed(`${result.stdout}
892
+ diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
838
893
  ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
839
894
  } catch {
840
895
  diagnostic = "container diagnostics unavailable";