@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.
- 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
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,211 @@
|
|
|
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;
|
|
145
|
+
try {
|
|
146
|
+
const host = new URL(raw).hostname.trim().toLowerCase();
|
|
147
|
+
return host ? host : void 0;
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function sandboxSourceHost(source) {
|
|
153
|
+
if (!source || source.kind !== "git") return;
|
|
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
|
+
}
|
|
5
209
|
function applySandboxLeasePatch(current, patch, updatedAt) {
|
|
6
210
|
return {
|
|
7
211
|
...current,
|
|
@@ -16,14 +220,21 @@ function applySandboxLeasePatch(current, patch, updatedAt) {
|
|
|
16
220
|
...patch.labels === void 0 ? {} : { labels: patch.labels },
|
|
17
221
|
...patch.nextRetryAt === void 0 ? {} : { nextRetryAt: patch.nextRetryAt ?? void 0 },
|
|
18
222
|
...patch.lastError === void 0 ? {} : { lastError: patch.lastError ?? void 0 },
|
|
223
|
+
...patch.checkpoint === void 0 ? {} : { checkpoint: patch.checkpoint ?? void 0 },
|
|
224
|
+
...patch.persistence === void 0 ? {} : { persistence: patch.persistence ?? void 0 },
|
|
225
|
+
...patch.restore === void 0 ? {} : { restore: patch.restore ?? void 0 },
|
|
19
226
|
updatedAt
|
|
20
227
|
};
|
|
21
228
|
}
|
|
229
|
+
function isSandboxListingUnsupported(err) {
|
|
230
|
+
return !!err && typeof err === "object" && err.listingUnsupported === true;
|
|
231
|
+
}
|
|
22
232
|
var DEFAULT_APP_LABEL = "claxedo";
|
|
23
233
|
function sandboxId(driver, workspaceId) {
|
|
24
234
|
return `${driver}-${workspaceId}`;
|
|
25
235
|
}
|
|
26
236
|
function ensureHostInput(input) {
|
|
237
|
+
const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
|
|
27
238
|
const labels = {
|
|
28
239
|
app: input.appLabel,
|
|
29
240
|
workspaceId: input.workspaceId,
|
|
@@ -37,7 +248,7 @@ function ensureHostInput(input) {
|
|
|
37
248
|
homeRegion: input.homeRegion,
|
|
38
249
|
epoch: input.lease.epoch,
|
|
39
250
|
labels,
|
|
40
|
-
bootSource: input.managerInput?.bootSource ?? { kind: "default" },
|
|
251
|
+
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
252
|
workspaceRoot: input.managerInput?.workspaceRoot ?? "/workspace",
|
|
42
253
|
runtimeCwd: input.managerInput?.runtimeCwd,
|
|
43
254
|
workspaceRuntimePort: input.managerInput?.workspaceRuntimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT,
|
|
@@ -46,17 +257,66 @@ function ensureHostInput(input) {
|
|
|
46
257
|
...input.managerInput?.secrets?.length ? { secrets: input.managerInput.secrets } : {},
|
|
47
258
|
source: input.managerInput?.source,
|
|
48
259
|
exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
|
|
49
|
-
|
|
260
|
+
// Withheld, never downgraded: `undefined` here is the same shape a caller
|
|
261
|
+
// that asked for no containment produces, so the throwing drivers take
|
|
262
|
+
// their allow-all path and their throws stay intact for anyone who hands
|
|
263
|
+
// them a policy directly.
|
|
264
|
+
...egress.action === "enforce" && input.managerInput?.net ? { net: input.managerInput.net } : {},
|
|
50
265
|
snapshot: input.managerInput?.snapshot
|
|
51
266
|
};
|
|
52
267
|
}
|
|
53
268
|
function createSandboxManager(options) {
|
|
269
|
+
const persistence = validateSandboxPersistenceCapabilities(options.driver.metadata.persistence);
|
|
270
|
+
if (!persistence.valid) throw new Error(`Invalid sandbox persistence capabilities: ${persistence.reason}`);
|
|
54
271
|
const staleAfterMs = options.staleAfterMs ?? 6e4;
|
|
55
272
|
const retryAfterMs = options.retryAfterMs ?? 2e3;
|
|
56
273
|
const now = options.now ?? Date.now;
|
|
57
274
|
const retryDelayMs = options.retryDelayMs ?? ((retryCount) => Math.min(6e4, 1e3 * 2 ** Math.max(0, retryCount - 1)));
|
|
58
275
|
const maxRetryCount = options.maxRetryCount ?? Number.POSITIVE_INFINITY;
|
|
59
276
|
const retryCapCooldownMs = options.retryCapCooldownMs ?? 10 * 6e4;
|
|
277
|
+
const egressControl = options.driver.metadata.egressControl;
|
|
278
|
+
const onEgressUnenforced = options.onEgressUnenforced ?? defaultEgressUnenforcedSink;
|
|
279
|
+
const lifecycleOperations = /* @__PURE__ */ new Map();
|
|
280
|
+
function reportEgressUnenforced(input) {
|
|
281
|
+
onEgressUnenforced({
|
|
282
|
+
phase: "ensure",
|
|
283
|
+
reason: "sandbox_egress_uncontained",
|
|
284
|
+
driver: options.driver.id,
|
|
285
|
+
egressControl,
|
|
286
|
+
message: egressUnenforcedMessage({
|
|
287
|
+
phase: "ensure",
|
|
288
|
+
driver: options.driver.id,
|
|
289
|
+
workspaceId: input.workspaceId,
|
|
290
|
+
requested: input.requested
|
|
291
|
+
}),
|
|
292
|
+
workspaceId: input.workspaceId,
|
|
293
|
+
requested: input.requested
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (egressControl === "none") {
|
|
297
|
+
const key = `${options.driver.id}|${egressControl}`;
|
|
298
|
+
if (!compositionEgressWarnings.has(key)) {
|
|
299
|
+
compositionEgressWarnings.add(key);
|
|
300
|
+
onEgressUnenforced({
|
|
301
|
+
phase: "composition",
|
|
302
|
+
reason: "sandbox_egress_uncontained",
|
|
303
|
+
driver: options.driver.id,
|
|
304
|
+
egressControl,
|
|
305
|
+
message: egressUnenforcedMessage({ phase: "composition", driver: options.driver.id })
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function lifecycle(workspaceId, kind, run) {
|
|
310
|
+
const current = lifecycleOperations.get(workspaceId);
|
|
311
|
+
if (current?.kind === kind) return current.promise;
|
|
312
|
+
const operation = (current?.promise.catch(() => void 0) ?? Promise.resolve()).then(run);
|
|
313
|
+
lifecycleOperations.set(workspaceId, { kind, promise: operation });
|
|
314
|
+
return operation.finally(() => {
|
|
315
|
+
if (lifecycleOperations.get(workspaceId)?.promise === operation) {
|
|
316
|
+
lifecycleOperations.delete(workspaceId);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
60
320
|
async function leaseTarget(lease) {
|
|
61
321
|
if (lease.status !== "ready" || !lease.sandboxId || !lease.url || !lease.hostId) {
|
|
62
322
|
return { status: "unavailable", reason: "runtime_lease_not_ready" };
|
|
@@ -117,7 +377,8 @@ function createSandboxManager(options) {
|
|
|
117
377
|
retryCount: 0,
|
|
118
378
|
nextRetryAt: null,
|
|
119
379
|
lastError: null,
|
|
120
|
-
labels: target.labels ?? ensure.labels
|
|
380
|
+
labels: target.labels ?? ensure.labels,
|
|
381
|
+
persistence: options.driver.metadata.persistence
|
|
121
382
|
});
|
|
122
383
|
const resolved = await leaseTarget(updated ?? lease);
|
|
123
384
|
if (resolved.status === "ready") return resolved;
|
|
@@ -153,6 +414,13 @@ function createSandboxManager(options) {
|
|
|
153
414
|
}
|
|
154
415
|
return {
|
|
155
416
|
async ensure(workspaceId, input) {
|
|
417
|
+
const egress = sandboxEgressDisposition(options.driver.metadata.egressControl, input.net);
|
|
418
|
+
if (egress.action === "withhold" && input.net) {
|
|
419
|
+
reportEgressUnenforced({ workspaceId, requested: input.net });
|
|
420
|
+
}
|
|
421
|
+
if (egress.action === "refuse") {
|
|
422
|
+
return { status: "unavailable", error: egress.reason, homeRegion: input.homeRegion };
|
|
423
|
+
}
|
|
156
424
|
const existing = await options.leaseStore.get(workspaceId);
|
|
157
425
|
if (existing?.nextRetryAt && existing.nextRetryAt > now()) {
|
|
158
426
|
if (existing.status === "acquiring") {
|
|
@@ -224,23 +492,50 @@ function createSandboxManager(options) {
|
|
|
224
492
|
if (!options.driver.snapshot) return { ok: false, reason: "snapshot_unsupported" };
|
|
225
493
|
return { ok: true, ...await options.driver.snapshot(target) };
|
|
226
494
|
},
|
|
495
|
+
async checkpoint(workspaceId, input) {
|
|
496
|
+
return await lifecycle(workspaceId, "checkpoint", () => captureSandboxCheckpoint({
|
|
497
|
+
workspaceId,
|
|
498
|
+
request: input,
|
|
499
|
+
leaseStore: options.leaseStore,
|
|
500
|
+
target: (id) => this.target(id),
|
|
501
|
+
snapshot: (id) => this.snapshot(id),
|
|
502
|
+
now
|
|
503
|
+
}));
|
|
504
|
+
},
|
|
505
|
+
async restore(workspaceId, input) {
|
|
506
|
+
return await lifecycle(workspaceId, "restore", () => restoreSandboxCheckpoint({
|
|
507
|
+
workspaceId,
|
|
508
|
+
request: input,
|
|
509
|
+
leaseStore: options.leaseStore,
|
|
510
|
+
ensure: (id, next) => this.ensure(id, next),
|
|
511
|
+
now
|
|
512
|
+
}));
|
|
513
|
+
},
|
|
227
514
|
async stop(workspaceId) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
515
|
+
return await lifecycle(workspaceId, "stop", async () => {
|
|
516
|
+
const lease = await options.leaseStore.get(workspaceId);
|
|
517
|
+
if (lease?.status === "stopped") return { ok: true, status: "stopped" };
|
|
518
|
+
const target = await this.target(workspaceId);
|
|
519
|
+
if (target.status !== "ready") return { ok: false, reason: target.reason };
|
|
520
|
+
if (options.driver.metadata.hostStopBehavior !== "not-supported") {
|
|
521
|
+
await (options.driver.suspend ?? options.driver.stop)?.(target);
|
|
522
|
+
} else {
|
|
523
|
+
await options.driver.stop?.(target);
|
|
524
|
+
}
|
|
525
|
+
await options.leaseStore.update(workspaceId, target.epoch, { status: "stopped" });
|
|
526
|
+
return { ok: true, status: "stopped" };
|
|
527
|
+
});
|
|
237
528
|
},
|
|
238
529
|
async destroy(workspaceId) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
530
|
+
return await lifecycle(workspaceId, "destroy", async () => {
|
|
531
|
+
const lease = await options.leaseStore.get(workspaceId);
|
|
532
|
+
if (lease?.status === "destroyed") return { ok: true, status: "destroyed" };
|
|
533
|
+
const target = await this.target(workspaceId);
|
|
534
|
+
if (target.status !== "ready") return { ok: false, reason: target.reason };
|
|
535
|
+
await options.driver.destroy?.(target);
|
|
536
|
+
await options.leaseStore.update(workspaceId, target.epoch, { status: "destroyed" });
|
|
537
|
+
return { ok: true, status: "destroyed" };
|
|
538
|
+
});
|
|
244
539
|
},
|
|
245
540
|
async release(workspaceId) {
|
|
246
541
|
const lease = await options.leaseStore.get(workspaceId);
|
|
@@ -254,7 +549,23 @@ function createSandboxManager(options) {
|
|
|
254
549
|
destroyed: [],
|
|
255
550
|
kept: [],
|
|
256
551
|
skipped: [],
|
|
257
|
-
failed: []
|
|
552
|
+
failed: [],
|
|
553
|
+
listingUnsupported: true,
|
|
554
|
+
driver: options.driver.id
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
let listed;
|
|
558
|
+
try {
|
|
559
|
+
listed = await options.driver.list();
|
|
560
|
+
} catch (err) {
|
|
561
|
+
if (!isSandboxListingUnsupported(err)) throw err;
|
|
562
|
+
return {
|
|
563
|
+
destroyed: [],
|
|
564
|
+
kept: [],
|
|
565
|
+
skipped: [],
|
|
566
|
+
failed: [],
|
|
567
|
+
listingUnsupported: true,
|
|
568
|
+
driver: options.driver.id
|
|
258
569
|
};
|
|
259
570
|
}
|
|
260
571
|
const leases = new Map((await options.leaseStore.list()).map((lease) => [lease.workspaceId, lease]));
|
|
@@ -265,7 +576,7 @@ function createSandboxManager(options) {
|
|
|
265
576
|
failed: []
|
|
266
577
|
};
|
|
267
578
|
const appLabel = options.appLabel ?? DEFAULT_APP_LABEL;
|
|
268
|
-
for (const target of
|
|
579
|
+
for (const target of listed) {
|
|
269
580
|
if (target.labels?.app !== appLabel) {
|
|
270
581
|
result.skipped.push({ target, reason: "unmanaged_app_label" });
|
|
271
582
|
continue;
|
|
@@ -323,6 +634,16 @@ function createSandboxManager(options) {
|
|
|
323
634
|
}
|
|
324
635
|
export {
|
|
325
636
|
DEFAULT_WORKSPACE_RUNTIME_PORT,
|
|
637
|
+
SANDBOX_BASELINE_EGRESS_HOSTS,
|
|
638
|
+
SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
639
|
+
SANDBOX_PACKAGE_REGISTRY_HOSTS,
|
|
326
640
|
applySandboxLeasePatch,
|
|
327
|
-
|
|
641
|
+
captureSandboxCheckpoint,
|
|
642
|
+
createSandboxManager,
|
|
643
|
+
hostedSandboxNetworkPolicy,
|
|
644
|
+
isSandboxListingUnsupported,
|
|
645
|
+
restoreSandboxCheckpoint,
|
|
646
|
+
sandboxEgressDisposition,
|
|
647
|
+
sandboxSourceHost,
|
|
648
|
+
validateSandboxPersistenceCapabilities
|
|
328
649
|
};
|
package/dist/lease-policy.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SandboxLeaseRow } from "./lease-types";
|
|
2
|
-
import type
|
|
2
|
+
import { type SandboxDriverID } from "./driver-catalog";
|
|
3
3
|
export type SandboxDecision = {
|
|
4
4
|
action: "skip";
|
|
5
5
|
reason: string;
|
|
@@ -45,5 +45,4 @@ export declare const DEFAULT_WORKSPACE_HOST_DECISION_CONFIG: SandboxDecisionConf
|
|
|
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
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;
|
|
49
48
|
export declare function nextSandboxRetryAt(retryCount: number, now: number, config?: SandboxDecisionConfig): number | null;
|