@claxedo/sandbox-manager 0.10.2 → 0.10.3

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.
@@ -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;
@@ -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,
@@ -410,10 +468,10 @@ var BoatApiError = class extends Error {
410
468
  }
411
469
  /** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
412
470
  providerKeyRefused;
413
- /** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
471
+ /** Create, fork and resume each count as one start (Boat's own refusal). */
414
472
  providerRateLimited;
415
473
  startLimit;
416
- /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
474
+ /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
417
475
  sandboxState;
418
476
  };
419
477
  function startCount(value) {
@@ -541,7 +599,7 @@ function createBoatClient(options) {
541
599
  },
542
600
  /**
543
601
  * `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).
602
+ * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
545
603
  */
546
604
  async update(id, input) {
547
605
  const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
@@ -658,12 +716,11 @@ function bootedImageFile(staged) {
658
716
  function bootedImage(prepareStdout) {
659
717
  return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
660
718
  }
661
- var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
662
719
  var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
663
720
  var treesFunctions = [
664
721
  `trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
665
722
  `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} ]; }`
723
+ `served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
667
724
  ].join(" && ");
668
725
  var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
669
726
  var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
@@ -685,7 +742,7 @@ function machineBootScripts(boot) {
685
742
  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
743
  const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
687
744
  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`;
745
+ 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
746
  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
747
  const prepare = [
691
748
  `chmod 600 ${boot.staged.env}`,
@@ -724,7 +781,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
724
781
  var CONTAINER_STOP_GRACE_SECONDS = 30;
725
782
  var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
726
783
  var STDERR_TAIL_CHARS = 600;
727
- var REDACTED_VALUE_MIN_LENGTH = 8;
728
784
  var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
729
785
  var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
730
786
  var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
@@ -747,11 +803,8 @@ function archivedOrThrow(err) {
747
803
  function sleep(ms) {
748
804
  return new Promise((resolve) => setTimeout(resolve, ms));
749
805
  }
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
806
  function scrubbedTail(text, secrets) {
754
- return scrubbed(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
807
+ return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
755
808
  }
756
809
  var BoatDriverError = class extends Error {
757
810
  constructor(message) {
@@ -861,7 +914,7 @@ function createBoatSandboxDriver(options) {
861
914
  command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
862
915
  timeoutSeconds: 10
863
916
  });
864
- diagnostic = runtimeBootDiagnostic(scrubbed(`${result.stdout}
917
+ diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
865
918
  ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
866
919
  } catch {
867
920
  diagnostic = "container diagnostics unavailable";
@@ -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,
@@ -378,10 +436,10 @@ var BoatApiError = class extends Error {
378
436
  }
379
437
  /** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
380
438
  providerKeyRefused;
381
- /** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
439
+ /** Create, fork and resume each count as one start (Boat's own refusal). */
382
440
  providerRateLimited;
383
441
  startLimit;
384
- /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
442
+ /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed`. */
385
443
  sandboxState;
386
444
  };
387
445
  function startCount(value) {
@@ -509,7 +567,7 @@ function createBoatClient(options) {
509
567
  },
510
568
  /**
511
569
  * `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).
570
+ * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated`.
513
571
  */
514
572
  async update(id, input) {
515
573
  const value = sandbox(await api(path(id), ["sandbox.updated", "sandbox.info"], { method: "PATCH", body: input }));
@@ -615,12 +673,11 @@ function bootedImageFile(staged) {
615
673
  function bootedImage(prepareStdout) {
616
674
  return prepareStdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
617
675
  }
618
- var VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
619
676
  var stubLabel = `docker image inspect --format '{{index .Config.Labels "${IMAGE_LABEL}"}}' ${STUB_IMAGE} 2>/dev/null`;
620
677
  var treesFunctions = [
621
678
  `trees_of() { printf '%s/${PERSISTENT_ROOT}/image-%s' "$(pwd)" "$(printf %s "$1" | sed 's/[^A-Za-z0-9._-]/-/g')"; }`,
622
679
  `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} ]; }`
680
+ `served() { ${servedRuntimeVersionTest(`"$1/${SANDBOX_IMAGE_RUNTIME_VERSION_FILE}"`)}; }`
624
681
  ].join(" && ");
625
682
  var IMAGE_DOWNLOAD_INTERRUPTED = "image_download_interrupted";
626
683
  var KEPT_IMAGE_SKIPPED = "kept_image_skipped:";
@@ -642,7 +699,7 @@ function machineBootScripts(boot) {
642
699
  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
700
  const seedNamed = `{ docker image inspect ${named} >/dev/null 2>&1 || pull_resuming ${named}; } && { [ -f ${trees}/${IMAGE_TREES_COMPLETE_MARKER} ] || { ${seedTrees}; }; } && ${seedStub}`;
644
701
  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`;
702
+ 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
703
  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
704
  const prepare = [
648
705
  `chmod 600 ${boot.staged.env}`,
@@ -697,7 +754,6 @@ var BOOT_TTL_SECONDS = 30 * 60;
697
754
  var CONTAINER_STOP_GRACE_SECONDS = 30;
698
755
  var HOST_RELAY_NOT_STOPPED = "The sandbox could not stop the localhost relay of this port. Try again.";
699
756
  var STDERR_TAIL_CHARS = 600;
700
- var REDACTED_VALUE_MIN_LENGTH = 8;
701
757
  var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
702
758
  var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
703
759
  var TRANSIENT_MARKERS = ["transport_failed", "idempotency_in_progress"];
@@ -720,11 +776,8 @@ function archivedOrThrow(err) {
720
776
  function sleep(ms) {
721
777
  return new Promise((resolve) => setTimeout(resolve, ms));
722
778
  }
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
779
  function scrubbedTail(text, secrets) {
727
- return scrubbed(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
780
+ return redactWorkspaceRuntimeBootSecrets(text, secrets).trim().slice(-STDERR_TAIL_CHARS);
728
781
  }
729
782
  var BoatDriverError = class extends Error {
730
783
  constructor(message) {
@@ -834,7 +887,7 @@ function createBoatSandboxDriver(options) {
834
887
  command: `docker inspect --format 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' ${shell(containerName)}; docker logs --tail 20 ${shell(containerName)} 2>&1`,
835
888
  timeoutSeconds: 10
836
889
  });
837
- diagnostic = runtimeBootDiagnostic(scrubbed(`${result.stdout}
890
+ diagnostic = runtimeBootDiagnostic(redactWorkspaceRuntimeBootSecrets(`${result.stdout}
838
891
  ${result.stderr}`, secrets), STDERR_TAIL_CHARS);
839
892
  } catch {
840
893
  diagnostic = "container diagnostics unavailable";
@@ -5,19 +5,16 @@ export type CloudflareSandboxDriverOptions = {
5
5
  workerUrl: string;
6
6
  /** Bearer token the Worker validates (its `API_TOKEN` secret). */
7
7
  apiToken: string;
8
- /** Port the workspace-runtime listens on inside the sandbox (exposed publicly). */
8
+ /** Port the workspace-runtime listens on inside the container, which the Worker's proxy route forwards to. */
9
9
  runtimePort?: number;
10
- /** Command that launches the workspace-runtime inside the image. */
11
10
  runtimeCommand?: string;
12
- /** Workspace directory inside the sandbox. */
13
11
  workspaceDir?: string;
14
12
  /**
15
13
  * Static control-plane config injected into every sandbox so the runtime can
16
- * verify relay-proxied requests. Never carries per-workspace secrets — those
17
- * (if any) belong to the future egress broker.
18
- * relayJwksUrl → WORKSPACE_RUNTIME_RELAY_JWKS_URL (relay public key for RAT verify)
19
- * relayVerifyPem → WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM (fallback to JWKS)
20
- * managementJwksUrl → WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL (config apply)
14
+ * verify relay-proxied requests; never a per-workspace secret.
15
+ * relayJwksUrl → WORKSPACE_RUNTIME_RELAY_JWKS_URL (relay host token keys)
16
+ * relayVerifyPem → WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM (fallback to JWKS)
17
+ * managementJwksUrl → WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL (config apply)
21
18
  */
22
19
  controlEnv?: WorkspaceRuntimeControlEnv;
23
20
  /** Default native harness injected as WORKSPACE_RUNTIME_NATIVE_HARNESS, for example "opencode". */
@@ -26,7 +23,6 @@ export type CloudflareSandboxDriverOptions = {
26
23
  env?: (input: SandboxDriverEnsureInput, sandbox: {
27
24
  id: string;
28
25
  }) => Record<string, string> | Promise<Record<string, string>>;
29
- /** Injected for tests. */
30
26
  fetch?: typeof fetch;
31
27
  /** ensure-runtime/HTTP timeout. */
32
28
  timeoutMs?: number;
@@ -12,6 +12,27 @@ var boatBilling = {
12
12
  large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
13
13
  }
14
14
  };
15
+ var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
16
+ var AGENT37_MONTH_HOURS = 730;
17
+ var agent37MachineShapes = {
18
+ small: { cpu: 2, memory: 4, disk: 10 },
19
+ default: { cpu: 4, memory: 8, disk: 20 },
20
+ large: { cpu: 4, memory: 16, disk: 40 }
21
+ };
22
+ function agent37Rate(shape) {
23
+ const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
24
+ return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
25
+ }
26
+ var agent37Billing = {
27
+ source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
28
+ 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.",
29
+ rates: {
30
+ small: agent37Rate(agent37MachineShapes.small),
31
+ default: agent37Rate(agent37MachineShapes.default),
32
+ large: agent37Rate(agent37MachineShapes.large)
33
+ },
34
+ excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
35
+ };
15
36
  var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
16
37
  var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
17
38
  var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
@@ -50,6 +71,7 @@ var sandboxDriverCredentialFields = {
50
71
  { key: "worker_url", label: "Worker URL" }
51
72
  ],
52
73
  boat: [{ key: "api_key", label: "API Key", secret: true }],
74
+ agent37: [{ key: "api_key", label: "API Key", secret: true }],
53
75
  docker: [{ key: "image", label: "Image" }]
54
76
  };
55
77
  var sandboxDriverLabels = {
@@ -57,6 +79,7 @@ var sandboxDriverLabels = {
57
79
  vercel: "Vercel",
58
80
  cloudflare: "Cloudflare",
59
81
  boat: "Boat",
82
+ agent37: "Agent37",
60
83
  docker: "Docker"
61
84
  };
62
85
  function cloudflareWorkerBaseUrl(input) {
@@ -139,8 +162,8 @@ var sandboxDriverCatalog = {
139
162
  hostWatchesIdle: true,
140
163
  targetAccess: "relay",
141
164
  secretBrokering: "native",
142
- // Credential handlers do not restrict unrelated destinations.
143
- egressControl: "none",
165
+ // The sandbox Worker starts the container without internet and routes each allowed host through itself; a Worker that cannot confirm that refuses the start.
166
+ egressControl: "hosts",
144
167
  // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
145
168
  persistence: {
146
169
  resume: "replacement-restore",
@@ -178,6 +201,33 @@ var sandboxDriverCatalog = {
178
201
  machineClasses: ["small", "default", "large"]
179
202
  }
180
203
  },
204
+ agent37: {
205
+ id: "agent37",
206
+ label: sandboxDriverLabels.agent37,
207
+ credentialFields: sandboxDriverCredentialFields.agent37,
208
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
209
+ runtimeEnv: "callback",
210
+ preparedImage: true,
211
+ billing: agent37Billing,
212
+ metadata: {
213
+ driverRunsIn: ["worker", "node"],
214
+ hostStopBehavior: "suspends-host",
215
+ hostResumeBehavior: "same-host",
216
+ targetAccess: "relay",
217
+ secretBrokering: "none",
218
+ // An instance's network policy lists domains, reached over HTTPS on 443 alone, and IPv4 ranges; private ranges stay blocked whatever it lists.
219
+ egressControl: "hosts-and-cidrs",
220
+ persistence: {
221
+ resume: "same-sandbox",
222
+ capture: "none",
223
+ clone: false,
224
+ captureSource: "not-applicable",
225
+ retention: "provider-managed",
226
+ restoreMount: "same-resource"
227
+ },
228
+ machineClasses: ["small", "default", "large"]
229
+ }
230
+ },
181
231
  docker: {
182
232
  id: "docker",
183
233
  label: sandboxDriverLabels.docker,
@@ -441,10 +491,15 @@ function createCloudflareSandboxDriver(options) {
441
491
  };
442
492
  });
443
493
  }
494
+ function networkPolicy(input) {
495
+ if (!input.net || input.net.mode === "allow-all") return void 0;
496
+ return { hosts: [...new Set(input.net.hosts ?? [])] };
497
+ }
444
498
  async function ensureHost(input) {
445
499
  const sandboxId = sandboxIdFor(input.workspaceId);
446
500
  const hostId = sandboxId;
447
501
  const egress = egressRegistrations(input);
502
+ const net = networkPolicy(input);
448
503
  const response = await call(
449
504
  sandboxId,
450
505
  "ensure-runtime",
@@ -457,7 +512,8 @@ function createCloudflareSandboxDriver(options) {
457
512
  // Not container env — labels are control-plane metadata.
458
513
  labels: input.labels,
459
514
  ...input.bootSource?.kind === "driver-snapshot" ? { restore: { snapshotId: input.bootSource.snapshotId }, minimumRuntimeVersion: MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION } : {},
460
- ...egress ? { egress } : {}
515
+ ...egress ? { egress } : {},
516
+ ...net ? { net } : {}
461
517
  }
462
518
  ).catch((error) => {
463
519
  if (error instanceof Error && error.name === "TimeoutError") return void 0;
@@ -477,6 +533,10 @@ function createCloudflareSandboxDriver(options) {
477
533
  const reason = `Cloudflare ensure-runtime failed (${status}): ${text(data.error) ?? "no runtime url"}`;
478
534
  throw status === 401 || status === 403 ? new SandboxProviderKeyRefusedError(reason) : new Error(reason);
479
535
  }
536
+ if (net && asRecord(data.egress)?.contained !== true) {
537
+ await call(sandboxId, "stop", { epoch: input.epoch }).catch(() => void 0);
538
+ throw new Error("Cloudflare sandbox Worker did not confirm egress containment; deploy the current sandbox Worker (scripts/sandbox/cloudflare-worker)");
539
+ }
480
540
  const targetOut = {
481
541
  workspaceId: input.workspaceId,
482
542
  sandboxId,
@@ -562,7 +622,7 @@ function createCloudflareSandboxDriver(options) {
562
622
  hostWatchesIdle: true,
563
623
  targetAccess: "relay",
564
624
  secretBrokering: "native",
565
- egressControl: "none",
625
+ egressControl: "hosts",
566
626
  persistence: sandboxDriverCatalog.cloudflare.metadata.persistence
567
627
  },
568
628
  ensureHost,