@claxedo/sandbox-manager 0.6.0 → 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.
- package/README.md +50 -1
- package/dist/checkpoint-manager.d.ts +47 -0
- package/dist/checkpoint-manager.mjs +116 -0
- package/dist/daytona-allow-list.d.ts +11 -0
- package/dist/daytona-allow-list.mjs +5 -1
- package/dist/driver-catalog.d.ts +9 -6
- package/dist/driver-catalog.mjs +150 -17
- package/dist/drivers/box.mjs +205 -10
- package/dist/drivers/cloudflare.d.ts +15 -1
- package/dist/drivers/cloudflare.mjs +301 -2
- package/dist/drivers/daytona.d.ts +33 -0
- package/dist/drivers/daytona.mjs +310 -2
- package/dist/drivers/docker.mjs +205 -10
- package/dist/drivers/exe.d.ts +26 -0
- package/dist/drivers/exe.mjs +469 -0
- package/dist/drivers/fetch-bridge.mjs +10 -1
- package/dist/drivers/modal.mjs +205 -10
- package/dist/drivers/vercel.d.ts +23 -4
- package/dist/drivers/vercel.mjs +225 -19
- package/dist/hosted-network-policy.d.ts +87 -0
- package/dist/hosted-network-policy.mjs +61 -0
- package/dist/image.d.ts +16 -1
- package/dist/image.mjs +14 -21
- package/dist/index.d.ts +220 -1
- package/dist/index.mjs +341 -20
- package/dist/lease-policy.d.ts +1 -2
- package/dist/lease-policy.mjs +203 -54
- package/dist/lease-types.d.ts +6 -1
- package/dist/runtime-version.mjs +1 -9
- package/dist/stores/memory.mjs +37 -2
- package/package.json +16 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
info(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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;
|