@claxedo/sandbox-manager 0.10.1 → 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.
Files changed (42) hide show
  1. package/README.md +100 -30
  2. package/dist/checkpoint-manager.mjs +19 -0
  3. package/dist/contract.d.ts +40 -17
  4. package/dist/driver-billing.d.ts +23 -0
  5. package/dist/driver-catalog.mjs +57 -3
  6. package/dist/driver-ensure-input.d.ts +22 -0
  7. package/dist/drivers/agent37-client.d.ts +87 -0
  8. package/dist/drivers/agent37.d.ts +25 -0
  9. package/dist/drivers/agent37.mjs +888 -0
  10. package/dist/drivers/boat-client.d.ts +4 -4
  11. package/dist/drivers/boat-machine.d.ts +80 -0
  12. package/dist/drivers/boat-preparation.test-support.d.ts +2 -0
  13. package/dist/drivers/boat-template.mjs +196 -92
  14. package/dist/drivers/boat.d.ts +0 -17
  15. package/dist/drivers/boat.mjs +200 -108
  16. package/dist/drivers/cloudflare.d.ts +5 -9
  17. package/dist/drivers/cloudflare.mjs +71 -7
  18. package/dist/drivers/docker.mjs +57 -2
  19. package/dist/drivers/local-brokering.d.ts +12 -0
  20. package/dist/drivers/local-brokering.mjs +99 -24
  21. package/dist/drivers/modal.mjs +57 -2
  22. package/dist/drivers/transient-error.d.ts +0 -1
  23. package/dist/drivers/vercel.mjs +61 -3
  24. package/dist/egress-policy.d.ts +33 -32
  25. package/dist/egress-warnings.d.ts +4 -6
  26. package/dist/hosted-network-policy.d.ts +10 -64
  27. package/dist/hosted-network-policy.mjs +1 -1
  28. package/dist/image-name.d.ts +0 -1
  29. package/dist/image-name.mjs +5 -0
  30. package/dist/image.d.ts +1 -0
  31. package/dist/image.mjs +6 -0
  32. package/dist/index.mjs +180 -112
  33. package/dist/json.d.ts +1 -10
  34. package/dist/lease-failure.d.ts +20 -1
  35. package/dist/lease-types.d.ts +1 -10
  36. package/dist/runtime-env.d.ts +6 -0
  37. package/dist/runtime-env.mjs +5 -0
  38. package/dist/runtime-version.d.ts +25 -7
  39. package/dist/runtime-version.mjs +13 -2
  40. package/dist/start-admission.d.ts +32 -0
  41. package/dist/start-telemetry.d.ts +5 -3
  42. package/package.json +12 -4
package/dist/index.mjs CHANGED
@@ -83,9 +83,9 @@ function sandboxNetworkPolicyWithBrokeredHosts(policy, secrets) {
83
83
  ...hosts.length ? { hosts } : {}
84
84
  };
85
85
  }
86
- function sandboxEgressDisposition(control, net) {
86
+ function sandboxEgressDisposition(control, net, uncontained = "refuse") {
87
87
  if (!net || net.mode === "allow-all") return { action: "enforce" };
88
- if (control === "none") return { action: "withhold", reason: "sandbox_egress_uncontained" };
88
+ if (control === "none") return { action: uncontained === "allow" ? "withhold" : "refuse", reason: "sandbox_egress_uncontained" };
89
89
  const hosts = net.hosts?.length ?? 0;
90
90
  const cidrs = net.cidrs?.length ?? 0;
91
91
  if (hosts === 0 && cidrs === 0) return { action: "enforce" };
@@ -110,6 +110,27 @@ var boatBilling = {
110
110
  large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
111
111
  }
112
112
  };
113
+ var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
114
+ var AGENT37_MONTH_HOURS = 730;
115
+ var agent37MachineShapes = {
116
+ small: { cpu: 2, memory: 4, disk: 10 },
117
+ default: { cpu: 4, memory: 8, disk: 20 },
118
+ large: { cpu: 4, memory: 16, disk: 40 }
119
+ };
120
+ function agent37Rate(shape) {
121
+ const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
122
+ return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
123
+ }
124
+ var agent37Billing = {
125
+ source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
126
+ 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.",
127
+ rates: {
128
+ small: agent37Rate(agent37MachineShapes.small),
129
+ default: agent37Rate(agent37MachineShapes.default),
130
+ large: agent37Rate(agent37MachineShapes.large)
131
+ },
132
+ excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
133
+ };
113
134
  var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
114
135
  var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
115
136
  var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
@@ -314,16 +335,17 @@ function isPrivateIpAddress(ip) {
314
335
  return isPrivateIpv4(embedded);
315
336
  return false;
316
337
  }
317
- var sandboxCheckpointUnusableCauses = ["expired", "runtime_outdated", "runtime_modified", "not_restorable"];
338
+ var sandboxCheckpointUnusableCauses = ["expired", "runtime_modified", "not_restorable"];
318
339
  function isSandboxCheckpointUnusableCause(value) {
319
340
  return sandboxCheckpointUnusableCauses.some((cause) => cause === value);
320
341
  }
321
- var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "docker"];
342
+ var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "agent37", "docker"];
322
343
  var sandboxDriverLabels = {
323
344
  modal: "Modal",
324
345
  vercel: "Vercel",
325
346
  cloudflare: "Cloudflare",
326
347
  boat: "Boat",
348
+ agent37: "Agent37",
327
349
  docker: "Docker"
328
350
  };
329
351
  function isSandboxDriverID(input) {
@@ -378,6 +400,17 @@ var SandboxRuntimeBootError = class extends Error {
378
400
  this.name = "SandboxRuntimeBootError";
379
401
  }
380
402
  };
403
+ function isSandboxImageDownloadFailure(err) {
404
+ return err instanceof Error && "imageDownloadFailed" in err && err.imageDownloadFailed === true;
405
+ }
406
+ var SandboxImageDownloadError = class extends Error {
407
+ imageDownloadFailed = true;
408
+ constructor(reason) {
409
+ super(reason);
410
+ this.name = "SandboxImageDownloadError";
411
+ }
412
+ };
413
+ var SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED = "update_download_interrupted";
381
414
  function isSandboxCheckpointUnusable(err) {
382
415
  return !!err && typeof err === "object" && isSandboxCheckpointUnusableCause(err.checkpointUnusable);
383
416
  }
@@ -423,6 +456,13 @@ function sandboxDriverLabel(driver) {
423
456
  }
424
457
  var PROVIDER_KEY_REFUSED = "provider_key_refused:";
425
458
  var PROVIDER_FAILED = "provider_failed:";
459
+ var EGRESS_UNCONTAINED = "sandbox_egress_uncontained:";
460
+ function sandboxEgressUncontainedError(driver) {
461
+ return `${EGRESS_UNCONTAINED}${driver}`;
462
+ }
463
+ function uncontainedEgressReason(provider) {
464
+ return `${provider} cannot contain a sandbox's network; an org admin can allow unrestricted internet for ${provider} machines in Settings \u2192 Sandbox providers.`;
465
+ }
426
466
  function sandboxProviderKeyRefusedError(driver, message) {
427
467
  return `${PROVIDER_KEY_REFUSED}${driver}: ${message}`;
428
468
  }
@@ -516,7 +556,8 @@ var SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
516
556
  var SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
517
557
  var CODED_REASONS = /* @__PURE__ */ new Map([
518
558
  [SANDBOX_RUNTIME_UNHEALTHY, "The workspace machine stopped answering."],
519
- [SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."]
559
+ [SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."],
560
+ [SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED, "The update couldn't finish downloading; your workspace is still running. Try again."]
520
561
  ]);
521
562
  function firstLine(text) {
522
563
  return text.split("\n", 1)[0]?.trim() ?? text;
@@ -530,6 +571,8 @@ function sandboxStartFailureReason(error) {
530
571
  if (keyRefused !== void 0) return `${sandboxDriverLabel(keyRefused)} refused the sandbox provider key; fix it in Settings \u2192 Sandbox providers.`;
531
572
  const failed = codedDriver(error, PROVIDER_FAILED);
532
573
  if (failed !== void 0) return `${sandboxDriverLabel(failed)} returned an error.`;
574
+ const uncontained = codedDriver(error, EGRESS_UNCONTAINED);
575
+ if (uncontained !== void 0) return uncontainedEgressReason(sandboxDriverLabel(uncontained));
533
576
  return CODED_REASONS.get(error) ?? firstLine(error);
534
577
  }
535
578
  var LEASE_ENDED_ERRORS = /* @__PURE__ */ new Set(["runtime_lease_changed", "runtime_lease_stopped", "runtime_lease_destroyed"]);
@@ -575,6 +618,11 @@ var PUBLIC_API_ERRORS = {
575
618
  org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
576
619
  org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
577
620
  org_invitation_rate_limited: { family: "access", status: 429, message: "Organization invitation limit exceeded" },
621
+ org_limit_reached: { family: "access", status: 403, message: "An account owns at most 3 organizations" },
622
+ org_creation_rate_limited: { family: "access", status: 429, message: "Organization creation limit exceeded" },
623
+ project_limit_reached: { family: "access", status: 402, message: "Free accounts have 3 projects. Get Cloud or Self-host for unlimited projects." },
624
+ seats_full: { family: "access", status: 402, message: "Add teammate seats to invite more people." },
625
+ email_verification_required: { family: "access", status: 403, message: "A verified email address is required" },
578
626
  org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
579
627
  org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
580
628
  team_not_found: { family: "access", status: 404, message: "Team not found" },
@@ -584,6 +632,12 @@ var PUBLIC_API_ERRORS = {
584
632
  project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
585
633
  project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
586
634
  resource_conflict: { family: "access", status: 409, message: "Organization or team authority changed concurrently" },
635
+ org_not_deletable: { family: "access", status: 409, message: "A personal organization goes with its account, and a deployment's with the deployment" },
636
+ org_has_workspaces: { family: "access", status: 409, message: "Delete the organization's workspaces first" },
637
+ account_owns_workspaces: { family: "access", status: 409, message: "Delete your workspaces first" },
638
+ account_founds_organization: { family: "access", status: 409, message: "Delete the organizations you founded first" },
639
+ account_has_cloud_subscription: { family: "access", status: 409, message: "Cancel Cloud before deleting this account" },
640
+ recent_sign_in_required: { family: "access", status: 403, message: "Sign in again to confirm this deletion" },
587
641
  session_not_found: { family: "session_share", status: 404, message: "This session is not on the control plane, so it cannot be shared from People yet." },
588
642
  session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
589
643
  session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
@@ -607,11 +661,19 @@ var PUBLIC_API_ERRORS = {
607
661
  enrollment_paused: { status: 403, message: "enrollment_paused" },
608
662
  enrollment_owner_ineligible: { status: 403, message: "enrollment_owner_ineligible" },
609
663
  enrollment_key_version_mismatch: { status: 403, message: "enrollment_key_version_mismatch" },
664
+ enrollment_denied: { status: 403, message: "enrollment_denied" },
665
+ enrollment_approval_expired: { status: 403, message: "enrollment_approval_expired" },
666
+ enrollment_awaiting_approval: { status: 403, message: "enrollment_awaiting_approval" },
610
667
  invitation_invalid: { status: 403, message: "invitation_invalid" },
611
668
  invitation_expired: { status: 410, message: "invitation_expired" },
612
669
  invitation_revoked: { status: 410, message: "invitation_revoked" },
613
670
  invitation_redeemed: { status: 409, message: "invitation_redeemed" },
614
671
  invitation_host_conflict: { status: 409, message: "invitation_host_conflict" },
672
+ pairing_invalid: { status: 403, message: "pairing_invalid" },
673
+ pairing_expired: { status: 410, message: "pairing_expired" },
674
+ pairing_used: { status: 409, message: "pairing_used" },
675
+ pairing_owner_signed_out: { status: 410, message: "pairing_owner_signed_out" },
676
+ pairing_decided: { status: 409, message: "pairing_decided" },
615
677
  enrollment_generation_superseded: { status: 409, message: "enrollment_generation_superseded" },
616
678
  host_assignment_outside_scope: { status: 400, message: "host_assignment_outside_scope" },
617
679
  host_sealing_key_undeclared: { status: 409, message: "host_sealing_key_undeclared" },
@@ -779,7 +841,7 @@ var SANDBOX_MODEL_PROVIDER_HOSTS = [
779
841
  "api.openai.com",
780
842
  "generativelanguage.googleapis.com",
781
843
  "openrouter.ai",
782
- // Model catalog + engine config, fetched on runner startup.
844
+ // The model catalog and engine config a runner fetches at startup.
783
845
  "models.dev",
784
846
  "opencode.ai"
785
847
  ];
@@ -865,13 +927,55 @@ async function refusedReason(leaseStore, workspaceId, epoch) {
865
927
  return "runtime_lease_not_serving";
866
928
  }
867
929
 
930
+ // src/driver-ensure-input.ts
931
+ function sandboxId(driver, workspaceId) {
932
+ return `${driver}-${workspaceId}`;
933
+ }
934
+ function leaseHostId(driver, workspaceId, lease, managerInput) {
935
+ return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
936
+ }
937
+ function ensureHostInput(input) {
938
+ const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net, input.managerInput?.uncontainedEgress);
939
+ const labels = {
940
+ app: input.appLabel,
941
+ workspaceId: input.workspaceId,
942
+ epoch: String(input.lease.epoch),
943
+ homeRegion: input.homeRegion,
944
+ ...input.managerInput?.labels
945
+ };
946
+ return {
947
+ workspaceId: input.workspaceId,
948
+ hostId: leaseHostId(input.driver, input.workspaceId, input.lease, input.managerInput),
949
+ homeRegion: input.homeRegion,
950
+ epoch: input.lease.epoch,
951
+ labels,
952
+ bootSource: input.managerInput?.bootSource ?? (input.lease.checkpoint && input.lease.status !== "ready" && input.driver.metadata.persistence.resume === "replacement-restore" ? { kind: "driver-snapshot", snapshotId: input.lease.checkpoint.providerReference } : { kind: "default" }),
953
+ workspaceRoot: input.managerInput?.workspaceRoot ?? "/workspace",
954
+ runtimeCwd: input.managerInput?.runtimeCwd,
955
+ workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
956
+ env: { ...input.managerInput?.env, ...input.leaseEnv },
957
+ // Brokered secrets ride their own channel — NEVER merged into labels or env.
958
+ // Presence of the key, not its length, is the signal: `[]` is the caller
959
+ // withdrawing every brokered secret, and dropping it would reach the driver
960
+ // as "preserve what you have".
961
+ ...input.managerInput?.secrets ? { secrets: input.managerInput.secrets } : {},
962
+ source: input.managerInput?.source,
963
+ ...input.managerInput?.updateImage ? { updateImage: true } : {},
964
+ ...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
965
+ exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
966
+ // Withheld where the start allows it, never downgraded: `undefined`
967
+ // here is the same shape a caller that asked for no containment produces,
968
+ // so the throwing drivers take their allow-all path and their throws stay
969
+ // intact for anyone who hands them a policy directly.
970
+ ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
971
+ snapshot: input.managerInput?.snapshot
972
+ };
973
+ }
974
+
868
975
  // src/egress-warnings.ts
869
976
  function egressUnenforcedMessage(input) {
870
- const scope = input.workspaceId ? `workspace ${input.workspaceId}` : "every sandbox it provisions";
871
- const withheld = input.requested ? ` The requested allowlist (${(input.requested.hosts ?? []).length} host(s), ${(input.requested.cidrs ?? []).length} cidr(s)) was withheld, not applied.` : "";
872
- return `[sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "${input.driver}" declares egressControl: "none", so ${scope} can reach ANY host on the internet, including attacker-controlled buckets \u2014 agent-authored code inside the sandbox has an unmonitored exfiltration path.${withheld} Select a driver that can enforce egress (vercel) to close it. See public-docs/sandbox-egress.md.`;
977
+ return `[sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "${input.driver}" declares egressControl: "none" and this start allows uncontained egress, so workspace ${input.workspaceId} can reach ANY host on the internet, including attacker-controlled buckets \u2014 agent-authored code inside the sandbox has an unmonitored exfiltration path. The requested allowlist (${(input.requested.hosts ?? []).length} host(s), ${(input.requested.cidrs ?? []).length} cidr(s)) was withheld, not applied. Select a driver that can enforce egress (cloudflare, vercel), or stop allowing uncontained egress so such starts are refused. See public-docs/sandbox-egress.md.`;
873
978
  }
874
- var compositionEgressWarnings = /* @__PURE__ */ new Set();
875
979
  var ensureEgressWarnings = /* @__PURE__ */ new Set();
876
980
  function defaultEgressUnenforcedSink(event) {
877
981
  console.warn(event.message);
@@ -880,26 +984,15 @@ function createEgressWarnings(input) {
880
984
  const { id, egressControl } = input.driver;
881
985
  const sink = input.sink ?? defaultEgressUnenforcedSink;
882
986
  const key = `${id}|${egressControl}`;
883
- if (egressControl === "none" && !compositionEgressWarnings.has(key)) {
884
- compositionEgressWarnings.add(key);
885
- sink({
886
- phase: "composition",
887
- reason: "sandbox_egress_uncontained",
888
- driver: id,
889
- egressControl,
890
- message: egressUnenforcedMessage({ phase: "composition", driver: id })
891
- });
892
- }
893
987
  return {
894
988
  withheld(start) {
895
989
  if (ensureEgressWarnings.has(key)) return;
896
990
  ensureEgressWarnings.add(key);
897
991
  sink({
898
- phase: "ensure",
899
992
  reason: "sandbox_egress_uncontained",
900
993
  driver: id,
901
994
  egressControl,
902
- message: egressUnenforcedMessage({ phase: "ensure", driver: id, workspaceId: start.workspaceId, requested: start.requested }),
995
+ message: egressUnenforcedMessage({ driver: id, workspaceId: start.workspaceId, requested: start.requested }),
903
996
  workspaceId: start.workspaceId,
904
997
  ...start.labels ? { labels: start.labels } : {},
905
998
  requested: start.requested
@@ -908,6 +1001,51 @@ function createEgressWarnings(input) {
908
1001
  };
909
1002
  }
910
1003
 
1004
+ // src/runtime-env.ts
1005
+ var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
1006
+ "WORKSPACE_RUNTIME_WORKSPACE_ID",
1007
+ "WORKSPACE_RUNTIME_HOST_ID",
1008
+ "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
1009
+ ];
1010
+ function workspaceRuntimeIdentityEnvConflicts(env) {
1011
+ if (!env) return [];
1012
+ return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
1013
+ }
1014
+ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
1015
+ ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
1016
+ "WORKSPACE_RUNTIME_DIRECTORY",
1017
+ "WORKSPACE_RUNTIME_PORT",
1018
+ "WORKSPACE_RUNTIME_HOST",
1019
+ "WORKSPACE_RUNTIME_SOURCE_KIND",
1020
+ "WORKSPACE_RUNTIME_GIT_REPO_URL",
1021
+ "WORKSPACE_RUNTIME_GIT_BRANCH",
1022
+ "WORKSPACE_RUNTIME_NATIVE_HARNESS",
1023
+ "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
1024
+ "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
1025
+ "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
1026
+ ]);
1027
+
1028
+ // src/start-admission.ts
1029
+ function refusedStart(input) {
1030
+ const { driver, workspaceId, start } = input;
1031
+ const egress = sandboxEgressDisposition(driver.metadata.egressControl, start.net, start.uncontainedEgress);
1032
+ if (egress.action === "withhold" && start.net) {
1033
+ input.egressWarnings.withheld({ workspaceId, ...start.labels ? { labels: start.labels } : {}, requested: start.net });
1034
+ }
1035
+ if (egress.action === "refuse") {
1036
+ const error = egress.reason === "sandbox_egress_uncontained" ? sandboxEgressUncontainedError(driver.id) : egress.reason;
1037
+ return { status: "unavailable", error, homeRegion: start.homeRegion };
1038
+ }
1039
+ const identityConflicts = workspaceRuntimeIdentityEnvConflicts(start.env);
1040
+ if (identityConflicts.length) {
1041
+ return { status: "unavailable", error: `sandbox env cannot set runtime identity: ${identityConflicts.join(", ")}`, homeRegion: start.homeRegion };
1042
+ }
1043
+ if (start.machineClass && !driver.metadata.machineClasses?.includes(start.machineClass)) {
1044
+ return { status: "unavailable", error: `sandbox_machine_class_unsupported: ${driver.id} has no ${start.machineClass} machines`, homeRegion: start.homeRegion };
1045
+ }
1046
+ return void 0;
1047
+ }
1048
+
911
1049
  // src/garbage-collect.ts
912
1050
  async function sweepSandboxes(input) {
913
1051
  const { driver, leaseStore, appLabel } = input;
@@ -997,13 +1135,15 @@ function createSandboxStartRecorder(options) {
997
1135
  * lease as it ends, so a poller reading the lease mid-boot sees how far the
998
1136
  * start has come. `opened` writes the epoch's `lease_decision` before the
999
1137
  * driver runs, so a stop that lands mid-boot finds the boot it ends on the
1000
- * row. A serving lease is not starting. An epoch acquired with a start it
1001
- * continues has that start and no phase yet; one acquired without begins
1002
- * its own here.
1138
+ * row. A serving lease is not starting, unless the step is a person's
1139
+ * update, which is a start of its own on the same epoch. An epoch acquired
1140
+ * with a start it continues has that start and no phase yet; one acquired
1141
+ * without begins its own here.
1003
1142
  */
1004
1143
  observe(lease, input) {
1005
- if (lease.status === "ready") return void 0;
1006
- let progress = lease.start ?? { requestedAt: input.enteredAt, startedAt: input.enteredAt, bootMode: input.bootMode, markedAt: input.enteredAt, phases: [] };
1144
+ if (lease.status === "ready" && !input.update) return void 0;
1145
+ const begun = { requestedAt: input.enteredAt, startedAt: input.enteredAt, bootMode: input.bootMode, markedAt: input.enteredAt, phases: [] };
1146
+ let progress = lease.status === "ready" ? begun : lease.start ?? begun;
1007
1147
  const ended = [];
1008
1148
  let markedAt = progress.markedAt;
1009
1149
  const mark = (phase) => {
@@ -1109,74 +1249,8 @@ async function startSandboxFresh(input) {
1109
1249
  return { ok: true, status };
1110
1250
  }
1111
1251
 
1112
- // src/runtime-env.ts
1113
- var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
1114
- "WORKSPACE_RUNTIME_WORKSPACE_ID",
1115
- "WORKSPACE_RUNTIME_HOST_ID",
1116
- "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
1117
- ];
1118
- function workspaceRuntimeIdentityEnvConflicts(env) {
1119
- if (!env) return [];
1120
- return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
1121
- }
1122
- var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
1123
- ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
1124
- "WORKSPACE_RUNTIME_DIRECTORY",
1125
- "WORKSPACE_RUNTIME_PORT",
1126
- "WORKSPACE_RUNTIME_HOST",
1127
- "WORKSPACE_RUNTIME_SOURCE_KIND",
1128
- "WORKSPACE_RUNTIME_GIT_REPO_URL",
1129
- "WORKSPACE_RUNTIME_GIT_BRANCH",
1130
- "WORKSPACE_RUNTIME_NATIVE_HARNESS",
1131
- "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
1132
- "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
1133
- "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
1134
- ]);
1135
-
1136
1252
  // src/manager.ts
1137
1253
  var DEFAULT_APP_LABEL = "claxedo";
1138
- function sandboxId(driver, workspaceId) {
1139
- return `${driver}-${workspaceId}`;
1140
- }
1141
- function leaseHostId(driver, workspaceId, lease, managerInput) {
1142
- return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
1143
- }
1144
- function ensureHostInput(input) {
1145
- const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
1146
- const labels = {
1147
- app: input.appLabel,
1148
- workspaceId: input.workspaceId,
1149
- epoch: String(input.lease.epoch),
1150
- homeRegion: input.homeRegion,
1151
- ...input.managerInput?.labels
1152
- };
1153
- return {
1154
- workspaceId: input.workspaceId,
1155
- hostId: leaseHostId(input.driver, input.workspaceId, input.lease, input.managerInput),
1156
- homeRegion: input.homeRegion,
1157
- epoch: input.lease.epoch,
1158
- labels,
1159
- bootSource: input.managerInput?.bootSource ?? (input.lease.checkpoint && input.lease.status !== "ready" && input.driver.metadata.persistence.resume === "replacement-restore" ? { kind: "driver-snapshot", snapshotId: input.lease.checkpoint.providerReference } : { kind: "default" }),
1160
- workspaceRoot: input.managerInput?.workspaceRoot ?? "/workspace",
1161
- runtimeCwd: input.managerInput?.runtimeCwd,
1162
- workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
1163
- env: { ...input.managerInput?.env, ...input.leaseEnv },
1164
- // Brokered secrets ride their own channel — NEVER merged into labels or env.
1165
- // Presence of the key, not its length, is the signal: `[]` is the caller
1166
- // withdrawing every brokered secret, and dropping it would reach the driver
1167
- // as "preserve what you have".
1168
- ...input.managerInput?.secrets ? { secrets: input.managerInput.secrets } : {},
1169
- source: input.managerInput?.source,
1170
- ...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
1171
- exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
1172
- // Withheld, never downgraded: `undefined` here is the same shape a caller
1173
- // that asked for no containment produces, so the throwing drivers take
1174
- // their allow-all path and their throws stay intact for anyone who hands
1175
- // them a policy directly.
1176
- ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
1177
- snapshot: input.managerInput?.snapshot
1178
- };
1179
- }
1180
1254
  function createSandboxManager(options) {
1181
1255
  const persistence = validateSandboxPersistenceCapabilities(options.driver.metadata.persistence);
1182
1256
  if (!persistence.valid) throw new Error(`Invalid sandbox persistence capabilities: ${persistence.reason}`);
@@ -1286,7 +1360,7 @@ function createSandboxManager(options) {
1286
1360
  }
1287
1361
  const resuming = Boolean(lease.sandboxId && ensure.bootSource?.kind === "default" && options.driver.resumeHost);
1288
1362
  const bootMode = resuming && lease.url ? "resume" : ensure.bootSource?.kind === "driver-snapshot" ? "restore" : "cold-start";
1289
- const start = starts.observe(lease, { bootMode, enteredAt, labels: ensure.labels });
1363
+ const start = starts.observe(lease, { bootMode, enteredAt, labels: ensure.labels, ...managerInput.updateImage ? { update: true } : {} });
1290
1364
  await start?.opened();
1291
1365
  ensure.onResource = async (resource) => {
1292
1366
  const recorded = await options.leaseStore.recordTarget(workspaceId, lease.epoch, {
@@ -1352,6 +1426,10 @@ function createSandboxManager(options) {
1352
1426
  const error = checkpointUnusable ? sandboxCheckpointUnusableError(checkpointUnusable, message) : bootFailed ? sandboxRuntimeBootFailedError(message) : startLimit ? sandboxProviderRateLimitedError(options.driver.id, startLimit) : isSandboxProviderKeyRefused(err) ? sandboxProviderKeyRefusedError(options.driver.id, message) : isSandboxStartRefusal(err) || sandboxLeaseEndedUnderStart(message) ? message : sandboxProviderFailedError(options.driver.id, message);
1353
1427
  if (lease.status === "ready" && !bootFailed) {
1354
1428
  const updated = await options.leaseStore.update(workspaceId, lease.epoch, { lastError: error }, "ready");
1429
+ if (managerInput.updateImage) {
1430
+ console.warn("A workspace's update to the deployment's image failed; it keeps serving its image", { workspaceId, epoch: lease.epoch, error });
1431
+ return { status: "unavailable", error: isSandboxImageDownloadFailure(err) ? SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED : error, epoch: lease.epoch, homeRegion };
1432
+ }
1355
1433
  if (updated) {
1356
1434
  const resolved = await leaseTarget(updated);
1357
1435
  if (resolved.status === "ready") return { ...resolved, stale: true };
@@ -1482,24 +1560,8 @@ function createSandboxManager(options) {
1482
1560
  return lease.sandboxId ? lease.nextRetryAt !== void 0 || stale : lease.nextRetryAt !== void 0 && !stale;
1483
1561
  }
1484
1562
  async function admit(workspaceId, input) {
1485
- const egress = sandboxEgressDisposition(options.driver.metadata.egressControl, input.net);
1486
- if (egress.action === "withhold" && input.net) {
1487
- egressWarnings.withheld({ workspaceId, ...input.labels ? { labels: input.labels } : {}, requested: input.net });
1488
- }
1489
- if (egress.action === "refuse") {
1490
- return { status: "unavailable", error: egress.reason, homeRegion: input.homeRegion };
1491
- }
1492
- const identityConflicts = workspaceRuntimeIdentityEnvConflicts(input.env);
1493
- if (identityConflicts.length) {
1494
- return {
1495
- status: "unavailable",
1496
- error: `sandbox env cannot set runtime identity: ${identityConflicts.join(", ")}`,
1497
- homeRegion: input.homeRegion
1498
- };
1499
- }
1500
- if (input.machineClass && !options.driver.metadata.machineClasses?.includes(input.machineClass)) {
1501
- return { status: "unavailable", error: `sandbox_machine_class_unsupported: ${options.driver.id} has no ${input.machineClass} machines`, homeRegion: input.homeRegion };
1502
- }
1563
+ const refused = refusedStart({ driver: options.driver, workspaceId, start: input, egressWarnings });
1564
+ if (refused) return refused;
1503
1565
  const existing = await options.leaseStore.get(workspaceId);
1504
1566
  const wait = waiting(existing);
1505
1567
  if (wait) return wait;
@@ -1698,12 +1760,16 @@ export {
1698
1760
  SANDBOX_RETRY_CAP_EXCEEDED,
1699
1761
  SANDBOX_RUNTIME_UNHEALTHY,
1700
1762
  SANDBOX_START_WINDOWS,
1763
+ SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED,
1701
1764
  SandboxCheckpointUnusableError,
1702
1765
  SandboxHostGoneError,
1766
+ SandboxImageDownloadError,
1703
1767
  SandboxProviderKeyRefusedError,
1704
1768
  SandboxProviderRateLimitedError,
1705
1769
  SandboxRuntimeBootError,
1706
1770
  SandboxStartRefusedError,
1771
+ agent37Billing,
1772
+ agent37MachineShapes,
1707
1773
  applySandboxLeaseFailure,
1708
1774
  applySandboxLeasePatch,
1709
1775
  applySandboxProvisionedTarget,
@@ -1719,6 +1785,7 @@ export {
1719
1785
  isSandboxCheckpointUnusable,
1720
1786
  isSandboxCheckpointUnusableCause,
1721
1787
  isSandboxHostGone,
1788
+ isSandboxImageDownloadFailure,
1722
1789
  isSandboxListingUnsupported,
1723
1790
  isSandboxProviderKeyRefused,
1724
1791
  isSandboxProviderRateLimited,
@@ -1731,6 +1798,7 @@ export {
1731
1798
  sandboxCheckpointUnusableCauses,
1732
1799
  sandboxCheckpointUnusableError,
1733
1800
  sandboxEgressDisposition,
1801
+ sandboxEgressUncontainedError,
1734
1802
  sandboxHostGoneError,
1735
1803
  sandboxLeaseEndedUnderStart,
1736
1804
  sandboxLeaseFailure,
package/dist/json.d.ts CHANGED
@@ -1,13 +1,4 @@
1
- /**
2
- * Reading untrusted JSON: driver API responses, CLI output, stored records.
3
- *
4
- * Drivers all face the same problem — a provider hands back `any`, and the code
5
- * needs a few named fields out of it. Narrowing through these keeps the check
6
- * and the type in the same place, instead of each site asserting a shape it
7
- * never verified. The object check is `@claxedo/helpers/guards`, re-exported
8
- * under the names the drivers use.
9
- */
10
1
  export { asRecord as record, isRecord } from "@claxedo/helpers/guards";
11
- /** A non-empty string, or undefined — the shape almost every provider field wants. */
2
+ /** An empty string is absent. */
12
3
  export declare function text(input: unknown): string | undefined;
13
4
  export declare function num(input: unknown): number | undefined;
@@ -15,6 +15,23 @@ export declare class SandboxRuntimeBootError extends Error implements SandboxRun
15
15
  readonly runtimeBootFailed: true;
16
16
  constructor(reason: string);
17
17
  }
18
+ /**
19
+ * A driver throws this shape when the image a start pulls did not finish
20
+ * downloading because the registry link kept breaking. Nothing the machine
21
+ * served has changed, so an update that fails this way leaves the workspace
22
+ * serving its image.
23
+ */
24
+ export type SandboxImageDownloadFailure = {
25
+ imageDownloadFailed: true;
26
+ message: string;
27
+ };
28
+ export declare function isSandboxImageDownloadFailure(err: unknown): err is SandboxImageDownloadFailure;
29
+ export declare class SandboxImageDownloadError extends Error implements SandboxImageDownloadFailure {
30
+ readonly imageDownloadFailed: true;
31
+ constructor(reason: string);
32
+ }
33
+ /** The answer to a person's update whose image download kept breaking; the lease keeps the provider's own text. */
34
+ export declare const SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED = "update_download_interrupted";
18
35
  export { isSandboxCheckpointUnusableCause, sandboxCheckpointUnusableCauses, type SandboxCheckpointUnusableCause };
19
36
  /**
20
37
  * A driver throws this shape when the checkpoint a start restores cannot be
@@ -74,6 +91,8 @@ export declare class SandboxStartRefusedError extends Error implements SandboxSt
74
91
  readonly startRefused: true;
75
92
  constructor(reason: string);
76
93
  }
94
+ /** The error a start answers when `driver` declares no egress control and the start does not allow uncontained egress. */
95
+ export declare function sandboxEgressUncontainedError(driver: string): string;
77
96
  /** The error a lease keeps when `driver`'s provider refused its key; the driver's message stays for the operator. */
78
97
  export declare function sandboxProviderKeyRefusedError(driver: string, message: string): string;
79
98
  /** The error a lease keeps when `driver` failed at its provider; the driver's message, which names hosts, requests and provider codes, stays for the operator. */
@@ -127,7 +146,7 @@ export declare function workspaceRuntimeBootReason(diagnostic: string): string |
127
146
  /**
128
147
  * What a driver keeps of a runtime's log within `maxChars`. A Node stack
129
148
  * under the boot-failure line is longer than the bound a driver puts on the
130
- * tail (600 chars on Boat, 2026-10-15 staging), so a tail alone keeps the
149
+ * tail (600 chars on Boat), so a tail alone keeps the
131
150
  * frames and loses the one line that names the reason.
132
151
  */
133
152
  export declare function runtimeBootDiagnostic(log: string, maxChars: number): string;
@@ -6,16 +6,7 @@ export type SandboxLeaseRow = {
6
6
  home_region?: string;
7
7
  epoch: number;
8
8
  status: SandboxLeaseRowStatus;
9
- /**
10
- * The id the driver was registered with, which is any string: the manager
11
- * dispatches on whatever a composition registers (`stores/d1.test.ts`
12
- * provisions one called `"test-provider"`), and every other type this value
13
- * flows between — `SandboxLease["driver"]`, `SandboxLeaseAcquireInput["driver"]`,
14
- * `SandboxTarget["driver"]["id"]` — is already `string`. Typing the row as the
15
- * closed `SandboxDriverID` made it the odd one out and forced every store to
16
- * assert into it. Narrow with `isSandboxDriverID` where a CATALOG entry is
17
- * needed, which is the only place the closed set is the right question.
18
- */
9
+ /** Any string: the manager dispatches on whatever a composition registers. Narrow with `isSandboxDriverID` where a catalog entry is needed. */
19
10
  driver: string;
20
11
  driver_resource_id: string | null;
21
12
  driver_snapshot_id: string | null;
@@ -8,6 +8,12 @@ export type WorkspaceRuntimeControlEnv = {
8
8
  export declare function workspaceRuntimeIdentityEnvConflicts(env: Record<string, string> | undefined): string[];
9
9
  export declare function assertWorkspaceRuntimeIdentityEnv(env: Record<string, string> | undefined): void;
10
10
  export declare function workspaceRuntimeBootSecrets(env: Record<string, string>): string[];
11
+ /**
12
+ * `text` with every boot secret in it replaced. Redact before bounding the
13
+ * text, so a secret the bound would cut in half is already gone. Values
14
+ * shorter than the floor (ports, flags) are left readable.
15
+ */
16
+ export declare function redactWorkspaceRuntimeBootSecrets(text: string, secrets: readonly string[]): string;
11
17
  export declare function workspaceRuntimeBootEnv(input: Parameters<typeof workspaceRuntimeTargetEnv>[0] & Parameters<typeof workspaceRuntimeSourceEnv>[0] & {
12
18
  env?: Record<string, string>;
13
19
  nativeHarness?: string;
@@ -31,6 +31,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
31
31
  function workspaceRuntimeBootSecrets(env) {
32
32
  return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
33
33
  }
34
+ var REDACTED_VALUE_MIN_LENGTH = 8;
35
+ function redactWorkspaceRuntimeBootSecrets(text, secrets) {
36
+ return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
37
+ }
34
38
  function workspaceRuntimeBootEnv(input) {
35
39
  assertWorkspaceRuntimeIdentityEnv(input.env);
36
40
  const env = {
@@ -73,6 +77,7 @@ function workspaceRuntimeServiceExposureEnv(input) {
73
77
  }
74
78
  export {
75
79
  assertWorkspaceRuntimeIdentityEnv,
80
+ redactWorkspaceRuntimeBootSecrets,
76
81
  workspaceRuntimeBootEnv,
77
82
  workspaceRuntimeBootSecrets,
78
83
  workspaceRuntimeIdentityEnvConflicts,
@@ -1,10 +1,28 @@
1
1
  export declare function workspaceRuntimeVersion(): string;
2
2
  /**
3
- * The oldest workspace-runtime version a driver may restore a workspace's
4
- * snapshot into running again. A filesystem snapshot carries the runtime it
5
- * was taken with, so a fix to the runtime reaches a workspace that keeps
6
- * waking only when this rises. Raise it, together with the runtime version,
7
- * for a security-relevant runtime change alone: every snapshot below it is
8
- * refused and its person must start the workspace fresh.
3
+ * The oldest workspace-runtime version this control plane and relay serve, for
4
+ * every driver: a workspace's snapshot restored, or the older image a Boat wake
5
+ * keeps serving instead of waiting on the deployment's. A filesystem snapshot
6
+ * carries the runtime it was taken with, and a kept image the runtime it
7
+ * shipped, so a fix to the runtime reaches a workspace that keeps waking only
8
+ * when this rises. Below it a Cloudflare restore is refused and its person
9
+ * must start the workspace fresh, and a Boat wake boots the deployment's image.
10
+ *
11
+ * Raise it, and the workspace-runtime version with it in the same commit, for
12
+ * a runtime security fix or for a control-plane or relay contract an older
13
+ * runtime cannot speak, and name the first cloud image published at the new
14
+ * version: the version-skew suite (`bun run --cwd packages/harness flows:skew`)
15
+ * runs the current control plane and relay against exactly that image.
16
+ * Bumping the version with it keeps the comparison honest: every image below
17
+ * the new version was built before the change. What it does not cover: the
18
+ * images built after the floor's at the same version are trusted, not tested.
9
19
  */
10
- export declare const MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION = "0.5.2";
20
+ export declare const SERVED_WORKSPACE_RUNTIME_FLOOR: {
21
+ readonly version: "0.10.1";
22
+ readonly image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8";
23
+ };
24
+ export declare const MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION: "0.10.1";
25
+ /** Where the sandbox image keeps the version of the runtime it ships, from the image's root. */
26
+ export declare const SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
27
+ /** A shell test that passes when the runtime version in `file`, a shell word, is one this control plane serves. */
28
+ export declare function servedRuntimeVersionTest(file: string): string;
@@ -3,8 +3,19 @@ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
3
  function workspaceRuntimeVersion() {
4
4
  return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
5
5
  }
6
- var MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION = "0.5.2";
6
+ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
7
+ version: "0.10.1",
8
+ image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
9
+ };
10
+ var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
11
+ var SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
12
+ function servedRuntimeVersionTest(file) {
13
+ 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} ]`;
14
+ }
7
15
  export {
8
- MINIMUM_RESTORABLE_WORKSPACE_RUNTIME_VERSION,
16
+ MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION,
17
+ SANDBOX_IMAGE_RUNTIME_VERSION_FILE,
18
+ SERVED_WORKSPACE_RUNTIME_FLOOR,
19
+ servedRuntimeVersionTest,
9
20
  workspaceRuntimeVersion
10
21
  };