@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
package/dist/drivers/daytona.mjs
CHANGED
|
@@ -5,6 +5,9 @@ import { Daytona } from "@daytona/sdk";
|
|
|
5
5
|
function formatDaytonaAllowList(cidrs) {
|
|
6
6
|
return cidrs.slice(0, 10).join(",");
|
|
7
7
|
}
|
|
8
|
+
function formatDaytonaDomainAllowList(hosts) {
|
|
9
|
+
return [...new Set(hosts)].join(",");
|
|
10
|
+
}
|
|
8
11
|
|
|
9
12
|
// src/runtime-env.ts
|
|
10
13
|
function workspaceRuntimeTargetEnv(input) {
|
|
@@ -33,11 +36,214 @@ function shell(input) {
|
|
|
33
36
|
// src/constants.ts
|
|
34
37
|
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
35
38
|
|
|
39
|
+
// src/hosted-network-policy.ts
|
|
40
|
+
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
41
|
+
"api.anthropic.com",
|
|
42
|
+
"api.openai.com",
|
|
43
|
+
"generativelanguage.googleapis.com",
|
|
44
|
+
"openrouter.ai",
|
|
45
|
+
// Model catalog + engine config, fetched on runner startup.
|
|
46
|
+
"models.dev",
|
|
47
|
+
"opencode.ai"
|
|
48
|
+
];
|
|
49
|
+
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
50
|
+
"registry.npmjs.org",
|
|
51
|
+
"nodejs.org",
|
|
52
|
+
"pypi.org",
|
|
53
|
+
"files.pythonhosted.org",
|
|
54
|
+
"crates.io",
|
|
55
|
+
"static.crates.io",
|
|
56
|
+
"index.crates.io",
|
|
57
|
+
"proxy.golang.org",
|
|
58
|
+
"sum.golang.org"
|
|
59
|
+
];
|
|
60
|
+
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
61
|
+
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
62
|
+
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
// src/driver-catalog.ts
|
|
66
|
+
var sandboxDriverCatalog = {
|
|
67
|
+
exe: {
|
|
68
|
+
id: "exe",
|
|
69
|
+
label: "exe.dev",
|
|
70
|
+
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
71
|
+
metadata: {
|
|
72
|
+
driverRunsIn: ["worker", "node"],
|
|
73
|
+
hostStopBehavior: "not-supported",
|
|
74
|
+
hostResumeBehavior: "same-host",
|
|
75
|
+
targetAccess: "relay",
|
|
76
|
+
secretBrokering: "none",
|
|
77
|
+
egressControl: "none",
|
|
78
|
+
persistence: {
|
|
79
|
+
resume: "same-sandbox",
|
|
80
|
+
capture: "none",
|
|
81
|
+
clone: true,
|
|
82
|
+
captureSource: "not-applicable",
|
|
83
|
+
retention: "provider-managed",
|
|
84
|
+
restoreMount: "same-resource"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
daytona: {
|
|
89
|
+
id: "daytona",
|
|
90
|
+
label: "Daytona",
|
|
91
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
92
|
+
metadata: {
|
|
93
|
+
driverRunsIn: ["worker", "node"],
|
|
94
|
+
hostStopBehavior: "suspends-host",
|
|
95
|
+
hostResumeBehavior: "same-host",
|
|
96
|
+
targetAccess: "relay",
|
|
97
|
+
secretBrokering: "native",
|
|
98
|
+
// Daytona is the only driver that can filter egress by NAME as well as
|
|
99
|
+
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
100
|
+
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
101
|
+
egressControl: "hosts-and-cidrs",
|
|
102
|
+
persistence: {
|
|
103
|
+
resume: "same-sandbox",
|
|
104
|
+
capture: "filesystem",
|
|
105
|
+
clone: false,
|
|
106
|
+
captureSource: "preserved",
|
|
107
|
+
retention: "provider-managed",
|
|
108
|
+
restoreMount: "new-resource"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
modal: {
|
|
113
|
+
id: "modal",
|
|
114
|
+
label: "Modal",
|
|
115
|
+
credentialFields: [
|
|
116
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
117
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
118
|
+
],
|
|
119
|
+
metadata: {
|
|
120
|
+
driverRunsIn: ["node"],
|
|
121
|
+
hostStopBehavior: "terminates-host",
|
|
122
|
+
hostResumeBehavior: "replacement-host",
|
|
123
|
+
targetAccess: "relay",
|
|
124
|
+
secretBrokering: "none",
|
|
125
|
+
// Modal can cut the network entirely (`blockNetwork`) but cannot express
|
|
126
|
+
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
127
|
+
// not containment for a workspace that has to clone and reach a model.
|
|
128
|
+
egressControl: "none",
|
|
129
|
+
persistence: {
|
|
130
|
+
resume: "replacement-restore",
|
|
131
|
+
capture: "filesystem",
|
|
132
|
+
clone: false,
|
|
133
|
+
captureSource: "preserved",
|
|
134
|
+
retention: "provider-managed",
|
|
135
|
+
restoreMount: "new-resource"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
vercel: {
|
|
140
|
+
id: "vercel",
|
|
141
|
+
label: "Vercel",
|
|
142
|
+
credentialFields: [
|
|
143
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
144
|
+
{ key: "team_id", label: "Team ID" },
|
|
145
|
+
{ key: "project_id", label: "Project ID" }
|
|
146
|
+
],
|
|
147
|
+
metadata: {
|
|
148
|
+
driverRunsIn: ["node"],
|
|
149
|
+
hostStopBehavior: "terminates-host",
|
|
150
|
+
hostResumeBehavior: "replacement-host",
|
|
151
|
+
targetAccess: "relay",
|
|
152
|
+
secretBrokering: "native",
|
|
153
|
+
// Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
|
|
154
|
+
// never CIDRs.
|
|
155
|
+
egressControl: "hosts",
|
|
156
|
+
persistence: {
|
|
157
|
+
resume: "replacement-restore",
|
|
158
|
+
capture: "filesystem",
|
|
159
|
+
clone: false,
|
|
160
|
+
captureSource: "stopped",
|
|
161
|
+
retention: "explicit",
|
|
162
|
+
restoreMount: "new-resource"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
cloudflare: {
|
|
167
|
+
id: "cloudflare",
|
|
168
|
+
label: "Cloudflare",
|
|
169
|
+
credentialFields: [
|
|
170
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
171
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
172
|
+
],
|
|
173
|
+
metadata: {
|
|
174
|
+
driverRunsIn: ["worker"],
|
|
175
|
+
hostStopBehavior: "not-supported",
|
|
176
|
+
hostResumeBehavior: "same-host",
|
|
177
|
+
targetAccess: "relay",
|
|
178
|
+
secretBrokering: "proxy",
|
|
179
|
+
// The Cloudflare Sandbox Worker exposes no egress filter: its broker
|
|
180
|
+
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
181
|
+
// to route brokered-credential requests through, not a boundary. The
|
|
182
|
+
// driver therefore drops `net` entirely — which is exactly why this
|
|
183
|
+
// must be declared, so the manager refuses instead of provisioning a
|
|
184
|
+
// sandbox the caller believes is contained.
|
|
185
|
+
egressControl: "none",
|
|
186
|
+
persistence: {
|
|
187
|
+
resume: "replacement-restore",
|
|
188
|
+
capture: "directories",
|
|
189
|
+
clone: false,
|
|
190
|
+
captureSource: "preserved",
|
|
191
|
+
retention: "provider-managed",
|
|
192
|
+
restoreMount: "copy-on-write"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
box: {
|
|
197
|
+
id: "box",
|
|
198
|
+
label: "Box",
|
|
199
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
200
|
+
metadata: {
|
|
201
|
+
driverRunsIn: ["node"],
|
|
202
|
+
hostStopBehavior: "suspends-host",
|
|
203
|
+
hostResumeBehavior: "same-host",
|
|
204
|
+
targetAccess: "relay",
|
|
205
|
+
secretBrokering: "none",
|
|
206
|
+
egressControl: "none",
|
|
207
|
+
persistence: {
|
|
208
|
+
resume: "same-sandbox",
|
|
209
|
+
capture: "none",
|
|
210
|
+
clone: false,
|
|
211
|
+
captureSource: "not-applicable",
|
|
212
|
+
retention: "provider-managed",
|
|
213
|
+
restoreMount: "same-resource"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
docker: {
|
|
218
|
+
id: "docker",
|
|
219
|
+
label: "Docker",
|
|
220
|
+
credentialFields: [{ key: "image", label: "Image" }],
|
|
221
|
+
metadata: {
|
|
222
|
+
driverRunsIn: ["local"],
|
|
223
|
+
hostStopBehavior: "terminates-host",
|
|
224
|
+
hostResumeBehavior: "same-host",
|
|
225
|
+
targetAccess: "loopback",
|
|
226
|
+
secretBrokering: "none",
|
|
227
|
+
egressControl: "none",
|
|
228
|
+
persistence: {
|
|
229
|
+
resume: "replacement-restore",
|
|
230
|
+
capture: "same-resource",
|
|
231
|
+
clone: false,
|
|
232
|
+
captureSource: "preserved",
|
|
233
|
+
retention: "provider-managed",
|
|
234
|
+
restoreMount: "same-resource"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
36
240
|
// src/drivers/daytona.ts
|
|
37
241
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
38
242
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
39
243
|
var DEFAULT_PREVIEW_EXPIRY_S = 3600;
|
|
40
244
|
var DEFAULT_OPERATION_TIMEOUT_S = 60;
|
|
245
|
+
var LIST_PAGE_SIZE = 100;
|
|
246
|
+
var MAX_LIST_PAGES = 100;
|
|
41
247
|
function labelName(workspaceId) {
|
|
42
248
|
return `claxedo-${workspaceId}`;
|
|
43
249
|
}
|
|
@@ -65,6 +271,15 @@ function createDefaultClient(options) {
|
|
|
65
271
|
for await (const sandbox of sdk.list({ labels })) return sandbox;
|
|
66
272
|
return void 0;
|
|
67
273
|
},
|
|
274
|
+
// Page 1 carries everything; later pages are empty. See DaytonaClientLike.list.
|
|
275
|
+
async list(labels, page = 1, limit) {
|
|
276
|
+
if (page > 1) return { items: [] };
|
|
277
|
+
const items = [];
|
|
278
|
+
for await (const sandbox of sdk.list({ ...labels ? { labels } : {}, ...limit ? { limit } : {} })) {
|
|
279
|
+
items.push(sandbox);
|
|
280
|
+
}
|
|
281
|
+
return { items };
|
|
282
|
+
},
|
|
68
283
|
create(params, operation) {
|
|
69
284
|
if (params.image) return sdk.create({ ...params, image: params.image }, operation);
|
|
70
285
|
return sdk.create(params, operation);
|
|
@@ -167,12 +382,29 @@ function createDaytonaSandboxDriver(options) {
|
|
|
167
382
|
}
|
|
168
383
|
return references;
|
|
169
384
|
}
|
|
385
|
+
async function applyNetworkPolicy(sandbox, input) {
|
|
386
|
+
const net = network(input);
|
|
387
|
+
if (Object.keys(net).length === 0) return "applied";
|
|
388
|
+
if (!sandbox.updateNetworkSettings) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`daytona sandbox client cannot reapply egress policy to existing sandbox ${sandbox.id}: the client does not support updateNetworkSettings, so the requested restricted policy cannot be guaranteed on reuse \u2014 refusing rather than serving the creation-time policy`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await sandbox.updateNetworkSettings(net);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if (transientDriverError(err)) return "retry";
|
|
397
|
+
throw err;
|
|
398
|
+
}
|
|
399
|
+
return "applied";
|
|
400
|
+
}
|
|
170
401
|
async function ensureHost(input) {
|
|
171
402
|
const hostId = labelName(input.workspaceId);
|
|
172
403
|
const bootSource = input.bootSource?.kind === "image" ? { image: input.bootSource.image } : { snapshot: input.bootSource?.kind === "driver-snapshot" ? input.bootSource.snapshotId : input.snapshot ?? options.baseSnapshot };
|
|
173
404
|
const net = network(input);
|
|
174
405
|
const secrets = await brokeredSecretReferences(input);
|
|
175
|
-
const
|
|
406
|
+
const existing = await findExisting(input.workspaceId);
|
|
407
|
+
const sandbox = existing ?? await client.create({
|
|
176
408
|
name: labelName(input.workspaceId),
|
|
177
409
|
...bootSource,
|
|
178
410
|
envVars: staticBootEnv(input, hostId, workspaceDirectory(input)),
|
|
@@ -188,6 +420,9 @@ function createDaytonaSandboxDriver(options) {
|
|
|
188
420
|
});
|
|
189
421
|
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
190
422
|
if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
|
|
423
|
+
if (existing && await applyNetworkPolicy(existing, input) === "retry") {
|
|
424
|
+
return { provisioning: true, retryAfterMs: 2e3 };
|
|
425
|
+
}
|
|
191
426
|
return readyTarget(input, sandbox, hostId);
|
|
192
427
|
}
|
|
193
428
|
return {
|
|
@@ -197,12 +432,74 @@ function createDaytonaSandboxDriver(options) {
|
|
|
197
432
|
hostStopBehavior: "suspends-host",
|
|
198
433
|
hostResumeBehavior: "same-host",
|
|
199
434
|
targetAccess: "relay",
|
|
200
|
-
secretBrokering: "native"
|
|
435
|
+
secretBrokering: "native",
|
|
436
|
+
egressControl: "hosts-and-cidrs",
|
|
437
|
+
persistence: sandboxDriverCatalog.daytona.metadata.persistence
|
|
201
438
|
},
|
|
202
439
|
ensureHost,
|
|
440
|
+
// Provider-state enumeration for `garbageCollect()`.
|
|
441
|
+
//
|
|
442
|
+
// Two label sets exist on a sandbox this driver created (`ensureHost`
|
|
443
|
+
// above): the manager's flat set — `app`, `workspaceId`, `epoch`,
|
|
444
|
+
// `homeRegion` (built at index.ts `ensureHostInput`) — and the dotted
|
|
445
|
+
// `claxedo.workspaceId` this driver adds for `findExisting`. Every target
|
|
446
|
+
// returned here carries `sandbox.labels` **as the provider reports them**,
|
|
447
|
+
// never labels reconstructed from ensure inputs: GC's ownership check reads
|
|
448
|
+
// `labels.app` and its identity check reads flat `labels.workspaceId` /
|
|
449
|
+
// `labels.epoch`, so a synthesized label set would decide the fate of a
|
|
450
|
+
// sandbox using values that never left this process. (That is the bug in
|
|
451
|
+
// the `exe` driver's `list()`, which sources labels from a per-process
|
|
452
|
+
// `Map` — after a restart it reports `labels: undefined` and GC skips
|
|
453
|
+
// every sandbox as unlabeled. Not fixed here; different owner.)
|
|
454
|
+
//
|
|
455
|
+
// The filter is deliberately WIDE: any claxedo ownership marker qualifies.
|
|
456
|
+
// Narrowing it here would re-create the defect W1 exists to remove — an
|
|
457
|
+
// orphan the sweep cannot see is an orphan that lives forever — and the
|
|
458
|
+
// authority on what may be DESTROYED is the manager's `app`-label check,
|
|
459
|
+
// which skips anything else as `unmanaged_app_label`. Visibility is this
|
|
460
|
+
// function's job; destruction is not.
|
|
461
|
+
//
|
|
462
|
+
// No preview URL is resolved: `getSignedPreviewUrl` is a per-sandbox
|
|
463
|
+
// round-trip and GC only needs identity, so a sweep over N orphans would
|
|
464
|
+
// cost N extra API calls to fill a field it then discards.
|
|
465
|
+
async list() {
|
|
466
|
+
if (!client.list) {
|
|
467
|
+
throw new Error("daytona sandbox client does not support listing");
|
|
468
|
+
}
|
|
469
|
+
const targets = [];
|
|
470
|
+
const seen = /* @__PURE__ */ new Set();
|
|
471
|
+
for (let page = 1; page <= MAX_LIST_PAGES; page++) {
|
|
472
|
+
const result = await client.list(void 0, page, LIST_PAGE_SIZE);
|
|
473
|
+
const items = result?.items ?? [];
|
|
474
|
+
for (const sandbox of items) {
|
|
475
|
+
const labels = sandbox.labels;
|
|
476
|
+
const workspaceId = labels?.workspaceId ?? labels?.["claxedo.workspaceId"];
|
|
477
|
+
if (!workspaceId || seen.has(sandbox.id)) continue;
|
|
478
|
+
seen.add(sandbox.id);
|
|
479
|
+
targets.push({
|
|
480
|
+
workspaceId,
|
|
481
|
+
sandboxId: sandbox.id,
|
|
482
|
+
// Identity, not a reachable address — never routed through.
|
|
483
|
+
url: sandbox.id,
|
|
484
|
+
// Must equal what `ensureHost` stored on the lease, or a LIVE
|
|
485
|
+
// sandbox fails GC's identity check and gets destroyed. Both paths
|
|
486
|
+
// derive it from `labelName(workspaceId)`.
|
|
487
|
+
hostId: labelName(workspaceId),
|
|
488
|
+
driverResourceId: sandbox.id,
|
|
489
|
+
labels,
|
|
490
|
+
driver: { id: "daytona", resourceId: sandbox.id }
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (items.length < LIST_PAGE_SIZE) break;
|
|
494
|
+
}
|
|
495
|
+
return targets;
|
|
496
|
+
},
|
|
203
497
|
async resumeHost(input) {
|
|
204
498
|
const sandbox = await sandboxById(input.lease.sandboxId);
|
|
205
499
|
if (!await ensureStarted(sandbox)) return { provisioning: true, retryAfterMs: 2e3 };
|
|
500
|
+
if (await applyNetworkPolicy(sandbox, input.ensure) === "retry") {
|
|
501
|
+
return { provisioning: true, retryAfterMs: 2e3 };
|
|
502
|
+
}
|
|
206
503
|
return readyTarget(input.ensure, sandbox, input.ensure.hostId ?? labelName(input.ensure.workspaceId));
|
|
207
504
|
},
|
|
208
505
|
async touch(target) {
|
|
@@ -219,11 +516,22 @@ function createDaytonaSandboxDriver(options) {
|
|
|
219
516
|
const status = err.response?.status ?? err.status;
|
|
220
517
|
if (status !== 404) throw err;
|
|
221
518
|
});
|
|
519
|
+
},
|
|
520
|
+
async snapshot(target) {
|
|
521
|
+
const sandbox = await sandboxById(target.sandboxId);
|
|
522
|
+
if (!sandbox._experimental_createSnapshot) {
|
|
523
|
+
throw new Error("Daytona sandbox client does not support filesystem snapshots");
|
|
524
|
+
}
|
|
525
|
+
const snapshotId = `claxedo-${target.workspaceId ?? target.sandboxId}-${Date.now()}`;
|
|
526
|
+
await sandbox._experimental_createSnapshot(snapshotId, operationTimeout);
|
|
527
|
+
return { snapshotId };
|
|
222
528
|
}
|
|
223
529
|
};
|
|
224
530
|
}
|
|
225
531
|
function network(input) {
|
|
226
532
|
if (!input.net || input.net.mode === "allow-all") return {};
|
|
533
|
+
const hosts = input.net.hosts ?? [];
|
|
534
|
+
if (hosts.length > 0) return { domainAllowList: formatDaytonaDomainAllowList(hosts) };
|
|
227
535
|
const cidrs = input.net.cidrs ?? [];
|
|
228
536
|
if (cidrs.length === 0) return { networkBlockAll: true };
|
|
229
537
|
return { networkAllowList: formatDaytonaAllowList(cidrs) };
|
package/dist/drivers/docker.mjs
CHANGED
|
@@ -37,16 +37,8 @@ import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
|
37
37
|
|
|
38
38
|
// src/runtime-version.ts
|
|
39
39
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
40
|
-
var versionCache;
|
|
41
40
|
function workspaceRuntimeVersion() {
|
|
42
|
-
|
|
43
|
-
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
44
|
-
if (envVersion) {
|
|
45
|
-
versionCache = envVersion;
|
|
46
|
-
return versionCache;
|
|
47
|
-
}
|
|
48
|
-
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
49
|
-
return versionCache;
|
|
41
|
+
return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
// src/image-name.ts
|
|
@@ -75,6 +67,207 @@ var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
|
75
67
|
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
76
68
|
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
77
69
|
|
|
70
|
+
// src/hosted-network-policy.ts
|
|
71
|
+
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
72
|
+
"api.anthropic.com",
|
|
73
|
+
"api.openai.com",
|
|
74
|
+
"generativelanguage.googleapis.com",
|
|
75
|
+
"openrouter.ai",
|
|
76
|
+
// Model catalog + engine config, fetched on runner startup.
|
|
77
|
+
"models.dev",
|
|
78
|
+
"opencode.ai"
|
|
79
|
+
];
|
|
80
|
+
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
81
|
+
"registry.npmjs.org",
|
|
82
|
+
"nodejs.org",
|
|
83
|
+
"pypi.org",
|
|
84
|
+
"files.pythonhosted.org",
|
|
85
|
+
"crates.io",
|
|
86
|
+
"static.crates.io",
|
|
87
|
+
"index.crates.io",
|
|
88
|
+
"proxy.golang.org",
|
|
89
|
+
"sum.golang.org"
|
|
90
|
+
];
|
|
91
|
+
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
92
|
+
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
93
|
+
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// src/driver-catalog.ts
|
|
97
|
+
var sandboxDriverCatalog = {
|
|
98
|
+
exe: {
|
|
99
|
+
id: "exe",
|
|
100
|
+
label: "exe.dev",
|
|
101
|
+
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
102
|
+
metadata: {
|
|
103
|
+
driverRunsIn: ["worker", "node"],
|
|
104
|
+
hostStopBehavior: "not-supported",
|
|
105
|
+
hostResumeBehavior: "same-host",
|
|
106
|
+
targetAccess: "relay",
|
|
107
|
+
secretBrokering: "none",
|
|
108
|
+
egressControl: "none",
|
|
109
|
+
persistence: {
|
|
110
|
+
resume: "same-sandbox",
|
|
111
|
+
capture: "none",
|
|
112
|
+
clone: true,
|
|
113
|
+
captureSource: "not-applicable",
|
|
114
|
+
retention: "provider-managed",
|
|
115
|
+
restoreMount: "same-resource"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
daytona: {
|
|
120
|
+
id: "daytona",
|
|
121
|
+
label: "Daytona",
|
|
122
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
123
|
+
metadata: {
|
|
124
|
+
driverRunsIn: ["worker", "node"],
|
|
125
|
+
hostStopBehavior: "suspends-host",
|
|
126
|
+
hostResumeBehavior: "same-host",
|
|
127
|
+
targetAccess: "relay",
|
|
128
|
+
secretBrokering: "native",
|
|
129
|
+
// Daytona is the only driver that can filter egress by NAME as well as
|
|
130
|
+
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
131
|
+
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
132
|
+
egressControl: "hosts-and-cidrs",
|
|
133
|
+
persistence: {
|
|
134
|
+
resume: "same-sandbox",
|
|
135
|
+
capture: "filesystem",
|
|
136
|
+
clone: false,
|
|
137
|
+
captureSource: "preserved",
|
|
138
|
+
retention: "provider-managed",
|
|
139
|
+
restoreMount: "new-resource"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
modal: {
|
|
144
|
+
id: "modal",
|
|
145
|
+
label: "Modal",
|
|
146
|
+
credentialFields: [
|
|
147
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
148
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
149
|
+
],
|
|
150
|
+
metadata: {
|
|
151
|
+
driverRunsIn: ["node"],
|
|
152
|
+
hostStopBehavior: "terminates-host",
|
|
153
|
+
hostResumeBehavior: "replacement-host",
|
|
154
|
+
targetAccess: "relay",
|
|
155
|
+
secretBrokering: "none",
|
|
156
|
+
// Modal can cut the network entirely (`blockNetwork`) but cannot express
|
|
157
|
+
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
158
|
+
// not containment for a workspace that has to clone and reach a model.
|
|
159
|
+
egressControl: "none",
|
|
160
|
+
persistence: {
|
|
161
|
+
resume: "replacement-restore",
|
|
162
|
+
capture: "filesystem",
|
|
163
|
+
clone: false,
|
|
164
|
+
captureSource: "preserved",
|
|
165
|
+
retention: "provider-managed",
|
|
166
|
+
restoreMount: "new-resource"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
vercel: {
|
|
171
|
+
id: "vercel",
|
|
172
|
+
label: "Vercel",
|
|
173
|
+
credentialFields: [
|
|
174
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
175
|
+
{ key: "team_id", label: "Team ID" },
|
|
176
|
+
{ key: "project_id", label: "Project ID" }
|
|
177
|
+
],
|
|
178
|
+
metadata: {
|
|
179
|
+
driverRunsIn: ["node"],
|
|
180
|
+
hostStopBehavior: "terminates-host",
|
|
181
|
+
hostResumeBehavior: "replacement-host",
|
|
182
|
+
targetAccess: "relay",
|
|
183
|
+
secretBrokering: "native",
|
|
184
|
+
// Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
|
|
185
|
+
// never CIDRs.
|
|
186
|
+
egressControl: "hosts",
|
|
187
|
+
persistence: {
|
|
188
|
+
resume: "replacement-restore",
|
|
189
|
+
capture: "filesystem",
|
|
190
|
+
clone: false,
|
|
191
|
+
captureSource: "stopped",
|
|
192
|
+
retention: "explicit",
|
|
193
|
+
restoreMount: "new-resource"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
cloudflare: {
|
|
198
|
+
id: "cloudflare",
|
|
199
|
+
label: "Cloudflare",
|
|
200
|
+
credentialFields: [
|
|
201
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
202
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
203
|
+
],
|
|
204
|
+
metadata: {
|
|
205
|
+
driverRunsIn: ["worker"],
|
|
206
|
+
hostStopBehavior: "not-supported",
|
|
207
|
+
hostResumeBehavior: "same-host",
|
|
208
|
+
targetAccess: "relay",
|
|
209
|
+
secretBrokering: "proxy",
|
|
210
|
+
// The Cloudflare Sandbox Worker exposes no egress filter: its broker
|
|
211
|
+
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
212
|
+
// to route brokered-credential requests through, not a boundary. The
|
|
213
|
+
// driver therefore drops `net` entirely — which is exactly why this
|
|
214
|
+
// must be declared, so the manager refuses instead of provisioning a
|
|
215
|
+
// sandbox the caller believes is contained.
|
|
216
|
+
egressControl: "none",
|
|
217
|
+
persistence: {
|
|
218
|
+
resume: "replacement-restore",
|
|
219
|
+
capture: "directories",
|
|
220
|
+
clone: false,
|
|
221
|
+
captureSource: "preserved",
|
|
222
|
+
retention: "provider-managed",
|
|
223
|
+
restoreMount: "copy-on-write"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
box: {
|
|
228
|
+
id: "box",
|
|
229
|
+
label: "Box",
|
|
230
|
+
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
231
|
+
metadata: {
|
|
232
|
+
driverRunsIn: ["node"],
|
|
233
|
+
hostStopBehavior: "suspends-host",
|
|
234
|
+
hostResumeBehavior: "same-host",
|
|
235
|
+
targetAccess: "relay",
|
|
236
|
+
secretBrokering: "none",
|
|
237
|
+
egressControl: "none",
|
|
238
|
+
persistence: {
|
|
239
|
+
resume: "same-sandbox",
|
|
240
|
+
capture: "none",
|
|
241
|
+
clone: false,
|
|
242
|
+
captureSource: "not-applicable",
|
|
243
|
+
retention: "provider-managed",
|
|
244
|
+
restoreMount: "same-resource"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
docker: {
|
|
249
|
+
id: "docker",
|
|
250
|
+
label: "Docker",
|
|
251
|
+
credentialFields: [{ key: "image", label: "Image" }],
|
|
252
|
+
metadata: {
|
|
253
|
+
driverRunsIn: ["local"],
|
|
254
|
+
hostStopBehavior: "terminates-host",
|
|
255
|
+
hostResumeBehavior: "same-host",
|
|
256
|
+
targetAccess: "loopback",
|
|
257
|
+
secretBrokering: "none",
|
|
258
|
+
egressControl: "none",
|
|
259
|
+
persistence: {
|
|
260
|
+
resume: "replacement-restore",
|
|
261
|
+
capture: "same-resource",
|
|
262
|
+
clone: false,
|
|
263
|
+
captureSource: "preserved",
|
|
264
|
+
retention: "provider-managed",
|
|
265
|
+
restoreMount: "same-resource"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
78
271
|
// src/drivers/docker.ts
|
|
79
272
|
var execFileAsync = promisify(execFile);
|
|
80
273
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
@@ -326,7 +519,9 @@ function createDockerSandboxDriver(options) {
|
|
|
326
519
|
hostStopBehavior: "terminates-host",
|
|
327
520
|
hostResumeBehavior: "same-host",
|
|
328
521
|
targetAccess: "loopback",
|
|
329
|
-
secretBrokering: "none"
|
|
522
|
+
secretBrokering: "none",
|
|
523
|
+
egressControl: "none",
|
|
524
|
+
persistence: sandboxDriverCatalog.docker.metadata.persistence
|
|
330
525
|
},
|
|
331
526
|
ensureHost,
|
|
332
527
|
resumeHost,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SandboxDriver, SandboxDriverEnsureInput } from "..";
|
|
2
|
+
type ExeVm = {
|
|
3
|
+
vm_name: string;
|
|
4
|
+
https_url?: string;
|
|
5
|
+
ssh_dest?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
/** Provider-side tags, written at create — the durable half of our labels. */
|
|
8
|
+
tags?: string[];
|
|
9
|
+
};
|
|
10
|
+
export type ExeSandboxDriverOptions = {
|
|
11
|
+
apiToken: string;
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
image?: string;
|
|
14
|
+
runtimeCommand?: string;
|
|
15
|
+
workspaceDir?: string;
|
|
16
|
+
runtimePort?: number;
|
|
17
|
+
runner?: string;
|
|
18
|
+
operationTimeoutMs?: number;
|
|
19
|
+
healthTimeoutMs?: number;
|
|
20
|
+
healthIntervalMs?: number;
|
|
21
|
+
fetchImpl?: typeof fetch;
|
|
22
|
+
env?: (input: SandboxDriverEnsureInput, vm: ExeVm) => Record<string, string> | Promise<Record<string, string>>;
|
|
23
|
+
};
|
|
24
|
+
export declare function exeWorkspaceName(workspaceId: string, epoch: number): string;
|
|
25
|
+
export declare function createExeSandboxDriver(options: ExeSandboxDriverOptions): SandboxDriver;
|
|
26
|
+
export {};
|