@claxedo/sandbox-manager 0.9.0 → 0.10.1

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 (67) hide show
  1. package/README.md +233 -26
  2. package/dist/checkpoint-manager.d.ts +29 -4
  3. package/dist/checkpoint-manager.mjs +133 -24
  4. package/dist/command.d.ts +9 -2
  5. package/dist/command.mjs +8 -8
  6. package/dist/contract.d.ts +687 -0
  7. package/dist/driver-billing.d.ts +29 -0
  8. package/dist/driver-catalog.d.ts +42 -7
  9. package/dist/driver-catalog.mjs +115 -161
  10. package/dist/drivers/boat-client.d.ts +95 -0
  11. package/dist/drivers/boat-named-snapshots.d.ts +31 -0
  12. package/dist/drivers/boat-template.d.ts +49 -0
  13. package/dist/drivers/boat-template.mjs +1196 -0
  14. package/dist/drivers/boat.d.ts +64 -0
  15. package/dist/drivers/boat.mjs +990 -0
  16. package/dist/drivers/cloudflare.d.ts +3 -3
  17. package/dist/drivers/cloudflare.mjs +208 -147
  18. package/dist/drivers/docker.d.ts +4 -3
  19. package/dist/drivers/docker.mjs +154 -126
  20. package/dist/drivers/local-brokering.d.ts +14 -0
  21. package/dist/drivers/local-brokering.mjs +1029 -0
  22. package/dist/drivers/modal.d.ts +2 -2
  23. package/dist/drivers/modal.mjs +121 -111
  24. package/dist/drivers/transient-error.d.ts +2 -0
  25. package/dist/drivers/vercel.d.ts +3 -2
  26. package/dist/drivers/vercel.mjs +151 -115
  27. package/dist/egress-policy.d.ts +134 -0
  28. package/dist/egress-warnings.d.ts +22 -0
  29. package/dist/garbage-collect.d.ts +14 -0
  30. package/dist/hosted-network-policy.d.ts +9 -2
  31. package/dist/hosted-network-policy.mjs +218 -1
  32. package/dist/image-name.d.ts +12 -2
  33. package/dist/image-name.mjs +14 -5
  34. package/dist/image.d.ts +1 -20
  35. package/dist/image.mjs +11 -88
  36. package/dist/index.d.ts +8 -578
  37. package/dist/index.mjs +1379 -320
  38. package/dist/lease-failure.d.ts +173 -0
  39. package/dist/lease-lifecycle.d.ts +65 -0
  40. package/dist/lease-retry-wait.d.ts +8 -0
  41. package/dist/lease-start.d.ts +21 -0
  42. package/dist/lease-types.d.ts +4 -3
  43. package/dist/manager.d.ts +2 -0
  44. package/dist/runtime-env.d.ts +4 -2
  45. package/dist/runtime-env.mjs +37 -2
  46. package/dist/runtime-snapshot.d.ts +49 -0
  47. package/dist/runtime-version.d.ts +9 -0
  48. package/dist/runtime-version.mjs +2 -0
  49. package/dist/service-url-exposure.d.ts +0 -1
  50. package/dist/service-url-exposure.mjs +0 -9
  51. package/dist/start-fresh.d.ts +17 -0
  52. package/dist/start-telemetry.d.ts +37 -0
  53. package/dist/stores/memory.d.ts +1 -1
  54. package/dist/stores/memory.mjs +63 -52
  55. package/package.json +26 -42
  56. package/dist/daytona-allow-list.d.ts +0 -16
  57. package/dist/daytona-allow-list.mjs +0 -11
  58. package/dist/drivers/box.d.ts +0 -52
  59. package/dist/drivers/box.mjs +0 -574
  60. package/dist/drivers/daytona.d.ts +0 -163
  61. package/dist/drivers/daytona.mjs +0 -683
  62. package/dist/drivers/exe.d.ts +0 -26
  63. package/dist/drivers/exe.mjs +0 -492
  64. package/dist/drivers/fetch-bridge.d.ts +0 -10
  65. package/dist/drivers/fetch-bridge.mjs +0 -126
  66. package/dist/lease-policy.d.ts +0 -52
  67. package/dist/lease-policy.mjs +0 -290
@@ -0,0 +1,29 @@
1
+ import type { SandboxMachineClass } from "@claxedo/sandbox-contract";
2
+ /** One size a provider sells, as its own catalog names and prices it. */
3
+ export type SandboxMachineRate = {
4
+ /** The provider's own name for the size. */
5
+ size: string;
6
+ vcpu: number;
7
+ memoryGb: number;
8
+ usdPerHour: number;
9
+ };
10
+ /**
11
+ * How a provider bills the machine a sandbox runs on, read from its published
12
+ * pricing. `rates.default` is also the one size of a driver that offers no
13
+ * choice. `excludes` names a component the hourly rate leaves out because the
14
+ * provider meters it on use rather than on time.
15
+ */
16
+ export type SandboxDriverBilling = {
17
+ source: {
18
+ url: string;
19
+ readOn: string;
20
+ };
21
+ meter: string;
22
+ rates: Partial<Record<SandboxMachineClass, SandboxMachineRate>>;
23
+ excludes?: string;
24
+ };
25
+ export declare const boatBilling: SandboxDriverBilling;
26
+ export declare const cloudflareBilling: SandboxDriverBilling;
27
+ /** The rate a sandbox of this class runs at; a driver with one size answers it for the class it was not asked for. */
28
+ export declare function sandboxMachineRate(billing: SandboxDriverBilling | undefined, machineClass: SandboxMachineClass | null): SandboxMachineRate | undefined;
29
+ export declare function sandboxMachineCostUsd(rate: SandboxMachineRate, activeMs: number): number;
@@ -1,8 +1,10 @@
1
- import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./index";
2
- import { defaultSandboxDriverID, listSandboxDrivers, sandboxDriverId, type SandboxDriverAuth, type SandboxDriverConfig, type SandboxDriverEnv, type SandboxDriverID } from "@claxedo/sandbox-contract";
3
- export { defaultSandboxDriverID, listSandboxDrivers, sandboxDriverId };
4
- export type SandboxDriverCatalogEntry = {
5
- id: SandboxDriverID;
1
+ import { validateSandboxPersistenceCapabilities, type SandboxDriverMetadata } from "./contract";
2
+ import { type SandboxDriverBilling } from "./driver-billing";
3
+ import { sandboxDriverId, type SandboxDriverEnv, type SandboxDriverID } from "@claxedo/sandbox-contract";
4
+ export { sandboxDriverId };
5
+ export { sandboxMachineCostUsd, sandboxMachineRate, type SandboxDriverBilling, type SandboxMachineRate } from "./driver-billing";
6
+ export type SandboxDriverCatalogEntry<ID extends string = SandboxDriverID> = {
7
+ id: ID;
6
8
  label: string;
7
9
  metadata: SandboxDriverMetadata;
8
10
  credentialFields: ReadonlyArray<{
@@ -10,9 +12,42 @@ export type SandboxDriverCatalogEntry = {
10
12
  label: string;
11
13
  secret?: boolean;
12
14
  }>;
15
+ runtimeNetwork: {
16
+ controlPlane: "docker-host" | "direct";
17
+ relay: "configured" | "local";
18
+ };
19
+ runtimeEnv: "ensure" | "callback";
20
+ preparedImage: boolean;
21
+ /** Absent where no published rate for the machines this driver places on has been established. */
22
+ billing?: SandboxDriverBilling;
13
23
  };
14
24
  export { validateSandboxPersistenceCapabilities };
25
+ export declare const localBrokeringTestDriverCatalogEntry: {
26
+ readonly id: "local-brokering-test";
27
+ readonly label: "Local brokering test driver";
28
+ readonly credentialFields: readonly [];
29
+ readonly runtimeNetwork: {
30
+ readonly controlPlane: "direct";
31
+ readonly relay: "local";
32
+ };
33
+ readonly runtimeEnv: "ensure";
34
+ readonly preparedImage: false;
35
+ readonly metadata: {
36
+ readonly driverRunsIn: ["local"];
37
+ readonly hostStopBehavior: "terminates-host";
38
+ readonly hostResumeBehavior: "same-host";
39
+ readonly targetAccess: "loopback";
40
+ readonly secretBrokering: "native";
41
+ readonly egressControl: "hosts";
42
+ readonly persistence: {
43
+ readonly resume: "same-sandbox";
44
+ readonly capture: "none";
45
+ readonly clone: false;
46
+ readonly captureSource: "not-applicable";
47
+ readonly retention: "provider-managed";
48
+ readonly restoreMount: "same-resource";
49
+ };
50
+ };
51
+ };
15
52
  export declare const sandboxDriverCatalog: Record<SandboxDriverID, SandboxDriverCatalogEntry>;
16
- export declare function sandboxDriverAuth<T extends SandboxDriverID>(cfg: SandboxDriverConfig | undefined, id: T, env?: SandboxDriverEnv): SandboxDriverAuth[T] | undefined;
17
- export declare function hasSandboxDriverAuth(cfg: SandboxDriverConfig | undefined, id: SandboxDriverID, env?: SandboxDriverEnv): boolean;
18
53
  export declare function dockerSandboxDriverEnabled(env?: SandboxDriverEnv): boolean;
@@ -1,30 +1,4 @@
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
1
+ // src/contract.ts
28
2
  function validateSandboxPersistenceCapabilities(input) {
29
3
  if (input.resume === "replacement-restore" && input.capture === "none") {
30
4
  return { valid: false, reason: "replacement restore requires a capture source" };
@@ -38,34 +12,43 @@ function validateSandboxPersistenceCapabilities(input) {
38
12
  if (input.capture === "same-resource" && input.restoreMount !== "same-resource") {
39
13
  return { valid: false, reason: "same-resource capture requires same-resource restore" };
40
14
  }
41
- if (input.restoreMount === "copy-on-write" && input.capture !== "directories") {
42
- return { valid: false, reason: "copy-on-write restore requires directory capture" };
43
- }
44
15
  return { valid: true };
45
16
  }
46
17
 
47
- // src/runtime-version.ts
48
- var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
49
- function workspaceRuntimeVersion() {
50
- return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
51
- }
52
-
53
- // src/image-name.ts
54
- var SNAPSHOT_SCHEMA_VERSION = 8;
55
- var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
56
- function snapshotVersion(value) {
57
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
58
- }
59
- function sandboxImageRepository(env = process.env) {
60
- const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
61
- return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
62
- }
63
- function buildIdSuffix(buildId, env) {
64
- const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
65
- return id ? `-${snapshotVersion(id)}` : "";
18
+ // src/driver-billing.ts
19
+ var HOUR_SECONDS = 3600;
20
+ var boatBilling = {
21
+ source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
22
+ meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
23
+ rates: {
24
+ small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
25
+ default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
26
+ large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
27
+ }
28
+ };
29
+ var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
30
+ var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
31
+ var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
32
+ var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
33
+ var cloudflareBilling = {
34
+ source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
35
+ meter: "Cloudflare bills a container for every 10 ms it runs: memory at $0.0000025 per GiB-second and disk at $0.00000007 per GB-second for the whole run, plus CPU at $0.00002 per vCPU-second of active use.",
36
+ rates: {
37
+ default: {
38
+ size: CLOUDFLARE_STANDARD_1.size,
39
+ vcpu: CLOUDFLARE_STANDARD_1.vcpu,
40
+ memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
41
+ usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
42
+ }
43
+ },
44
+ excludes: `active CPU, billed at $${CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE} per vCPU-second and not measured by Claxedo, and the monthly allowance a Workers Paid plan includes`
45
+ };
46
+ function sandboxMachineRate(billing, machineClass) {
47
+ if (!billing) return void 0;
48
+ return billing.rates[machineClass ?? "default"] ?? (Object.keys(billing.rates).length === 1 ? billing.rates.default : void 0);
66
49
  }
67
- function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
68
- return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
50
+ function sandboxMachineCostUsd(rate, activeMs) {
51
+ return rate.usdPerHour * activeMs / (HOUR_SECONDS * 1e3);
69
52
  }
70
53
 
71
54
  // ../sandbox-contract/dist/index.mjs
@@ -78,10 +61,8 @@ function trimToUndefined(value) {
78
61
  const trimmed = value.trim();
79
62
  return trimmed.length > 0 ? trimmed : void 0;
80
63
  }
81
- var sandboxDriverIds = ["exe", "daytona", "modal", "vercel", "cloudflare", "box", "docker"];
64
+ var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "docker"];
82
65
  var sandboxDriverCredentialFields = {
83
- exe: [{ key: "api_token", label: "API Token", secret: true }],
84
- daytona: [{ key: "api_key", label: "API Key", secret: true }],
85
66
  modal: [
86
67
  { key: "token_id", label: "Token ID", secret: true },
87
68
  { key: "token_secret", label: "Token Secret", secret: true }
@@ -95,16 +76,14 @@ var sandboxDriverCredentialFields = {
95
76
  { key: "api_token", label: "API Token", secret: true },
96
77
  { key: "worker_url", label: "Worker URL" }
97
78
  ],
98
- box: [{ key: "api_key", label: "API Key", secret: true }],
79
+ boat: [{ key: "api_key", label: "API Key", secret: true }],
99
80
  docker: [{ key: "image", label: "Image" }]
100
81
  };
101
82
  var sandboxDriverLabels = {
102
- exe: "exe.dev",
103
- daytona: "Daytona",
104
83
  modal: "Modal",
105
84
  vercel: "Vercel",
106
85
  cloudflare: "Cloudflare",
107
- box: "Box",
86
+ boat: "Boat",
108
87
  docker: "Docker"
109
88
  };
110
89
  function isSandboxDriverID(input) {
@@ -117,14 +96,6 @@ function dockerSandboxDriverEnabled(env = process.env) {
117
96
  return enabled(env.CLAXEDO_ENABLE_DOCKER_SANDBOX) || enabled(env.CLAXEDO_DEV_DOCKER_SANDBOX);
118
97
  }
119
98
  function sandboxDriverAuthValues(cfg, id, env = process.env) {
120
- if (id === "daytona") {
121
- const api_key = trimToUndefined(cfg?.auth?.daytona?.api_key);
122
- return api_key ? { api_key } : void 0;
123
- }
124
- if (id === "exe") {
125
- const api_token = trimToUndefined(cfg?.auth?.exe?.api_token) ?? trimToUndefined(env.EXE_DEV_API_TOKEN);
126
- return api_token ? { api_token } : void 0;
127
- }
128
99
  if (id === "modal") {
129
100
  const token_id = trimToUndefined(cfg?.auth?.modal?.token_id) ?? trimToUndefined(env.MODAL_TOKEN_ID);
130
101
  const token_secret = trimToUndefined(cfg?.auth?.modal?.token_secret) ?? trimToUndefined(env.MODAL_TOKEN_SECRET);
@@ -138,93 +109,72 @@ function sandboxDriverAuthValues(cfg, id, env = process.env) {
138
109
  }
139
110
  if (id === "cloudflare") {
140
111
  const api_token = trimToUndefined(cfg?.auth?.cloudflare?.api_token) ?? trimToUndefined(env.CLOUDFLARE_API_TOKEN);
141
- const worker_url = trimToUndefined(cfg?.auth?.cloudflare?.worker_url) ?? trimToUndefined(env.CLOUDFLARE_SANDBOX_WORKER_URL);
112
+ const configuredUrl = trimToUndefined(cfg?.auth?.cloudflare?.worker_url) ?? trimToUndefined(env.CLOUDFLARE_SANDBOX_WORKER_URL);
113
+ const worker_url = configuredUrl ? cloudflareWorkerBaseUrl(configuredUrl) : void 0;
142
114
  return api_token && worker_url ? { api_token, worker_url } : void 0;
143
115
  }
144
- if (id === "box") {
145
- const api_key = trimToUndefined(cfg?.auth?.box?.api_key) ?? trimToUndefined(env.BOX_API_KEY);
116
+ if (id === "boat") {
117
+ const api_key = trimToUndefined(cfg?.auth?.boat?.api_key) ?? trimToUndefined(env.BOAT_API_KEY);
146
118
  return api_key ? { api_key } : void 0;
147
119
  }
148
- if (!dockerSandboxDriverEnabled(env)) return void 0;
120
+ if (!dockerSandboxDriverEnabled(env))
121
+ return;
149
122
  const image = trimToUndefined(cfg?.auth?.docker?.image) ?? trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_IMAGE) ?? trimToUndefined(env.CLAXEDO_SANDBOX_IMAGE);
150
123
  return image ? { image } : {};
151
124
  }
125
+ function cloudflareWorkerBaseUrl(input) {
126
+ let url;
127
+ try {
128
+ url = new URL(input.trim());
129
+ } catch {
130
+ throw new Error("Cloudflare Worker URL must be a valid HTTPS endpoint");
131
+ }
132
+ if (url.protocol !== "https:" || url.username || url.password || input.includes("?") || input.includes("#")) {
133
+ throw new Error("Cloudflare Worker URL requires HTTPS without credentials, query or fragment");
134
+ }
135
+ return url.origin + url.pathname.replace(/\/+$/, "");
136
+ }
152
137
  function sandboxDriverId(input, cfg, env = process.env) {
153
- if (!isSandboxDriverID(input)) return void 0;
154
- if (input === "docker" && !sandboxDriverAuthValues(cfg, input, env)) return void 0;
138
+ if (!isSandboxDriverID(input))
139
+ return;
140
+ if (input === "docker" && !sandboxDriverAuthValues(cfg, input, env))
141
+ return;
155
142
  return input;
156
143
  }
157
- function defaultSandboxDriverID(cfg, env = process.env) {
158
- return sandboxDriverId(cfg?.default_driver, cfg, env) ?? (enabled(env.CLAXEDO_DOCKER_SANDBOX_DEFAULT) && sandboxDriverAuthValues(cfg, "docker", env) ? "docker" : "daytona");
159
- }
160
- function listSandboxDrivers(cfg, env = process.env, managedDriverIds = /* @__PURE__ */ new Set()) {
161
- const defaultDriver = defaultSandboxDriverID(cfg, env);
162
- return {
163
- default_driver: defaultDriver,
164
- drivers: sandboxDriverIds.filter((id) => id !== "docker" || !!sandboxDriverAuthValues(cfg, "docker", env)).map((id) => {
165
- const configuredFromConfigOrEnv = !!sandboxDriverAuthValues(cfg, id, env);
166
- const configured = id === "docker" ? configuredFromConfigOrEnv : configuredFromConfigOrEnv || managedDriverIds.has(id);
167
- return {
168
- id,
169
- label: sandboxDriverLabels[id],
170
- fields: sandboxDriverCredentialFields[id],
171
- configured,
172
- source: configuredFromConfigOrEnv ? "config" : configured ? "managed" : "none",
173
- default: id === defaultDriver
174
- };
175
- })
176
- };
177
- }
178
144
 
179
145
  // src/driver-catalog.ts
180
- var sandboxDriverCatalog = {
181
- exe: {
182
- id: "exe",
183
- label: sandboxDriverLabels.exe,
184
- credentialFields: sandboxDriverCredentialFields.exe,
185
- metadata: {
186
- driverRunsIn: ["worker", "node"],
187
- hostStopBehavior: "not-supported",
188
- hostResumeBehavior: "same-host",
189
- targetAccess: "relay",
190
- secretBrokering: "none",
191
- egressControl: "none",
192
- persistence: {
193
- resume: "same-sandbox",
194
- capture: "none",
195
- clone: true,
196
- captureSource: "not-applicable",
197
- retention: "provider-managed",
198
- restoreMount: "same-resource"
199
- }
200
- }
201
- },
202
- daytona: {
203
- id: "daytona",
204
- label: sandboxDriverLabels.daytona,
205
- credentialFields: sandboxDriverCredentialFields.daytona,
206
- metadata: {
207
- driverRunsIn: ["worker", "node"],
208
- hostStopBehavior: "suspends-host",
209
- hostResumeBehavior: "same-host",
210
- targetAccess: "relay",
211
- secretBrokering: "native",
212
- // The driver translates names to domainAllowList, or addresses to networkAllowList.
213
- egressControl: "hosts-and-cidrs",
214
- persistence: {
215
- resume: "same-sandbox",
216
- capture: "filesystem",
217
- clone: false,
218
- captureSource: "preserved",
219
- retention: "provider-managed",
220
- restoreMount: "new-resource"
221
- }
146
+ var localBrokeringTestDriverCatalogEntry = {
147
+ id: "local-brokering-test",
148
+ label: "Local brokering test driver",
149
+ credentialFields: [],
150
+ runtimeNetwork: { controlPlane: "direct", relay: "local" },
151
+ runtimeEnv: "ensure",
152
+ preparedImage: false,
153
+ metadata: {
154
+ driverRunsIn: ["local"],
155
+ hostStopBehavior: "terminates-host",
156
+ hostResumeBehavior: "same-host",
157
+ targetAccess: "loopback",
158
+ secretBrokering: "native",
159
+ egressControl: "hosts",
160
+ persistence: {
161
+ resume: "same-sandbox",
162
+ capture: "none",
163
+ clone: false,
164
+ captureSource: "not-applicable",
165
+ retention: "provider-managed",
166
+ restoreMount: "same-resource"
222
167
  }
223
- },
168
+ }
169
+ };
170
+ var sandboxDriverCatalog = {
224
171
  modal: {
225
172
  id: "modal",
226
173
  label: sandboxDriverLabels.modal,
227
174
  credentialFields: sandboxDriverCredentialFields.modal,
175
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
176
+ runtimeEnv: "callback",
177
+ preparedImage: true,
228
178
  metadata: {
229
179
  driverRunsIn: ["node"],
230
180
  hostStopBehavior: "terminates-host",
@@ -247,6 +197,9 @@ var sandboxDriverCatalog = {
247
197
  id: "vercel",
248
198
  label: sandboxDriverLabels.vercel,
249
199
  credentialFields: sandboxDriverCredentialFields.vercel,
200
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
201
+ runtimeEnv: "callback",
202
+ preparedImage: false,
250
203
  metadata: {
251
204
  driverRunsIn: ["node"],
252
205
  hostStopBehavior: "terminates-host",
@@ -269,30 +222,40 @@ var sandboxDriverCatalog = {
269
222
  id: "cloudflare",
270
223
  label: sandboxDriverLabels.cloudflare,
271
224
  credentialFields: sandboxDriverCredentialFields.cloudflare,
225
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
226
+ runtimeEnv: "callback",
227
+ preparedImage: false,
228
+ billing: cloudflareBilling,
272
229
  metadata: {
273
230
  driverRunsIn: ["worker"],
274
- hostStopBehavior: "not-supported",
231
+ hostStopBehavior: "terminates-host",
275
232
  hostResumeBehavior: "same-host",
233
+ hostWatchesIdle: true,
276
234
  targetAccess: "relay",
277
235
  secretBrokering: "native",
278
236
  // Credential handlers do not restrict unrelated destinations.
279
237
  egressControl: "none",
238
+ // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
280
239
  persistence: {
281
240
  resume: "replacement-restore",
282
- capture: "directories",
241
+ capture: "filesystem",
283
242
  clone: false,
284
243
  captureSource: "preserved",
285
244
  retention: "provider-managed",
286
- restoreMount: "copy-on-write"
245
+ restoreMount: "new-resource"
287
246
  }
288
247
  }
289
248
  },
290
- box: {
291
- id: "box",
292
- label: sandboxDriverLabels.box,
293
- credentialFields: sandboxDriverCredentialFields.box,
249
+ boat: {
250
+ id: "boat",
251
+ label: sandboxDriverLabels.boat,
252
+ credentialFields: sandboxDriverCredentialFields.boat,
253
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
254
+ runtimeEnv: "callback",
255
+ preparedImage: true,
256
+ billing: boatBilling,
294
257
  metadata: {
295
- driverRunsIn: ["node"],
258
+ driverRunsIn: ["worker", "node"],
296
259
  hostStopBehavior: "suspends-host",
297
260
  hostResumeBehavior: "same-host",
298
261
  targetAccess: "relay",
@@ -305,13 +268,17 @@ var sandboxDriverCatalog = {
305
268
  captureSource: "not-applicable",
306
269
  retention: "provider-managed",
307
270
  restoreMount: "same-resource"
308
- }
271
+ },
272
+ machineClasses: ["small", "default", "large"]
309
273
  }
310
274
  },
311
275
  docker: {
312
276
  id: "docker",
313
277
  label: sandboxDriverLabels.docker,
314
278
  credentialFields: sandboxDriverCredentialFields.docker,
279
+ runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
280
+ runtimeEnv: "callback",
281
+ preparedImage: true,
315
282
  metadata: {
316
283
  driverRunsIn: ["local"],
317
284
  hostStopBehavior: "terminates-host",
@@ -330,28 +297,15 @@ var sandboxDriverCatalog = {
330
297
  }
331
298
  }
332
299
  };
333
- function sandboxDriverAuth(cfg, id, env = process.env) {
334
- if (id === "docker") {
335
- const configured = sandboxDriverAuthValues(cfg, "docker", env);
336
- if (!configured) return void 0;
337
- const image = configured.image ?? defaultSandboxImage(workspaceRuntimeVersion(), void 0, env);
338
- return { image };
339
- }
340
- return sandboxDriverAuthValues(cfg, id, env);
341
- }
342
- function hasSandboxDriverAuth(cfg, id, env = process.env) {
343
- return !!sandboxDriverAuth(cfg, id, env);
344
- }
345
300
  function dockerSandboxDriverEnabled2(env = process.env) {
346
301
  return dockerSandboxDriverEnabled(env);
347
302
  }
348
303
  export {
349
- defaultSandboxDriverID,
350
304
  dockerSandboxDriverEnabled2 as dockerSandboxDriverEnabled,
351
- hasSandboxDriverAuth,
352
- listSandboxDrivers,
353
- sandboxDriverAuth,
305
+ localBrokeringTestDriverCatalogEntry,
354
306
  sandboxDriverCatalog,
355
307
  sandboxDriverId,
308
+ sandboxMachineCostUsd,
309
+ sandboxMachineRate,
356
310
  validateSandboxPersistenceCapabilities
357
311
  };
@@ -0,0 +1,95 @@
1
+ import type { SandboxMachineClass } from "@claxedo/sandbox-contract";
2
+ import { type SandboxStartLimit, type SandboxStartWindow } from "../lease-failure";
3
+ export type BoatSandboxState = "init" | "provisioning" | "provisioned" | "cloning" | "ready" | "idle" | "running" | "archiving" | "archived" | "error" | "cancelled";
4
+ /** `hydrated`: Boat has downloaded every file of the snapshot the sandbox started from (docs.boat.dev/readiness). */
5
+ export type BoatSandbox = {
6
+ id: string;
7
+ state: BoatSandboxState;
8
+ hydrated: boolean;
9
+ };
10
+ export type BoatCommandResult = {
11
+ success: boolean;
12
+ stdout: string;
13
+ stderr: string;
14
+ exitCode: number | null;
15
+ timedOut: boolean;
16
+ };
17
+ export type BoatFetch = (input: string, init?: RequestInit) => Promise<Response>;
18
+ /**
19
+ * What Boat's 429 `rate_limited` says about the account's start limit: the
20
+ * window that is used up, its limit, and the starts used of each window. It
21
+ * names no time the window reopens (2026-10-15), so none is kept.
22
+ */
23
+ export type BoatStartLimit = {
24
+ window: SandboxStartWindow;
25
+ limit: number;
26
+ starts: Partial<Record<SandboxStartWindow, {
27
+ limit: number;
28
+ used: number;
29
+ }>>;
30
+ };
31
+ export declare class BoatApiError extends Error {
32
+ readonly code: string;
33
+ readonly status?: number | undefined;
34
+ /** Boat answers 401 or 403 to the API key itself, never to a sandbox it was asked about. */
35
+ readonly providerKeyRefused: boolean;
36
+ /** Create, fork and resume each count as one start (Boat's own refusal, 2026-10-15). */
37
+ readonly providerRateLimited?: SandboxStartLimit;
38
+ readonly startLimit?: BoatStartLimit;
39
+ /** The state Boat's refusal gives the sandbox it refused, e.g. `archived` on a 400 `stop_failed` (2026-10-17). */
40
+ readonly sandboxState?: BoatSandboxState;
41
+ constructor(message: string, code: string, status?: number | undefined, refusal?: {
42
+ startLimit?: BoatStartLimit;
43
+ sandboxState?: BoatSandboxState;
44
+ });
45
+ }
46
+ export declare function invalid(): never;
47
+ export declare function requiredText(value: unknown): string;
48
+ export type BoatClientOptions = {
49
+ apiKey: string;
50
+ baseUrl?: string;
51
+ timeoutMs?: number;
52
+ fetchImpl?: BoatFetch;
53
+ };
54
+ export type BoatApi = ReturnType<typeof createBoatApi>;
55
+ /** One Boat v1 call: the bearer key, the deadline, the bounded envelope, and the envelope types the call accepts. */
56
+ export declare function createBoatApi(options: BoatClientOptions): (path: string, types: readonly string[], init?: {
57
+ method?: string;
58
+ body?: unknown;
59
+ headers?: Record<string, string>;
60
+ timeoutMs?: number;
61
+ }) => Promise<Record<string, unknown>>;
62
+ export declare function createBoatClient(options: BoatClientOptions): {
63
+ /** `from` is a named snapshot or a share code (`bsh_…`) the sandbox starts from; omitted, Boat's base machine. */
64
+ create(input: {
65
+ idempotencyKey: string;
66
+ ttlSeconds: number | null;
67
+ type?: SandboxMachineClass;
68
+ from?: string;
69
+ }): Promise<BoatSandbox>;
70
+ get(id: string): Promise<BoatSandbox>;
71
+ /** `type` resumes the sandbox onto that machine size; omitted, it keeps the size it has (docs.boat.dev/machines/change-size). */
72
+ resume(id: string, input: {
73
+ ttlSeconds: number | null;
74
+ type?: SandboxMachineClass;
75
+ }): Promise<void>;
76
+ /**
77
+ * `ttlSeconds` sets the auto-stop deadline to now plus that many seconds; `null` removes it.
78
+ * Boat's reference example answers `sandbox.info`; the API answers `sandbox.updated` (2026-10-13).
79
+ */
80
+ update(id: string, input: {
81
+ ttlSeconds: number | null;
82
+ }): Promise<BoatSandbox>;
83
+ stop(id: string): Promise<void>;
84
+ delete(id: string): Promise<void>;
85
+ writeFile(id: string, input: {
86
+ path: string;
87
+ content: string;
88
+ }): Promise<void>;
89
+ /** `ready` once a command runs, which Boat answers the moment the sandbox is ready; `starting` when Boat's hold ended first. */
90
+ whenReady(id: string): Promise<"ready" | "starting">;
91
+ command(id: string, input: {
92
+ command: string;
93
+ timeoutSeconds?: number;
94
+ }): Promise<BoatCommandResult>;
95
+ };
@@ -0,0 +1,31 @@
1
+ import { type BoatClientOptions } from "./boat-client";
2
+ export type BoatNamedSnapshotStatus = "saving" | "ready" | "failed";
3
+ export type BoatNamedSnapshot = {
4
+ name: string;
5
+ status: BoatNamedSnapshotStatus;
6
+ createdAt: string;
7
+ };
8
+ export type BoatSnapshotShare = {
9
+ id: string;
10
+ status: "active" | "revoked";
11
+ createdAt: string;
12
+ };
13
+ /** Boat's named snapshots and their share codes (docs.boat.dev/snapshots/templates). */
14
+ export declare function createBoatNamedSnapshotClient(options: BoatClientOptions): {
15
+ /** A stopped sandbox is saved from its last snapshot; the save settles in the background. */
16
+ save(input: {
17
+ sandboxId: string;
18
+ name: string;
19
+ }): Promise<BoatNamedSnapshot>;
20
+ get(name: string): Promise<BoatNamedSnapshot | undefined>;
21
+ /** Newest first. */
22
+ list(): Promise<BoatNamedSnapshot[]>;
23
+ delete(name: string): Promise<void>;
24
+ /** Boat shows the code only in this answer and keeps a hash of it. */
25
+ share(name: string): Promise<{
26
+ shareCode: string;
27
+ share: BoatSnapshotShare;
28
+ }>;
29
+ shares(name: string): Promise<BoatSnapshotShare[]>;
30
+ revokeShare(name: string, shareId: string): Promise<void>;
31
+ };
@@ -0,0 +1,49 @@
1
+ import { type BoatRuntimeTemplate, type BoatSandboxDriverOptions } from "./boat";
2
+ import { type BoatClientOptions } from "./boat-client";
3
+ export type BoatRuntimeTemplateOptions = BoatClientOptions & {
4
+ image: string;
5
+ /** Template names start with it, and pruning removes only names that do. */
6
+ prefix?: string;
7
+ /** How many templates of the prefix pruning keeps, the image's own and the newest others. */
8
+ keep?: number;
9
+ log?: (line: string) => void;
10
+ pollIntervalMs?: number;
11
+ hydrationTimeoutMs?: number;
12
+ stopTimeoutMs?: number;
13
+ saveTimeoutMs?: number;
14
+ driver?: Pick<BoatSandboxDriverOptions, "statePollMs" | "provisionTimeoutMs" | "healthIntervalMs" | "healthTimeoutMs">;
15
+ };
16
+ /** The image reference is content-addressed (its tag names the build), and the seeded trees are named by it, so it keys the template. */
17
+ export declare function boatRuntimeTemplateName(image: string, prefix?: string): Promise<string>;
18
+ /**
19
+ * Saves the template once per image: a ready template is reused, one still
20
+ * saving is waited for, and a missing or failed one is built on a builder
21
+ * sandbox that is deleted whatever the outcome.
22
+ */
23
+ export declare function ensureBoatRuntimeTemplate(options: BoatRuntimeTemplateOptions): Promise<{
24
+ name: string;
25
+ built: boolean;
26
+ }>;
27
+ /**
28
+ * A new share code for the image's template, revoking none: until the deploy
29
+ * that carries it has succeeded, the running Worker creates with the code it
30
+ * holds. `shareId` names the code without being the secret.
31
+ */
32
+ export declare function shareBoatRuntimeTemplate(options: BoatRuntimeTemplateOptions): Promise<{
33
+ template: BoatRuntimeTemplate;
34
+ shareId: string;
35
+ }>;
36
+ /**
37
+ * Run once the deploy carrying `deployedShareId` succeeded: every other code of
38
+ * the deployed image's template is revoked, those of earlier deploys and of
39
+ * deploys that failed alike.
40
+ */
41
+ export declare function revokeBoatRuntimeTemplateShares(options: BoatRuntimeTemplateOptions, deployedShareId: string): Promise<string[]>;
42
+ /**
43
+ * Run once the deploy of `options.image` succeeded, so the template it keeps
44
+ * is the one the live Worker creates from. Each template past Boat's ten free
45
+ * named snapshots costs $1.70 a month, and a template serves only the image it
46
+ * was built for, so templates of the prefix beyond the newest `keep` (the
47
+ * deployed image's among them) are removed; Boat removes their codes with them.
48
+ */
49
+ export declare function pruneBoatRuntimeTemplates(options: BoatRuntimeTemplateOptions): Promise<string[]>;