@claxedo/sandbox-manager 0.5.2 → 0.7.0

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.
package/README.md CHANGED
@@ -99,7 +99,7 @@ Per-provider mechanism (from each provider's official docs):
99
99
  | Driver | `secretBrokering` | Mechanism |
100
100
  | --- | --- | --- |
101
101
  | Daytona | `native` | `daytona.secret.create({name, value, hosts})`; the sandbox env holds an opaque `dtn_secret_…` placeholder and an egress proxy substitutes the real value only for allowlisted hosts ([docs](https://www.daytona.io/docs/en/secrets/)). |
102
- | Vercel | `native` | Firewall header-transform (`updateNetworkPolicy`): the value is spliced onto egress to the allowlisted hosts as `header`; the sandbox makes an unauthenticated request, egress restricted to brokered hosts ([docs](https://vercel.com/docs/sandbox/concepts/firewall)). |
102
+ | Vercel | `native` | Firewall header-transform (`updateNetworkPolicy`): the value is spliced onto egress to the allowlisted hosts as `header`, so the sandbox makes an unauthenticated request ([docs](https://vercel.com/docs/sandbox/concepts/firewall)). `updateNetworkPolicy` replaces the whole policy, so the driver sends the **union** of the create-time allow-list and the brokered hosts — attaching a credential must not revoke egress the caller was already granted (a mid-run `npm install` would start failing), nor widen it to a brokered host the create-time policy never approved. |
103
103
  | Cloudflare | `proxy` | Implemented via the official [Worker-proxy pattern](https://developers.cloudflare.com/sandbox/guides/proxy-requests/): the driver sends the value to the sandbox Worker (API-token-gated, never into the container); the Worker stores it in KV, mints a short-lived per-sandbox JWT, and sets `CLAXEDO_EGRESS_PROXY_URL`/`CLAXEDO_EGRESS_TOKEN`/`CLAXEDO_EGRESS_HOSTS` in the container. The sandbox routes brokered-host egress through the Worker, which validates the JWT and injects the credential as `header` — the raw value never enters the sandbox. The reusable core is `@claxedo/sandbox-manager/drivers/cloudflare-egress` (mint/verify + `handleEgressRequest`). |
104
104
  | Modal | `none` | Modal [Secrets](https://modal.com/docs/guide/secrets) are an encrypted by-reference store, but exposed as **readable env vars** inside the sandbox — they cannot satisfy the never-readable contract. |
105
105
  | Docker, fetch | `none` | Plaintext env only. |
@@ -120,6 +120,55 @@ never logged, and never captured in a driver snapshot.
120
120
  > improves storage hygiene for *readable* credentials but does not hide them
121
121
  > from the sandbox.
122
122
 
123
+ ## Egress containment
124
+
125
+ `SandboxManagerInput.net` restricts what the sandbox can reach on the network.
126
+ It is a *separate* control from brokered secrets, with a **different** posture,
127
+ and the difference matters:
128
+
129
+ - **Brokered secrets fail closed.** A driver that cannot broker refuses to
130
+ provision (`error: "secret_brokering_unsupported"`).
131
+ - **Egress does not.** Only `daytona` (names + CIDRs) and `vercel` (names) can
132
+ enforce an allowlist. Every other driver declares
133
+ `metadata.egressControl: "none"`, and for those the manager **withholds** the
134
+ policy and provisions anyway — the sandbox runs with unrestricted egress.
135
+
136
+ Withholding rather than passing is deliberate: `exe`, `docker`, `modal` and
137
+ `box` throw when handed a restricted policy, and `cloudflare` and the fetch
138
+ bridge accept one and silently ignore it. Withholding at the manager means the
139
+ throwing drivers never see a policy (their throws stay as their own last line of
140
+ defence) and the silently-dropping ones stop pretending.
141
+
142
+ The gap is never silent. `createSandboxManager` warns at composition, and again
143
+ each time a policy is withheld:
144
+
145
+ ```text
146
+ [sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "cloudflare" declares
147
+ egressControl: "none", so workspace ws_1 can reach ANY host on the internet …
148
+ ```
149
+
150
+ Pass `onEgressUnenforced` to route that into telemetry instead of `console.warn`
151
+ — it receives a structured `SandboxEgressUnenforcedEvent`. Overriding the sink
152
+ replaces the console warning, so only do it if the replacement is as visible.
153
+
154
+ **Reuse and resume reapply the policy.** A restricted policy only reaches a
155
+ provider as *creation* parameters, so a sandbox handed back rather than created
156
+ — reuse in `ensureHost`, resume in `resumeHost` — would otherwise run on the
157
+ policy in force when it was first created, possibly a wider one from an earlier
158
+ caller. Daytona's allow-list is mutable post-create
159
+ (`Sandbox.updateNetworkSettings`), so `daytona` reapplies the requested policy
160
+ on both paths. A client too old to expose that call makes the driver **refuse
161
+ the reuse**: a policy reported as applied but not in force is worse than no
162
+ sandbox. Requesting no containment leaves the existing policy alone rather than
163
+ clearing it — "not requested" is not "please unrestrict".
164
+
165
+ `driver.metadata.egressControl` is the machine-readable source of truth, and
166
+ `sandboxEgressDisposition(control, net)` is the pure predicate the manager uses,
167
+ so you can ask the same question before composing. One exception stays fail
168
+ closed: a hosts-only driver handed an address-only policy is refused
169
+ (`sandbox_egress_policy_unenforceable`) rather than degraded, because that
170
+ driver *does* enforce egress — it just cannot express that encoding.
171
+
123
172
  > SDK conformance: the Daytona secret API and Vercel network-policy transform
124
173
  > shapes follow the providers' official docs and are validated structurally
125
174
  > against the pinned SDK types; like all provider calls in this package they
@@ -0,0 +1,47 @@
1
+ import type { SandboxCheckpointReference, SandboxEnsureResult, SandboxLease, SandboxLeaseStore, SandboxManagerInput, SandboxSnapshotManagerResult, SandboxTargetResult } from ".";
2
+ export type SandboxCheckpointRuntime = {
3
+ freeze: (policy: "drain" | "interrupt") => Promise<void>;
4
+ flush: () => Promise<void>;
5
+ scrub: () => Promise<void>;
6
+ resume: () => Promise<void>;
7
+ reconcile: (input: {
8
+ epoch: number;
9
+ checkpointId: string;
10
+ }) => Promise<void>;
11
+ };
12
+ export type SandboxCheckpointCaptureInput = {
13
+ runtime: SandboxCheckpointRuntime;
14
+ policy?: "drain" | "interrupt";
15
+ retentionExpiresAt?: number;
16
+ };
17
+ export type SandboxCheckpointRestoreInput = {
18
+ runtime: SandboxCheckpointRuntime;
19
+ checkpointId?: string;
20
+ ensure?: Omit<SandboxManagerInput, "homeRegion">;
21
+ };
22
+ export type SandboxCheckpointResult = {
23
+ status: "ready";
24
+ lease: SandboxLease;
25
+ checkpoint: SandboxCheckpointReference;
26
+ } | {
27
+ status: "provisioning";
28
+ lease: SandboxLease;
29
+ checkpoint: SandboxCheckpointReference;
30
+ retryAfterMs: number;
31
+ };
32
+ export declare function captureSandboxCheckpoint(input: {
33
+ workspaceId: string;
34
+ request: SandboxCheckpointCaptureInput;
35
+ leaseStore: SandboxLeaseStore;
36
+ target: (workspaceId: string) => Promise<SandboxTargetResult>;
37
+ snapshot: (workspaceId: string) => Promise<SandboxSnapshotManagerResult>;
38
+ now?: () => number;
39
+ checkpointId?: () => string;
40
+ }): Promise<SandboxCheckpointResult>;
41
+ export declare function restoreSandboxCheckpoint(input: {
42
+ workspaceId: string;
43
+ request: SandboxCheckpointRestoreInput;
44
+ leaseStore: SandboxLeaseStore;
45
+ ensure: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
46
+ now?: () => number;
47
+ }): Promise<SandboxCheckpointResult>;
@@ -0,0 +1,116 @@
1
+ // src/checkpoint-manager.ts
2
+ async function captureSandboxCheckpoint(input) {
3
+ const lease = await requireLease(input.leaseStore, input.workspaceId);
4
+ if (lease.status !== "ready") throw new Error("workspace_checkpoint_lease_not_ready");
5
+ const persistence = lease.persistence;
6
+ if (!persistence || persistence.capture === "none") throw new Error("workspace_checkpoint_unsupported");
7
+ const target = await input.target(input.workspaceId);
8
+ if (target.status !== "ready") throw new Error(target.reason);
9
+ await input.request.runtime.freeze(input.request.policy ?? "drain");
10
+ let capturedSource = false;
11
+ try {
12
+ await input.request.runtime.flush();
13
+ await input.request.runtime.scrub();
14
+ const captured = persistence.capture === "same-resource" ? { ok: true, snapshotId: target.driverResourceId ?? target.sandboxId } : await input.snapshot(input.workspaceId);
15
+ if (!captured.ok) throw new Error(captured.reason);
16
+ capturedSource = true;
17
+ const capturedAt = (input.now ?? Date.now)();
18
+ const checkpoint = {
19
+ id: input.checkpointId?.() ?? `cp_${lease.epoch}_${crypto.randomUUID()}`,
20
+ providerReference: captured.snapshotId,
21
+ sourceEpoch: lease.epoch,
22
+ capturedAt,
23
+ metadata: {
24
+ scope: persistence.capture,
25
+ sourceBehavior: requireCaptureSource(persistence.captureSource),
26
+ ...input.request.retentionExpiresAt === void 0 ? {} : { retentionExpiresAt: input.request.retentionExpiresAt },
27
+ restoreMount: requireRestoreMount(persistence.restoreMount)
28
+ }
29
+ };
30
+ const updated = await input.leaseStore.update(input.workspaceId, lease.epoch, {
31
+ checkpoint,
32
+ restore: null,
33
+ ...persistence.captureSource === "stopped" || persistence.captureSource === "deleted" ? { status: "stopped" } : {}
34
+ });
35
+ if (!updated) throw new Error("workspace_checkpoint_epoch_fenced");
36
+ return { status: "ready", lease: updated, checkpoint };
37
+ } finally {
38
+ if (!capturedSource || persistence.captureSource === "preserved") {
39
+ await input.request.runtime.resume();
40
+ }
41
+ }
42
+ }
43
+ async function restoreSandboxCheckpoint(input) {
44
+ const lease = await requireLease(input.leaseStore, input.workspaceId);
45
+ const checkpoint = lease.checkpoint;
46
+ if (!checkpoint || input.request.checkpointId && checkpoint.id !== input.request.checkpointId) {
47
+ throw new Error("workspace_checkpoint_not_found");
48
+ }
49
+ if (lease.restore?.state === "ready" && lease.restore.checkpointId === checkpoint.id) {
50
+ return { status: "ready", lease, checkpoint };
51
+ }
52
+ const requestedAt = lease.restore?.checkpointId === checkpoint.id ? lease.restore.requestedAt : (input.now ?? Date.now)();
53
+ const restoring = lease.restore?.checkpointId === checkpoint.id && lease.restore.state !== "pending" ? lease.restore : {
54
+ checkpointId: checkpoint.id,
55
+ sourceEpoch: checkpoint.sourceEpoch,
56
+ state: "restoring",
57
+ requestedAt,
58
+ startedAt: (input.now ?? Date.now)()
59
+ };
60
+ const staged = lease.status === "ready" && lease.epoch === checkpoint.sourceEpoch ? await input.leaseStore.update(input.workspaceId, lease.epoch, { status: "stopped", restore: restoring }) : await input.leaseStore.update(input.workspaceId, lease.epoch, { restore: restoring });
61
+ if (!staged) throw new Error("workspace_restore_epoch_fenced");
62
+ const result = await input.ensure(input.workspaceId, {
63
+ homeRegion: staged.homeRegion,
64
+ ...input.request.ensure,
65
+ bootSource: checkpoint.metadata.restoreMount === "same-resource" ? { kind: "default" } : { kind: "driver-snapshot", snapshotId: checkpoint.providerReference }
66
+ });
67
+ const current = await requireLease(input.leaseStore, input.workspaceId);
68
+ if (result.status === "provisioning") {
69
+ return { status: "provisioning", lease: current, checkpoint, retryAfterMs: result.retryAfterMs };
70
+ }
71
+ if (result.status !== "ready") {
72
+ const failedAt = (input.now ?? Date.now)();
73
+ await input.leaseStore.update(input.workspaceId, current.epoch, {
74
+ restore: {
75
+ checkpointId: checkpoint.id,
76
+ sourceEpoch: checkpoint.sourceEpoch,
77
+ state: "failed",
78
+ requestedAt,
79
+ startedAt: "startedAt" in restoring ? restoring.startedAt : void 0,
80
+ failedAt,
81
+ error: result.error ?? "workspace_restore_unavailable"
82
+ }
83
+ });
84
+ throw new Error(result.error ?? "workspace_restore_unavailable");
85
+ }
86
+ await input.request.runtime.reconcile({ epoch: result.epoch, checkpointId: checkpoint.id });
87
+ const ready = await input.leaseStore.update(input.workspaceId, result.epoch, {
88
+ restore: {
89
+ checkpointId: checkpoint.id,
90
+ sourceEpoch: checkpoint.sourceEpoch,
91
+ state: "ready",
92
+ requestedAt,
93
+ startedAt: "startedAt" in restoring && restoring.startedAt !== void 0 ? restoring.startedAt : (input.now ?? Date.now)(),
94
+ completedAt: (input.now ?? Date.now)()
95
+ }
96
+ });
97
+ if (!ready) throw new Error("workspace_restore_epoch_fenced");
98
+ return { status: "ready", lease: ready, checkpoint };
99
+ }
100
+ async function requireLease(store, workspaceId) {
101
+ const lease = await store.get(workspaceId);
102
+ if (!lease) throw new Error("workspace_checkpoint_lease_missing");
103
+ return lease;
104
+ }
105
+ function requireCaptureSource(value) {
106
+ if (value === "preserved" || value === "stopped" || value === "deleted") return value;
107
+ throw new Error("workspace_checkpoint_capabilities_invalid");
108
+ }
109
+ function requireRestoreMount(value) {
110
+ if (value === "same-resource" || value === "copy-on-write" || value === "new-resource") return value;
111
+ throw new Error("workspace_checkpoint_capabilities_invalid");
112
+ }
113
+ export {
114
+ captureSandboxCheckpoint,
115
+ restoreSandboxCheckpoint
116
+ };
@@ -3,3 +3,14 @@
3
3
  * Daytona currently accepts at most 10 entries.
4
4
  */
5
5
  export declare function formatDaytonaAllowList(cidrs: string[]): string;
6
+ /**
7
+ * Format hostnames for Daytona SDK `domainAllowList` parameter.
8
+ *
9
+ * Deliberately does NOT truncate, unlike the CIDR formatter above. Silently
10
+ * dropping entries from an egress allowlist is the worst of both worlds: the
11
+ * sandbox stays contained, but it loses reachability the caller believed it
12
+ * granted and nothing anywhere says so. Daytona documents no cap on
13
+ * `domainAllowList`; if one exists, an over-long list fails the create call
14
+ * loudly, which is the outcome we want.
15
+ */
16
+ export declare function formatDaytonaDomainAllowList(hosts: string[]): string;
@@ -2,6 +2,10 @@
2
2
  function formatDaytonaAllowList(cidrs) {
3
3
  return cidrs.slice(0, 10).join(",");
4
4
  }
5
+ function formatDaytonaDomainAllowList(hosts) {
6
+ return [...new Set(hosts)].join(",");
7
+ }
5
8
  export {
6
- formatDaytonaAllowList
9
+ formatDaytonaAllowList,
10
+ formatDaytonaDomainAllowList
7
11
  };
@@ -1,7 +1,10 @@
1
- import type { SandboxDriverMetadata } from "./index";
2
- export declare const sandboxDriverIds: readonly ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
1
+ import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./index";
2
+ export declare const sandboxDriverIds: readonly ["exe", "daytona", "modal", "vercel", "cloudflare", "box", "docker"];
3
3
  export type SandboxDriverID = (typeof sandboxDriverIds)[number];
4
4
  export type SandboxDriverAuth = {
5
+ exe?: {
6
+ api_token?: string;
7
+ };
5
8
  daytona?: {
6
9
  api_key?: string;
7
10
  };
@@ -39,17 +42,18 @@ export type SandboxDriverCatalogEntry = {
39
42
  secret?: boolean;
40
43
  }>;
41
44
  };
45
+ export { validateSandboxPersistenceCapabilities };
42
46
  export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
43
47
  export declare function isSandboxDriverID(input: string | undefined): input is SandboxDriverID;
44
- export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel" | undefined;
48
+ export declare function sandboxDriverId(input: string | undefined, cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): "box" | "cloudflare" | "daytona" | "docker" | "exe" | "modal" | "vercel" | undefined;
45
49
  export declare function defaultSandboxDriverID(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv): SandboxDriverID;
46
50
  export declare function sandboxDriverAuth<T extends SandboxDriverID>(cfg: SandboxDriverConfig | undefined, id: T, env?: SandboxDriverEnv): SandboxDriverAuth[T] | undefined;
47
51
  export declare function hasSandboxDriverAuth(cfg: SandboxDriverConfig | undefined, id: SandboxDriverID, env?: SandboxDriverEnv): boolean;
48
52
  export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
49
53
  export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: SandboxDriverEnv, managedDriverIds?: ReadonlySet<string>): {
50
- default_driver: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
54
+ default_driver: "box" | "cloudflare" | "daytona" | "docker" | "exe" | "modal" | "vercel";
51
55
  drivers: {
52
- id: "box" | "cloudflare" | "daytona" | "docker" | "modal" | "vercel";
56
+ id: "box" | "cloudflare" | "daytona" | "docker" | "exe" | "modal" | "vercel";
53
57
  label: string;
54
58
  fields: {
55
59
  key: string;
@@ -62,4 +66,3 @@ export declare function listSandboxDrivers(cfg?: SandboxDriverConfig, env?: Sand
62
66
  }[];
63
67
  };
64
68
  type SandboxDriverEnv = Record<string, string | undefined>;
65
- export {};
@@ -1,15 +1,53 @@
1
+ // src/hosted-network-policy.ts
2
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
3
+ "api.anthropic.com",
4
+ "api.openai.com",
5
+ "generativelanguage.googleapis.com",
6
+ "openrouter.ai",
7
+ // Model catalog + engine config, fetched on runner startup.
8
+ "models.dev",
9
+ "opencode.ai"
10
+ ];
11
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
12
+ "registry.npmjs.org",
13
+ "nodejs.org",
14
+ "pypi.org",
15
+ "files.pythonhosted.org",
16
+ "crates.io",
17
+ "static.crates.io",
18
+ "index.crates.io",
19
+ "proxy.golang.org",
20
+ "sum.golang.org"
21
+ ];
22
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
23
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
24
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
25
+ ];
26
+
27
+ // src/index.ts
28
+ function validateSandboxPersistenceCapabilities(input) {
29
+ if (input.resume === "replacement-restore" && input.capture === "none") {
30
+ return { valid: false, reason: "replacement restore requires a capture source" };
31
+ }
32
+ if (input.capture === "none" && input.captureSource !== "not-applicable") {
33
+ return { valid: false, reason: "capture source behavior requires capture support" };
34
+ }
35
+ if (input.capture !== "none" && input.captureSource === "not-applicable") {
36
+ return { valid: false, reason: "capture support requires source behavior" };
37
+ }
38
+ if (input.capture === "same-resource" && input.restoreMount !== "same-resource") {
39
+ return { valid: false, reason: "same-resource capture requires same-resource restore" };
40
+ }
41
+ if (input.restoreMount === "copy-on-write" && input.capture !== "directories") {
42
+ return { valid: false, reason: "copy-on-write restore requires directory capture" };
43
+ }
44
+ return { valid: true };
45
+ }
46
+
1
47
  // src/runtime-version.ts
2
48
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
- var versionCache;
4
49
  function workspaceRuntimeVersion() {
5
- if (versionCache) return versionCache;
6
- const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
7
- if (envVersion) {
8
- versionCache = envVersion;
9
- return versionCache;
10
- }
11
- versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
12
- return versionCache;
50
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
13
51
  }
14
52
 
15
53
  // src/image-name.ts
@@ -31,8 +69,29 @@ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env =
31
69
  }
32
70
 
33
71
  // src/driver-catalog.ts
34
- var sandboxDriverIds = ["daytona", "modal", "vercel", "cloudflare", "box", "docker"];
72
+ var sandboxDriverIds = ["exe", "daytona", "modal", "vercel", "cloudflare", "box", "docker"];
35
73
  var sandboxDriverCatalog = {
74
+ exe: {
75
+ id: "exe",
76
+ label: "exe.dev",
77
+ credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
78
+ metadata: {
79
+ driverRunsIn: ["worker", "node"],
80
+ hostStopBehavior: "not-supported",
81
+ hostResumeBehavior: "same-host",
82
+ targetAccess: "relay",
83
+ secretBrokering: "none",
84
+ egressControl: "none",
85
+ persistence: {
86
+ resume: "same-sandbox",
87
+ capture: "none",
88
+ clone: true,
89
+ captureSource: "not-applicable",
90
+ retention: "provider-managed",
91
+ restoreMount: "same-resource"
92
+ }
93
+ }
94
+ },
36
95
  daytona: {
37
96
  id: "daytona",
38
97
  label: "Daytona",
@@ -42,7 +101,19 @@ var sandboxDriverCatalog = {
42
101
  hostStopBehavior: "suspends-host",
43
102
  hostResumeBehavior: "same-host",
44
103
  targetAccess: "relay",
45
- secretBrokering: "native"
104
+ secretBrokering: "native",
105
+ // Daytona is the only driver that can filter egress by NAME as well as
106
+ // by address: `domainAllowList` (names) alongside `networkAllowList`
107
+ // (CIDRs), with `networkBlockAll` as the deny-all floor.
108
+ egressControl: "hosts-and-cidrs",
109
+ persistence: {
110
+ resume: "same-sandbox",
111
+ capture: "filesystem",
112
+ clone: false,
113
+ captureSource: "preserved",
114
+ retention: "provider-managed",
115
+ restoreMount: "new-resource"
116
+ }
46
117
  }
47
118
  },
48
119
  modal: {
@@ -57,7 +128,19 @@ var sandboxDriverCatalog = {
57
128
  hostStopBehavior: "terminates-host",
58
129
  hostResumeBehavior: "replacement-host",
59
130
  targetAccess: "relay",
60
- secretBrokering: "none"
131
+ secretBrokering: "none",
132
+ // Modal can cut the network entirely (`blockNetwork`) but cannot express
133
+ // an allowlist, and its driver throws on a host policy. A blackout is
134
+ // not containment for a workspace that has to clone and reach a model.
135
+ egressControl: "none",
136
+ persistence: {
137
+ resume: "replacement-restore",
138
+ capture: "filesystem",
139
+ clone: false,
140
+ captureSource: "preserved",
141
+ retention: "provider-managed",
142
+ restoreMount: "new-resource"
143
+ }
61
144
  }
62
145
  },
63
146
  vercel: {
@@ -73,7 +156,18 @@ var sandboxDriverCatalog = {
73
156
  hostStopBehavior: "terminates-host",
74
157
  hostResumeBehavior: "replacement-host",
75
158
  targetAccess: "relay",
76
- secretBrokering: "native"
159
+ secretBrokering: "native",
160
+ // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
161
+ // never CIDRs.
162
+ egressControl: "hosts",
163
+ persistence: {
164
+ resume: "replacement-restore",
165
+ capture: "filesystem",
166
+ clone: false,
167
+ captureSource: "stopped",
168
+ retention: "explicit",
169
+ restoreMount: "new-resource"
170
+ }
77
171
  }
78
172
  },
79
173
  cloudflare: {
@@ -88,7 +182,22 @@ var sandboxDriverCatalog = {
88
182
  hostStopBehavior: "not-supported",
89
183
  hostResumeBehavior: "same-host",
90
184
  targetAccess: "relay",
91
- secretBrokering: "proxy"
185
+ secretBrokering: "proxy",
186
+ // The Cloudflare Sandbox Worker exposes no egress filter: its broker
187
+ // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
188
+ // to route brokered-credential requests through, not a boundary. The
189
+ // driver therefore drops `net` entirely — which is exactly why this
190
+ // must be declared, so the manager refuses instead of provisioning a
191
+ // sandbox the caller believes is contained.
192
+ egressControl: "none",
193
+ persistence: {
194
+ resume: "replacement-restore",
195
+ capture: "directories",
196
+ clone: false,
197
+ captureSource: "preserved",
198
+ retention: "provider-managed",
199
+ restoreMount: "copy-on-write"
200
+ }
92
201
  }
93
202
  },
94
203
  box: {
@@ -100,7 +209,16 @@ var sandboxDriverCatalog = {
100
209
  hostStopBehavior: "suspends-host",
101
210
  hostResumeBehavior: "same-host",
102
211
  targetAccess: "relay",
103
- secretBrokering: "none"
212
+ secretBrokering: "none",
213
+ egressControl: "none",
214
+ persistence: {
215
+ resume: "same-sandbox",
216
+ capture: "none",
217
+ clone: false,
218
+ captureSource: "not-applicable",
219
+ retention: "provider-managed",
220
+ restoreMount: "same-resource"
221
+ }
104
222
  }
105
223
  },
106
224
  docker: {
@@ -112,7 +230,16 @@ var sandboxDriverCatalog = {
112
230
  hostStopBehavior: "terminates-host",
113
231
  hostResumeBehavior: "same-host",
114
232
  targetAccess: "loopback",
115
- secretBrokering: "none"
233
+ secretBrokering: "none",
234
+ egressControl: "none",
235
+ persistence: {
236
+ resume: "replacement-restore",
237
+ capture: "same-resource",
238
+ clone: false,
239
+ captureSource: "preserved",
240
+ retention: "provider-managed",
241
+ restoreMount: "same-resource"
242
+ }
116
243
  }
117
244
  }
118
245
  };
@@ -129,6 +256,7 @@ function defaultSandboxDriverID(cfg, env = process.env) {
129
256
  }
130
257
  function sandboxDriverAuth(cfg, id, env = process.env) {
131
258
  if (id === "daytona") return authDaytona(cfg);
259
+ if (id === "exe") return authExe(cfg, env);
132
260
  if (id === "modal") return authModal(cfg, env);
133
261
  if (id === "vercel") return authVercel(cfg, env);
134
262
  if (id === "cloudflare") return authCloudflare(cfg, env);
@@ -176,6 +304,10 @@ function authDaytona(cfg) {
176
304
  const api_key = clean(cfg?.auth?.daytona?.api_key);
177
305
  return api_key ? { api_key } : void 0;
178
306
  }
307
+ function authExe(cfg, env) {
308
+ const api_token = clean(cfg?.auth?.exe?.api_token) ?? clean(env.EXE_DEV_API_TOKEN);
309
+ return api_token ? { api_token } : void 0;
310
+ }
179
311
  function authModal(cfg, env) {
180
312
  const token_id = clean(cfg?.auth?.modal?.token_id) ?? clean(env.MODAL_TOKEN_ID);
181
313
  const token_secret = clean(cfg?.auth?.modal?.token_secret) ?? clean(env.MODAL_TOKEN_SECRET);
@@ -211,5 +343,6 @@ export {
211
343
  sandboxDriverAuth,
212
344
  sandboxDriverCatalog,
213
345
  sandboxDriverId,
214
- sandboxDriverIds
346
+ sandboxDriverIds,
347
+ validateSandboxPersistenceCapabilities
215
348
  };