@claxedo/sandbox-manager 0.7.0 → 0.10.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.
Files changed (70) hide show
  1. package/README.md +245 -42
  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 +43 -58
  9. package/dist/driver-catalog.mjs +192 -229
  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 +5 -8
  17. package/dist/drivers/cloudflare.mjs +364 -213
  18. package/dist/drivers/docker.d.ts +4 -3
  19. package/dist/drivers/docker.mjs +244 -165
  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 +4 -7
  23. package/dist/drivers/modal.mjs +220 -155
  24. package/dist/drivers/transient-error.d.ts +13 -0
  25. package/dist/drivers/vercel.d.ts +21 -23
  26. package/dist/drivers/vercel.mjs +279 -169
  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 +11 -4
  31. package/dist/hosted-network-policy.mjs +221 -4
  32. package/dist/image-name.d.ts +12 -2
  33. package/dist/image-name.mjs +43 -0
  34. package/dist/image.d.ts +1 -20
  35. package/dist/image.mjs +11 -66
  36. package/dist/index.d.ts +8 -533
  37. package/dist/index.mjs +1420 -315
  38. package/dist/json.d.ts +13 -0
  39. package/dist/lease-failure.d.ts +173 -0
  40. package/dist/lease-lifecycle.d.ts +65 -0
  41. package/dist/lease-retry-wait.d.ts +8 -0
  42. package/dist/lease-start.d.ts +21 -0
  43. package/dist/lease-types.d.ts +15 -5
  44. package/dist/manager.d.ts +2 -0
  45. package/dist/runtime-env.d.ts +15 -2
  46. package/dist/runtime-env.mjs +50 -1
  47. package/dist/runtime-snapshot.d.ts +49 -0
  48. package/dist/runtime-version.d.ts +9 -0
  49. package/dist/runtime-version.mjs +2 -0
  50. package/dist/service-url-exposure.d.ts +1 -3
  51. package/dist/service-url-exposure.mjs +1 -26
  52. package/dist/start-fresh.d.ts +17 -0
  53. package/dist/start-telemetry.d.ts +37 -0
  54. package/dist/stores/memory.d.ts +1 -1
  55. package/dist/stores/memory.mjs +65 -54
  56. package/package.json +67 -81
  57. package/dist/daytona-allow-list.d.ts +0 -16
  58. package/dist/daytona-allow-list.mjs +0 -11
  59. package/dist/drivers/box.d.ts +0 -52
  60. package/dist/drivers/box.mjs +0 -499
  61. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  62. package/dist/drivers/cloudflare-egress.mjs +0 -113
  63. package/dist/drivers/daytona.d.ts +0 -127
  64. package/dist/drivers/daytona.mjs +0 -541
  65. package/dist/drivers/exe.d.ts +0 -26
  66. package/dist/drivers/exe.mjs +0 -469
  67. package/dist/drivers/fetch-bridge.d.ts +0 -10
  68. package/dist/drivers/fetch-bridge.mjs +0 -111
  69. package/dist/lease-policy.d.ts +0 -48
  70. package/dist/lease-policy.mjs +0 -280
@@ -1,17 +1,18 @@
1
- import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
1
+ import type { SandboxDriver, SandboxDriverEnsureInput } from "../contract";
2
2
  type SandboxEnv = Record<string, string | undefined>;
3
3
  export type DockerCommandResult = {
4
4
  stdout?: string | Buffer;
5
5
  stderr?: string | Buffer;
6
6
  };
7
- export type DockerCommandRunner = (args: string[], timeoutMs?: number) => Promise<DockerCommandResult>;
7
+ /** `env` joins the docker CLI's own environment; a `--env NAME` argument with no value reads NAME from it. */
8
+ export type DockerCommandRunner = (args: string[], timeoutMs?: number, env?: Record<string, string>) => Promise<DockerCommandResult>;
8
9
  export type DockerHealthFetch = (input: string, init?: RequestInit) => Promise<Response>;
9
10
  export type DockerSandboxDriverOptions = {
10
11
  image: string;
11
12
  runtimePort?: number;
12
13
  runtimeCommand?: string;
13
14
  workspaceDir?: string;
14
- runner?: string;
15
+ nativeHarness?: string;
15
16
  env?: (input: SandboxDriverEnsureInput, host: {
16
17
  id: string;
17
18
  }) => Record<string, string> | Promise<Record<string, string>>;
@@ -5,7 +5,61 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { promisify } from "node:util";
7
7
 
8
+ // ../claxedo-helpers/dist/string.mjs
9
+ function isString(value) {
10
+ return typeof value === "string";
11
+ }
12
+ function trimToUndefined(value) {
13
+ if (!isString(value))
14
+ return;
15
+ const trimmed = value.trim();
16
+ return trimmed.length > 0 ? trimmed : void 0;
17
+ }
18
+
8
19
  // src/runtime-env.ts
20
+ var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
21
+ "WORKSPACE_RUNTIME_WORKSPACE_ID",
22
+ "WORKSPACE_RUNTIME_HOST_ID",
23
+ "WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
24
+ ];
25
+ function workspaceRuntimeIdentityEnvConflicts(env) {
26
+ if (!env) return [];
27
+ return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
28
+ }
29
+ function assertWorkspaceRuntimeIdentityEnv(env) {
30
+ const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
31
+ if (conflicts.length === 0) return;
32
+ throw new Error(
33
+ `sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
34
+ );
35
+ }
36
+ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
37
+ ...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
38
+ "WORKSPACE_RUNTIME_DIRECTORY",
39
+ "WORKSPACE_RUNTIME_PORT",
40
+ "WORKSPACE_RUNTIME_HOST",
41
+ "WORKSPACE_RUNTIME_SOURCE_KIND",
42
+ "WORKSPACE_RUNTIME_GIT_REPO_URL",
43
+ "WORKSPACE_RUNTIME_GIT_BRANCH",
44
+ "WORKSPACE_RUNTIME_NATIVE_HARNESS",
45
+ "WORKSPACE_RUNTIME_RELAY_JWKS_URL",
46
+ "WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
47
+ "WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
48
+ ]);
49
+ function workspaceRuntimeBootEnv(input) {
50
+ assertWorkspaceRuntimeIdentityEnv(input.env);
51
+ const env = {
52
+ ...workspaceRuntimeTargetEnv(input),
53
+ ...workspaceRuntimeSourceEnv(input),
54
+ ...input.env
55
+ };
56
+ if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
57
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
58
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
59
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
60
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
61
+ return env;
62
+ }
9
63
  function workspaceRuntimeTargetEnv(input) {
10
64
  return {
11
65
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -32,9 +86,6 @@ function shell(input) {
32
86
  // src/constants.ts
33
87
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
34
88
 
35
- // src/image.ts
36
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
37
-
38
89
  // src/runtime-version.ts
39
90
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
40
91
  function workspaceRuntimeVersion() {
@@ -47,6 +98,12 @@ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
47
98
  function snapshotVersion(value) {
48
99
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
49
100
  }
101
+ function assertSandboxImageReference(image) {
102
+ if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
103
+ throw new Error(`sandbox image reference is not a legal image identifier: ${JSON.stringify(image)}`);
104
+ }
105
+ return image;
106
+ }
50
107
  function sandboxImageRepository(env = process.env) {
51
108
  const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
52
109
  return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
@@ -55,107 +112,87 @@ function buildIdSuffix(buildId, env) {
55
112
  const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
56
113
  return id ? `-${snapshotVersion(id)}` : "";
57
114
  }
58
- function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
59
- return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
60
- }
61
- function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
62
- return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
115
+ var AGENT_PLUGINS_TAG = "-agent-plugins";
116
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env, options = {}) {
117
+ const feature = options.agentPlugins ? AGENT_PLUGINS_TAG : "";
118
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}${feature}-v${SNAPSHOT_SCHEMA_VERSION}`;
63
119
  }
64
120
 
65
121
  // src/image.ts
66
122
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
67
123
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
68
- var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
69
124
 
70
- // src/hosted-network-policy.ts
71
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
72
- "api.anthropic.com",
73
- "api.openai.com",
74
- "generativelanguage.googleapis.com",
75
- "openrouter.ai",
76
- // Model catalog + engine config, fetched on runner startup.
77
- "models.dev",
78
- "opencode.ai"
79
- ];
80
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
81
- "registry.npmjs.org",
82
- "nodejs.org",
83
- "pypi.org",
84
- "files.pythonhosted.org",
85
- "crates.io",
86
- "static.crates.io",
87
- "index.crates.io",
88
- "proxy.golang.org",
89
- "sum.golang.org"
90
- ];
91
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
92
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
93
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
94
- ];
125
+ // src/driver-billing.ts
126
+ var HOUR_SECONDS = 3600;
127
+ var boatBilling = {
128
+ source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
129
+ meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
130
+ rates: {
131
+ small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
132
+ default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
133
+ large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
134
+ }
135
+ };
136
+ var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
137
+ var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
138
+ var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
139
+ var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
140
+ var cloudflareBilling = {
141
+ source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
142
+ 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.",
143
+ rates: {
144
+ default: {
145
+ size: CLOUDFLARE_STANDARD_1.size,
146
+ vcpu: CLOUDFLARE_STANDARD_1.vcpu,
147
+ memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
148
+ usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
149
+ }
150
+ },
151
+ 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`
152
+ };
153
+
154
+ // ../sandbox-contract/dist/index.mjs
155
+ var sandboxDriverCredentialFields = {
156
+ modal: [
157
+ { key: "token_id", label: "Token ID", secret: true },
158
+ { key: "token_secret", label: "Token Secret", secret: true }
159
+ ],
160
+ vercel: [
161
+ { key: "access_token", label: "Access Token", secret: true },
162
+ { key: "team_id", label: "Team ID" },
163
+ { key: "project_id", label: "Project ID" }
164
+ ],
165
+ cloudflare: [
166
+ { key: "api_token", label: "API Token", secret: true },
167
+ { key: "worker_url", label: "Worker URL" }
168
+ ],
169
+ boat: [{ key: "api_key", label: "API Key", secret: true }],
170
+ docker: [{ key: "image", label: "Image" }]
171
+ };
172
+ var sandboxDriverLabels = {
173
+ modal: "Modal",
174
+ vercel: "Vercel",
175
+ cloudflare: "Cloudflare",
176
+ boat: "Boat",
177
+ docker: "Docker"
178
+ };
95
179
 
96
180
  // src/driver-catalog.ts
97
181
  var sandboxDriverCatalog = {
98
- exe: {
99
- id: "exe",
100
- label: "exe.dev",
101
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
102
- metadata: {
103
- driverRunsIn: ["worker", "node"],
104
- hostStopBehavior: "not-supported",
105
- hostResumeBehavior: "same-host",
106
- targetAccess: "relay",
107
- secretBrokering: "none",
108
- egressControl: "none",
109
- persistence: {
110
- resume: "same-sandbox",
111
- capture: "none",
112
- clone: true,
113
- captureSource: "not-applicable",
114
- retention: "provider-managed",
115
- restoreMount: "same-resource"
116
- }
117
- }
118
- },
119
- daytona: {
120
- id: "daytona",
121
- label: "Daytona",
122
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
123
- metadata: {
124
- driverRunsIn: ["worker", "node"],
125
- hostStopBehavior: "suspends-host",
126
- hostResumeBehavior: "same-host",
127
- targetAccess: "relay",
128
- secretBrokering: "native",
129
- // Daytona is the only driver that can filter egress by NAME as well as
130
- // by address: `domainAllowList` (names) alongside `networkAllowList`
131
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
132
- egressControl: "hosts-and-cidrs",
133
- persistence: {
134
- resume: "same-sandbox",
135
- capture: "filesystem",
136
- clone: false,
137
- captureSource: "preserved",
138
- retention: "provider-managed",
139
- restoreMount: "new-resource"
140
- }
141
- }
142
- },
143
182
  modal: {
144
183
  id: "modal",
145
- label: "Modal",
146
- credentialFields: [
147
- { key: "token_id", label: "Token ID", secret: true },
148
- { key: "token_secret", label: "Token Secret", secret: true }
149
- ],
184
+ label: sandboxDriverLabels.modal,
185
+ credentialFields: sandboxDriverCredentialFields.modal,
186
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
187
+ runtimeEnv: "callback",
188
+ preparedImage: true,
150
189
  metadata: {
151
190
  driverRunsIn: ["node"],
152
191
  hostStopBehavior: "terminates-host",
153
192
  hostResumeBehavior: "replacement-host",
154
193
  targetAccess: "relay",
155
194
  secretBrokering: "none",
156
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
157
- // an allowlist, and its driver throws on a host policy. A blackout is
158
- // not containment for a workspace that has to clone and reach a model.
195
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
159
196
  egressControl: "none",
160
197
  persistence: {
161
198
  resume: "replacement-restore",
@@ -169,20 +206,18 @@ var sandboxDriverCatalog = {
169
206
  },
170
207
  vercel: {
171
208
  id: "vercel",
172
- label: "Vercel",
173
- credentialFields: [
174
- { key: "access_token", label: "Access Token", secret: true },
175
- { key: "team_id", label: "Team ID" },
176
- { key: "project_id", label: "Project ID" }
177
- ],
209
+ label: sandboxDriverLabels.vercel,
210
+ credentialFields: sandboxDriverCredentialFields.vercel,
211
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
212
+ runtimeEnv: "callback",
213
+ preparedImage: false,
178
214
  metadata: {
179
215
  driverRunsIn: ["node"],
180
216
  hostStopBehavior: "terminates-host",
181
217
  hostResumeBehavior: "replacement-host",
182
218
  targetAccess: "relay",
183
219
  secretBrokering: "native",
184
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
185
- // never CIDRs.
220
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
186
221
  egressControl: "hosts",
187
222
  persistence: {
188
223
  resume: "replacement-restore",
@@ -196,40 +231,42 @@ var sandboxDriverCatalog = {
196
231
  },
197
232
  cloudflare: {
198
233
  id: "cloudflare",
199
- label: "Cloudflare",
200
- credentialFields: [
201
- { key: "api_token", label: "API Token", secret: true },
202
- { key: "worker_url", label: "Worker URL" }
203
- ],
234
+ label: sandboxDriverLabels.cloudflare,
235
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
236
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
237
+ runtimeEnv: "callback",
238
+ preparedImage: false,
239
+ billing: cloudflareBilling,
204
240
  metadata: {
205
241
  driverRunsIn: ["worker"],
206
- hostStopBehavior: "not-supported",
242
+ hostStopBehavior: "terminates-host",
207
243
  hostResumeBehavior: "same-host",
244
+ hostWatchesIdle: true,
208
245
  targetAccess: "relay",
209
- secretBrokering: "proxy",
210
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
211
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
212
- // to route brokered-credential requests through, not a boundary. The
213
- // driver therefore drops `net` entirely — which is exactly why this
214
- // must be declared, so the manager refuses instead of provisioning a
215
- // sandbox the caller believes is contained.
246
+ secretBrokering: "native",
247
+ // Credential handlers do not restrict unrelated destinations.
216
248
  egressControl: "none",
249
+ // A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
217
250
  persistence: {
218
251
  resume: "replacement-restore",
219
- capture: "directories",
252
+ capture: "filesystem",
220
253
  clone: false,
221
254
  captureSource: "preserved",
222
255
  retention: "provider-managed",
223
- restoreMount: "copy-on-write"
256
+ restoreMount: "new-resource"
224
257
  }
225
258
  }
226
259
  },
227
- box: {
228
- id: "box",
229
- label: "Box",
230
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
260
+ boat: {
261
+ id: "boat",
262
+ label: sandboxDriverLabels.boat,
263
+ credentialFields: sandboxDriverCredentialFields.boat,
264
+ runtimeNetwork: { controlPlane: "direct", relay: "configured" },
265
+ runtimeEnv: "callback",
266
+ preparedImage: true,
267
+ billing: boatBilling,
231
268
  metadata: {
232
- driverRunsIn: ["node"],
269
+ driverRunsIn: ["worker", "node"],
233
270
  hostStopBehavior: "suspends-host",
234
271
  hostResumeBehavior: "same-host",
235
272
  targetAccess: "relay",
@@ -242,13 +279,17 @@ var sandboxDriverCatalog = {
242
279
  captureSource: "not-applicable",
243
280
  retention: "provider-managed",
244
281
  restoreMount: "same-resource"
245
- }
282
+ },
283
+ machineClasses: ["small", "default", "large"]
246
284
  }
247
285
  },
248
286
  docker: {
249
287
  id: "docker",
250
- label: "Docker",
251
- credentialFields: [{ key: "image", label: "Image" }],
288
+ label: sandboxDriverLabels.docker,
289
+ credentialFields: sandboxDriverCredentialFields.docker,
290
+ runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
291
+ runtimeEnv: "callback",
292
+ preparedImage: true,
252
293
  metadata: {
253
294
  driverRunsIn: ["local"],
254
295
  hostStopBehavior: "terminates-host",
@@ -268,6 +309,48 @@ var sandboxDriverCatalog = {
268
309
  }
269
310
  };
270
311
 
312
+ // ../claxedo-helpers/dist/guards.mjs
313
+ function isRecord(value) {
314
+ return value !== null && typeof value === "object" && !Array.isArray(value);
315
+ }
316
+
317
+ // src/json.ts
318
+ function num(input) {
319
+ return typeof input === "number" ? input : void 0;
320
+ }
321
+
322
+ // src/drivers/transient-error.ts
323
+ function textOf(input) {
324
+ return typeof input === "string" ? input : "";
325
+ }
326
+ function driverErrorSignals(err) {
327
+ const shaped = isRecord(err) ? err : {};
328
+ const nested = isRecord(shaped.response) ? shaped.response : {};
329
+ return {
330
+ // `response.status` is the axios shape; `statusCode` is what SDK-native
331
+ // error classes expose instead.
332
+ status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
333
+ text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
334
+ };
335
+ }
336
+ function isTransientDriverError(err, markers) {
337
+ const { status, text } = driverErrorSignals(err);
338
+ if (status !== void 0 && status >= 500) return true;
339
+ return markers.some((marker) => text.includes(marker));
340
+ }
341
+ var TRANSIENT_DRIVER_RETRY_MS = 2e3;
342
+
343
+ // ../claxedo-helpers/dist/deadline.mjs
344
+ async function withDeadlineSignal(timeoutMs, work, parent) {
345
+ const deadline = new AbortController();
346
+ const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
347
+ try {
348
+ return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
349
+ } finally {
350
+ clearTimeout(timer);
351
+ }
352
+ }
353
+
271
354
  // src/drivers/docker.ts
272
355
  var execFileAsync = promisify(execFile);
273
356
  var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
@@ -278,13 +361,13 @@ var DEFAULT_WORKSPACE_DIR = "/workspace";
278
361
  var DOCKER_LABEL = "claxedo";
279
362
  var AUTH_STAGE_DIR = "/tmp/claxedo-auth";
280
363
  function dockerSandboxSyncLocalAuth(env = process.env) {
281
- return !disabled(env.CLAXEDO_DOCKER_SANDBOX_SYNC_LOCAL_AUTH);
364
+ return enabledFlag(env.CLAXEDO_DOCKER_SANDBOX_SYNC_LOCAL_AUTH);
282
365
  }
283
366
  function dockerSandboxAuthHome(env = process.env) {
284
- return clean(env.CLAXEDO_DOCKER_SANDBOX_AUTH_HOME) ?? clean(env.HOME) ?? os.homedir();
367
+ return trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_AUTH_HOME) ?? trimToUndefined(env.HOME) ?? os.homedir();
285
368
  }
286
369
  function dockerSandboxImageFromEnv(env = process.env) {
287
- return clean(env.CLAXEDO_DOCKER_SANDBOX_IMAGE) ?? clean(env.CLAXEDO_SANDBOX_IMAGE) ?? SANDBOX_IMAGE;
370
+ return trimToUndefined(env.CLAXEDO_DOCKER_SANDBOX_IMAGE) ?? trimToUndefined(env.CLAXEDO_SANDBOX_IMAGE) ?? SANDBOX_IMAGE;
288
371
  }
289
372
  function parseDockerPort(input) {
290
373
  const line = input.trim().split(/\r?\n/)[0]?.trim();
@@ -315,21 +398,18 @@ function dockerLocalAuthCandidates(home) {
315
398
  }
316
399
  ];
317
400
  }
318
- function clean(input) {
319
- const txt = input?.trim();
320
- return txt ? txt : void 0;
321
- }
322
- function disabled(input) {
323
- return ["0", "false", "no", "off"].includes(input?.trim().toLowerCase() ?? "");
401
+ function enabledFlag(input) {
402
+ return ["1", "true", "yes", "on"].includes(input?.trim().toLowerCase() ?? "");
324
403
  }
325
404
  function nameFor(workspaceId) {
326
405
  const cleanId = workspaceId.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
327
406
  return `claxedo-${cleanId || "workspace"}-${Date.now().toString(36)}`.slice(0, 63);
328
407
  }
329
- async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS) {
408
+ async function docker(args, timeoutMs = DEFAULT_OPERATION_TIMEOUT_MS, env) {
330
409
  return execFileAsync("docker", args, {
331
410
  maxBuffer: 20 * 1024 * 1024,
332
- timeout: timeoutMs
411
+ timeout: timeoutMs,
412
+ ...env ? { env: { ...process.env, ...env } } : {}
333
413
  });
334
414
  }
335
415
  function sleep(ms) {
@@ -340,22 +420,25 @@ async function stageLocalAgentAuth(containerId, input) {
340
420
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "claxedo-docker-auth-"));
341
421
  try {
342
422
  const stagedRoot = path.join(tmp, "root");
343
- const copied = await Promise.all(dockerLocalAuthCandidates(input.authHome).map(async (item) => {
344
- const stat = await fs.stat(item.source).catch(() => void 0);
345
- if (!stat) return false;
346
- if (item.kind === "file" && !stat.isFile()) return false;
347
- if (item.kind === "dir" && !stat.isDirectory()) return false;
423
+ let copied = false;
424
+ for (const item of dockerLocalAuthCandidates(input.authHome)) {
425
+ const stat = await fs.stat(item.source).catch((error) => {
426
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
427
+ throw error;
428
+ });
429
+ if (!stat) continue;
430
+ if (item.kind === "file" && !stat.isFile()) continue;
431
+ if (item.kind === "dir" && !stat.isDirectory()) continue;
348
432
  const target = path.join(stagedRoot, item.target.replace(/^\/root\/?/, ""));
349
433
  await fs.mkdir(path.dirname(target), { recursive: true });
350
434
  if (item.kind === "dir") await fs.cp(item.source, target, { recursive: true });
351
435
  else await fs.copyFile(item.source, target);
352
- return true;
353
- }));
354
- if (!copied.some(Boolean)) return;
436
+ copied = true;
437
+ }
438
+ if (!copied) return;
355
439
  await input.docker(["cp", stagedRoot, `${containerId}:${AUTH_STAGE_DIR}`], input.timeoutMs);
356
440
  } finally {
357
- await fs.rm(tmp, { recursive: true, force: true }).catch(() => {
358
- });
441
+ await fs.rm(tmp, { recursive: true, force: true });
359
442
  }
360
443
  }
361
444
  function authHydrationScript() {
@@ -377,16 +460,15 @@ function bootScript(directory, runtimeCommand) {
377
460
  `exec ${runtimeCommand}`
378
461
  ].join(" && ");
379
462
  }
380
- function envArgs(input) {
381
- return Object.entries(input).flatMap(([key, value]) => ["--env", `${key}=${value}`]);
463
+ function envNameArgs(input) {
464
+ return Object.keys(input).flatMap((key) => ["--env", key]);
382
465
  }
383
466
  function labelArgs(input) {
384
467
  return Object.entries(input).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
385
468
  }
469
+ var TRANSIENT_MARKERS = ["timeout", "temporarily unavailable", "connection refused"];
386
470
  function transientDriverError(err) {
387
- const shaped = err;
388
- const text = `${shaped.code ?? ""} ${shaped.message ?? ""}`.toLowerCase();
389
- return text.includes("timeout") || text.includes("temporarily unavailable") || text.includes("connection refused");
471
+ return isTransientDriverError(err, TRANSIENT_MARKERS);
390
472
  }
391
473
  function createDockerSandboxDriver(options) {
392
474
  const dockerCommand = options.docker ?? docker;
@@ -408,22 +490,21 @@ function createDockerSandboxDriver(options) {
408
490
  if (input.bootSource?.kind === "driver-snapshot") {
409
491
  throw new Error("Docker SandboxDriver does not support driver snapshots; use image boot");
410
492
  }
411
- return input.bootSource?.kind === "image" ? input.bootSource.image : input.snapshot ?? options.image;
493
+ return assertSandboxImageReference(
494
+ input.bootSource?.kind === "image" ? input.bootSource.image : input.snapshot ?? options.image
495
+ );
412
496
  }
413
497
  function bootEnv(input, hostId) {
414
- const env = {
415
- ...workspaceRuntimeTargetEnv({
416
- workspaceId: input.workspaceId,
417
- hostId,
418
- directory: workspaceDirectory(input),
419
- port: runtimePort(input),
420
- host: "0.0.0.0"
421
- }),
422
- ...workspaceRuntimeSourceEnv({ source: input.source }),
423
- ...input.env
424
- };
425
- if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
426
- return env;
498
+ return workspaceRuntimeBootEnv({
499
+ workspaceId: input.workspaceId,
500
+ hostId,
501
+ directory: workspaceDirectory(input),
502
+ port: runtimePort(input),
503
+ host: "0.0.0.0",
504
+ source: input.source,
505
+ env: input.env,
506
+ nativeHarness: options.nativeHarness
507
+ });
427
508
  }
428
509
  async function readyTarget(input, sandboxId, hostId) {
429
510
  const out = await dockerCommand(["port", sandboxId, `${runtimePort(input)}/tcp`], timeoutMs);
@@ -448,9 +529,7 @@ function createDockerSandboxDriver(options) {
448
529
  let error = "workspace runtime not ready";
449
530
  while (Date.now() < until) {
450
531
  try {
451
- const res = await healthFetch(`${url}/global/health`, {
452
- signal: AbortSignal.timeout(Math.min(2e3, Math.max(1, healthTimeoutMs)))
453
- });
532
+ const res = await withDeadlineSignal(Math.min(2e3, Math.max(1, healthTimeoutMs)), (signal) => healthFetch(`${url}/global/health`, { signal }));
454
533
  if (res.ok) return;
455
534
  error = `${res.status} ${res.statusText}`;
456
535
  } catch (err) {
@@ -480,17 +559,17 @@ function createDockerSandboxDriver(options) {
480
559
  ...labelArgs(input.labels),
481
560
  "-p",
482
561
  `127.0.0.1::${runtimePort(input)}`,
483
- ...envArgs(env),
562
+ ...envNameArgs(env),
484
563
  "--entrypoint",
485
564
  "sh",
486
565
  image(input),
487
566
  "-lc",
488
567
  bootScript(workspaceDirectory(input), runtimeCommand)
489
- ], 12e4).catch((err) => {
568
+ ], 12e4, env).catch((err) => {
490
569
  if (transientDriverError(err)) return void 0;
491
570
  throw err;
492
571
  });
493
- if (!created) return { provisioning: true, retryAfterMs: 2e3 };
572
+ if (!created) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
494
573
  const sandboxId = String(created.stdout ?? "").trim() || hostId;
495
574
  await stageLocalAgentAuth(sandboxId, { authHome, syncLocalAuth, docker: dockerCommand, timeoutMs });
496
575
  await dockerCommand(["start", sandboxId], timeoutMs);
@@ -499,7 +578,7 @@ function createDockerSandboxDriver(options) {
499
578
  async function resumeHost(input) {
500
579
  const sandboxId = input.lease.sandboxId ?? input.lease.sandbox_id;
501
580
  if (!sandboxId) return ensureHost(input.ensure);
502
- await dockerCommand(["inspect", "-f", "{{.Id}}", sandboxId], timeoutMs).catch(() => void 0);
581
+ await dockerCommand(["inspect", "-f", "{{.Id}}", sandboxId], timeoutMs);
503
582
  await dockerCommand(["start", sandboxId], timeoutMs).catch((err) => {
504
583
  if (transientDriverError(err)) return void 0;
505
584
  throw err;
@@ -0,0 +1,14 @@
1
+ import type { SandboxDriver } from "../contract";
2
+ import { localBrokeringTestDriverCatalogEntry } from "../driver-catalog";
3
+ export type LocalBrokeringDriverOptions = {
4
+ root: string;
5
+ executable: string;
6
+ args: string[];
7
+ allowedOrigins: readonly string[];
8
+ directOrigins: readonly string[];
9
+ upstreams?: Readonly<Record<string, string>>;
10
+ inheritedEnv?: Record<string, string>;
11
+ };
12
+ export declare function createLocalBrokeringSandboxDriver(options: LocalBrokeringDriverOptions): SandboxDriver & {
13
+ catalogEntry: typeof localBrokeringTestDriverCatalogEntry;
14
+ };