@claxedo/sandbox-manager 0.7.0 → 0.9.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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_WORKSPACE_RUNTIME_PORT } from "./constants";
2
- import type { SandboxDriverID } from "./driver-catalog";
2
+ import type { SandboxDriverID, SandboxSecretBrokering } from "@claxedo/sandbox-contract";
3
+ export type { SandboxSecretBrokering } from "@claxedo/sandbox-contract";
3
4
  import { type SandboxCheckpointCaptureInput, type SandboxCheckpointRestoreInput, type SandboxCheckpointResult } from "./checkpoint-manager";
4
5
  export { DEFAULT_WORKSPACE_RUNTIME_PORT };
5
6
  export * from "./checkpoint-manager";
@@ -72,25 +73,16 @@ export type SandboxDriverMetadata = {
72
73
  * How the driver can honor a `SandboxBrokeredSecret` — a credential the
73
74
  * sandbox may USE for outbound requests but must NEVER be able to READ.
74
75
  *
75
- * - `"native"` — the provider brokers it automatically on egress to the
76
- * allowlisted hosts, with no extra infrastructure: Daytona secret
77
- * placeholders + egress proxy; Vercel firewall header transforms. The
78
- * driver injects it during `ensureHost`.
79
- * - `"proxy"` — brokering is achievable but only via a host-operated egress
80
- * proxy the driver routes through (Cloudflare's official Worker-proxy
81
- * pattern: the sandbox gets a short-lived JWT, egress hits the Worker,
82
- * the Worker injects the real credential — the raw value never enters the
83
- * sandbox). Not automatic; requires the host's proxy to be wired.
76
+ * - `"native"` — the provider brokers it on egress to the allowlisted hosts
77
+ * with no extra infrastructure of ours: Daytona secret placeholders,
78
+ * Vercel firewall header transforms, Cloudflare outbound handlers reading
79
+ * the value from KV. The driver installs it during `ensureHost`.
84
80
  * - `"none"` — no way to keep the value out of sandbox processes. The
85
81
  * provider may still have an encrypted secret STORE (e.g. Modal secrets),
86
- * but those are exposed as readable env vars, so they cannot satisfy the
82
+ * but it is exposed as a readable env var, which cannot satisfy the
87
83
  * never-readable contract.
88
- *
89
- * The manager fails closed unless the driver actually injects brokered
90
- * secrets (today: `"native"` only). `"proxy"` records an achievable ceiling
91
- * that is not yet wired and therefore also fails closed.
92
84
  */
93
- secretBrokering: "native" | "proxy" | "none";
85
+ secretBrokering: SandboxSecretBrokering;
94
86
  /**
95
87
  * How the driver can enforce a RESTRICTED `SandboxNetworkPolicy` — i.e.
96
88
  * whether the sandbox's outbound network can actually be contained.
@@ -196,7 +188,7 @@ export type SandboxEgressUnenforcedEvent = {
196
188
  *
197
189
  * Guarantees enforced by the manager: brokered secrets are never written to
198
190
  * labels, never logged, and never captured in a driver snapshot. A driver
199
- * whose `metadata.secretBrokering !== "native"` cannot honor them and the
191
+ * whose `metadata.secretBrokering` is not `"native"` cannot honor them and the
200
192
  * manager refuses to provision (fail-closed).
201
193
  */
202
194
  export type SandboxBrokeredSecret = {
@@ -213,7 +205,49 @@ export type SandboxBrokeredSecret = {
213
205
  * the sandbox to attach itself (Daytona).
214
206
  */
215
207
  header?: string;
208
+ /**
209
+ * The authentication scheme `header` carries, when it has one (`Bearer`).
210
+ *
211
+ * A driver that WRITES the whole header composes `"<scheme> <value>"`; one
212
+ * that SUBSTITUTES a placeholder the sandbox already wrote after the scheme
213
+ * ignores it, because the scheme is in the request before the value is.
214
+ */
215
+ scheme?: string;
216
+ /**
217
+ * The methods and path prefixes the credential may be attached to, on top of
218
+ * `hosts`. A vendor host serves far more than the routes a turn needs —
219
+ * api.anthropic.com also answers the organization-admin API — and everything
220
+ * sharing the sandbox reaches the same host.
221
+ *
222
+ * Optional so a producer that has not been taught to state a policy still
223
+ * type-checks, and fails closed instead: a driver that can express them
224
+ * attaches the credential only within them, so an absent or empty policy
225
+ * names a host and nothing else, and nothing is spendable at a host alone. A
226
+ * driver whose provider cannot express them documents that it drops them,
227
+ * and the host allowlist is all the containment there is.
228
+ */
229
+ methods?: readonly string[];
230
+ pathPrefixes?: readonly string[];
216
231
  };
232
+ /**
233
+ * What the sandbox presents so a header-injecting driver recognizes the request
234
+ * as one asking for that secret.
235
+ *
236
+ * Carries no authority: the value is attached at the edge, keyed by the host
237
+ * and this string, and the string itself is never accepted by a vendor. The
238
+ * Cloudflare sandbox Worker is deployed on its own and cannot depend on this
239
+ * package, so it matches on its own copy of the prefix; a driver minting
240
+ * anything else sends every brokered request upstream bare, which is what
241
+ * `brokered-placeholder.test.ts` exists to catch.
242
+ */
243
+ export declare function brokeredSecretPlaceholder(name: string): string;
244
+ /**
245
+ * The environment a header-injecting driver must add so the sandbox can present
246
+ * each secret. Daytona is absent from this: its own mount fills the same
247
+ * variables with the placeholder it substitutes, and an env entry would shadow
248
+ * it with a string Daytona does not know.
249
+ */
250
+ export declare function brokeredPlaceholderEnv(secrets: readonly SandboxBrokeredSecret[] | undefined): Record<string, string>;
217
251
  export type SandboxExposure = {
218
252
  kind: "loopback";
219
253
  } | {
@@ -290,6 +324,8 @@ export type SandboxNetworkPolicy = {
290
324
  hosts?: string[];
291
325
  cidrs?: string[];
292
326
  };
327
+ /** Brokered destinations are the minimum egress needed to use the credential. */
328
+ export declare function sandboxNetworkPolicyWithBrokeredHosts(policy: SandboxNetworkPolicy, secrets: readonly SandboxBrokeredSecret[] | undefined): SandboxNetworkPolicy;
293
329
  export type SandboxBootSource = {
294
330
  kind: "image";
295
331
  image: string;
@@ -431,6 +467,14 @@ export type SandboxRegisterInput = {
431
467
  now?: number;
432
468
  };
433
469
  export type SandboxHeartbeatInput = SandboxRegisterInput;
470
+ /**
471
+ * What a provisioning cycle is actually doing, for honest progress UI.
472
+ * `restore` = booting a replacement restored from the workspace's snapshot
473
+ * (files come back), `resume` = restarting the same paused resource,
474
+ * `cold-start` = a fresh sandbox with no prior state to bring back (first
475
+ * boot, or nothing snapshot-able survived).
476
+ */
477
+ export type SandboxBootMode = "restore" | "resume" | "cold-start";
434
478
  export type SandboxEnsureResult = ({
435
479
  status: "ready";
436
480
  } & SandboxTarget & {
@@ -441,6 +485,7 @@ export type SandboxEnsureResult = ({
441
485
  retryAfterMs: number;
442
486
  epoch: number;
443
487
  homeRegion: SandboxRegion;
488
+ bootMode?: SandboxBootMode;
444
489
  } | {
445
490
  status: "unavailable";
446
491
  retryAfterMs?: number;
package/dist/index.mjs CHANGED
@@ -141,16 +141,16 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
141
141
  ];
142
142
  function hostOf(value) {
143
143
  const raw = value?.trim();
144
- if (!raw) return;
144
+ if (!raw) return void 0;
145
145
  try {
146
146
  const host = new URL(raw).hostname.trim().toLowerCase();
147
147
  return host ? host : void 0;
148
148
  } catch {
149
- return;
149
+ return void 0;
150
150
  }
151
151
  }
152
152
  function sandboxSourceHost(source) {
153
- if (!source || source.kind !== "git") return;
153
+ if (!source || source.kind !== "git") return void 0;
154
154
  return hostOf(source.repoUrl);
155
155
  }
156
156
  function hostedSandboxNetworkPolicy(input) {
@@ -206,6 +206,12 @@ var compositionEgressWarnings = /* @__PURE__ */ new Set();
206
206
  function defaultEgressUnenforcedSink(event) {
207
207
  console.warn(event.message);
208
208
  }
209
+ function brokeredSecretPlaceholder(name) {
210
+ return `claxedo-broker:${name}`;
211
+ }
212
+ function brokeredPlaceholderEnv(secrets) {
213
+ return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
214
+ }
209
215
  function applySandboxLeasePatch(current, patch, updatedAt) {
210
216
  return {
211
217
  ...current,
@@ -226,6 +232,17 @@ function applySandboxLeasePatch(current, patch, updatedAt) {
226
232
  updatedAt
227
233
  };
228
234
  }
235
+ function sandboxNetworkPolicyWithBrokeredHosts(policy, secrets) {
236
+ if (policy.mode !== "restricted" || !secrets?.length) return policy;
237
+ const hosts = [.../* @__PURE__ */ new Set([
238
+ ...policy.hosts ?? [],
239
+ ...secrets.flatMap((secret) => secret.hosts)
240
+ ])].toSorted();
241
+ return {
242
+ ...policy,
243
+ ...hosts.length ? { hosts } : {}
244
+ };
245
+ }
229
246
  function isSandboxListingUnsupported(err) {
230
247
  return !!err && typeof err === "object" && err.listingUnsupported === true;
231
248
  }
@@ -254,14 +271,17 @@ function ensureHostInput(input) {
254
271
  workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
255
272
  env: input.managerInput?.env ?? {},
256
273
  // Brokered secrets ride their own channel — NEVER merged into labels or env.
257
- ...input.managerInput?.secrets?.length ? { secrets: input.managerInput.secrets } : {},
274
+ // Presence of the key, not its length, is the signal: `[]` is the caller
275
+ // withdrawing every brokered secret, and dropping it would reach the driver
276
+ // as "preserve what you have".
277
+ ...input.managerInput?.secrets ? { secrets: input.managerInput.secrets } : {},
258
278
  source: input.managerInput?.source,
259
279
  exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
260
280
  // Withheld, never downgraded: `undefined` here is the same shape a caller
261
281
  // that asked for no containment produces, so the throwing drivers take
262
282
  // their allow-all path and their throws stay intact for anyone who hands
263
283
  // them a policy directly.
264
- ...egress.action === "enforce" && input.managerInput?.net ? { net: input.managerInput.net } : {},
284
+ ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
265
285
  snapshot: input.managerInput?.snapshot
266
286
  };
267
287
  }
@@ -306,17 +326,27 @@ function createSandboxManager(options) {
306
326
  });
307
327
  }
308
328
  }
309
- function lifecycle(workspaceId, kind, run) {
310
- const current = lifecycleOperations.get(workspaceId);
311
- if (current?.kind === kind) return current.promise;
312
- const operation = (current?.promise.catch(() => void 0) ?? Promise.resolve()).then(run);
313
- lifecycleOperations.set(workspaceId, { kind, promise: operation });
329
+ function lifecycleQueue(current) {
330
+ return current?.promise.catch(() => void 0) ?? Promise.resolve();
331
+ }
332
+ function lifecycleSettle(workspaceId, entry, operation) {
333
+ lifecycleOperations.set(workspaceId, entry);
314
334
  return operation.finally(() => {
315
- if (lifecycleOperations.get(workspaceId)?.promise === operation) {
316
- lifecycleOperations.delete(workspaceId);
317
- }
335
+ if (lifecycleOperations.get(workspaceId) === entry) lifecycleOperations.delete(workspaceId);
318
336
  });
319
337
  }
338
+ function checkpointLifecycle(workspaceId, kind, run) {
339
+ const current = lifecycleOperations.get(workspaceId);
340
+ if (current?.kind === kind) return current.promise;
341
+ const operation = lifecycleQueue(current).then(run);
342
+ return lifecycleSettle(workspaceId, { kind, promise: operation }, operation);
343
+ }
344
+ function mutationLifecycle(workspaceId, kind, run) {
345
+ const current = lifecycleOperations.get(workspaceId);
346
+ if (current?.kind === kind) return current.promise;
347
+ const operation = lifecycleQueue(current).then(run);
348
+ return lifecycleSettle(workspaceId, { kind, promise: operation }, operation);
349
+ }
320
350
  async function leaseTarget(lease) {
321
351
  if (lease.status !== "ready" || !lease.sandboxId || !lease.url || !lease.hostId) {
322
352
  return { status: "unavailable", reason: "runtime_lease_not_ready" };
@@ -337,6 +367,12 @@ function createSandboxManager(options) {
337
367
  homeRegion: lease.homeRegion
338
368
  };
339
369
  }
370
+ function leaseBootMode(lease) {
371
+ const restoring = Boolean(lease.checkpoint) && lease.status !== "ready" && options.driver.metadata.persistence.resume === "replacement-restore";
372
+ if (restoring) return "restore";
373
+ if (lease.sandboxId && options.driver.resumeHost) return "resume";
374
+ return "cold-start";
375
+ }
340
376
  async function provision(workspaceId, lease, homeRegion, managerInput) {
341
377
  try {
342
378
  const ensure = ensureHostInput({
@@ -355,7 +391,8 @@ function createSandboxManager(options) {
355
391
  homeRegion
356
392
  };
357
393
  }
358
- const target = lease.sandboxId && ensure.bootSource?.kind === "default" && options.driver.resumeHost ? await options.driver.resumeHost({ lease, ensure }) : await options.driver.ensureHost(ensure);
394
+ const resuming = Boolean(lease.sandboxId && ensure.bootSource?.kind === "default" && options.driver.resumeHost);
395
+ const target = resuming ? await options.driver.resumeHost({ lease, ensure }) : await options.driver.ensureHost(ensure);
359
396
  if ("provisioning" in target) {
360
397
  await options.leaseStore.update(workspaceId, lease.epoch, {
361
398
  status: "acquiring",
@@ -365,7 +402,11 @@ function createSandboxManager(options) {
365
402
  status: "provisioning",
366
403
  retryAfterMs: target.retryAfterMs,
367
404
  epoch: lease.epoch,
368
- homeRegion
405
+ homeRegion,
406
+ // Honest progress for the connect UI: which of the three boot paths
407
+ // this cycle is on. Derived from the same inputs that CHOSE the path
408
+ // above, so it cannot drift from what actually ran.
409
+ bootMode: resuming ? "resume" : ensure.bootSource?.kind === "driver-snapshot" ? "restore" : "cold-start"
369
410
  };
370
411
  }
371
412
  const updated = await options.leaseStore.update(workspaceId, lease.epoch, {
@@ -428,7 +469,8 @@ function createSandboxManager(options) {
428
469
  status: "provisioning",
429
470
  retryAfterMs: existing.nextRetryAt - now(),
430
471
  epoch: existing.epoch,
431
- homeRegion: existing.homeRegion
472
+ homeRegion: existing.homeRegion,
473
+ bootMode: leaseBootMode(existing)
432
474
  };
433
475
  }
434
476
  return {
@@ -464,7 +506,8 @@ function createSandboxManager(options) {
464
506
  status: "provisioning",
465
507
  retryAfterMs: acquired.retryAfterMs,
466
508
  epoch: acquired.lease.epoch,
467
- homeRegion: acquired.lease.homeRegion
509
+ homeRegion: acquired.lease.homeRegion,
510
+ bootMode: leaseBootMode(acquired.lease)
468
511
  };
469
512
  }
470
513
  return provision(workspaceId, acquired.lease, input.homeRegion, input);
@@ -493,7 +536,7 @@ function createSandboxManager(options) {
493
536
  return { ok: true, ...await options.driver.snapshot(target) };
494
537
  },
495
538
  async checkpoint(workspaceId, input) {
496
- return await lifecycle(workspaceId, "checkpoint", () => captureSandboxCheckpoint({
539
+ return await checkpointLifecycle(workspaceId, "checkpoint", () => captureSandboxCheckpoint({
497
540
  workspaceId,
498
541
  request: input,
499
542
  leaseStore: options.leaseStore,
@@ -503,7 +546,7 @@ function createSandboxManager(options) {
503
546
  }));
504
547
  },
505
548
  async restore(workspaceId, input) {
506
- return await lifecycle(workspaceId, "restore", () => restoreSandboxCheckpoint({
549
+ return await checkpointLifecycle(workspaceId, "restore", () => restoreSandboxCheckpoint({
507
550
  workspaceId,
508
551
  request: input,
509
552
  leaseStore: options.leaseStore,
@@ -512,7 +555,7 @@ function createSandboxManager(options) {
512
555
  }));
513
556
  },
514
557
  async stop(workspaceId) {
515
- return await lifecycle(workspaceId, "stop", async () => {
558
+ return await mutationLifecycle(workspaceId, "stop", async () => {
516
559
  const lease = await options.leaseStore.get(workspaceId);
517
560
  if (lease?.status === "stopped") return { ok: true, status: "stopped" };
518
561
  const target = await this.target(workspaceId);
@@ -527,7 +570,7 @@ function createSandboxManager(options) {
527
570
  });
528
571
  },
529
572
  async destroy(workspaceId) {
530
- return await lifecycle(workspaceId, "destroy", async () => {
573
+ return await mutationLifecycle(workspaceId, "destroy", async () => {
531
574
  const lease = await options.leaseStore.get(workspaceId);
532
575
  if (lease?.status === "destroyed") return { ok: true, status: "destroyed" };
533
576
  const target = await this.target(workspaceId);
@@ -617,7 +660,7 @@ function createSandboxManager(options) {
617
660
  const expectedEpoch = input.epoch ?? current.epoch;
618
661
  if (current.epoch !== expectedEpoch) return { ok: false, reason: "runtime_lease_epoch_mismatch" };
619
662
  const timestamp = input.now ?? now();
620
- const status = input.status === "unhealthy" || input.ok === false ? "unavailable" : input.ok ? "ready" : current.status;
663
+ const status = input.status === "unhealthy" || !input.ok ? "unavailable" : input.ok ? "ready" : current.status;
621
664
  const updated = await options.leaseStore.update(workspaceId, expectedEpoch, {
622
665
  status,
623
666
  ...input.sandboxId ? { sandboxId: input.sandboxId } : {},
@@ -638,12 +681,15 @@ export {
638
681
  SANDBOX_MODEL_PROVIDER_HOSTS,
639
682
  SANDBOX_PACKAGE_REGISTRY_HOSTS,
640
683
  applySandboxLeasePatch,
684
+ brokeredPlaceholderEnv,
685
+ brokeredSecretPlaceholder,
641
686
  captureSandboxCheckpoint,
642
687
  createSandboxManager,
643
688
  hostedSandboxNetworkPolicy,
644
689
  isSandboxListingUnsupported,
645
690
  restoreSandboxCheckpoint,
646
691
  sandboxEgressDisposition,
692
+ sandboxNetworkPolicyWithBrokeredHosts,
647
693
  sandboxSourceHost,
648
694
  validateSandboxPersistenceCapabilities
649
695
  };
package/dist/json.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Reading untrusted JSON: driver API responses, CLI output, stored records.
3
+ *
4
+ * Drivers all face the same problem — a provider hands back `any`, and the code
5
+ * needs a few named fields out of it. Narrowing through these keeps the check
6
+ * and the type in the same place, instead of each site asserting a shape it
7
+ * never verified. The object check is `@claxedo/helpers/guards`, re-exported
8
+ * under the names the drivers use.
9
+ */
10
+ export { asRecord as record, isRecord } from "@claxedo/helpers/guards";
11
+ /** A non-empty string, or undefined — the shape almost every provider field wants. */
12
+ export declare function text(input: unknown): string | undefined;
13
+ export declare function num(input: unknown): number | undefined;
@@ -1,5 +1,5 @@
1
1
  import type { SandboxLeaseRow } from "./lease-types";
2
- import { type SandboxDriverID } from "./driver-catalog";
2
+ import type { SandboxDriverID } from "@claxedo/sandbox-contract";
3
3
  export type SandboxDecision = {
4
4
  action: "skip";
5
5
  reason: string;
@@ -44,5 +44,9 @@ export type SandboxDriverPlacement = {
44
44
  export declare const DEFAULT_WORKSPACE_HOST_DECISION_CONFIG: SandboxDecisionConfig;
45
45
  export declare function sandboxDriverPlacement(driver: SandboxDriverID): SandboxDriverPlacement;
46
46
  export declare function decideSandboxStart(lease: SandboxLeaseRow, placement: SandboxDriverPlacement, now: number, config?: SandboxDecisionConfig): SandboxDecision;
47
- export declare function decideSandboxHealthFailure(lease: SandboxLeaseRow, _placement: SandboxDriverPlacement, now: number, config?: SandboxDecisionConfig): SandboxDecision;
47
+ /**
48
+ * Health-failure policy is placement-independent: retry budget and backoff are
49
+ * the same question for every driver, so this deliberately takes no placement.
50
+ */
51
+ export declare function decideSandboxHealthFailure(lease: SandboxLeaseRow, now: number, config?: SandboxDecisionConfig): SandboxDecision;
48
52
  export declare function nextSandboxRetryAt(retryCount: number, now: number, config?: SandboxDecisionConfig): number | null;
@@ -24,12 +24,42 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
24
24
  ...SANDBOX_PACKAGE_REGISTRY_HOSTS
25
25
  ];
26
26
 
27
+ // ../sandbox-contract/dist/index.mjs
28
+ var sandboxDriverCredentialFields = {
29
+ exe: [{ key: "api_token", label: "API Token", secret: true }],
30
+ daytona: [{ key: "api_key", label: "API Key", secret: true }],
31
+ modal: [
32
+ { key: "token_id", label: "Token ID", secret: true },
33
+ { key: "token_secret", label: "Token Secret", secret: true }
34
+ ],
35
+ vercel: [
36
+ { key: "access_token", label: "Access Token", secret: true },
37
+ { key: "team_id", label: "Team ID" },
38
+ { key: "project_id", label: "Project ID" }
39
+ ],
40
+ cloudflare: [
41
+ { key: "api_token", label: "API Token", secret: true },
42
+ { key: "worker_url", label: "Worker URL" }
43
+ ],
44
+ box: [{ key: "api_key", label: "API Key", secret: true }],
45
+ docker: [{ key: "image", label: "Image" }]
46
+ };
47
+ var sandboxDriverLabels = {
48
+ exe: "exe.dev",
49
+ daytona: "Daytona",
50
+ modal: "Modal",
51
+ vercel: "Vercel",
52
+ cloudflare: "Cloudflare",
53
+ box: "Box",
54
+ docker: "Docker"
55
+ };
56
+
27
57
  // src/driver-catalog.ts
28
58
  var sandboxDriverCatalog = {
29
59
  exe: {
30
60
  id: "exe",
31
- label: "exe.dev",
32
- credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
61
+ label: sandboxDriverLabels.exe,
62
+ credentialFields: sandboxDriverCredentialFields.exe,
33
63
  metadata: {
34
64
  driverRunsIn: ["worker", "node"],
35
65
  hostStopBehavior: "not-supported",
@@ -49,17 +79,15 @@ var sandboxDriverCatalog = {
49
79
  },
50
80
  daytona: {
51
81
  id: "daytona",
52
- label: "Daytona",
53
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
82
+ label: sandboxDriverLabels.daytona,
83
+ credentialFields: sandboxDriverCredentialFields.daytona,
54
84
  metadata: {
55
85
  driverRunsIn: ["worker", "node"],
56
86
  hostStopBehavior: "suspends-host",
57
87
  hostResumeBehavior: "same-host",
58
88
  targetAccess: "relay",
59
89
  secretBrokering: "native",
60
- // Daytona is the only driver that can filter egress by NAME as well as
61
- // by address: `domainAllowList` (names) alongside `networkAllowList`
62
- // (CIDRs), with `networkBlockAll` as the deny-all floor.
90
+ // The driver translates names to domainAllowList, or addresses to networkAllowList.
63
91
  egressControl: "hosts-and-cidrs",
64
92
  persistence: {
65
93
  resume: "same-sandbox",
@@ -73,20 +101,15 @@ var sandboxDriverCatalog = {
73
101
  },
74
102
  modal: {
75
103
  id: "modal",
76
- label: "Modal",
77
- credentialFields: [
78
- { key: "token_id", label: "Token ID", secret: true },
79
- { key: "token_secret", label: "Token Secret", secret: true }
80
- ],
104
+ label: sandboxDriverLabels.modal,
105
+ credentialFields: sandboxDriverCredentialFields.modal,
81
106
  metadata: {
82
107
  driverRunsIn: ["node"],
83
108
  hostStopBehavior: "terminates-host",
84
109
  hostResumeBehavior: "replacement-host",
85
110
  targetAccess: "relay",
86
111
  secretBrokering: "none",
87
- // Modal can cut the network entirely (`blockNetwork`) but cannot express
88
- // an allowlist, and its driver throws on a host policy. A blackout is
89
- // not containment for a workspace that has to clone and reach a model.
112
+ // The driver does not install Modal's domain allowlist or alpha sidecar.
90
113
  egressControl: "none",
91
114
  persistence: {
92
115
  resume: "replacement-restore",
@@ -100,20 +123,15 @@ var sandboxDriverCatalog = {
100
123
  },
101
124
  vercel: {
102
125
  id: "vercel",
103
- label: "Vercel",
104
- credentialFields: [
105
- { key: "access_token", label: "Access Token", secret: true },
106
- { key: "team_id", label: "Team ID" },
107
- { key: "project_id", label: "Project ID" }
108
- ],
126
+ label: sandboxDriverLabels.vercel,
127
+ credentialFields: sandboxDriverCredentialFields.vercel,
109
128
  metadata: {
110
129
  driverRunsIn: ["node"],
111
130
  hostStopBehavior: "terminates-host",
112
131
  hostResumeBehavior: "replacement-host",
113
132
  targetAccess: "relay",
114
133
  secretBrokering: "native",
115
- // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
116
- // never CIDRs.
134
+ // The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
117
135
  egressControl: "hosts",
118
136
  persistence: {
119
137
  resume: "replacement-restore",
@@ -127,23 +145,15 @@ var sandboxDriverCatalog = {
127
145
  },
128
146
  cloudflare: {
129
147
  id: "cloudflare",
130
- label: "Cloudflare",
131
- credentialFields: [
132
- { key: "api_token", label: "API Token", secret: true },
133
- { key: "worker_url", label: "Worker URL" }
134
- ],
148
+ label: sandboxDriverLabels.cloudflare,
149
+ credentialFields: sandboxDriverCredentialFields.cloudflare,
135
150
  metadata: {
136
151
  driverRunsIn: ["worker"],
137
152
  hostStopBehavior: "not-supported",
138
153
  hostResumeBehavior: "same-host",
139
154
  targetAccess: "relay",
140
- secretBrokering: "proxy",
141
- // The Cloudflare Sandbox Worker exposes no egress filter: its broker
142
- // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
143
- // to route brokered-credential requests through, not a boundary. The
144
- // driver therefore drops `net` entirely — which is exactly why this
145
- // must be declared, so the manager refuses instead of provisioning a
146
- // sandbox the caller believes is contained.
155
+ secretBrokering: "native",
156
+ // Credential handlers do not restrict unrelated destinations.
147
157
  egressControl: "none",
148
158
  persistence: {
149
159
  resume: "replacement-restore",
@@ -157,8 +167,8 @@ var sandboxDriverCatalog = {
157
167
  },
158
168
  box: {
159
169
  id: "box",
160
- label: "Box",
161
- credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
170
+ label: sandboxDriverLabels.box,
171
+ credentialFields: sandboxDriverCredentialFields.box,
162
172
  metadata: {
163
173
  driverRunsIn: ["node"],
164
174
  hostStopBehavior: "suspends-host",
@@ -178,8 +188,8 @@ var sandboxDriverCatalog = {
178
188
  },
179
189
  docker: {
180
190
  id: "docker",
181
- label: "Docker",
182
- credentialFields: [{ key: "image", label: "Image" }],
191
+ label: sandboxDriverLabels.docker,
192
+ credentialFields: sandboxDriverCredentialFields.docker,
183
193
  metadata: {
184
194
  driverRunsIn: ["local"],
185
195
  hostStopBehavior: "terminates-host",
@@ -236,7 +246,7 @@ function decideSandboxStart(lease, placement, now, config = DEFAULT_WORKSPACE_HO
236
246
  }
237
247
  return decideStartMethod(lease, placement);
238
248
  }
239
- function decideSandboxHealthFailure(lease, _placement, now, config = DEFAULT_WORKSPACE_HOST_DECISION_CONFIG) {
249
+ function decideSandboxHealthFailure(lease, now, config = DEFAULT_WORKSPACE_HOST_DECISION_CONFIG) {
240
250
  if (lease.status !== "ready" && lease.status !== "unhealthy") {
241
251
  return { action: "skip", reason: `status is ${lease.status}, health check not applicable` };
242
252
  }
@@ -1,4 +1,3 @@
1
- import type { SandboxDriverID } from "./driver-catalog";
2
1
  import type { SandboxCheckpointReference, SandboxPersistenceCapabilities, SandboxRestoreStatus } from "./index";
3
2
  export type SandboxLeaseRowStatus = "pending" | "acquiring" | "starting" | "ready" | "unhealthy" | "backoff" | "stopping" | "stopped" | "destroyed" | "failed";
4
3
  export type SandboxComputeClass = "small" | "medium" | "large" | "gpu";
@@ -8,7 +7,17 @@ export type SandboxLeaseRow = {
8
7
  home_region?: string;
9
8
  epoch: number;
10
9
  status: SandboxLeaseRowStatus;
11
- driver: SandboxDriverID;
10
+ /**
11
+ * The id the driver was registered with, which is any string: the manager
12
+ * dispatches on whatever a composition registers (`stores/d1.test.ts`
13
+ * provisions one called `"test-provider"`), and every other type this value
14
+ * flows between — `SandboxLease["driver"]`, `SandboxLeaseAcquireInput["driver"]`,
15
+ * `SandboxTarget["driver"]["id"]` — is already `string`. Typing the row as the
16
+ * closed `SandboxDriverID` made it the odd one out and forced every store to
17
+ * assert into it. Narrow with `isSandboxDriverID` where a CATALOG entry is
18
+ * needed, which is the only place the closed set is the right question.
19
+ */
20
+ driver: string;
12
21
  driver_resource_id: string | null;
13
22
  driver_snapshot_id: string | null;
14
23
  sandbox_id: string | null;
@@ -1,4 +1,15 @@
1
- import type { SandboxDriverID } from "./driver-catalog";
1
+ import type { SandboxDriverID } from "@claxedo/sandbox-contract";
2
+ export type WorkspaceRuntimeControlEnv = {
3
+ relayJwksUrl?: string;
4
+ relayVerifyPem?: string;
5
+ managementJwksUrl?: string;
6
+ sessionAuthorityUrl?: string;
7
+ };
8
+ export declare function workspaceRuntimeBootEnv(input: Parameters<typeof workspaceRuntimeTargetEnv>[0] & Parameters<typeof workspaceRuntimeSourceEnv>[0] & {
9
+ env?: Record<string, string>;
10
+ runner?: string;
11
+ controlEnv?: WorkspaceRuntimeControlEnv;
12
+ }): Record<string, string>;
2
13
  export declare function workspaceRuntimeTargetEnv(input: {
3
14
  workspaceId: string;
4
15
  hostId?: string;
@@ -1,4 +1,17 @@
1
1
  // src/runtime-env.ts
2
+ function workspaceRuntimeBootEnv(input) {
3
+ const env = {
4
+ ...workspaceRuntimeTargetEnv(input),
5
+ ...workspaceRuntimeSourceEnv(input),
6
+ ...input.env
7
+ };
8
+ if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
9
+ if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
10
+ if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
11
+ if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
12
+ if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
13
+ return env;
14
+ }
2
15
  function workspaceRuntimeTargetEnv(input) {
3
16
  return {
4
17
  WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
@@ -27,6 +40,7 @@ function workspaceRuntimeServiceExposureEnv(input) {
27
40
  };
28
41
  }
29
42
  export {
43
+ workspaceRuntimeBootEnv,
30
44
  workspaceRuntimeServiceExposureEnv,
31
45
  workspaceRuntimeSourceEnv,
32
46
  workspaceRuntimeTargetEnv
@@ -1,4 +1,4 @@
1
- import type { SandboxDriverID } from "./driver-catalog";
1
+ import type { SandboxDriverID } from "@claxedo/sandbox-contract";
2
2
  export type SandboxServiceUrlExposure = {
3
3
  source: "driver-service-url";
4
4
  access: "private" | "public" | "driver-authenticated" | "unknown";
@@ -7,4 +7,3 @@ export type SandboxServiceUrlExposure = {
7
7
  note?: string;
8
8
  };
9
9
  export declare function sandboxServiceUrlExposure(driver: SandboxDriverID): SandboxServiceUrlExposure;
10
- export declare function sandboxServiceUrlExposureEnv(driver: SandboxDriverID): Record<string, string>;
@@ -1,15 +1,3 @@
1
- // src/runtime-env.ts
2
- function workspaceRuntimeServiceExposureEnv(input) {
3
- return {
4
- ...input.driver === "docker" ? { WORKSPACE_RUNTIME_HOST: "0.0.0.0" } : {},
5
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_SOURCE: input.source,
6
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_ACCESS: input.access,
7
- WORKSPACE_RUNTIME_SERVICE_EXPOSURE_DRIVER: input.driver,
8
- ...input.fallbackAccess ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_FALLBACK_ACCESS: input.fallbackAccess } : {},
9
- ...input.note ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_NOTE: input.note } : {}
10
- };
11
- }
12
-
13
1
  // src/service-url-exposure.ts
14
2
  function sandboxServiceUrlExposure(driver) {
15
3
  if (driver === "docker") {
@@ -35,10 +23,6 @@ function sandboxServiceUrlExposure(driver) {
35
23
  driver
36
24
  };
37
25
  }
38
- function sandboxServiceUrlExposureEnv(driver) {
39
- return workspaceRuntimeServiceExposureEnv(sandboxServiceUrlExposure(driver));
40
- }
41
26
  export {
42
- sandboxServiceUrlExposure,
43
- sandboxServiceUrlExposureEnv
27
+ sandboxServiceUrlExposure
44
28
  };