@claxedo/sandbox-manager 0.5.2 → 0.7.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.
@@ -0,0 +1,61 @@
1
+ // src/hosted-network-policy.ts
2
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
3
+ "api.anthropic.com",
4
+ "api.openai.com",
5
+ "generativelanguage.googleapis.com",
6
+ "openrouter.ai",
7
+ // Model catalog + engine config, fetched on runner startup.
8
+ "models.dev",
9
+ "opencode.ai"
10
+ ];
11
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
12
+ "registry.npmjs.org",
13
+ "nodejs.org",
14
+ "pypi.org",
15
+ "files.pythonhosted.org",
16
+ "crates.io",
17
+ "static.crates.io",
18
+ "index.crates.io",
19
+ "proxy.golang.org",
20
+ "sum.golang.org"
21
+ ];
22
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
23
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
24
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
25
+ ];
26
+ function hostOf(value) {
27
+ const raw = value?.trim();
28
+ if (!raw) return;
29
+ try {
30
+ const host = new URL(raw).hostname.trim().toLowerCase();
31
+ return host ? host : void 0;
32
+ } catch {
33
+ return;
34
+ }
35
+ }
36
+ function sandboxSourceHost(source) {
37
+ if (!source || source.kind !== "git") return;
38
+ return hostOf(source.repoUrl);
39
+ }
40
+ function hostedSandboxNetworkPolicy(input) {
41
+ const hosts = /* @__PURE__ */ new Set();
42
+ for (const entry of input.controlPlane) {
43
+ const host = hostOf(entry) ?? entry?.trim().toLowerCase();
44
+ if (host) hosts.add(host);
45
+ }
46
+ const sourceHost = sandboxSourceHost(input.source);
47
+ if (sourceHost) hosts.add(sourceHost);
48
+ for (const host of SANDBOX_BASELINE_EGRESS_HOSTS) hosts.add(host);
49
+ for (const host of input.extraHosts ?? []) {
50
+ const trimmed = host.trim().toLowerCase();
51
+ if (trimmed) hosts.add(trimmed);
52
+ }
53
+ return { mode: "restricted", hosts: [...hosts] };
54
+ }
55
+ export {
56
+ SANDBOX_BASELINE_EGRESS_HOSTS,
57
+ SANDBOX_MODEL_PROVIDER_HOSTS,
58
+ SANDBOX_PACKAGE_REGISTRY_HOSTS,
59
+ hostedSandboxNetworkPolicy,
60
+ sandboxSourceHost
61
+ };
package/dist/image.d.ts CHANGED
@@ -1,8 +1,23 @@
1
1
  import { 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
22
  export declare function buildSandboxImage(): Image;
8
- export declare function ensureSnapshot(daytona: Daytona): Promise<string>;
23
+ export declare function ensureSnapshot(daytona: Daytona, options?: SandboxImageOptions): Promise<string>;
package/dist/image.mjs CHANGED
@@ -11,16 +11,8 @@ var RUNTIME_DIR = "/opt/workspace-runtime";
11
11
 
12
12
  // src/runtime-version.ts
13
13
  var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
14
- var versionCache;
15
14
  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;
15
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
24
16
  }
25
17
 
26
18
  // src/image-name.ts
@@ -45,10 +37,10 @@ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env =
45
37
  }
46
38
 
47
39
  // src/image.ts
48
- var log = {
49
- info(_message, _context) {
50
- }
51
- };
40
+ function defaultImageLogSink(message, context) {
41
+ if (context) console.info(message, context);
42
+ else console.info(message);
43
+ }
52
44
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
53
45
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
54
46
  var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
@@ -58,22 +50,23 @@ function buildSandboxImage() {
58
50
  function snapshotUsable(state) {
59
51
  return state === "ready" || state === "active";
60
52
  }
61
- async function createSnapshot(daytona) {
62
- log.info("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
53
+ async function createSnapshot(daytona, log) {
54
+ log("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
63
55
  await daytona.snapshot.create(
64
56
  { name: SNAPSHOT_NAME, image: buildSandboxImage() },
65
57
  {
66
58
  timeout: 120,
67
- onLogs: (chunk) => log.info("[snapshot]", { text: chunk })
59
+ onLogs: (chunk) => log("[snapshot]", { text: chunk })
68
60
  }
69
61
  );
70
62
  return SNAPSHOT_NAME;
71
63
  }
72
- async function ensureSnapshot(daytona) {
64
+ async function ensureSnapshot(daytona, options = {}) {
65
+ const log = options.log ?? defaultImageLogSink;
73
66
  try {
74
67
  const existing = await daytona.snapshot.get(SNAPSHOT_NAME);
75
68
  if (existing.state === "inactive") {
76
- log.info("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
69
+ log("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
77
70
  await daytona.snapshot.activate({ id: existing.id });
78
71
  for (let i = 0; i < 120; i++) {
79
72
  const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
@@ -84,12 +77,12 @@ async function ensureSnapshot(daytona) {
84
77
  throw new Error("Snapshot activation timed out");
85
78
  }
86
79
  if (existing.state === "building") {
87
- log.info("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
80
+ log("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
88
81
  for (let i = 0; i < 300; i++) {
89
82
  const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
90
83
  if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
91
84
  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` });
85
+ if (i % 15 === 0) log("Still waiting for snapshot...", { state: snap.state, elapsed: `${i * 2}s` });
93
86
  await new Promise((resolve) => setTimeout(resolve, 2e3));
94
87
  }
95
88
  throw new Error("Snapshot build timed out after 10 minutes");
@@ -98,7 +91,7 @@ async function ensureSnapshot(daytona) {
98
91
  } catch (err) {
99
92
  if (!(err instanceof DaytonaNotFoundError)) throw err;
100
93
  }
101
- return createSnapshot(daytona);
94
+ return createSnapshot(daytona, log);
102
95
  }
103
96
  export {
104
97
  RUNTIME_DIR,
package/dist/index.d.ts CHANGED
@@ -1,8 +1,64 @@
1
1
  import { DEFAULT_WORKSPACE_RUNTIME_PORT } from "./constants";
2
2
  import type { SandboxDriverID } from "./driver-catalog";
3
+ import { type SandboxCheckpointCaptureInput, type SandboxCheckpointRestoreInput, type SandboxCheckpointResult } from "./checkpoint-manager";
3
4
  export { DEFAULT_WORKSPACE_RUNTIME_PORT };
5
+ export * from "./checkpoint-manager";
6
+ export * from "./hosted-network-policy";
4
7
  export type SandboxRegion = string;
5
8
  export type SandboxDriverId = SandboxDriverID | "fetch";
9
+ export type SandboxCaptureScope = "none" | "same-resource" | "filesystem" | "directories";
10
+ export type SandboxCaptureSourceBehavior = "not-applicable" | "preserved" | "stopped" | "deleted";
11
+ export type SandboxRestoreMountBehavior = "not-applicable" | "same-resource" | "copy-on-write" | "new-resource";
12
+ export type SandboxPersistenceCapabilities = {
13
+ resume: "same-sandbox" | "replacement-restore";
14
+ capture: SandboxCaptureScope;
15
+ clone: boolean;
16
+ captureSource: SandboxCaptureSourceBehavior;
17
+ retention: "not-applicable" | "provider-managed" | "explicit";
18
+ restoreMount: SandboxRestoreMountBehavior;
19
+ };
20
+ export declare function validateSandboxPersistenceCapabilities(input: SandboxPersistenceCapabilities): {
21
+ valid: false;
22
+ reason: string;
23
+ } | {
24
+ reason?: undefined;
25
+ valid: true;
26
+ };
27
+ export type SandboxCheckpointReference = {
28
+ id: string;
29
+ providerReference: string;
30
+ sourceEpoch: number;
31
+ capturedAt: number;
32
+ metadata: {
33
+ scope: Exclude<SandboxCaptureScope, "none">;
34
+ sourceBehavior: Exclude<SandboxCaptureSourceBehavior, "not-applicable">;
35
+ retentionExpiresAt?: number;
36
+ restoreMount: Exclude<SandboxRestoreMountBehavior, "not-applicable">;
37
+ };
38
+ };
39
+ type SandboxRestoreIdentity = {
40
+ checkpointId: string;
41
+ sourceEpoch: number;
42
+ };
43
+ export type SandboxRestoreStatus = (SandboxRestoreIdentity & {
44
+ state: "pending";
45
+ requestedAt: number;
46
+ }) | (SandboxRestoreIdentity & {
47
+ state: "restoring";
48
+ requestedAt: number;
49
+ startedAt: number;
50
+ }) | (SandboxRestoreIdentity & {
51
+ state: "ready";
52
+ requestedAt: number;
53
+ startedAt: number;
54
+ completedAt: number;
55
+ }) | (SandboxRestoreIdentity & {
56
+ state: "failed";
57
+ requestedAt: number;
58
+ startedAt?: number;
59
+ failedAt: number;
60
+ error: string;
61
+ });
6
62
  export type SandboxDriverMetadata = {
7
63
  /** Where the sandbox driver code can execute. This is not sandbox infrastructure. */
8
64
  driverRunsIn: Array<"worker" | "node" | "local">;
@@ -35,6 +91,100 @@ export type SandboxDriverMetadata = {
35
91
  * that is not yet wired and therefore also fails closed.
36
92
  */
37
93
  secretBrokering: "native" | "proxy" | "none";
94
+ /**
95
+ * How the driver can enforce a RESTRICTED `SandboxNetworkPolicy` — i.e.
96
+ * whether the sandbox's outbound network can actually be contained.
97
+ *
98
+ * A `SandboxNetworkPolicy` states the same allowance in up to two encodings:
99
+ * `hosts` (names) and `cidrs` (addresses). A driver contains egress if it
100
+ * enforces AT LEAST ONE encoding the policy carries and blocks everything
101
+ * else; the encodings are alternatives, not additive requirements.
102
+ *
103
+ * - `"hosts-and-cidrs"` — the provider filters by name AND by address
104
+ * (Daytona: `domainAllowList` + `networkAllowList`).
105
+ * - `"hosts"` — the provider filters by hostname only (Vercel firewall).
106
+ * - `"none"` — the driver cannot express an egress allowlist. This covers
107
+ * BOTH drivers that throw when handed a restricted policy (exe, docker,
108
+ * box, and modal for host policy) AND — more dangerously — drivers that
109
+ * silently ignore `net` and run wide open (cloudflare, the fetch bridge).
110
+ * Modal is `"none"` even though it can cut the network entirely: a total
111
+ * blackout is not an allowlist and cannot serve a workspace that has to
112
+ * clone a repo and reach a model provider.
113
+ *
114
+ * The manager reads this to decide what the driver is handed:
115
+ * `sandboxEgressDisposition` enforces where it can and WITHHOLDS (loudly)
116
+ * where it cannot. See `SandboxEgressDisposition` for the posture and
117
+ * `public-docs/sandbox-egress.md` for the operator-facing consequence.
118
+ */
119
+ egressControl: SandboxEgressControl;
120
+ persistence: SandboxPersistenceCapabilities;
121
+ };
122
+ /** @see SandboxDriverMetadata.egressControl */
123
+ export type SandboxEgressControl = "none" | "hosts" | "hosts-and-cidrs";
124
+ /**
125
+ * What the manager does with a caller's `net` for a given driver.
126
+ *
127
+ * Owner directive (2026-07-28): *"for egress in sandbox enforce where we can
128
+ * and document where we cant."* That replaces the previous fail-closed refusal
129
+ * for drivers with no egress control at all — provisioning now proceeds, and
130
+ * the gap is made loud instead of fatal.
131
+ *
132
+ * - `"enforce"` — the driver gets the policy verbatim and contains the
133
+ * sandbox. Also the answer when the caller asked for no containment
134
+ * (absent / `allow-all`), which is nothing to enforce.
135
+ * - `"withhold"` — the driver declares `egressControl: "none"`, so there is no
136
+ * containment to be had. The policy is NOT handed down: half the `"none"`
137
+ * drivers throw on a restricted policy (exe, docker, modal, box) and the
138
+ * other half silently drop it (cloudflare, the fetch bridge). Withholding at
139
+ * the manager means the throwing drivers never see one — their throws stay
140
+ * in place as their own last line of defence — and the silently-dropping
141
+ * ones stop pretending. The sandbox comes up with UNRESTRICTED egress, which
142
+ * is a real exposure, so the manager warns (see `onEgressUnenforced`).
143
+ * - `"refuse"` — the driver HAS egress control but cannot express this
144
+ * particular policy's encoding (a hosts-only driver handed an address-only
145
+ * allowlist). Deliberately still fail-closed: this is a composition mistake
146
+ * on an otherwise-enforcing driver, and degrading it to "withhold" would
147
+ * weaken the enforcing path. `hostedSandboxNetworkPolicy` only ever emits
148
+ * hosts, so no production path reaches it.
149
+ *
150
+ * Pure, like `validateSandboxPersistenceCapabilities`, so the manager, the
151
+ * composition layer, and tests can all ask the same question.
152
+ */
153
+ export type SandboxEgressDisposition = {
154
+ action: "enforce";
155
+ } | {
156
+ action: "withhold";
157
+ reason: "sandbox_egress_uncontained";
158
+ } | {
159
+ action: "refuse";
160
+ reason: "sandbox_egress_policy_unenforceable";
161
+ };
162
+ /** @see SandboxEgressDisposition */
163
+ export declare function sandboxEgressDisposition(control: SandboxEgressControl, net: SandboxNetworkPolicy | undefined): SandboxEgressDisposition;
164
+ /**
165
+ * Emitted whenever a restricted egress policy is withheld because the composed
166
+ * driver declares no egress control. This is the "document where we can't" half
167
+ * of the directive expressed at runtime: an unrestricted sandbox that nobody
168
+ * was told about is the original finding, so the degrade is never silent.
169
+ *
170
+ * Two phases, because a per-create line is easy to miss in request logs:
171
+ * - `"composition"` — once per `createSandboxManager` per driver, at boot.
172
+ * - `"ensure"` — every time a caller's policy is actually withheld, naming the
173
+ * workspace and the allowlist that did not take effect.
174
+ */
175
+ export type SandboxEgressUnenforcedEvent = {
176
+ phase: "composition" | "ensure";
177
+ reason: "sandbox_egress_uncontained";
178
+ /** `SandboxDriver.id` of the composed driver. */
179
+ driver: string;
180
+ /** Always `"none"` today — carried so a sink can group without re-deriving. */
181
+ egressControl: SandboxEgressControl;
182
+ /** Ready-to-log sentence. Sinks that only forward text can use this alone. */
183
+ message: string;
184
+ /** Present on `"ensure"`. */
185
+ workspaceId?: string;
186
+ /** The allowlist the caller asked for and did NOT get. Present on `"ensure"`. */
187
+ requested?: SandboxNetworkPolicy;
38
188
  };
39
189
  /**
40
190
  * A credential the SANDBOX must be able to USE for outbound requests but must
@@ -92,6 +242,9 @@ export type SandboxLease = {
92
242
  lastHeartbeatAt?: number;
93
243
  lastActivityAt?: number;
94
244
  labels?: Record<string, string>;
245
+ checkpoint?: SandboxCheckpointReference;
246
+ persistence?: SandboxPersistenceCapabilities;
247
+ restore?: SandboxRestoreStatus;
95
248
  };
96
249
  export type SandboxLeaseAcquireInput = {
97
250
  homeRegion: SandboxRegion;
@@ -112,6 +265,12 @@ export type SandboxLeasePatch = Partial<Pick<SandboxLease, "status" | "sandboxId
112
265
  nextRetryAt?: number | null;
113
266
  /** `null` clears the stored value; `undefined` leaves it unchanged. */
114
267
  lastError?: string | null;
268
+ /** `null` clears the stored value; `undefined` leaves it unchanged. */
269
+ checkpoint?: SandboxCheckpointReference | null;
270
+ /** `null` clears the stored value; `undefined` leaves it unchanged. */
271
+ persistence?: SandboxPersistenceCapabilities | null;
272
+ /** `null` clears the stored value; `undefined` leaves it unchanged. */
273
+ restore?: SandboxRestoreStatus | null;
115
274
  };
116
275
  /**
117
276
  * Canonical SandboxLeasePatch merge semantics shared by every lease store driver:
@@ -162,7 +321,16 @@ export type SandboxDriverEnsureInput = {
162
321
  secrets?: SandboxBrokeredSecret[];
163
322
  source?: SandboxSource;
164
323
  exposure?: SandboxExposure;
165
- /** Optional egress policy (allow-all when omitted). */
324
+ /**
325
+ * Egress policy. Omitted means allow-all, which is only appropriate for a
326
+ * single-tenant deployment the operator already trusts.
327
+ *
328
+ * A driver can rely on this being ABSENT whenever it declares
329
+ * `egressControl: "none"`: the manager withholds the policy rather than hand
330
+ * it to a driver that would either throw on it or silently drop it. A driver
331
+ * that receives a restricted policy here is one that declared it can enforce
332
+ * it. @see SandboxEgressDisposition
333
+ */
166
334
  net?: SandboxNetworkPolicy;
167
335
  /** Optional snapshot/image to boot from (user-supplied or pre-built). */
168
336
  snapshot?: string;
@@ -183,6 +351,11 @@ export type SandboxTarget = {
183
351
  export type SandboxSnapshotResult = {
184
352
  snapshotId: string;
185
353
  };
354
+ export type SandboxCommandResult = {
355
+ stdout: string;
356
+ stderr: string;
357
+ exitCode: number;
358
+ };
186
359
  export type SandboxDriver = {
187
360
  id: string;
188
361
  metadata: SandboxDriverMetadata;
@@ -203,6 +376,11 @@ export type SandboxDriver = {
203
376
  stop?: (target: SandboxTarget) => Promise<void>;
204
377
  destroy?: (target: SandboxTarget) => Promise<void>;
205
378
  snapshot?: (target: SandboxTarget) => Promise<SandboxSnapshotResult>;
379
+ inspect?: (target: SandboxTarget) => Promise<SandboxTarget | undefined>;
380
+ exec?: (target: SandboxTarget, command: string) => Promise<SandboxCommandResult>;
381
+ clone?: (target: SandboxTarget, input: {
382
+ name: string;
383
+ }) => Promise<SandboxTarget>;
206
384
  };
207
385
  export type SandboxManager = {
208
386
  ensure: (workspaceId: string, input: SandboxManagerInput) => Promise<SandboxEnsureResult>;
@@ -211,6 +389,8 @@ export type SandboxManager = {
211
389
  target: (workspaceId: string) => Promise<SandboxTargetResult>;
212
390
  touch: (workspaceId: string) => Promise<SandboxTouchResult>;
213
391
  snapshot: (workspaceId: string) => Promise<SandboxSnapshotManagerResult>;
392
+ checkpoint: (workspaceId: string, input: SandboxCheckpointCaptureInput) => Promise<SandboxCheckpointResult>;
393
+ restore: (workspaceId: string, input: SandboxCheckpointRestoreInput) => Promise<SandboxCheckpointResult>;
214
394
  stop: (workspaceId: string) => Promise<SandboxMutationResult>;
215
395
  destroy: (workspaceId: string) => Promise<SandboxMutationResult>;
216
396
  release: (workspaceId: string) => Promise<{
@@ -232,6 +412,10 @@ export type SandboxManagerInput = {
232
412
  secrets?: SandboxBrokeredSecret[];
233
413
  source?: SandboxSource;
234
414
  exposure?: SandboxExposure;
415
+ /**
416
+ * @see SandboxDriverEnsureInput.net — enforced by a capable driver, withheld
417
+ * (with a warning) by one that declares `egressControl: "none"`.
418
+ */
235
419
  net?: SandboxNetworkPolicy;
236
420
  snapshot?: string;
237
421
  };
@@ -301,7 +485,31 @@ export type SandboxGarbageCollectResult = {
301
485
  target: SandboxTarget;
302
486
  error: string;
303
487
  }>;
488
+ /**
489
+ * Set when the driver cannot enumerate provider state, so the four arrays
490
+ * above mean "we could not look" rather than "nothing is orphaned". Absent on
491
+ * a real sweep. Callers must treat it as a FAILED sweep: four empty arrays
492
+ * reported as success is the exact defect finding B1 names.
493
+ */
494
+ listingUnsupported?: true;
495
+ /** Driver id, so an ops report can name which driver could not look. */
496
+ driver?: string;
304
497
  };
498
+ /**
499
+ * A driver's `list()` may throw this shape to say "my backing service cannot
500
+ * enumerate", as distinct from "listing failed this time". `garbageCollect()`
501
+ * converts it into `listingUnsupported` (a loud, non-fatal capability gap);
502
+ * every other error propagates as a real sweep failure.
503
+ *
504
+ * Structural rather than an `instanceof` check on purpose: drivers are published
505
+ * as separate entry points and bundlers duplicate class identities, so a
506
+ * nominal check would silently fall through to "transient failure" in exactly
507
+ * the deployment that needs the distinction.
508
+ */
509
+ export type SandboxListingUnsupported = {
510
+ listingUnsupported: true;
511
+ };
512
+ export declare function isSandboxListingUnsupported(err: unknown): err is SandboxListingUnsupported;
305
513
  export type SandboxManagerOptions = {
306
514
  leaseStore: SandboxLeaseStore;
307
515
  driver: SandboxDriver;
@@ -312,5 +520,16 @@ export type SandboxManagerOptions = {
312
520
  retryCapCooldownMs?: number;
313
521
  now?: () => number;
314
522
  appLabel?: string;
523
+ /**
524
+ * Sink for the loud half of "enforce where we can, document where we can't".
525
+ * Called at composition time and again every time a restricted policy is
526
+ * withheld from a driver that cannot enforce it.
527
+ *
528
+ * Defaults to `console.warn(event.message)`. Pass your own to route the gap
529
+ * into a telemetry pipeline instead — but note the default is deliberately
530
+ * unconditional: an operator who never wires a sink still gets the warning.
531
+ * Pass `() => {}` only if you have another way to surface it.
532
+ */
533
+ onEgressUnenforced?: (event: SandboxEgressUnenforcedEvent) => void;
315
534
  };
316
535
  export declare function createSandboxManager(options: SandboxManagerOptions): SandboxManager;