@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
@@ -1,67 +1,17 @@
1
- /**
2
- * The egress allowlist a HOSTED, multi-tenant sandbox is provisioned with.
3
- *
4
- * A hosted sandbox runs agent-authored code on the operator's infrastructure,
5
- * next to other tenants' sandboxes, holding a checkout of the user's private
6
- * repository and (via `SandboxBrokeredSecret`) the ability to spend the
7
- * operator's model credits. The default for that shape of workload is
8
- * restrictive, not allow-all: the interesting attack is not "the agent
9
- * downloads a package", it is "the agent POSTs the repository to a host the
10
- * attacker controls". An allowlist is what makes that fail.
11
- *
12
- * Every entry below has to earn its place, so the list is assembled from three
13
- * sources of truth rather than from taste:
14
- *
15
- * 1. DERIVED PER REQUEST — the control plane the sandbox reports back to and
16
- * the git host it clones from. These differ per deployment and per
17
- * workspace, so they are inputs, not constants.
18
- * 2. MODEL PROVIDERS — the endpoints an agent harness must reach to do any
19
- * work at all. Taken from the harness→provider mapping the credential
20
- * layer already maintains (`PROVIDER_TO_GROUP` /`DEFAULT_ALLOWLIST` in
21
- * claxedo-server's `network/types.ts`), narrowed to the API hostnames.
22
- * 3. PACKAGE REGISTRIES — an agent that cannot install a dependency cannot
23
- * edit a real repository. Same source, again narrowed.
24
- *
25
- * What is deliberately NOT here, despite being in that same default list:
26
- * general-purpose object storage and CDN wildcards (`*.workers.dev`,
27
- * `r2.cloudflarestorage.com`, `storage.googleapis.com`,
28
- * `*.blob.core.windows.net`, `*.amazonaws.com`). Every one of those is a
29
- * bucket an attacker can create in their own account, so allowlisting them
30
- * hands back exactly the exfiltration channel the policy exists to close. A
31
- * deployment that genuinely needs one adds it through `extraHosts` as an
32
- * explicit, auditable decision.
33
- *
34
- * This module is data + string handling only, so it runs unchanged inside the
35
- * hosted Worker (no DNS, no node builtins). That matters: name-based policy is
36
- * the only kind the Worker can compute, which is why `egressControl` treats
37
- * name filtering as the capability that qualifies a driver for hosted use.
38
- */
39
1
  import type { SandboxSource } from "./contract";
40
2
  import type { SandboxNetworkPolicy } from "./egress-policy";
41
- /**
42
- * Model-provider endpoints. Derived from the harness→provider groups in
43
- * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
44
- * anthropic, codex-app-server/cursor-sdk → openai,
45
- * google → google_ai; plus the aggregator and catalog endpoints the engine
46
- * itself fetches at startup.
47
- */
48
3
  export declare const SANDBOX_MODEL_PROVIDER_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai"];
49
4
  /**
50
- * Package registries. Narrowed from the npm / pypi / rust / golang groups of
51
- * `DEFAULT_ALLOWLIST` to the hostnames a package install actually contacts —
52
- * the wildcard CDN entries in those groups are excluded on purpose (see the
53
- * module comment).
5
+ * Object-storage and CDN wildcards (`*.workers.dev`, `*.amazonaws.com`,
6
+ * `storage.googleapis.com`) stay out: each is a bucket an attacker can create
7
+ * in their own account, which reopens the exfiltration channel the allowlist
8
+ * closes. A deployment that needs one adds it through `extraHosts`.
54
9
  */
55
10
  export declare const SANDBOX_PACKAGE_REGISTRY_HOSTS: readonly ["registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
56
- /** The deployment-independent floor: providers + registries. */
57
11
  export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai", "registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
58
12
  /**
59
- * The git host this workspace clones from.
60
- *
61
- * Scoped to the ONE repository host in the request rather than to a forge
62
- * allowlist, matching how `githubCloneSecret` scopes the clone
63
- * credential it brokers (`hosts: ["github.com"]`) — the credential and the
64
- * network policy should describe the same reachable surface.
13
+ * The one repository host the request clones from, matching the hosts the
14
+ * brokered clone credential is scoped to.
65
15
  *
66
16
  * The host is caller-selected, so only a public spelling earns an allowlist
67
17
  * entry: `169.254.169.254` or `localhost` named as a repoUrl would otherwise
@@ -78,17 +28,13 @@ export type HostedSandboxNetworkPolicyInput = {
78
28
  * plus register/heartbeat. URLs or bare hostnames; blanks are ignored.
79
29
  */
80
30
  controlPlane: Array<string | undefined>;
81
- /** Workspace source — its git host is allowed, nothing else on that forge. */
31
+ /** Its git host is allowed, nothing else on that forge. */
82
32
  source?: SandboxSource;
83
- /** Operator escape hatch for deployment-specific hosts. */
84
33
  extraHosts?: readonly string[];
85
34
  };
86
35
  /**
87
- * Build the restricted policy for a hosted sandbox.
88
- *
89
- * Always `mode: "restricted"`. There is no input that makes this return
90
- * allow-all — a hosted sandbox with an empty or unresolvable control-plane URL
91
- * is a misconfiguration, and answering it with "then allow everything" is how
92
- * the gap this closes came to exist in the first place.
36
+ * Always `mode: "restricted"`: a hosted sandbox with an empty or unresolvable
37
+ * control-plane URL is a misconfiguration, never a reason to allow everything.
38
+ * Name-based only (no DNS), so it also runs inside the Worker.
93
39
  */
94
40
  export declare function hostedSandboxNetworkPolicy(input: HostedSandboxNetworkPolicyInput): SandboxNetworkPolicy;
@@ -221,7 +221,7 @@ var SANDBOX_MODEL_PROVIDER_HOSTS = [
221
221
  "api.openai.com",
222
222
  "generativelanguage.googleapis.com",
223
223
  "openrouter.ai",
224
- // Model catalog + engine config, fetched on runner startup.
224
+ // The model catalog and engine config a runner fetches at startup.
225
225
  "models.dev",
226
226
  "opencode.ai"
227
227
  ];
@@ -13,5 +13,4 @@ export declare function sandboxImageRepository(env?: SandboxImageEnv): string;
13
13
  export declare function defaultSandboxImage(version?: string, buildId?: string, env?: SandboxImageEnv, options?: {
14
14
  agentPlugins?: boolean;
15
15
  }): string;
16
- /** Whether an image's tag names a build that carries the Agent Plugins runtime entry. */
17
16
  export declare function sandboxImageCarriesAgentPlugins(image: string): boolean;
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;
@@ -318,12 +339,13 @@ var sandboxCheckpointUnusableCauses = ["expired", "runtime_modified", "not_resto
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) {
@@ -434,6 +456,13 @@ function sandboxDriverLabel(driver) {
434
456
  }
435
457
  var PROVIDER_KEY_REFUSED = "provider_key_refused:";
436
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
+ }
437
466
  function sandboxProviderKeyRefusedError(driver, message) {
438
467
  return `${PROVIDER_KEY_REFUSED}${driver}: ${message}`;
439
468
  }
@@ -524,10 +553,8 @@ function sandboxRuntimeBootFailure(error) {
524
553
  return workspaceRuntimeBootReason(diagnostic) ?? diagnostic;
525
554
  }
526
555
  var SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
527
- var SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
528
556
  var CODED_REASONS = /* @__PURE__ */ new Map([
529
557
  [SANDBOX_RUNTIME_UNHEALTHY, "The workspace machine stopped answering."],
530
- [SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."],
531
558
  [SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED, "The update couldn't finish downloading; your workspace is still running. Try again."]
532
559
  ]);
533
560
  function firstLine(text) {
@@ -542,6 +569,8 @@ function sandboxStartFailureReason(error) {
542
569
  if (keyRefused !== void 0) return `${sandboxDriverLabel(keyRefused)} refused the sandbox provider key; fix it in Settings \u2192 Sandbox providers.`;
543
570
  const failed = codedDriver(error, PROVIDER_FAILED);
544
571
  if (failed !== void 0) return `${sandboxDriverLabel(failed)} returned an error.`;
572
+ const uncontained = codedDriver(error, EGRESS_UNCONTAINED);
573
+ if (uncontained !== void 0) return uncontainedEgressReason(sandboxDriverLabel(uncontained));
545
574
  return CODED_REASONS.get(error) ?? firstLine(error);
546
575
  }
547
576
  var LEASE_ENDED_ERRORS = /* @__PURE__ */ new Set(["runtime_lease_changed", "runtime_lease_stopped", "runtime_lease_destroyed"]);
@@ -587,6 +616,11 @@ var PUBLIC_API_ERRORS = {
587
616
  org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
588
617
  org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
589
618
  org_invitation_rate_limited: { family: "access", status: 429, message: "Organization invitation limit exceeded" },
619
+ org_limit_reached: { family: "access", status: 403, message: "An account owns at most 3 organizations" },
620
+ org_creation_rate_limited: { family: "access", status: 429, message: "Organization creation limit exceeded" },
621
+ project_limit_reached: { family: "access", status: 402, message: "Free accounts have 3 projects. Get Cloud or Self-host for unlimited projects." },
622
+ seats_full: { family: "access", status: 402, message: "Add teammate seats to invite more people." },
623
+ email_verification_required: { family: "access", status: 403, message: "A verified email address is required" },
590
624
  org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
591
625
  org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
592
626
  team_not_found: { family: "access", status: 404, message: "Team not found" },
@@ -596,6 +630,12 @@ var PUBLIC_API_ERRORS = {
596
630
  project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
597
631
  project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
598
632
  resource_conflict: { family: "access", status: 409, message: "Organization or team authority changed concurrently" },
633
+ org_not_deletable: { family: "access", status: 409, message: "A personal organization goes with its account, and a deployment's with the deployment" },
634
+ org_has_workspaces: { family: "access", status: 409, message: "Delete the organization's workspaces first" },
635
+ account_owns_workspaces: { family: "access", status: 409, message: "Delete your workspaces first" },
636
+ account_founds_organization: { family: "access", status: 409, message: "Delete the organizations you founded first" },
637
+ account_has_cloud_subscription: { family: "access", status: 409, message: "Cancel Cloud before deleting this account" },
638
+ recent_sign_in_required: { family: "access", status: 403, message: "Sign in again to confirm this deletion" },
599
639
  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." },
600
640
  session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
601
641
  session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
@@ -799,7 +839,7 @@ var SANDBOX_MODEL_PROVIDER_HOSTS = [
799
839
  "api.openai.com",
800
840
  "generativelanguage.googleapis.com",
801
841
  "openrouter.ai",
802
- // Model catalog + engine config, fetched on runner startup.
842
+ // The model catalog and engine config a runner fetches at startup.
803
843
  "models.dev",
804
844
  "opencode.ai"
805
845
  ];
@@ -893,7 +933,7 @@ function leaseHostId(driver, workspaceId, lease, managerInput) {
893
933
  return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
894
934
  }
895
935
  function ensureHostInput(input) {
896
- const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
936
+ const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net, input.managerInput?.uncontainedEgress);
897
937
  const labels = {
898
938
  app: input.appLabel,
899
939
  workspaceId: input.workspaceId,
@@ -921,10 +961,10 @@ function ensureHostInput(input) {
921
961
  ...input.managerInput?.updateImage ? { updateImage: true } : {},
922
962
  ...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
923
963
  exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
924
- // Withheld, never downgraded: `undefined` here is the same shape a caller
925
- // that asked for no containment produces, so the throwing drivers take
926
- // their allow-all path and their throws stay intact for anyone who hands
927
- // them a policy directly.
964
+ // Withheld where the start allows it, never downgraded: `undefined`
965
+ // here is the same shape a caller that asked for no containment produces,
966
+ // so the throwing drivers take their allow-all path and their throws stay
967
+ // intact for anyone who hands them a policy directly.
928
968
  ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
929
969
  snapshot: input.managerInput?.snapshot
930
970
  };
@@ -932,11 +972,8 @@ function ensureHostInput(input) {
932
972
 
933
973
  // src/egress-warnings.ts
934
974
  function egressUnenforcedMessage(input) {
935
- const scope = input.workspaceId ? `workspace ${input.workspaceId}` : "every sandbox it provisions";
936
- const withheld = input.requested ? ` The requested allowlist (${(input.requested.hosts ?? []).length} host(s), ${(input.requested.cidrs ?? []).length} cidr(s)) was withheld, not applied.` : "";
937
- 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.`;
975
+ 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.`;
938
976
  }
939
- var compositionEgressWarnings = /* @__PURE__ */ new Set();
940
977
  var ensureEgressWarnings = /* @__PURE__ */ new Set();
941
978
  function defaultEgressUnenforcedSink(event) {
942
979
  console.warn(event.message);
@@ -945,26 +982,15 @@ function createEgressWarnings(input) {
945
982
  const { id, egressControl } = input.driver;
946
983
  const sink = input.sink ?? defaultEgressUnenforcedSink;
947
984
  const key = `${id}|${egressControl}`;
948
- if (egressControl === "none" && !compositionEgressWarnings.has(key)) {
949
- compositionEgressWarnings.add(key);
950
- sink({
951
- phase: "composition",
952
- reason: "sandbox_egress_uncontained",
953
- driver: id,
954
- egressControl,
955
- message: egressUnenforcedMessage({ phase: "composition", driver: id })
956
- });
957
- }
958
985
  return {
959
986
  withheld(start) {
960
987
  if (ensureEgressWarnings.has(key)) return;
961
988
  ensureEgressWarnings.add(key);
962
989
  sink({
963
- phase: "ensure",
964
990
  reason: "sandbox_egress_uncontained",
965
991
  driver: id,
966
992
  egressControl,
967
- message: egressUnenforcedMessage({ phase: "ensure", driver: id, workspaceId: start.workspaceId, requested: start.requested }),
993
+ message: egressUnenforcedMessage({ driver: id, workspaceId: start.workspaceId, requested: start.requested }),
968
994
  workspaceId: start.workspaceId,
969
995
  ...start.labels ? { labels: start.labels } : {},
970
996
  requested: start.requested
@@ -973,6 +999,51 @@ function createEgressWarnings(input) {
973
999
  };
974
1000
  }
975
1001
 
1002
+ // src/runtime-env.ts
1003
+ var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
1004
+ "WORKSPACE_RUNTIME_WORKSPACE_ID",
1005
+ "WORKSPACE_RUNTIME_HOST_ID",
1006
+ "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
1007
+ ];
1008
+ function workspaceRuntimeIdentityEnvConflicts(env) {
1009
+ if (!env) return [];
1010
+ return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
1011
+ }
1012
+ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
1013
+ ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
1014
+ "WORKSPACE_RUNTIME_DIRECTORY",
1015
+ "WORKSPACE_RUNTIME_PORT",
1016
+ "WORKSPACE_RUNTIME_HOST",
1017
+ "WORKSPACE_RUNTIME_SOURCE_KIND",
1018
+ "WORKSPACE_RUNTIME_GIT_REPO_URL",
1019
+ "WORKSPACE_RUNTIME_GIT_BRANCH",
1020
+ "WORKSPACE_RUNTIME_NATIVE_HARNESS",
1021
+ "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
1022
+ "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
1023
+ "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
1024
+ ]);
1025
+
1026
+ // src/start-admission.ts
1027
+ function refusedStart(input) {
1028
+ const { driver, workspaceId, start } = input;
1029
+ const egress = sandboxEgressDisposition(driver.metadata.egressControl, start.net, start.uncontainedEgress);
1030
+ if (egress.action === "withhold" && start.net) {
1031
+ input.egressWarnings.withheld({ workspaceId, ...start.labels ? { labels: start.labels } : {}, requested: start.net });
1032
+ }
1033
+ if (egress.action === "refuse") {
1034
+ const error = egress.reason === "sandbox_egress_uncontained" ? sandboxEgressUncontainedError(driver.id) : egress.reason;
1035
+ return { status: "unavailable", error, homeRegion: start.homeRegion };
1036
+ }
1037
+ const identityConflicts = workspaceRuntimeIdentityEnvConflicts(start.env);
1038
+ if (identityConflicts.length) {
1039
+ return { status: "unavailable", error: `sandbox env cannot set runtime identity: ${identityConflicts.join(", ")}`, homeRegion: start.homeRegion };
1040
+ }
1041
+ if (start.machineClass && !driver.metadata.machineClasses?.includes(start.machineClass)) {
1042
+ return { status: "unavailable", error: `sandbox_machine_class_unsupported: ${driver.id} has no ${start.machineClass} machines`, homeRegion: start.homeRegion };
1043
+ }
1044
+ return void 0;
1045
+ }
1046
+
976
1047
  // src/garbage-collect.ts
977
1048
  async function sweepSandboxes(input) {
978
1049
  const { driver, leaseStore, appLabel } = input;
@@ -1176,30 +1247,6 @@ async function startSandboxFresh(input) {
1176
1247
  return { ok: true, status };
1177
1248
  }
1178
1249
 
1179
- // src/runtime-env.ts
1180
- var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
1181
- "WORKSPACE_RUNTIME_WORKSPACE_ID",
1182
- "WORKSPACE_RUNTIME_HOST_ID",
1183
- "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
1184
- ];
1185
- function workspaceRuntimeIdentityEnvConflicts(env) {
1186
- if (!env) return [];
1187
- return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
1188
- }
1189
- var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
1190
- ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
1191
- "WORKSPACE_RUNTIME_DIRECTORY",
1192
- "WORKSPACE_RUNTIME_PORT",
1193
- "WORKSPACE_RUNTIME_HOST",
1194
- "WORKSPACE_RUNTIME_SOURCE_KIND",
1195
- "WORKSPACE_RUNTIME_GIT_REPO_URL",
1196
- "WORKSPACE_RUNTIME_GIT_BRANCH",
1197
- "WORKSPACE_RUNTIME_NATIVE_HARNESS",
1198
- "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
1199
- "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
1200
- "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
1201
- ]);
1202
-
1203
1250
  // src/manager.ts
1204
1251
  var DEFAULT_APP_LABEL = "claxedo";
1205
1252
  function createSandboxManager(options) {
@@ -1494,14 +1541,6 @@ function createSandboxManager(options) {
1494
1541
  ...existing.retryCount >= maxRetryCount ? { exhausted: true } : {}
1495
1542
  };
1496
1543
  }
1497
- if (existing.status === "unavailable" && existing.retryCount >= maxRetryCount && existing.nextRetryAt === void 0 && sandboxProviderRateLimit(existing.lastError) !== "day") {
1498
- return {
1499
- status: "unavailable",
1500
- error: existing.lastError ?? SANDBOX_RETRY_CAP_EXCEEDED,
1501
- epoch: existing.epoch,
1502
- homeRegion: existing.homeRegion
1503
- };
1504
- }
1505
1544
  return void 0;
1506
1545
  }
1507
1546
  function continues(lease) {
@@ -1511,24 +1550,8 @@ function createSandboxManager(options) {
1511
1550
  return lease.sandboxId ? lease.nextRetryAt !== void 0 || stale : lease.nextRetryAt !== void 0 && !stale;
1512
1551
  }
1513
1552
  async function admit(workspaceId, input) {
1514
- const egress = sandboxEgressDisposition(options.driver.metadata.egressControl, input.net);
1515
- if (egress.action === "withhold" && input.net) {
1516
- egressWarnings.withheld({ workspaceId, ...input.labels ? { labels: input.labels } : {}, requested: input.net });
1517
- }
1518
- if (egress.action === "refuse") {
1519
- return { status: "unavailable", error: egress.reason, homeRegion: input.homeRegion };
1520
- }
1521
- const identityConflicts = workspaceRuntimeIdentityEnvConflicts(input.env);
1522
- if (identityConflicts.length) {
1523
- return {
1524
- status: "unavailable",
1525
- error: `sandbox env cannot set runtime identity: ${identityConflicts.join(", ")}`,
1526
- homeRegion: input.homeRegion
1527
- };
1528
- }
1529
- if (input.machineClass && !options.driver.metadata.machineClasses?.includes(input.machineClass)) {
1530
- return { status: "unavailable", error: `sandbox_machine_class_unsupported: ${options.driver.id} has no ${input.machineClass} machines`, homeRegion: input.homeRegion };
1531
- }
1553
+ const refused = refusedStart({ driver: options.driver, workspaceId, start: input, egressWarnings });
1554
+ if (refused) return refused;
1532
1555
  const existing = await options.leaseStore.get(workspaceId);
1533
1556
  const wait = waiting(existing);
1534
1557
  if (wait) return wait;
@@ -1724,7 +1747,6 @@ export {
1724
1747
  SANDBOX_IMAGE_LABEL,
1725
1748
  SANDBOX_MODEL_PROVIDER_HOSTS,
1726
1749
  SANDBOX_PACKAGE_REGISTRY_HOSTS,
1727
- SANDBOX_RETRY_CAP_EXCEEDED,
1728
1750
  SANDBOX_RUNTIME_UNHEALTHY,
1729
1751
  SANDBOX_START_WINDOWS,
1730
1752
  SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED,
@@ -1735,6 +1757,8 @@ export {
1735
1757
  SandboxProviderRateLimitedError,
1736
1758
  SandboxRuntimeBootError,
1737
1759
  SandboxStartRefusedError,
1760
+ agent37Billing,
1761
+ agent37MachineShapes,
1738
1762
  applySandboxLeaseFailure,
1739
1763
  applySandboxLeasePatch,
1740
1764
  applySandboxProvisionedTarget,
@@ -1763,6 +1787,7 @@ export {
1763
1787
  sandboxCheckpointUnusableCauses,
1764
1788
  sandboxCheckpointUnusableError,
1765
1789
  sandboxEgressDisposition,
1790
+ sandboxEgressUncontainedError,
1766
1791
  sandboxHostGoneError,
1767
1792
  sandboxLeaseEndedUnderStart,
1768
1793
  sandboxLeaseFailure,
package/dist/json.d.ts CHANGED
@@ -1,13 +1 @@
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
- export { asRecord as record, isRecord } from "@claxedo/helpers/guards";
11
- /** A non-empty string, or undefined — the shape almost every provider field wants. */
12
- export declare function text(input: unknown): string | undefined;
13
- export declare function num(input: unknown): number | undefined;
1
+ export { asNumber as num, asRecord as record, isRecord, nonEmptyString as text } from "@claxedo/helpers/guards";
@@ -91,6 +91,8 @@ export declare class SandboxStartRefusedError extends Error implements SandboxSt
91
91
  readonly startRefused: true;
92
92
  constructor(reason: string);
93
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;
94
96
  /** The error a lease keeps when `driver`'s provider refused its key; the driver's message stays for the operator. */
95
97
  export declare function sandboxProviderKeyRefusedError(driver: string, message: string): string;
96
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. */
@@ -144,7 +146,7 @@ export declare function workspaceRuntimeBootReason(diagnostic: string): string |
144
146
  /**
145
147
  * What a driver keeps of a runtime's log within `maxChars`. A Node stack
146
148
  * under the boot-failure line is longer than the bound a driver puts on the
147
- * 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
148
150
  * frames and loses the one line that names the reason.
149
151
  */
150
152
  export declare function runtimeBootDiagnostic(log: string, maxChars: number): string;
@@ -156,8 +158,6 @@ export declare function runtimeBootDiagnostic(log: string, maxChars: number): st
156
158
  export declare function sandboxRuntimeBootFailure(error: string | undefined): string | undefined;
157
159
  /** The error a lease keeps when the runtime it serves stopped answering its health reports. */
158
160
  export declare const SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
159
- /** The error a capped lease answers when it kept none of its own. */
160
- export declare const SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
161
161
  /**
162
162
  * The one line a person is told for a start step's error: the runtime's own
163
163
  * reason for a boot that failed, the provider's refusal in Claxedo's words,
@@ -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;
@@ -1,4 +1,3 @@
1
- import type { SandboxDriverID } from "@claxedo/sandbox-contract";
2
1
  export type WorkspaceRuntimeControlEnv = {
3
2
  relayJwksUrl?: string;
4
3
  relayVerifyPem?: string;
@@ -8,6 +7,12 @@ export type WorkspaceRuntimeControlEnv = {
8
7
  export declare function workspaceRuntimeIdentityEnvConflicts(env: Record<string, string> | undefined): string[];
9
8
  export declare function assertWorkspaceRuntimeIdentityEnv(env: Record<string, string> | undefined): void;
10
9
  export declare function workspaceRuntimeBootSecrets(env: Record<string, string>): string[];
10
+ /**
11
+ * `text` with every boot secret in it replaced. Redact before bounding the
12
+ * text, so a secret the bound would cut in half is already gone. Values
13
+ * shorter than the floor (ports, flags) are left readable.
14
+ */
15
+ export declare function redactWorkspaceRuntimeBootSecrets(text: string, secrets: readonly string[]): string;
11
16
  export declare function workspaceRuntimeBootEnv(input: Parameters<typeof workspaceRuntimeTargetEnv>[0] & Parameters<typeof workspaceRuntimeSourceEnv>[0] & {
12
17
  env?: Record<string, string>;
13
18
  nativeHarness?: string;
@@ -29,9 +34,3 @@ export declare function workspaceRuntimeSourceEnv(input: {
29
34
  kind: "empty";
30
35
  };
31
36
  }): Record<string, string>;
32
- export declare function workspaceRuntimeServiceExposureEnv(input: {
33
- driver: SandboxDriverID;
34
- source: "driver-service-url";
35
- access: "private" | "public" | "driver-authenticated" | "unknown";
36
- note?: string;
37
- }): Record<string, 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 = {
@@ -62,21 +66,12 @@ function workspaceRuntimeSourceEnv(input) {
62
66
  ...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
63
67
  };
64
68
  }
65
- function workspaceRuntimeServiceExposureEnv(input) {
66
- return {
67
- ...input.driver === "docker" ? { WORKSPACE_RUNTIME_HOST: "0.0.0.0" } : {},
68
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_SOURCE: input.source,
69
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_ACCESS: input.access,
70
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_DRIVER: input.driver,
71
- ...input.note ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_NOTE: input.note } : {}
72
- };
73
- }
74
69
  export {
75
70
  assertWorkspaceRuntimeIdentityEnv,
71
+ redactWorkspaceRuntimeBootSecrets,
76
72
  workspaceRuntimeBootEnv,
77
73
  workspaceRuntimeBootSecrets,
78
74
  workspaceRuntimeIdentityEnvConflicts,
79
- workspaceRuntimeServiceExposureEnv,
80
75
  workspaceRuntimeSourceEnv,
81
76
  workspaceRuntimeTargetEnv
82
77
  };
@@ -22,3 +22,7 @@ export declare const SERVED_WORKSPACE_RUNTIME_FLOOR: {
22
22
  readonly image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8";
23
23
  };
24
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;
@@ -8,8 +8,14 @@ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
8
8
  image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
9
9
  };
10
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
+ }
11
15
  export {
12
16
  MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION,
17
+ SANDBOX_IMAGE_RUNTIME_VERSION_FILE,
13
18
  SERVED_WORKSPACE_RUNTIME_FLOOR,
19
+ servedRuntimeVersionTest,
14
20
  workspaceRuntimeVersion
15
21
  };
@@ -0,0 +1,32 @@
1
+ import type { SandboxDriver, SandboxEnsureResult, SandboxManagerInput } from "./contract";
2
+ import type { EgressWarnings } from "./egress-warnings";
3
+ export type SandboxStartRefusal = Extract<SandboxEnsureResult, {
4
+ status: "unavailable";
5
+ }>;
6
+ /**
7
+ * The composition's standing answers to a start's input, decided BEFORE a
8
+ * lease is touched: none of them is a provisioning failure, so none may burn a
9
+ * lease epoch or enter retry backoff, and each repeats until the composition
10
+ * or the input changes.
11
+ *
12
+ * Egress: a caller that hands over a restricted policy is stating that this
13
+ * sandbox must not reach the open internet. That is honoured where the driver
14
+ * can honour it. Where it cannot, the start is refused, unless the start
15
+ * allows uncontained egress: then the sandbox runs with the policy withheld
16
+ * (`ensureHostInput` never hands it down) and the gap is reported here, once
17
+ * per driver per isolate. `sandbox_egress_policy_unenforceable` is refused too,
18
+ * and deliberately so: that driver DOES enforce egress, it just cannot express
19
+ * this policy's encoding, and degrading an enforcing driver to "unrestricted"
20
+ * would weaken the path that does enforce.
21
+ *
22
+ * Identity: caller env restating the identity the driver's target reports,
23
+ * which `recordTarget` persists, would boot the runtime bound to a hostId the
24
+ * lease never authorized; the driver rejects it again at compose time for
25
+ * callers that reach it directly.
26
+ */
27
+ export declare function refusedStart(input: {
28
+ driver: SandboxDriver;
29
+ workspaceId: string;
30
+ start: SandboxManagerInput;
31
+ egressWarnings: EgressWarnings;
32
+ }): SandboxStartRefusal | undefined;