@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
@@ -0,0 +1,34 @@
1
+ // src/runtime-version.ts
2
+ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
+ function workspaceRuntimeVersion() {
4
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
5
+ }
6
+
7
+ // src/image-name.ts
8
+ var SNAPSHOT_SCHEMA_VERSION = 8;
9
+ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
10
+ function snapshotVersion(value) {
11
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
12
+ }
13
+ function sandboxImageRepository(env = process.env) {
14
+ const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
15
+ return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
16
+ }
17
+ function buildIdSuffix(buildId, env) {
18
+ const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
19
+ return id ? `-${snapshotVersion(id)}` : "";
20
+ }
21
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
22
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
23
+ }
24
+ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
25
+ return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
26
+ }
27
+ export {
28
+ DEFAULT_SANDBOX_IMAGE_REPOSITORY,
29
+ SNAPSHOT_SCHEMA_VERSION,
30
+ defaultSandboxImage,
31
+ defaultSnapshotName,
32
+ sandboxImageRepository,
33
+ snapshotVersion
34
+ };
package/dist/image.d.ts CHANGED
@@ -1,8 +1,23 @@
1
- import { Daytona, Image } from "@daytona/sdk";
1
+ import type { Daytona, Image } from "@daytona/sdk";
2
2
  export { RUNTIME_DIR, WORKSPACE_RUNTIME_PORT, WORKSPACE_DIR } from "./defaults";
3
3
  export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, defaultSandboxImage, defaultSnapshotName, } from "./image-name";
4
+ /**
5
+ * Sink for snapshot-build progress. Every message here reports a long
6
+ * operation an operator is actively waiting on — a snapshot build takes
7
+ * minutes, and the Daytona builder's `onLogs` stream is the only view into
8
+ * whether it is progressing or wedged.
9
+ *
10
+ * Defaults to `console.info`, deliberately unconditional: the same reasoning as
11
+ * `onEgressUnenforced` in index.ts — someone who never wires a sink still sees
12
+ * the build. Pass your own to route it into a logger; pass `() => {}` only if
13
+ * you have another way to observe the build.
14
+ */
15
+ export type SandboxImageLogSink = (message: string, context?: Record<string, unknown>) => void;
16
+ export type SandboxImageOptions = {
17
+ log?: SandboxImageLogSink;
18
+ };
4
19
  export declare const SANDBOX_IMAGE_REPOSITORY: string;
5
20
  export declare const SANDBOX_IMAGE: string;
6
21
  export declare const SNAPSHOT_NAME: string;
7
- export declare function buildSandboxImage(): Image;
8
- export declare function ensureSnapshot(daytona: Daytona): Promise<string>;
22
+ export declare function buildSandboxImage(): Promise<Image>;
23
+ export declare function ensureSnapshot(daytona: Daytona, options?: SandboxImageOptions): Promise<string>;
package/dist/image.mjs CHANGED
@@ -1,6 +1,3 @@
1
- // src/image.ts
2
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
3
-
4
1
  // src/constants.ts
5
2
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
6
3
 
@@ -11,16 +8,8 @@ var RUNTIME_DIR = "/opt/workspace-runtime";
11
8
 
12
9
  // src/runtime-version.ts
13
10
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
14
- var versionCache;
15
11
  function workspaceRuntimeVersion() {
16
- if (versionCache) return versionCache;
17
- const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
18
- if (envVersion) {
19
- versionCache = envVersion;
20
- return versionCache;
21
- }
22
- versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
23
- return versionCache;
12
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
24
13
  }
25
14
 
26
15
  // src/image-name.ts
@@ -44,61 +33,87 @@ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env =
44
33
  return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
45
34
  }
46
35
 
36
+ // ../claxedo-helpers/dist/guards.mjs
37
+ function isRecord(value) {
38
+ return value !== null && typeof value === "object" && !Array.isArray(value);
39
+ }
40
+ function asRecord(value) {
41
+ return isRecord(value) ? value : void 0;
42
+ }
43
+
44
+ // src/json.ts
45
+ function text(input) {
46
+ return typeof input === "string" && input.length > 0 ? input : void 0;
47
+ }
48
+
47
49
  // src/image.ts
48
- var log = {
49
- info(_message, _context) {
50
- }
51
- };
50
+ function defaultImageLogSink(message, context) {
51
+ if (context) console.info(message, context);
52
+ else console.info(message);
53
+ }
52
54
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
53
55
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
54
56
  var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
55
- function buildSandboxImage() {
57
+ async function buildSandboxImage() {
58
+ const { Image } = await import("@daytona/sdk");
56
59
  return Image.base(SANDBOX_IMAGE);
57
60
  }
58
61
  function snapshotUsable(state) {
59
62
  return state === "ready" || state === "active";
60
63
  }
61
- async function createSnapshot(daytona) {
62
- log.info("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
64
+ function snapshotView(input) {
65
+ const row = asRecord(input) ?? {};
66
+ return { id: text(row.id), state: text(row.state), errorReason: text(row.errorReason) };
67
+ }
68
+ async function activateSnapshot(snapshots, id) {
69
+ const activate = asRecord(snapshots)?.activate;
70
+ if (typeof activate !== "function") throw new Error("Daytona SDK does not support snapshot activation");
71
+ await activate.call(snapshots, { id });
72
+ }
73
+ async function createSnapshot(daytona, log) {
74
+ log("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
63
75
  await daytona.snapshot.create(
64
- { name: SNAPSHOT_NAME, image: buildSandboxImage() },
76
+ { name: SNAPSHOT_NAME, image: await buildSandboxImage() },
65
77
  {
66
78
  timeout: 120,
67
- onLogs: (chunk) => log.info("[snapshot]", { text: chunk })
79
+ onLogs: (chunk) => log("[snapshot]", { text: chunk })
68
80
  }
69
81
  );
70
82
  return SNAPSHOT_NAME;
71
83
  }
72
- async function ensureSnapshot(daytona) {
84
+ async function ensureSnapshot(daytona, options = {}) {
85
+ const log = options.log ?? defaultImageLogSink;
73
86
  try {
74
- const existing = await daytona.snapshot.get(SNAPSHOT_NAME);
87
+ const existing = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
75
88
  if (existing.state === "inactive") {
76
- log.info("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
77
- await daytona.snapshot.activate({ id: existing.id });
89
+ log("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
90
+ if (!existing.id) throw new Error("Daytona snapshot is inactive but reports no id to activate");
91
+ await activateSnapshot(daytona.snapshot, existing.id);
78
92
  for (let i = 0; i < 120; i++) {
79
- const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
93
+ const snap = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
80
94
  if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
81
- if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason}`);
95
+ if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason ?? "unknown"}`);
82
96
  await new Promise((resolve) => setTimeout(resolve, 2e3));
83
97
  }
84
98
  throw new Error("Snapshot activation timed out");
85
99
  }
86
100
  if (existing.state === "building") {
87
- log.info("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
101
+ log("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
88
102
  for (let i = 0; i < 300; i++) {
89
- const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
103
+ const snap = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
90
104
  if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
91
- if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason}`);
92
- if (i % 15 === 0) log.info("Still waiting for snapshot...", { state: snap.state, elapsed: `${i * 2}s` });
105
+ if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason ?? "unknown"}`);
106
+ if (i % 15 === 0) log("Still waiting for snapshot...", { state: snap.state, elapsed: `${i * 2}s` });
93
107
  await new Promise((resolve) => setTimeout(resolve, 2e3));
94
108
  }
95
109
  throw new Error("Snapshot build timed out after 10 minutes");
96
110
  }
97
111
  return SNAPSHOT_NAME;
98
112
  } catch (err) {
113
+ const { DaytonaNotFoundError } = await import("@daytona/sdk");
99
114
  if (!(err instanceof DaytonaNotFoundError)) throw err;
100
115
  }
101
- return createSnapshot(daytona);
116
+ return createSnapshot(daytona, log);
102
117
  }
103
118
  export {
104
119
  RUNTIME_DIR,
package/dist/index.d.ts CHANGED
@@ -1,8 +1,65 @@
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";
4
+ import { type SandboxCheckpointCaptureInput, type SandboxCheckpointRestoreInput, type SandboxCheckpointResult } from "./checkpoint-manager";
3
5
  export { DEFAULT_WORKSPACE_RUNTIME_PORT };
6
+ export * from "./checkpoint-manager";
7
+ export * from "./hosted-network-policy";
4
8
  export type SandboxRegion = string;
5
9
  export type SandboxDriverId = SandboxDriverID | "fetch";
10
+ export type SandboxCaptureScope = "none" | "same-resource" | "filesystem" | "directories";
11
+ export type SandboxCaptureSourceBehavior = "not-applicable" | "preserved" | "stopped" | "deleted";
12
+ export type SandboxRestoreMountBehavior = "not-applicable" | "same-resource" | "copy-on-write" | "new-resource";
13
+ export type SandboxPersistenceCapabilities = {
14
+ resume: "same-sandbox" | "replacement-restore";
15
+ capture: SandboxCaptureScope;
16
+ clone: boolean;
17
+ captureSource: SandboxCaptureSourceBehavior;
18
+ retention: "not-applicable" | "provider-managed" | "explicit";
19
+ restoreMount: SandboxRestoreMountBehavior;
20
+ };
21
+ export declare function validateSandboxPersistenceCapabilities(input: SandboxPersistenceCapabilities): {
22
+ valid: false;
23
+ reason: string;
24
+ } | {
25
+ reason?: undefined;
26
+ valid: true;
27
+ };
28
+ export type SandboxCheckpointReference = {
29
+ id: string;
30
+ providerReference: string;
31
+ sourceEpoch: number;
32
+ capturedAt: number;
33
+ metadata: {
34
+ scope: Exclude<SandboxCaptureScope, "none">;
35
+ sourceBehavior: Exclude<SandboxCaptureSourceBehavior, "not-applicable">;
36
+ retentionExpiresAt?: number;
37
+ restoreMount: Exclude<SandboxRestoreMountBehavior, "not-applicable">;
38
+ };
39
+ };
40
+ type SandboxRestoreIdentity = {
41
+ checkpointId: string;
42
+ sourceEpoch: number;
43
+ };
44
+ export type SandboxRestoreStatus = (SandboxRestoreIdentity & {
45
+ state: "pending";
46
+ requestedAt: number;
47
+ }) | (SandboxRestoreIdentity & {
48
+ state: "restoring";
49
+ requestedAt: number;
50
+ startedAt: number;
51
+ }) | (SandboxRestoreIdentity & {
52
+ state: "ready";
53
+ requestedAt: number;
54
+ startedAt: number;
55
+ completedAt: number;
56
+ }) | (SandboxRestoreIdentity & {
57
+ state: "failed";
58
+ requestedAt: number;
59
+ startedAt?: number;
60
+ failedAt: number;
61
+ error: string;
62
+ });
6
63
  export type SandboxDriverMetadata = {
7
64
  /** Where the sandbox driver code can execute. This is not sandbox infrastructure. */
8
65
  driverRunsIn: Array<"worker" | "node" | "local">;
@@ -16,25 +73,110 @@ export type SandboxDriverMetadata = {
16
73
  * How the driver can honor a `SandboxBrokeredSecret` — a credential the
17
74
  * sandbox may USE for outbound requests but must NEVER be able to READ.
18
75
  *
19
- * - `"native"` — the provider brokers it automatically on egress to the
20
- * allowlisted hosts, with no extra infrastructure: Daytona secret
21
- * placeholders + egress proxy; Vercel firewall header transforms. The
22
- * driver injects it during `ensureHost`.
23
- * - `"proxy"` — brokering is achievable but only via a host-operated egress
24
- * proxy the driver routes through (Cloudflare's official Worker-proxy
25
- * pattern: the sandbox gets a short-lived JWT, egress hits the Worker,
26
- * the Worker injects the real credential — the raw value never enters the
27
- * 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`.
28
80
  * - `"none"` — no way to keep the value out of sandbox processes. The
29
81
  * provider may still have an encrypted secret STORE (e.g. Modal secrets),
30
- * 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
31
83
  * never-readable contract.
84
+ */
85
+ secretBrokering: SandboxSecretBrokering;
86
+ /**
87
+ * How the driver can enforce a RESTRICTED `SandboxNetworkPolicy` — i.e.
88
+ * whether the sandbox's outbound network can actually be contained.
89
+ *
90
+ * A `SandboxNetworkPolicy` states the same allowance in up to two encodings:
91
+ * `hosts` (names) and `cidrs` (addresses). A driver contains egress if it
92
+ * enforces AT LEAST ONE encoding the policy carries and blocks everything
93
+ * else; the encodings are alternatives, not additive requirements.
94
+ *
95
+ * - `"hosts-and-cidrs"` — the provider filters by name AND by address
96
+ * (Daytona: `domainAllowList` + `networkAllowList`).
97
+ * - `"hosts"` — the provider filters by hostname only (Vercel firewall).
98
+ * - `"none"` — the driver cannot express an egress allowlist. This covers
99
+ * BOTH drivers that throw when handed a restricted policy (exe, docker,
100
+ * box, and modal for host policy) AND — more dangerously — drivers that
101
+ * silently ignore `net` and run wide open (cloudflare, the fetch bridge).
102
+ * Modal is `"none"` even though it can cut the network entirely: a total
103
+ * blackout is not an allowlist and cannot serve a workspace that has to
104
+ * clone a repo and reach a model provider.
32
105
  *
33
- * The manager fails closed unless the driver actually injects brokered
34
- * secrets (today: `"native"` only). `"proxy"` records an achievable ceiling
35
- * that is not yet wired and therefore also fails closed.
106
+ * The manager reads this to decide what the driver is handed:
107
+ * `sandboxEgressDisposition` enforces where it can and WITHHOLDS (loudly)
108
+ * where it cannot. See `SandboxEgressDisposition` for the posture and
109
+ * `public-docs/sandbox-egress.md` for the operator-facing consequence.
36
110
  */
37
- secretBrokering: "native" | "proxy" | "none";
111
+ egressControl: SandboxEgressControl;
112
+ persistence: SandboxPersistenceCapabilities;
113
+ };
114
+ /** @see SandboxDriverMetadata.egressControl */
115
+ export type SandboxEgressControl = "none" | "hosts" | "hosts-and-cidrs";
116
+ /**
117
+ * What the manager does with a caller's `net` for a given driver.
118
+ *
119
+ * Owner directive (2026-07-28): *"for egress in sandbox enforce where we can
120
+ * and document where we cant."* That replaces the previous fail-closed refusal
121
+ * for drivers with no egress control at all — provisioning now proceeds, and
122
+ * the gap is made loud instead of fatal.
123
+ *
124
+ * - `"enforce"` — the driver gets the policy verbatim and contains the
125
+ * sandbox. Also the answer when the caller asked for no containment
126
+ * (absent / `allow-all`), which is nothing to enforce.
127
+ * - `"withhold"` — the driver declares `egressControl: "none"`, so there is no
128
+ * containment to be had. The policy is NOT handed down: half the `"none"`
129
+ * drivers throw on a restricted policy (exe, docker, modal, box) and the
130
+ * other half silently drop it (cloudflare, the fetch bridge). Withholding at
131
+ * the manager means the throwing drivers never see one — their throws stay
132
+ * in place as their own last line of defence — and the silently-dropping
133
+ * ones stop pretending. The sandbox comes up with UNRESTRICTED egress, which
134
+ * is a real exposure, so the manager warns (see `onEgressUnenforced`).
135
+ * - `"refuse"` — the driver HAS egress control but cannot express this
136
+ * particular policy's encoding (a hosts-only driver handed an address-only
137
+ * allowlist). Deliberately still fail-closed: this is a composition mistake
138
+ * on an otherwise-enforcing driver, and degrading it to "withhold" would
139
+ * weaken the enforcing path. `hostedSandboxNetworkPolicy` only ever emits
140
+ * hosts, so no production path reaches it.
141
+ *
142
+ * Pure, like `validateSandboxPersistenceCapabilities`, so the manager, the
143
+ * composition layer, and tests can all ask the same question.
144
+ */
145
+ export type SandboxEgressDisposition = {
146
+ action: "enforce";
147
+ } | {
148
+ action: "withhold";
149
+ reason: "sandbox_egress_uncontained";
150
+ } | {
151
+ action: "refuse";
152
+ reason: "sandbox_egress_policy_unenforceable";
153
+ };
154
+ /** @see SandboxEgressDisposition */
155
+ export declare function sandboxEgressDisposition(control: SandboxEgressControl, net: SandboxNetworkPolicy | undefined): SandboxEgressDisposition;
156
+ /**
157
+ * Emitted whenever a restricted egress policy is withheld because the composed
158
+ * driver declares no egress control. This is the "document where we can't" half
159
+ * of the directive expressed at runtime: an unrestricted sandbox that nobody
160
+ * was told about is the original finding, so the degrade is never silent.
161
+ *
162
+ * Two phases, because a per-create line is easy to miss in request logs:
163
+ * - `"composition"` — once per `createSandboxManager` per driver, at boot.
164
+ * - `"ensure"` — every time a caller's policy is actually withheld, naming the
165
+ * workspace and the allowlist that did not take effect.
166
+ */
167
+ export type SandboxEgressUnenforcedEvent = {
168
+ phase: "composition" | "ensure";
169
+ reason: "sandbox_egress_uncontained";
170
+ /** `SandboxDriver.id` of the composed driver. */
171
+ driver: string;
172
+ /** Always `"none"` today — carried so a sink can group without re-deriving. */
173
+ egressControl: SandboxEgressControl;
174
+ /** Ready-to-log sentence. Sinks that only forward text can use this alone. */
175
+ message: string;
176
+ /** Present on `"ensure"`. */
177
+ workspaceId?: string;
178
+ /** The allowlist the caller asked for and did NOT get. Present on `"ensure"`. */
179
+ requested?: SandboxNetworkPolicy;
38
180
  };
39
181
  /**
40
182
  * A credential the SANDBOX must be able to USE for outbound requests but must
@@ -46,7 +188,7 @@ export type SandboxDriverMetadata = {
46
188
  *
47
189
  * Guarantees enforced by the manager: brokered secrets are never written to
48
190
  * labels, never logged, and never captured in a driver snapshot. A driver
49
- * whose `metadata.secretBrokering !== "native"` cannot honor them and the
191
+ * whose `metadata.secretBrokering` is not `"native"` cannot honor them and the
50
192
  * manager refuses to provision (fail-closed).
51
193
  */
52
194
  export type SandboxBrokeredSecret = {
@@ -63,7 +205,49 @@ export type SandboxBrokeredSecret = {
63
205
  * the sandbox to attach itself (Daytona).
64
206
  */
65
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[];
66
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>;
67
251
  export type SandboxExposure = {
68
252
  kind: "loopback";
69
253
  } | {
@@ -92,6 +276,9 @@ export type SandboxLease = {
92
276
  lastHeartbeatAt?: number;
93
277
  lastActivityAt?: number;
94
278
  labels?: Record<string, string>;
279
+ checkpoint?: SandboxCheckpointReference;
280
+ persistence?: SandboxPersistenceCapabilities;
281
+ restore?: SandboxRestoreStatus;
95
282
  };
96
283
  export type SandboxLeaseAcquireInput = {
97
284
  homeRegion: SandboxRegion;
@@ -112,6 +299,12 @@ export type SandboxLeasePatch = Partial<Pick<SandboxLease, "status" | "sandboxId
112
299
  nextRetryAt?: number | null;
113
300
  /** `null` clears the stored value; `undefined` leaves it unchanged. */
114
301
  lastError?: string | null;
302
+ /** `null` clears the stored value; `undefined` leaves it unchanged. */
303
+ checkpoint?: SandboxCheckpointReference | null;
304
+ /** `null` clears the stored value; `undefined` leaves it unchanged. */
305
+ persistence?: SandboxPersistenceCapabilities | null;
306
+ /** `null` clears the stored value; `undefined` leaves it unchanged. */
307
+ restore?: SandboxRestoreStatus | null;
115
308
  };
116
309
  /**
117
310
  * Canonical SandboxLeasePatch merge semantics shared by every lease store driver:
@@ -131,6 +324,8 @@ export type SandboxNetworkPolicy = {
131
324
  hosts?: string[];
132
325
  cidrs?: string[];
133
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;
134
329
  export type SandboxBootSource = {
135
330
  kind: "image";
136
331
  image: string;
@@ -162,7 +357,16 @@ export type SandboxDriverEnsureInput = {
162
357
  secrets?: SandboxBrokeredSecret[];
163
358
  source?: SandboxSource;
164
359
  exposure?: SandboxExposure;
165
- /** Optional egress policy (allow-all when omitted). */
360
+ /**
361
+ * Egress policy. Omitted means allow-all, which is only appropriate for a
362
+ * single-tenant deployment the operator already trusts.
363
+ *
364
+ * A driver can rely on this being ABSENT whenever it declares
365
+ * `egressControl: "none"`: the manager withholds the policy rather than hand
366
+ * it to a driver that would either throw on it or silently drop it. A driver
367
+ * that receives a restricted policy here is one that declared it can enforce
368
+ * it. @see SandboxEgressDisposition
369
+ */
166
370
  net?: SandboxNetworkPolicy;
167
371
  /** Optional snapshot/image to boot from (user-supplied or pre-built). */
168
372
  snapshot?: string;
@@ -183,6 +387,11 @@ export type SandboxTarget = {
183
387
  export type SandboxSnapshotResult = {
184
388
  snapshotId: string;
185
389
  };
390
+ export type SandboxCommandResult = {
391
+ stdout: string;
392
+ stderr: string;
393
+ exitCode: number;
394
+ };
186
395
  export type SandboxDriver = {
187
396
  id: string;
188
397
  metadata: SandboxDriverMetadata;
@@ -203,6 +412,11 @@ export type SandboxDriver = {
203
412
  stop?: (target: SandboxTarget) => Promise<void>;
204
413
  destroy?: (target: SandboxTarget) => Promise<void>;
205
414
  snapshot?: (target: SandboxTarget) => Promise<SandboxSnapshotResult>;
415
+ inspect?: (target: SandboxTarget) => Promise<SandboxTarget | undefined>;
416
+ exec?: (target: SandboxTarget, command: string) => Promise<SandboxCommandResult>;
417
+ clone?: (target: SandboxTarget, input: {
418
+ name: string;
419
+ }) => Promise<SandboxTarget>;
206
420
  };
207
421
  export type SandboxManager = {
208
422
  ensure: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
@@ -211,6 +425,8 @@ export type SandboxManager = {
211
425
  target: (workspaceId: string) => Promise<SandboxTargetResult>;
212
426
  touch: (workspaceId: string) => Promise<SandboxTouchResult>;
213
427
  snapshot: (workspaceId: string) => Promise<SandboxSnapshotManagerResult>;
428
+ checkpoint: (workspaceId: string, input: SandboxCheckpointCaptureInput) => Promise<SandboxCheckpointResult>;
429
+ restore: (workspaceId: string, input: SandboxCheckpointRestoreInput) => Promise<SandboxCheckpointResult>;
214
430
  stop: (workspaceId: string) => Promise<SandboxMutationResult>;
215
431
  destroy: (workspaceId: string) => Promise<SandboxMutationResult>;
216
432
  release: (workspaceId: string) => Promise<{
@@ -232,6 +448,10 @@ export type SandboxManagerInput = {
232
448
  secrets?: SandboxBrokeredSecret[];
233
449
  source?: SandboxSource;
234
450
  exposure?: SandboxExposure;
451
+ /**
452
+ * @see SandboxDriverEnsureInput.net — enforced by a capable driver, withheld
453
+ * (with a warning) by one that declares `egressControl: "none"`.
454
+ */
235
455
  net?: SandboxNetworkPolicy;
236
456
  snapshot?: string;
237
457
  };
@@ -247,6 +467,14 @@ export type SandboxRegisterInput = {
247
467
  now?: number;
248
468
  };
249
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";
250
478
  export type SandboxEnsureResult = ({
251
479
  status: "ready";
252
480
  } & SandboxTarget & {
@@ -257,6 +485,7 @@ export type SandboxEnsureResult = ({
257
485
  retryAfterMs: number;
258
486
  epoch: number;
259
487
  homeRegion: SandboxRegion;
488
+ bootMode?: SandboxBootMode;
260
489
  } | {
261
490
  status: "unavailable";
262
491
  retryAfterMs?: number;
@@ -301,7 +530,31 @@ export type SandboxGarbageCollectResult = {
301
530
  target: SandboxTarget;
302
531
  error: string;
303
532
  }>;
533
+ /**
534
+ * Set when the driver cannot enumerate provider state, so the four arrays
535
+ * above mean "we could not look" rather than "nothing is orphaned". Absent on
536
+ * a real sweep. Callers must treat it as a FAILED sweep: four empty arrays
537
+ * reported as success is the exact defect finding B1 names.
538
+ */
539
+ listingUnsupported?: true;
540
+ /** Driver id, so an ops report can name which driver could not look. */
541
+ driver?: string;
304
542
  };
543
+ /**
544
+ * A driver's `list()` may throw this shape to say "my backing service cannot
545
+ * enumerate", as distinct from "listing failed this time". `garbageCollect()`
546
+ * converts it into `listingUnsupported` (a loud, non-fatal capability gap);
547
+ * every other error propagates as a real sweep failure.
548
+ *
549
+ * Structural rather than an `instanceof` check on purpose: drivers are published
550
+ * as separate entry points and bundlers duplicate class identities, so a
551
+ * nominal check would silently fall through to "transient failure" in exactly
552
+ * the deployment that needs the distinction.
553
+ */
554
+ export type SandboxListingUnsupported = {
555
+ listingUnsupported: true;
556
+ };
557
+ export declare function isSandboxListingUnsupported(err: unknown): err is SandboxListingUnsupported;
305
558
  export type SandboxManagerOptions = {
306
559
  leaseStore: SandboxLeaseStore;
307
560
  driver: SandboxDriver;
@@ -312,5 +565,16 @@ export type SandboxManagerOptions = {
312
565
  retryCapCooldownMs?: number;
313
566
  now?: () => number;
314
567
  appLabel?: string;
568
+ /**
569
+ * Sink for the loud half of "enforce where we can, document where we can't".
570
+ * Called at composition time and again every time a restricted policy is
571
+ * withheld from a driver that cannot enforce it.
572
+ *
573
+ * Defaults to `console.warn(event.message)`. Pass your own to route the gap
574
+ * into a telemetry pipeline instead — but note the default is deliberately
575
+ * unconditional: an operator who never wires a sink still gets the warning.
576
+ * Pass `() => {}` only if you have another way to surface it.
577
+ */
578
+ onEgressUnenforced?: (event: SandboxEgressUnenforcedEvent) => void;
315
579
  };
316
580
  export declare function createSandboxManager(options: SandboxManagerOptions): SandboxManager;