@claxedo/sandbox-manager 0.9.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 (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
@@ -1,574 +0,0 @@
1
- // ../claxedo-helpers/dist/string.mjs
2
- function isString(value) {
3
- return typeof value === "string";
4
- }
5
- function trimToUndefined(value) {
6
- if (!isString(value))
7
- return;
8
- const trimmed = value.trim();
9
- return trimmed.length > 0 ? trimmed : void 0;
10
- }
11
-
12
- // src/runtime-env.ts
13
- function workspaceRuntimeBootEnv(input) {
14
- const env = {
15
- ...workspaceRuntimeTargetEnv(input),
16
- ...workspaceRuntimeSourceEnv(input),
17
- ...input.env
18
- };
19
- if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
20
- if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
21
- if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
22
- if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
23
- if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
24
- return env;
25
- }
26
- function workspaceRuntimeTargetEnv(input) {
27
- return {
28
- WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
29
- ...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
30
- WORKSPACE_RUNTIME_DIRECTORY: input.directory,
31
- WORKSPACE_RUNTIME_PORT: String(input.port),
32
- ...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
33
- };
34
- }
35
- function workspaceRuntimeSourceEnv(input) {
36
- if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
37
- return {
38
- WORKSPACE_RUNTIME_SOURCE_KIND: "git",
39
- WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
40
- ...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
41
- };
42
- }
43
-
44
- // src/command.ts
45
- function shell(input) {
46
- return `'${input.replace(/'/g, `'"'"'`)}'`;
47
- }
48
-
49
- // src/constants.ts
50
- var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
51
-
52
- // src/runtime-version.ts
53
- var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
54
- function workspaceRuntimeVersion() {
55
- return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
56
- }
57
-
58
- // src/image-name.ts
59
- var SNAPSHOT_SCHEMA_VERSION = 8;
60
- var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
61
- function snapshotVersion(value) {
62
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
63
- }
64
- function sandboxImageRepository(env = process.env) {
65
- const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
66
- return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
67
- }
68
- function buildIdSuffix(buildId, env) {
69
- const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
70
- return id ? `-${snapshotVersion(id)}` : "";
71
- }
72
- function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
73
- return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
74
- }
75
- function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
76
- return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
77
- }
78
-
79
- // ../claxedo-helpers/dist/guards.mjs
80
- function isRecord(value) {
81
- return value !== null && typeof value === "object" && !Array.isArray(value);
82
- }
83
- function asRecord(value) {
84
- return isRecord(value) ? value : void 0;
85
- }
86
-
87
- // src/json.ts
88
- function text(input) {
89
- return typeof input === "string" && input.length > 0 ? input : void 0;
90
- }
91
- function num(input) {
92
- return typeof input === "number" ? input : void 0;
93
- }
94
-
95
- // src/image.ts
96
- var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
97
- var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
98
- var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
99
-
100
- // src/hosted-network-policy.ts
101
- var SANDBOX_MODEL_PROVIDER_HOSTS = [
102
- "api.anthropic.com",
103
- "api.openai.com",
104
- "generativelanguage.googleapis.com",
105
- "openrouter.ai",
106
- // Model catalog + engine config, fetched on runner startup.
107
- "models.dev",
108
- "opencode.ai"
109
- ];
110
- var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
111
- "registry.npmjs.org",
112
- "nodejs.org",
113
- "pypi.org",
114
- "files.pythonhosted.org",
115
- "crates.io",
116
- "static.crates.io",
117
- "index.crates.io",
118
- "proxy.golang.org",
119
- "sum.golang.org"
120
- ];
121
- var SANDBOX_BASELINE_EGRESS_HOSTS = [
122
- ...SANDBOX_MODEL_PROVIDER_HOSTS,
123
- ...SANDBOX_PACKAGE_REGISTRY_HOSTS
124
- ];
125
-
126
- // ../sandbox-contract/dist/index.mjs
127
- var sandboxDriverCredentialFields = {
128
- exe: [{ key: "api_token", label: "API Token", secret: true }],
129
- daytona: [{ key: "api_key", label: "API Key", secret: true }],
130
- modal: [
131
- { key: "token_id", label: "Token ID", secret: true },
132
- { key: "token_secret", label: "Token Secret", secret: true }
133
- ],
134
- vercel: [
135
- { key: "access_token", label: "Access Token", secret: true },
136
- { key: "team_id", label: "Team ID" },
137
- { key: "project_id", label: "Project ID" }
138
- ],
139
- cloudflare: [
140
- { key: "api_token", label: "API Token", secret: true },
141
- { key: "worker_url", label: "Worker URL" }
142
- ],
143
- box: [{ key: "api_key", label: "API Key", secret: true }],
144
- docker: [{ key: "image", label: "Image" }]
145
- };
146
- var sandboxDriverLabels = {
147
- exe: "exe.dev",
148
- daytona: "Daytona",
149
- modal: "Modal",
150
- vercel: "Vercel",
151
- cloudflare: "Cloudflare",
152
- box: "Box",
153
- docker: "Docker"
154
- };
155
-
156
- // src/driver-catalog.ts
157
- var sandboxDriverCatalog = {
158
- exe: {
159
- id: "exe",
160
- label: sandboxDriverLabels.exe,
161
- credentialFields: sandboxDriverCredentialFields.exe,
162
- metadata: {
163
- driverRunsIn: ["worker", "node"],
164
- hostStopBehavior: "not-supported",
165
- hostResumeBehavior: "same-host",
166
- targetAccess: "relay",
167
- secretBrokering: "none",
168
- egressControl: "none",
169
- persistence: {
170
- resume: "same-sandbox",
171
- capture: "none",
172
- clone: true,
173
- captureSource: "not-applicable",
174
- retention: "provider-managed",
175
- restoreMount: "same-resource"
176
- }
177
- }
178
- },
179
- daytona: {
180
- id: "daytona",
181
- label: sandboxDriverLabels.daytona,
182
- credentialFields: sandboxDriverCredentialFields.daytona,
183
- metadata: {
184
- driverRunsIn: ["worker", "node"],
185
- hostStopBehavior: "suspends-host",
186
- hostResumeBehavior: "same-host",
187
- targetAccess: "relay",
188
- secretBrokering: "native",
189
- // The driver translates names to domainAllowList, or addresses to networkAllowList.
190
- egressControl: "hosts-and-cidrs",
191
- persistence: {
192
- resume: "same-sandbox",
193
- capture: "filesystem",
194
- clone: false,
195
- captureSource: "preserved",
196
- retention: "provider-managed",
197
- restoreMount: "new-resource"
198
- }
199
- }
200
- },
201
- modal: {
202
- id: "modal",
203
- label: sandboxDriverLabels.modal,
204
- credentialFields: sandboxDriverCredentialFields.modal,
205
- metadata: {
206
- driverRunsIn: ["node"],
207
- hostStopBehavior: "terminates-host",
208
- hostResumeBehavior: "replacement-host",
209
- targetAccess: "relay",
210
- secretBrokering: "none",
211
- // The driver does not install Modal's domain allowlist or alpha sidecar.
212
- egressControl: "none",
213
- persistence: {
214
- resume: "replacement-restore",
215
- capture: "filesystem",
216
- clone: false,
217
- captureSource: "preserved",
218
- retention: "provider-managed",
219
- restoreMount: "new-resource"
220
- }
221
- }
222
- },
223
- vercel: {
224
- id: "vercel",
225
- label: sandboxDriverLabels.vercel,
226
- credentialFields: sandboxDriverCredentialFields.vercel,
227
- metadata: {
228
- driverRunsIn: ["node"],
229
- hostStopBehavior: "terminates-host",
230
- hostResumeBehavior: "replacement-host",
231
- targetAccess: "relay",
232
- secretBrokering: "native",
233
- // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
234
- egressControl: "hosts",
235
- persistence: {
236
- resume: "replacement-restore",
237
- capture: "filesystem",
238
- clone: false,
239
- captureSource: "stopped",
240
- retention: "explicit",
241
- restoreMount: "new-resource"
242
- }
243
- }
244
- },
245
- cloudflare: {
246
- id: "cloudflare",
247
- label: sandboxDriverLabels.cloudflare,
248
- credentialFields: sandboxDriverCredentialFields.cloudflare,
249
- metadata: {
250
- driverRunsIn: ["worker"],
251
- hostStopBehavior: "not-supported",
252
- hostResumeBehavior: "same-host",
253
- targetAccess: "relay",
254
- secretBrokering: "native",
255
- // Credential handlers do not restrict unrelated destinations.
256
- egressControl: "none",
257
- persistence: {
258
- resume: "replacement-restore",
259
- capture: "directories",
260
- clone: false,
261
- captureSource: "preserved",
262
- retention: "provider-managed",
263
- restoreMount: "copy-on-write"
264
- }
265
- }
266
- },
267
- box: {
268
- id: "box",
269
- label: sandboxDriverLabels.box,
270
- credentialFields: sandboxDriverCredentialFields.box,
271
- metadata: {
272
- driverRunsIn: ["node"],
273
- hostStopBehavior: "suspends-host",
274
- hostResumeBehavior: "same-host",
275
- targetAccess: "relay",
276
- secretBrokering: "none",
277
- egressControl: "none",
278
- persistence: {
279
- resume: "same-sandbox",
280
- capture: "none",
281
- clone: false,
282
- captureSource: "not-applicable",
283
- retention: "provider-managed",
284
- restoreMount: "same-resource"
285
- }
286
- }
287
- },
288
- docker: {
289
- id: "docker",
290
- label: sandboxDriverLabels.docker,
291
- credentialFields: sandboxDriverCredentialFields.docker,
292
- metadata: {
293
- driverRunsIn: ["local"],
294
- hostStopBehavior: "terminates-host",
295
- hostResumeBehavior: "same-host",
296
- targetAccess: "loopback",
297
- secretBrokering: "none",
298
- egressControl: "none",
299
- persistence: {
300
- resume: "replacement-restore",
301
- capture: "same-resource",
302
- clone: false,
303
- captureSource: "preserved",
304
- retention: "provider-managed",
305
- restoreMount: "same-resource"
306
- }
307
- }
308
- }
309
- };
310
-
311
- // src/drivers/transient-error.ts
312
- function textOf(input) {
313
- return typeof input === "string" ? input : "";
314
- }
315
- function driverErrorSignals(err) {
316
- const shaped = isRecord(err) ? err : {};
317
- const nested = isRecord(shaped.response) ? shaped.response : {};
318
- return {
319
- status: num(nested.status) ?? num(shaped.status),
320
- text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
321
- };
322
- }
323
- function isTransientDriverError(err, markers) {
324
- const { status, text: text2 } = driverErrorSignals(err);
325
- if (status !== void 0 && status >= 500) return true;
326
- return markers.some((marker) => text2.includes(marker));
327
- }
328
-
329
- // src/drivers/box.ts
330
- var DEFAULT_BASE_URL = "https://ascii.dev/api/box/v1";
331
- var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
332
- var DEFAULT_WORKSPACE_DIR = "/workspace";
333
- var DEFAULT_CONTAINER_NAME = "claxedo-runtime";
334
- var DEFAULT_PROVISION_TIMEOUT_MS = 12e4;
335
- var DEFAULT_PROVISION_INTERVAL_MS = 2e3;
336
- var DEFAULT_HEALTH_TIMEOUT_MS = 6e4;
337
- var DEFAULT_HEALTH_INTERVAL_MS = 1e3;
338
- var DEFAULT_OPERATION_TIMEOUT_MS = 12e4;
339
- var READY_STATES = /* @__PURE__ */ new Set(["ready", "idle", "running"]);
340
- var PENDING_STATES = /* @__PURE__ */ new Set(["init", "provisioning", "provisioned", "cloning"]);
341
- function sleep(ms) {
342
- return new Promise((resolve) => setTimeout(resolve, ms));
343
- }
344
- function clean(input) {
345
- const txt = input?.trim();
346
- return txt ? txt : void 0;
347
- }
348
- var TRANSIENT_MARKERS = ["timeout", "unavailable", "econnreset", "network"];
349
- function transientDriverError(err) {
350
- return isTransientDriverError(err, TRANSIENT_MARKERS);
351
- }
352
- var BoxApiError = class extends Error {
353
- status;
354
- constructor(message, status) {
355
- super(message);
356
- this.name = "BoxApiError";
357
- this.status = status;
358
- }
359
- };
360
- function createBoxSandboxDriver(options) {
361
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
362
- const image = options.image ?? SANDBOX_IMAGE;
363
- const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
364
- const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
365
- const containerName = options.containerName ?? DEFAULT_CONTAINER_NAME;
366
- const ttlSeconds = options.ttlSeconds === void 0 ? null : options.ttlSeconds;
367
- const provisionTimeoutMs = options.provisionTimeoutMs ?? DEFAULT_PROVISION_TIMEOUT_MS;
368
- const provisionIntervalMs = options.provisionIntervalMs ?? DEFAULT_PROVISION_INTERVAL_MS;
369
- const healthTimeoutMs = options.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
370
- const healthIntervalMs = options.healthIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
371
- const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
372
- const fetchImpl = options.fetchImpl ?? fetch;
373
- async function api(path, init) {
374
- let res;
375
- try {
376
- res = await fetchImpl(`${baseUrl}${path}`, {
377
- method: init?.method ?? "GET",
378
- headers: {
379
- Authorization: `Bearer ${options.apiKey}`,
380
- ...init?.body !== void 0 ? { "Content-Type": "application/json" } : {}
381
- },
382
- ...init?.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
383
- signal: AbortSignal.timeout(operationTimeoutMs)
384
- });
385
- } catch (err) {
386
- throw new BoxApiError(err instanceof Error ? err.message : String(err));
387
- }
388
- const payload = await res.text();
389
- const body = (payload ? asRecord(JSON.parse(payload)) : void 0) ?? {};
390
- if (!res.ok || body.ok === false) {
391
- const detail = clean(text(body.error)) ?? clean(text(body.message)) ?? res.statusText;
392
- throw new BoxApiError(`Box API ${init?.method ?? "GET"} ${path} failed: ${detail}`, res.status);
393
- }
394
- return body;
395
- }
396
- function boxOf(body) {
397
- const box = asRecord(body.box);
398
- const id = text(box?.id);
399
- const state = text(box?.state);
400
- if (!box || !id || !state) throw new BoxApiError("Box API response omitted its box envelope");
401
- return {
402
- id,
403
- state,
404
- ...text(box.name) ? { name: text(box.name) } : {},
405
- ...box.url === void 0 ? {} : { url: text(box.url) ?? null },
406
- ...box.ip === void 0 ? {} : { ip: text(box.ip) ?? null },
407
- ...box.subdomain === void 0 ? {} : { subdomain: text(box.subdomain) ?? null },
408
- ...box.createdAt === void 0 ? {} : { createdAt: text(box.createdAt) ?? null }
409
- };
410
- }
411
- function runtimePort(input) {
412
- return input.workspaceRuntimePort ?? options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
413
- }
414
- function workspaceDirectory(input) {
415
- return input.workspaceRoot ?? workspaceDir;
416
- }
417
- function resolveImage(input) {
418
- if (input.bootSource?.kind === "image") return input.bootSource.image;
419
- return input.snapshot ?? image;
420
- }
421
- function bootEnv(input, hostId) {
422
- return workspaceRuntimeBootEnv({
423
- workspaceId: input.workspaceId,
424
- hostId,
425
- directory: workspaceDirectory(input),
426
- port: runtimePort(input),
427
- host: "0.0.0.0",
428
- source: input.source,
429
- env: input.env,
430
- runner: options.runner
431
- });
432
- }
433
- async function exec(boxId, command) {
434
- const body = await api(`/boxes/${boxId}/commands`, {
435
- method: "POST",
436
- body: { command }
437
- });
438
- const result = asRecord(body.result) ?? body;
439
- return {
440
- stdout: typeof result.stdout === "string" ? result.stdout : "",
441
- stderr: typeof result.stderr === "string" ? result.stderr : "",
442
- exitCode: typeof result.exitCode === "number" ? result.exitCode : 0
443
- };
444
- }
445
- async function execOrThrow(boxId, command, label) {
446
- const result = await exec(boxId, command);
447
- if ((result.exitCode ?? 0) !== 0) {
448
- throw new BoxApiError(`Box ${boxId} ${label} failed (exit ${result.exitCode}): ${trimToUndefined(result.stderr) ?? trimToUndefined(result.stdout) ?? ""}`);
449
- }
450
- return result;
451
- }
452
- async function waitUntilReady(boxId) {
453
- const until = Date.now() + provisionTimeoutMs;
454
- for (; ; ) {
455
- const box = boxOf(await api(`/boxes/${boxId}`));
456
- if (READY_STATES.has(box.state)) return true;
457
- if (!PENDING_STATES.has(box.state)) {
458
- throw new BoxApiError(`Box ${boxId} entered non-ready state: ${box.state}`);
459
- }
460
- if (Date.now() >= until) return false;
461
- await sleep(provisionIntervalMs);
462
- }
463
- }
464
- function dockerRunScript(input, env) {
465
- const port = runtimePort(input);
466
- const directory = workspaceDirectory(input);
467
- const bootScript = [`mkdir -p ${shell(directory)}`, `cd ${shell(directory)}`, `exec ${runtimeCommand}`].join(" && ");
468
- const envArgs = Object.entries(env).map(([key, value]) => `--env ${shell(`${key}=${value}`)}`).join(" ");
469
- const login = options.registryAuth ? `echo ${shell(options.registryAuth.password)} | docker login ${shell(options.registryAuth.server)} --username ${shell(options.registryAuth.username)} --password-stdin && ` : "";
470
- return `${login}docker rm -f ${containerName} >/dev/null 2>&1 || true; docker run -d --name ${containerName} -p ${port}:${port} ${envArgs} --entrypoint sh ${shell(resolveImage(input))} -lc ${shell(bootScript)}`;
471
- }
472
- async function waitForHealth(boxId, input) {
473
- const port = runtimePort(input);
474
- const until = Date.now() + healthTimeoutMs;
475
- let last = "workspace runtime not ready";
476
- while (Date.now() < until) {
477
- const probe = await exec(
478
- boxId,
479
- `curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:${port}/global/health || true`
480
- );
481
- const code = trimToUndefined(probe.stdout);
482
- if (code === "200") return;
483
- last = `health probe returned ${code ?? "no response"}`;
484
- await sleep(healthIntervalMs);
485
- }
486
- throw new BoxApiError(`Box ${boxId} runtime did not become healthy: ${last}`);
487
- }
488
- async function boot(boxId, input, hostId) {
489
- const env = {
490
- ...bootEnv(input, hostId),
491
- ...await options.env?.(input, { id: hostId })
492
- };
493
- const port = runtimePort(input);
494
- await execOrThrow(boxId, dockerRunScript(input, env), "docker run");
495
- await execOrThrow(boxId, `host ${port}`, "host publish");
496
- await waitForHealth(boxId, input);
497
- const urlResult = await execOrThrow(boxId, `host url ${port}`, "host url");
498
- const url = trimToUndefined(urlResult.stdout?.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop());
499
- if (!url) throw new BoxApiError(`Box ${boxId} did not return a public URL for port ${port}`);
500
- return {
501
- workspaceId: input.workspaceId,
502
- sandboxId: boxId,
503
- url,
504
- hostId,
505
- driverResourceId: boxId,
506
- driver: { id: "box", resourceId: boxId },
507
- labels: input.labels
508
- };
509
- }
510
- function assertNetwork(input) {
511
- if (input.net && input.net.mode !== "allow-all") {
512
- throw new BoxApiError("Box SandboxDriver cannot enforce host-based network policy");
513
- }
514
- }
515
- async function ensureHost(input) {
516
- assertNetwork(input);
517
- const hostId = input.hostId ?? `box-${input.workspaceId}`;
518
- const created = await api("/boxes", {
519
- method: "POST",
520
- body: { ttlSeconds }
521
- }).catch((err) => {
522
- if (transientDriverError(err)) return void 0;
523
- throw err;
524
- });
525
- if (!created) return { provisioning: true, retryAfterMs: provisionIntervalMs };
526
- const boxId = boxOf(created).id;
527
- const ready = await waitUntilReady(boxId);
528
- if (!ready) return { provisioning: true, retryAfterMs: provisionIntervalMs };
529
- return boot(boxId, input, hostId);
530
- }
531
- async function resumeHost(input) {
532
- const boxId = input.lease.sandboxId;
533
- if (!boxId) return ensureHost(input.ensure);
534
- assertNetwork(input.ensure);
535
- const hostId = input.ensure.hostId ?? input.lease.hostId ?? `box-${input.ensure.workspaceId}`;
536
- try {
537
- await api(`/boxes/${boxId}/resume`, { method: "POST" });
538
- const ready = await waitUntilReady(boxId);
539
- if (!ready) return { provisioning: true, retryAfterMs: provisionIntervalMs };
540
- return await boot(boxId, input.ensure, hostId);
541
- } catch (err) {
542
- if (transientDriverError(err)) return { provisioning: true, retryAfterMs: provisionIntervalMs };
543
- return ensureHost(input.ensure);
544
- }
545
- }
546
- async function stop(target) {
547
- await api(`/boxes/${target.sandboxId}/stop`, { method: "POST" });
548
- }
549
- async function destroy(target) {
550
- await api(`/boxes/${target.sandboxId}`, { method: "DELETE" });
551
- }
552
- return {
553
- id: "box",
554
- metadata: {
555
- driverRunsIn: ["node"],
556
- hostStopBehavior: "suspends-host",
557
- hostResumeBehavior: "same-host",
558
- targetAccess: "relay",
559
- secretBrokering: "none",
560
- egressControl: "none",
561
- persistence: sandboxDriverCatalog.box.metadata.persistence
562
- },
563
- ensureHost,
564
- resumeHost,
565
- async touch() {
566
- },
567
- suspend: stop,
568
- stop,
569
- destroy
570
- };
571
- }
572
- export {
573
- createBoxSandboxDriver
574
- };