@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.
@@ -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
  }
@@ -542,6 +571,8 @@ function sandboxStartFailureReason(error) {
542
571
  if (keyRefused !== void 0) return `${sandboxDriverLabel(keyRefused)} refused the sandbox provider key; fix it in Settings \u2192 Sandbox providers.`;
543
572
  const failed = codedDriver(error, PROVIDER_FAILED);
544
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));
545
576
  return CODED_REASONS.get(error) ?? firstLine(error);
546
577
  }
547
578
  var LEASE_ENDED_ERRORS = /* @__PURE__ */ new Set(["runtime_lease_changed", "runtime_lease_stopped", "runtime_lease_destroyed"]);
@@ -587,6 +618,11 @@ var PUBLIC_API_ERRORS = {
587
618
  org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
588
619
  org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
589
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" },
590
626
  org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
591
627
  org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
592
628
  team_not_found: { family: "access", status: 404, message: "Team not found" },
@@ -596,6 +632,12 @@ var PUBLIC_API_ERRORS = {
596
632
  project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
597
633
  project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
598
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" },
599
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." },
600
642
  session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
601
643
  session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
@@ -799,7 +841,7 @@ var SANDBOX_MODEL_PROVIDER_HOSTS = [
799
841
  "api.openai.com",
800
842
  "generativelanguage.googleapis.com",
801
843
  "openrouter.ai",
802
- // Model catalog + engine config, fetched on runner startup.
844
+ // The model catalog and engine config a runner fetches at startup.
803
845
  "models.dev",
804
846
  "opencode.ai"
805
847
  ];
@@ -893,7 +935,7 @@ function leaseHostId(driver, workspaceId, lease, managerInput) {
893
935
  return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
894
936
  }
895
937
  function ensureHostInput(input) {
896
- const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
938
+ const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net, input.managerInput?.uncontainedEgress);
897
939
  const labels = {
898
940
  app: input.appLabel,
899
941
  workspaceId: input.workspaceId,
@@ -921,10 +963,10 @@ function ensureHostInput(input) {
921
963
  ...input.managerInput?.updateImage ? { updateImage: true } : {},
922
964
  ...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
923
965
  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.
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.
928
970
  ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
929
971
  snapshot: input.managerInput?.snapshot
930
972
  };
@@ -932,11 +974,8 @@ function ensureHostInput(input) {
932
974
 
933
975
  // src/egress-warnings.ts
934
976
  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.`;
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.`;
938
978
  }
939
- var compositionEgressWarnings = /* @__PURE__ */ new Set();
940
979
  var ensureEgressWarnings = /* @__PURE__ */ new Set();
941
980
  function defaultEgressUnenforcedSink(event) {
942
981
  console.warn(event.message);
@@ -945,26 +984,15 @@ function createEgressWarnings(input) {
945
984
  const { id, egressControl } = input.driver;
946
985
  const sink = input.sink ?? defaultEgressUnenforcedSink;
947
986
  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
987
  return {
959
988
  withheld(start) {
960
989
  if (ensureEgressWarnings.has(key)) return;
961
990
  ensureEgressWarnings.add(key);
962
991
  sink({
963
- phase: "ensure",
964
992
  reason: "sandbox_egress_uncontained",
965
993
  driver: id,
966
994
  egressControl,
967
- message: egressUnenforcedMessage({ phase: "ensure", driver: id, workspaceId: start.workspaceId, requested: start.requested }),
995
+ message: egressUnenforcedMessage({ driver: id, workspaceId: start.workspaceId, requested: start.requested }),
968
996
  workspaceId: start.workspaceId,
969
997
  ...start.labels ? { labels: start.labels } : {},
970
998
  requested: start.requested
@@ -973,6 +1001,51 @@ function createEgressWarnings(input) {
973
1001
  };
974
1002
  }
975
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
+
976
1049
  // src/garbage-collect.ts
977
1050
  async function sweepSandboxes(input) {
978
1051
  const { driver, leaseStore, appLabel } = input;
@@ -1176,30 +1249,6 @@ async function startSandboxFresh(input) {
1176
1249
  return { ok: true, status };
1177
1250
  }
1178
1251
 
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
1252
  // src/manager.ts
1204
1253
  var DEFAULT_APP_LABEL = "claxedo";
1205
1254
  function createSandboxManager(options) {
@@ -1511,24 +1560,8 @@ function createSandboxManager(options) {
1511
1560
  return lease.sandboxId ? lease.nextRetryAt !== void 0 || stale : lease.nextRetryAt !== void 0 && !stale;
1512
1561
  }
1513
1562
  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
- }
1563
+ const refused = refusedStart({ driver: options.driver, workspaceId, start: input, egressWarnings });
1564
+ if (refused) return refused;
1532
1565
  const existing = await options.leaseStore.get(workspaceId);
1533
1566
  const wait = waiting(existing);
1534
1567
  if (wait) return wait;
@@ -1735,6 +1768,8 @@ export {
1735
1768
  SandboxProviderRateLimitedError,
1736
1769
  SandboxRuntimeBootError,
1737
1770
  SandboxStartRefusedError,
1771
+ agent37Billing,
1772
+ agent37MachineShapes,
1738
1773
  applySandboxLeaseFailure,
1739
1774
  applySandboxLeasePatch,
1740
1775
  applySandboxProvisionedTarget,
@@ -1763,6 +1798,7 @@ export {
1763
1798
  sandboxCheckpointUnusableCauses,
1764
1799
  sandboxCheckpointUnusableError,
1765
1800
  sandboxEgressDisposition,
1801
+ sandboxEgressUncontainedError,
1766
1802
  sandboxHostGoneError,
1767
1803
  sandboxLeaseEndedUnderStart,
1768
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;
@@ -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;
@@ -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,
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "type": "module",
3
- "description": "Sandbox lifecycle manager with epoch-based leases and pluggable drivers for Modal, Vercel Sandbox, Cloudflare, Boat, and Docker",
3
+ "description": "Sandbox lifecycle manager with epoch-based leases and pluggable drivers for Modal, Vercel Sandbox, Cloudflare, Boat, Agent37, and Docker",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,7 @@
15
15
  "cloudflare",
16
16
  "docker",
17
17
  "boat",
18
+ "agent37",
18
19
  "agents"
19
20
  ],
20
21
  "main": "./dist/index.mjs",
@@ -62,6 +63,13 @@
62
63
  "import": "./dist/driver-catalog.mjs",
63
64
  "default": "./dist/driver-catalog.mjs"
64
65
  },
66
+ "./drivers/agent37": {
67
+ "development": "./src/drivers/agent37.ts",
68
+ "bun": "./src/drivers/agent37.ts",
69
+ "types": "./dist/drivers/agent37.d.ts",
70
+ "import": "./dist/drivers/agent37.mjs",
71
+ "default": "./dist/drivers/agent37.mjs"
72
+ },
65
73
  "./drivers/boat": {
66
74
  "development": "./src/drivers/boat.ts",
67
75
  "bun": "./src/drivers/boat.ts",
@@ -163,7 +171,7 @@
163
171
  "access": "public"
164
172
  },
165
173
  "name": "@claxedo/sandbox-manager",
166
- "version": "0.10.2",
174
+ "version": "0.10.3",
167
175
  "scripts": {
168
176
  "build": "bun scripts/build.ts",
169
177
  "prepublishOnly": "npm run build",
@@ -172,8 +180,8 @@
172
180
  "test": "mkdir -p .artifacts/unit && node ../../script/test-deadline.mjs bun test src --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml"
173
181
  },
174
182
  "dependencies": {
175
- "@claxedo/helpers": "0.2.2",
176
- "@claxedo/sandbox-contract": "0.10.2",
183
+ "@claxedo/helpers": "0.2.3",
184
+ "@claxedo/sandbox-contract": "0.10.3",
177
185
  "@vercel/sandbox": "3.3.0",
178
186
  "modal": "0.10.1"
179
187
  },