@claxedo/sandbox-manager 0.6.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.
Files changed (41) hide show
  1. package/README.md +60 -15
  2. package/dist/checkpoint-manager.d.ts +47 -0
  3. package/dist/checkpoint-manager.mjs +116 -0
  4. package/dist/daytona-allow-list.d.ts +11 -0
  5. package/dist/daytona-allow-list.mjs +5 -1
  6. package/dist/driver-catalog.d.ts +5 -52
  7. package/dist/driver-catalog.mjs +263 -121
  8. package/dist/drivers/box.mjs +309 -39
  9. package/dist/drivers/cloudflare.d.ts +18 -7
  10. package/dist/drivers/cloudflare.mjs +423 -34
  11. package/dist/drivers/daytona.d.ts +82 -13
  12. package/dist/drivers/daytona.mjs +495 -45
  13. package/dist/drivers/docker.mjs +284 -38
  14. package/dist/drivers/exe.d.ts +26 -0
  15. package/dist/drivers/exe.mjs +492 -0
  16. package/dist/drivers/fetch-bridge.mjs +29 -5
  17. package/dist/drivers/modal.d.ts +2 -5
  18. package/dist/drivers/modal.mjs +296 -46
  19. package/dist/drivers/transient-error.d.ts +11 -0
  20. package/dist/drivers/vercel.d.ts +27 -11
  21. package/dist/drivers/vercel.mjs +339 -59
  22. package/dist/hosted-network-policy.d.ts +87 -0
  23. package/dist/hosted-network-policy.mjs +61 -0
  24. package/dist/image-name.mjs +34 -0
  25. package/dist/image.d.ts +18 -3
  26. package/dist/image.mjs +47 -32
  27. package/dist/index.d.ts +281 -17
  28. package/dist/index.mjs +393 -26
  29. package/dist/json.d.ts +13 -0
  30. package/dist/lease-policy.d.ts +6 -3
  31. package/dist/lease-policy.mjs +213 -54
  32. package/dist/lease-types.d.ts +17 -3
  33. package/dist/runtime-env.d.ts +12 -1
  34. package/dist/runtime-env.mjs +14 -0
  35. package/dist/runtime-version.mjs +1 -9
  36. package/dist/service-url-exposure.d.ts +1 -2
  37. package/dist/service-url-exposure.mjs +1 -17
  38. package/dist/stores/memory.mjs +39 -4
  39. package/package.json +67 -51
  40. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  41. package/dist/drivers/cloudflare-egress.mjs +0 -113
package/dist/index.mjs CHANGED
@@ -1,7 +1,217 @@
1
1
  // src/constants.ts
2
2
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
3
3
 
4
+ // src/checkpoint-manager.ts
5
+ async function captureSandboxCheckpoint(input) {
6
+ const lease = await requireLease(input.leaseStore, input.workspaceId);
7
+ if (lease.status !== "ready") throw new Error("workspace_checkpoint_lease_not_ready");
8
+ const persistence = lease.persistence;
9
+ if (!persistence || persistence.capture === "none") throw new Error("workspace_checkpoint_unsupported");
10
+ const target = await input.target(input.workspaceId);
11
+ if (target.status !== "ready") throw new Error(target.reason);
12
+ await input.request.runtime.freeze(input.request.policy ?? "drain");
13
+ let capturedSource = false;
14
+ try {
15
+ await input.request.runtime.flush();
16
+ await input.request.runtime.scrub();
17
+ const captured = persistence.capture === "same-resource" ? { ok: true, snapshotId: target.driverResourceId ?? target.sandboxId } : await input.snapshot(input.workspaceId);
18
+ if (!captured.ok) throw new Error(captured.reason);
19
+ capturedSource = true;
20
+ const capturedAt = (input.now ?? Date.now)();
21
+ const checkpoint = {
22
+ id: input.checkpointId?.() ?? `cp_${lease.epoch}_${crypto.randomUUID()}`,
23
+ providerReference: captured.snapshotId,
24
+ sourceEpoch: lease.epoch,
25
+ capturedAt,
26
+ metadata: {
27
+ scope: persistence.capture,
28
+ sourceBehavior: requireCaptureSource(persistence.captureSource),
29
+ ...input.request.retentionExpiresAt === void 0 ? {} : { retentionExpiresAt: input.request.retentionExpiresAt },
30
+ restoreMount: requireRestoreMount(persistence.restoreMount)
31
+ }
32
+ };
33
+ const updated = await input.leaseStore.update(input.workspaceId, lease.epoch, {
34
+ checkpoint,
35
+ restore: null,
36
+ ...persistence.captureSource === "stopped" || persistence.captureSource === "deleted" ? { status: "stopped" } : {}
37
+ });
38
+ if (!updated) throw new Error("workspace_checkpoint_epoch_fenced");
39
+ return { status: "ready", lease: updated, checkpoint };
40
+ } finally {
41
+ if (!capturedSource || persistence.captureSource === "preserved") {
42
+ await input.request.runtime.resume();
43
+ }
44
+ }
45
+ }
46
+ async function restoreSandboxCheckpoint(input) {
47
+ const lease = await requireLease(input.leaseStore, input.workspaceId);
48
+ const checkpoint = lease.checkpoint;
49
+ if (!checkpoint || input.request.checkpointId && checkpoint.id !== input.request.checkpointId) {
50
+ throw new Error("workspace_checkpoint_not_found");
51
+ }
52
+ if (lease.restore?.state === "ready" && lease.restore.checkpointId === checkpoint.id) {
53
+ return { status: "ready", lease, checkpoint };
54
+ }
55
+ const requestedAt = lease.restore?.checkpointId === checkpoint.id ? lease.restore.requestedAt : (input.now ?? Date.now)();
56
+ const restoring = lease.restore?.checkpointId === checkpoint.id && lease.restore.state !== "pending" ? lease.restore : {
57
+ checkpointId: checkpoint.id,
58
+ sourceEpoch: checkpoint.sourceEpoch,
59
+ state: "restoring",
60
+ requestedAt,
61
+ startedAt: (input.now ?? Date.now)()
62
+ };
63
+ const staged = lease.status === "ready" && lease.epoch === checkpoint.sourceEpoch ? await input.leaseStore.update(input.workspaceId, lease.epoch, { status: "stopped", restore: restoring }) : await input.leaseStore.update(input.workspaceId, lease.epoch, { restore: restoring });
64
+ if (!staged) throw new Error("workspace_restore_epoch_fenced");
65
+ const result = await input.ensure(input.workspaceId, {
66
+ homeRegion: staged.homeRegion,
67
+ ...input.request.ensure,
68
+ bootSource: checkpoint.metadata.restoreMount === "same-resource" ? { kind: "default" } : { kind: "driver-snapshot", snapshotId: checkpoint.providerReference }
69
+ });
70
+ const current = await requireLease(input.leaseStore, input.workspaceId);
71
+ if (result.status === "provisioning") {
72
+ return { status: "provisioning", lease: current, checkpoint, retryAfterMs: result.retryAfterMs };
73
+ }
74
+ if (result.status !== "ready") {
75
+ const failedAt = (input.now ?? Date.now)();
76
+ await input.leaseStore.update(input.workspaceId, current.epoch, {
77
+ restore: {
78
+ checkpointId: checkpoint.id,
79
+ sourceEpoch: checkpoint.sourceEpoch,
80
+ state: "failed",
81
+ requestedAt,
82
+ startedAt: "startedAt" in restoring ? restoring.startedAt : void 0,
83
+ failedAt,
84
+ error: result.error ?? "workspace_restore_unavailable"
85
+ }
86
+ });
87
+ throw new Error(result.error ?? "workspace_restore_unavailable");
88
+ }
89
+ await input.request.runtime.reconcile({ epoch: result.epoch, checkpointId: checkpoint.id });
90
+ const ready = await input.leaseStore.update(input.workspaceId, result.epoch, {
91
+ restore: {
92
+ checkpointId: checkpoint.id,
93
+ sourceEpoch: checkpoint.sourceEpoch,
94
+ state: "ready",
95
+ requestedAt,
96
+ startedAt: "startedAt" in restoring && restoring.startedAt !== void 0 ? restoring.startedAt : (input.now ?? Date.now)(),
97
+ completedAt: (input.now ?? Date.now)()
98
+ }
99
+ });
100
+ if (!ready) throw new Error("workspace_restore_epoch_fenced");
101
+ return { status: "ready", lease: ready, checkpoint };
102
+ }
103
+ async function requireLease(store, workspaceId) {
104
+ const lease = await store.get(workspaceId);
105
+ if (!lease) throw new Error("workspace_checkpoint_lease_missing");
106
+ return lease;
107
+ }
108
+ function requireCaptureSource(value) {
109
+ if (value === "preserved" || value === "stopped" || value === "deleted") return value;
110
+ throw new Error("workspace_checkpoint_capabilities_invalid");
111
+ }
112
+ function requireRestoreMount(value) {
113
+ if (value === "same-resource" || value === "copy-on-write" || value === "new-resource") return value;
114
+ throw new Error("workspace_checkpoint_capabilities_invalid");
115
+ }
116
+
117
+ // src/hosted-network-policy.ts
118
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
119
+ "api.anthropic.com",
120
+ "api.openai.com",
121
+ "generativelanguage.googleapis.com",
122
+ "openrouter.ai",
123
+ // Model catalog + engine config, fetched on runner startup.
124
+ "models.dev",
125
+ "opencode.ai"
126
+ ];
127
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
128
+ "registry.npmjs.org",
129
+ "nodejs.org",
130
+ "pypi.org",
131
+ "files.pythonhosted.org",
132
+ "crates.io",
133
+ "static.crates.io",
134
+ "index.crates.io",
135
+ "proxy.golang.org",
136
+ "sum.golang.org"
137
+ ];
138
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
139
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
140
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
141
+ ];
142
+ function hostOf(value) {
143
+ const raw = value?.trim();
144
+ if (!raw) return void 0;
145
+ try {
146
+ const host = new URL(raw).hostname.trim().toLowerCase();
147
+ return host ? host : void 0;
148
+ } catch {
149
+ return void 0;
150
+ }
151
+ }
152
+ function sandboxSourceHost(source) {
153
+ if (!source || source.kind !== "git") return void 0;
154
+ return hostOf(source.repoUrl);
155
+ }
156
+ function hostedSandboxNetworkPolicy(input) {
157
+ const hosts = /* @__PURE__ */ new Set();
158
+ for (const entry of input.controlPlane) {
159
+ const host = hostOf(entry) ?? entry?.trim().toLowerCase();
160
+ if (host) hosts.add(host);
161
+ }
162
+ const sourceHost = sandboxSourceHost(input.source);
163
+ if (sourceHost) hosts.add(sourceHost);
164
+ for (const host of SANDBOX_BASELINE_EGRESS_HOSTS) hosts.add(host);
165
+ for (const host of input.extraHosts ?? []) {
166
+ const trimmed = host.trim().toLowerCase();
167
+ if (trimmed) hosts.add(trimmed);
168
+ }
169
+ return { mode: "restricted", hosts: [...hosts] };
170
+ }
171
+
4
172
  // src/index.ts
173
+ function validateSandboxPersistenceCapabilities(input) {
174
+ if (input.resume === "replacement-restore" && input.capture === "none") {
175
+ return { valid: false, reason: "replacement restore requires a capture source" };
176
+ }
177
+ if (input.capture === "none" && input.captureSource !== "not-applicable") {
178
+ return { valid: false, reason: "capture source behavior requires capture support" };
179
+ }
180
+ if (input.capture !== "none" && input.captureSource === "not-applicable") {
181
+ return { valid: false, reason: "capture support requires source behavior" };
182
+ }
183
+ if (input.capture === "same-resource" && input.restoreMount !== "same-resource") {
184
+ return { valid: false, reason: "same-resource capture requires same-resource restore" };
185
+ }
186
+ if (input.restoreMount === "copy-on-write" && input.capture !== "directories") {
187
+ return { valid: false, reason: "copy-on-write restore requires directory capture" };
188
+ }
189
+ return { valid: true };
190
+ }
191
+ function sandboxEgressDisposition(control, net) {
192
+ if (!net || net.mode === "allow-all") return { action: "enforce" };
193
+ if (control === "none") return { action: "withhold", reason: "sandbox_egress_uncontained" };
194
+ const hosts = net.hosts?.length ?? 0;
195
+ const cidrs = net.cidrs?.length ?? 0;
196
+ if (hosts === 0 && cidrs === 0) return { action: "enforce" };
197
+ if (hosts > 0) return { action: "enforce" };
198
+ return control === "hosts-and-cidrs" ? { action: "enforce" } : { action: "refuse", reason: "sandbox_egress_policy_unenforceable" };
199
+ }
200
+ function egressUnenforcedMessage(input) {
201
+ const scope = input.workspaceId ? `workspace ${input.workspaceId}` : "every sandbox it provisions";
202
+ const withheld = input.requested ? ` The requested allowlist (${(input.requested.hosts ?? []).length} host(s), ${(input.requested.cidrs ?? []).length} cidr(s)) was withheld, not applied.` : "";
203
+ return `[sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "${input.driver}" declares egressControl: "none", so ${scope} can reach ANY host on the internet, including attacker-controlled buckets \u2014 agent-authored code inside the sandbox has an unmonitored exfiltration path.${withheld} Select a driver that can enforce egress (daytona, vercel) to close it. See public-docs/sandbox-egress.md.`;
204
+ }
205
+ var compositionEgressWarnings = /* @__PURE__ */ new Set();
206
+ function defaultEgressUnenforcedSink(event) {
207
+ console.warn(event.message);
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
+ }
5
215
  function applySandboxLeasePatch(current, patch, updatedAt) {
6
216
  return {
7
217
  ...current,
@@ -16,14 +226,32 @@ function applySandboxLeasePatch(current, patch, updatedAt) {
16
226
  ...patch.labels === void 0 ? {} : { labels: patch.labels },
17
227
  ...patch.nextRetryAt === void 0 ? {} : { nextRetryAt: patch.nextRetryAt ?? void 0 },
18
228
  ...patch.lastError === void 0 ? {} : { lastError: patch.lastError ?? void 0 },
229
+ ...patch.checkpoint === void 0 ? {} : { checkpoint: patch.checkpoint ?? void 0 },
230
+ ...patch.persistence === void 0 ? {} : { persistence: patch.persistence ?? void 0 },
231
+ ...patch.restore === void 0 ? {} : { restore: patch.restore ?? void 0 },
19
232
  updatedAt
20
233
  };
21
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
+ }
246
+ function isSandboxListingUnsupported(err) {
247
+ return !!err && typeof err === "object" && err.listingUnsupported === true;
248
+ }
22
249
  var DEFAULT_APP_LABEL = "claxedo";
23
250
  function sandboxId(driver, workspaceId) {
24
251
  return `${driver}-${workspaceId}`;
25
252
  }
26
253
  function ensureHostInput(input) {
254
+ const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
27
255
  const labels = {
28
256
  app: input.appLabel,
29
257
  workspaceId: input.workspaceId,
@@ -37,26 +265,88 @@ function ensureHostInput(input) {
37
265
  homeRegion: input.homeRegion,
38
266
  epoch: input.lease.epoch,
39
267
  labels,
40
- bootSource: input.managerInput?.bootSource ?? { kind: "default" },
268
+ bootSource: input.managerInput?.bootSource ?? (input.lease.checkpoint && input.lease.status !== "ready" && input.driver.metadata.persistence.resume === "replacement-restore" ? { kind: "driver-snapshot", snapshotId: input.lease.checkpoint.providerReference } : { kind: "default" }),
41
269
  workspaceRoot: input.managerInput?.workspaceRoot ?? "/workspace",
42
270
  runtimeCwd: input.managerInput?.runtimeCwd,
43
271
  workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
44
272
  env: input.managerInput?.env ?? {},
45
273
  // Brokered secrets ride their own channel — NEVER merged into labels or env.
46
- ...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 } : {},
47
278
  source: input.managerInput?.source,
48
279
  exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
49
- net: input.managerInput?.net,
280
+ // Withheld, never downgraded: `undefined` here is the same shape a caller
281
+ // that asked for no containment produces, so the throwing drivers take
282
+ // their allow-all path and their throws stay intact for anyone who hands
283
+ // them a policy directly.
284
+ ...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
50
285
  snapshot: input.managerInput?.snapshot
51
286
  };
52
287
  }
53
288
  function createSandboxManager(options) {
289
+ const persistence = validateSandboxPersistenceCapabilities(options.driver.metadata.persistence);
290
+ if (!persistence.valid) throw new Error(`Invalid sandbox persistence capabilities: ${persistence.reason}`);
54
291
  const staleAfterMs = options.staleAfterMs ?? 6e4;
55
292
  const retryAfterMs = options.retryAfterMs ?? 2e3;
56
293
  const now = options.now ?? Date.now;
57
294
  const retryDelayMs = options.retryDelayMs ?? ((retryCount) => Math.min(6e4, 1e3 * 2 ** Math.max(0, retryCount - 1)));
58
295
  const maxRetryCount = options.maxRetryCount ?? Number.POSITIVE_INFINITY;
59
296
  const retryCapCooldownMs = options.retryCapCooldownMs ?? 10 * 6e4;
297
+ const egressControl = options.driver.metadata.egressControl;
298
+ const onEgressUnenforced = options.onEgressUnenforced ?? defaultEgressUnenforcedSink;
299
+ const lifecycleOperations = /* @__PURE__ */ new Map();
300
+ function reportEgressUnenforced(input) {
301
+ onEgressUnenforced({
302
+ phase: "ensure",
303
+ reason: "sandbox_egress_uncontained",
304
+ driver: options.driver.id,
305
+ egressControl,
306
+ message: egressUnenforcedMessage({
307
+ phase: "ensure",
308
+ driver: options.driver.id,
309
+ workspaceId: input.workspaceId,
310
+ requested: input.requested
311
+ }),
312
+ workspaceId: input.workspaceId,
313
+ requested: input.requested
314
+ });
315
+ }
316
+ if (egressControl === "none") {
317
+ const key = `${options.driver.id}|${egressControl}`;
318
+ if (!compositionEgressWarnings.has(key)) {
319
+ compositionEgressWarnings.add(key);
320
+ onEgressUnenforced({
321
+ phase: "composition",
322
+ reason: "sandbox_egress_uncontained",
323
+ driver: options.driver.id,
324
+ egressControl,
325
+ message: egressUnenforcedMessage({ phase: "composition", driver: options.driver.id })
326
+ });
327
+ }
328
+ }
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);
334
+ return operation.finally(() => {
335
+ if (lifecycleOperations.get(workspaceId) === entry) lifecycleOperations.delete(workspaceId);
336
+ });
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
+ }
60
350
  async function leaseTarget(lease) {
61
351
  if (lease.status !== "ready" || !lease.sandboxId || !lease.url || !lease.hostId) {
62
352
  return { status: "unavailable", reason: "runtime_lease_not_ready" };
@@ -77,6 +367,12 @@ function createSandboxManager(options) {
77
367
  homeRegion: lease.homeRegion
78
368
  };
79
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
+ }
80
376
  async function provision(workspaceId, lease, homeRegion, managerInput) {
81
377
  try {
82
378
  const ensure = ensureHostInput({
@@ -95,7 +391,8 @@ function createSandboxManager(options) {
95
391
  homeRegion
96
392
  };
97
393
  }
98
- 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);
99
396
  if ("provisioning" in target) {
100
397
  await options.leaseStore.update(workspaceId, lease.epoch, {
101
398
  status: "acquiring",
@@ -105,7 +402,11 @@ function createSandboxManager(options) {
105
402
  status: "provisioning",
106
403
  retryAfterMs: target.retryAfterMs,
107
404
  epoch: lease.epoch,
108
- 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"
109
410
  };
110
411
  }
111
412
  const updated = await options.leaseStore.update(workspaceId, lease.epoch, {
@@ -117,7 +418,8 @@ function createSandboxManager(options) {
117
418
  retryCount: 0,
118
419
  nextRetryAt: null,
119
420
  lastError: null,
120
- labels: target.labels ?? ensure.labels
421
+ labels: target.labels ?? ensure.labels,
422
+ persistence: options.driver.metadata.persistence
121
423
  });
122
424
  const resolved = await leaseTarget(updated ?? lease);
123
425
  if (resolved.status === "ready") return resolved;
@@ -153,6 +455,13 @@ function createSandboxManager(options) {
153
455
  }
154
456
  return {
155
457
  async ensure(workspaceId, input) {
458
+ const egress = sandboxEgressDisposition(options.driver.metadata.egressControl, input.net);
459
+ if (egress.action === "withhold" && input.net) {
460
+ reportEgressUnenforced({ workspaceId, requested: input.net });
461
+ }
462
+ if (egress.action === "refuse") {
463
+ return { status: "unavailable", error: egress.reason, homeRegion: input.homeRegion };
464
+ }
156
465
  const existing = await options.leaseStore.get(workspaceId);
157
466
  if (existing?.nextRetryAt && existing.nextRetryAt > now()) {
158
467
  if (existing.status === "acquiring") {
@@ -160,7 +469,8 @@ function createSandboxManager(options) {
160
469
  status: "provisioning",
161
470
  retryAfterMs: existing.nextRetryAt - now(),
162
471
  epoch: existing.epoch,
163
- homeRegion: existing.homeRegion
472
+ homeRegion: existing.homeRegion,
473
+ bootMode: leaseBootMode(existing)
164
474
  };
165
475
  }
166
476
  return {
@@ -196,7 +506,8 @@ function createSandboxManager(options) {
196
506
  status: "provisioning",
197
507
  retryAfterMs: acquired.retryAfterMs,
198
508
  epoch: acquired.lease.epoch,
199
- homeRegion: acquired.lease.homeRegion
509
+ homeRegion: acquired.lease.homeRegion,
510
+ bootMode: leaseBootMode(acquired.lease)
200
511
  };
201
512
  }
202
513
  return provision(workspaceId, acquired.lease, input.homeRegion, input);
@@ -224,23 +535,50 @@ function createSandboxManager(options) {
224
535
  if (!options.driver.snapshot) return { ok: false, reason: "snapshot_unsupported" };
225
536
  return { ok: true, ...await options.driver.snapshot(target) };
226
537
  },
538
+ async checkpoint(workspaceId, input) {
539
+ return await checkpointLifecycle(workspaceId, "checkpoint", () => captureSandboxCheckpoint({
540
+ workspaceId,
541
+ request: input,
542
+ leaseStore: options.leaseStore,
543
+ target: (id) => this.target(id),
544
+ snapshot: (id) => this.snapshot(id),
545
+ now
546
+ }));
547
+ },
548
+ async restore(workspaceId, input) {
549
+ return await checkpointLifecycle(workspaceId, "restore", () => restoreSandboxCheckpoint({
550
+ workspaceId,
551
+ request: input,
552
+ leaseStore: options.leaseStore,
553
+ ensure: (id, next) => this.ensure(id, next),
554
+ now
555
+ }));
556
+ },
227
557
  async stop(workspaceId) {
228
- const target = await this.target(workspaceId);
229
- if (target.status !== "ready") return { ok: false, reason: target.reason };
230
- if (options.driver.metadata.hostStopBehavior !== "not-supported") {
231
- await (options.driver.suspend ?? options.driver.stop)?.(target);
232
- } else {
233
- await options.driver.stop?.(target);
234
- }
235
- await options.leaseStore.update(workspaceId, target.epoch, { status: "stopped" });
236
- return { ok: true, status: "stopped" };
558
+ return await mutationLifecycle(workspaceId, "stop", async () => {
559
+ const lease = await options.leaseStore.get(workspaceId);
560
+ if (lease?.status === "stopped") return { ok: true, status: "stopped" };
561
+ const target = await this.target(workspaceId);
562
+ if (target.status !== "ready") return { ok: false, reason: target.reason };
563
+ if (options.driver.metadata.hostStopBehavior !== "not-supported") {
564
+ await (options.driver.suspend ?? options.driver.stop)?.(target);
565
+ } else {
566
+ await options.driver.stop?.(target);
567
+ }
568
+ await options.leaseStore.update(workspaceId, target.epoch, { status: "stopped" });
569
+ return { ok: true, status: "stopped" };
570
+ });
237
571
  },
238
572
  async destroy(workspaceId) {
239
- const target = await this.target(workspaceId);
240
- if (target.status !== "ready") return { ok: false, reason: target.reason };
241
- await options.driver.destroy?.(target);
242
- await options.leaseStore.update(workspaceId, target.epoch, { status: "destroyed" });
243
- return { ok: true, status: "destroyed" };
573
+ return await mutationLifecycle(workspaceId, "destroy", async () => {
574
+ const lease = await options.leaseStore.get(workspaceId);
575
+ if (lease?.status === "destroyed") return { ok: true, status: "destroyed" };
576
+ const target = await this.target(workspaceId);
577
+ if (target.status !== "ready") return { ok: false, reason: target.reason };
578
+ await options.driver.destroy?.(target);
579
+ await options.leaseStore.update(workspaceId, target.epoch, { status: "destroyed" });
580
+ return { ok: true, status: "destroyed" };
581
+ });
244
582
  },
245
583
  async release(workspaceId) {
246
584
  const lease = await options.leaseStore.get(workspaceId);
@@ -254,7 +592,23 @@ function createSandboxManager(options) {
254
592
  destroyed: [],
255
593
  kept: [],
256
594
  skipped: [],
257
- failed: []
595
+ failed: [],
596
+ listingUnsupported: true,
597
+ driver: options.driver.id
598
+ };
599
+ }
600
+ let listed;
601
+ try {
602
+ listed = await options.driver.list();
603
+ } catch (err) {
604
+ if (!isSandboxListingUnsupported(err)) throw err;
605
+ return {
606
+ destroyed: [],
607
+ kept: [],
608
+ skipped: [],
609
+ failed: [],
610
+ listingUnsupported: true,
611
+ driver: options.driver.id
258
612
  };
259
613
  }
260
614
  const leases = new Map((await options.leaseStore.list()).map((lease) => [lease.workspaceId, lease]));
@@ -265,7 +619,7 @@ function createSandboxManager(options) {
265
619
  failed: []
266
620
  };
267
621
  const appLabel = options.appLabel ?? DEFAULT_APP_LABEL;
268
- for (const target of await options.driver.list()) {
622
+ for (const target of listed) {
269
623
  if (target.labels?.app !== appLabel) {
270
624
  result.skipped.push({ target, reason: "unmanaged_app_label" });
271
625
  continue;
@@ -306,7 +660,7 @@ function createSandboxManager(options) {
306
660
  const expectedEpoch = input.epoch ?? current.epoch;
307
661
  if (current.epoch !== expectedEpoch) return { ok: false, reason: "runtime_lease_epoch_mismatch" };
308
662
  const timestamp = input.now ?? now();
309
- 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;
310
664
  const updated = await options.leaseStore.update(workspaceId, expectedEpoch, {
311
665
  status,
312
666
  ...input.sandboxId ? { sandboxId: input.sandboxId } : {},
@@ -323,6 +677,19 @@ function createSandboxManager(options) {
323
677
  }
324
678
  export {
325
679
  DEFAULT_WORKSPACE_RUNTIME_PORT,
680
+ SANDBOX_BASELINE_EGRESS_HOSTS,
681
+ SANDBOX_MODEL_PROVIDER_HOSTS,
682
+ SANDBOX_PACKAGE_REGISTRY_HOSTS,
326
683
  applySandboxLeasePatch,
327
- createSandboxManager
684
+ brokeredPlaceholderEnv,
685
+ brokeredSecretPlaceholder,
686
+ captureSandboxCheckpoint,
687
+ createSandboxManager,
688
+ hostedSandboxNetworkPolicy,
689
+ isSandboxListingUnsupported,
690
+ restoreSandboxCheckpoint,
691
+ sandboxEgressDisposition,
692
+ sandboxNetworkPolicyWithBrokeredHosts,
693
+ sandboxSourceHost,
694
+ validateSandboxPersistenceCapabilities
328
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,6 +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;
48
- export declare function decideSandboxIdle(lease: SandboxLeaseRow, activeHoldCount: number, 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;
49
52
  export declare function nextSandboxRetryAt(retryCount: number, now: number, config?: SandboxDecisionConfig): number | null;