@claxedo/sandbox-manager 0.6.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -15
- 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 +5 -52
- package/dist/driver-catalog.mjs +263 -121
- package/dist/drivers/box.mjs +309 -39
- package/dist/drivers/cloudflare.d.ts +18 -7
- package/dist/drivers/cloudflare.mjs +423 -34
- package/dist/drivers/daytona.d.ts +82 -13
- package/dist/drivers/daytona.mjs +495 -45
- package/dist/drivers/docker.mjs +284 -38
- package/dist/drivers/exe.d.ts +26 -0
- package/dist/drivers/exe.mjs +492 -0
- package/dist/drivers/fetch-bridge.mjs +29 -5
- package/dist/drivers/modal.d.ts +2 -5
- package/dist/drivers/modal.mjs +296 -46
- package/dist/drivers/transient-error.d.ts +11 -0
- package/dist/drivers/vercel.d.ts +27 -11
- package/dist/drivers/vercel.mjs +339 -59
- package/dist/hosted-network-policy.d.ts +87 -0
- package/dist/hosted-network-policy.mjs +61 -0
- package/dist/image-name.mjs +34 -0
- package/dist/image.d.ts +18 -3
- package/dist/image.mjs +47 -32
- package/dist/index.d.ts +281 -17
- package/dist/index.mjs +393 -26
- package/dist/json.d.ts +13 -0
- package/dist/lease-policy.d.ts +6 -3
- package/dist/lease-policy.mjs +213 -54
- package/dist/lease-types.d.ts +17 -3
- package/dist/runtime-env.d.ts +12 -1
- package/dist/runtime-env.mjs +14 -0
- package/dist/runtime-version.mjs +1 -9
- package/dist/service-url-exposure.d.ts +1 -2
- package/dist/service-url-exposure.mjs +1 -17
- package/dist/stores/memory.mjs +39 -4
- package/package.json +67 -51
- package/dist/drivers/cloudflare-egress.d.ts +0 -40
- package/dist/drivers/cloudflare-egress.mjs +0 -113
package/dist/drivers/vercel.mjs
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
3
|
+
|
|
4
|
+
// src/hosted-network-policy.ts
|
|
5
|
+
var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
6
|
+
"api.anthropic.com",
|
|
7
|
+
"api.openai.com",
|
|
8
|
+
"generativelanguage.googleapis.com",
|
|
9
|
+
"openrouter.ai",
|
|
10
|
+
// Model catalog + engine config, fetched on runner startup.
|
|
11
|
+
"models.dev",
|
|
12
|
+
"opencode.ai"
|
|
13
|
+
];
|
|
14
|
+
var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
|
|
15
|
+
"registry.npmjs.org",
|
|
16
|
+
"nodejs.org",
|
|
17
|
+
"pypi.org",
|
|
18
|
+
"files.pythonhosted.org",
|
|
19
|
+
"crates.io",
|
|
20
|
+
"static.crates.io",
|
|
21
|
+
"index.crates.io",
|
|
22
|
+
"proxy.golang.org",
|
|
23
|
+
"sum.golang.org"
|
|
24
|
+
];
|
|
25
|
+
var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
26
|
+
...SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
27
|
+
...SANDBOX_PACKAGE_REGISTRY_HOSTS
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
function brokeredSecretPlaceholder(name) {
|
|
32
|
+
return `claxedo-broker:${name}`;
|
|
33
|
+
}
|
|
34
|
+
function brokeredPlaceholderEnv(secrets) {
|
|
35
|
+
return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
|
|
36
|
+
}
|
|
3
37
|
|
|
4
38
|
// src/runtime-env.ts
|
|
39
|
+
function workspaceRuntimeBootEnv(input) {
|
|
40
|
+
const env = {
|
|
41
|
+
...workspaceRuntimeTargetEnv(input),
|
|
42
|
+
...workspaceRuntimeSourceEnv(input),
|
|
43
|
+
...input.env
|
|
44
|
+
};
|
|
45
|
+
if (input.runner) env.WORKSPACE_RUNTIME_RUNNER = input.runner;
|
|
46
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
47
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
48
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
49
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
50
|
+
return env;
|
|
51
|
+
}
|
|
5
52
|
function workspaceRuntimeTargetEnv(input) {
|
|
6
53
|
return {
|
|
7
54
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -22,16 +69,8 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
22
69
|
|
|
23
70
|
// src/runtime-version.ts
|
|
24
71
|
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
25
|
-
var versionCache;
|
|
26
72
|
function workspaceRuntimeVersion() {
|
|
27
|
-
|
|
28
|
-
const envVersion = process.env.WORKSPACE_RUNTIME_VERSION;
|
|
29
|
-
if (envVersion) {
|
|
30
|
-
versionCache = envVersion;
|
|
31
|
-
return versionCache;
|
|
32
|
-
}
|
|
33
|
-
versionCache = DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
34
|
-
return versionCache;
|
|
73
|
+
return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
35
74
|
}
|
|
36
75
|
|
|
37
76
|
// src/command.ts
|
|
@@ -39,12 +78,225 @@ function shell(input) {
|
|
|
39
78
|
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
40
79
|
}
|
|
41
80
|
|
|
42
|
-
// src/constants.ts
|
|
43
|
-
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
44
|
-
|
|
45
81
|
// src/defaults.ts
|
|
46
82
|
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
47
83
|
|
|
84
|
+
// ../sandbox-contract/dist/index.mjs
|
|
85
|
+
var sandboxDriverCredentialFields = {
|
|
86
|
+
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
87
|
+
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
88
|
+
modal: [
|
|
89
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
90
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
91
|
+
],
|
|
92
|
+
vercel: [
|
|
93
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
94
|
+
{ key: "team_id", label: "Team ID" },
|
|
95
|
+
{ key: "project_id", label: "Project ID" }
|
|
96
|
+
],
|
|
97
|
+
cloudflare: [
|
|
98
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
99
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
100
|
+
],
|
|
101
|
+
box: [{ key: "api_key", label: "API Key", secret: true }],
|
|
102
|
+
docker: [{ key: "image", label: "Image" }]
|
|
103
|
+
};
|
|
104
|
+
var sandboxDriverLabels = {
|
|
105
|
+
exe: "exe.dev",
|
|
106
|
+
daytona: "Daytona",
|
|
107
|
+
modal: "Modal",
|
|
108
|
+
vercel: "Vercel",
|
|
109
|
+
cloudflare: "Cloudflare",
|
|
110
|
+
box: "Box",
|
|
111
|
+
docker: "Docker"
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/driver-catalog.ts
|
|
115
|
+
var sandboxDriverCatalog = {
|
|
116
|
+
exe: {
|
|
117
|
+
id: "exe",
|
|
118
|
+
label: sandboxDriverLabels.exe,
|
|
119
|
+
credentialFields: sandboxDriverCredentialFields.exe,
|
|
120
|
+
metadata: {
|
|
121
|
+
driverRunsIn: ["worker", "node"],
|
|
122
|
+
hostStopBehavior: "not-supported",
|
|
123
|
+
hostResumeBehavior: "same-host",
|
|
124
|
+
targetAccess: "relay",
|
|
125
|
+
secretBrokering: "none",
|
|
126
|
+
egressControl: "none",
|
|
127
|
+
persistence: {
|
|
128
|
+
resume: "same-sandbox",
|
|
129
|
+
capture: "none",
|
|
130
|
+
clone: true,
|
|
131
|
+
captureSource: "not-applicable",
|
|
132
|
+
retention: "provider-managed",
|
|
133
|
+
restoreMount: "same-resource"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
daytona: {
|
|
138
|
+
id: "daytona",
|
|
139
|
+
label: sandboxDriverLabels.daytona,
|
|
140
|
+
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
141
|
+
metadata: {
|
|
142
|
+
driverRunsIn: ["worker", "node"],
|
|
143
|
+
hostStopBehavior: "suspends-host",
|
|
144
|
+
hostResumeBehavior: "same-host",
|
|
145
|
+
targetAccess: "relay",
|
|
146
|
+
secretBrokering: "native",
|
|
147
|
+
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
148
|
+
egressControl: "hosts-and-cidrs",
|
|
149
|
+
persistence: {
|
|
150
|
+
resume: "same-sandbox",
|
|
151
|
+
capture: "filesystem",
|
|
152
|
+
clone: false,
|
|
153
|
+
captureSource: "preserved",
|
|
154
|
+
retention: "provider-managed",
|
|
155
|
+
restoreMount: "new-resource"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
modal: {
|
|
160
|
+
id: "modal",
|
|
161
|
+
label: sandboxDriverLabels.modal,
|
|
162
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
163
|
+
metadata: {
|
|
164
|
+
driverRunsIn: ["node"],
|
|
165
|
+
hostStopBehavior: "terminates-host",
|
|
166
|
+
hostResumeBehavior: "replacement-host",
|
|
167
|
+
targetAccess: "relay",
|
|
168
|
+
secretBrokering: "none",
|
|
169
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
170
|
+
egressControl: "none",
|
|
171
|
+
persistence: {
|
|
172
|
+
resume: "replacement-restore",
|
|
173
|
+
capture: "filesystem",
|
|
174
|
+
clone: false,
|
|
175
|
+
captureSource: "preserved",
|
|
176
|
+
retention: "provider-managed",
|
|
177
|
+
restoreMount: "new-resource"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
vercel: {
|
|
182
|
+
id: "vercel",
|
|
183
|
+
label: sandboxDriverLabels.vercel,
|
|
184
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
185
|
+
metadata: {
|
|
186
|
+
driverRunsIn: ["node"],
|
|
187
|
+
hostStopBehavior: "terminates-host",
|
|
188
|
+
hostResumeBehavior: "replacement-host",
|
|
189
|
+
targetAccess: "relay",
|
|
190
|
+
secretBrokering: "native",
|
|
191
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
192
|
+
egressControl: "hosts",
|
|
193
|
+
persistence: {
|
|
194
|
+
resume: "replacement-restore",
|
|
195
|
+
capture: "filesystem",
|
|
196
|
+
clone: false,
|
|
197
|
+
captureSource: "stopped",
|
|
198
|
+
retention: "explicit",
|
|
199
|
+
restoreMount: "new-resource"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
cloudflare: {
|
|
204
|
+
id: "cloudflare",
|
|
205
|
+
label: sandboxDriverLabels.cloudflare,
|
|
206
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
207
|
+
metadata: {
|
|
208
|
+
driverRunsIn: ["worker"],
|
|
209
|
+
hostStopBehavior: "not-supported",
|
|
210
|
+
hostResumeBehavior: "same-host",
|
|
211
|
+
targetAccess: "relay",
|
|
212
|
+
secretBrokering: "native",
|
|
213
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
214
|
+
egressControl: "none",
|
|
215
|
+
persistence: {
|
|
216
|
+
resume: "replacement-restore",
|
|
217
|
+
capture: "directories",
|
|
218
|
+
clone: false,
|
|
219
|
+
captureSource: "preserved",
|
|
220
|
+
retention: "provider-managed",
|
|
221
|
+
restoreMount: "copy-on-write"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
box: {
|
|
226
|
+
id: "box",
|
|
227
|
+
label: sandboxDriverLabels.box,
|
|
228
|
+
credentialFields: sandboxDriverCredentialFields.box,
|
|
229
|
+
metadata: {
|
|
230
|
+
driverRunsIn: ["node"],
|
|
231
|
+
hostStopBehavior: "suspends-host",
|
|
232
|
+
hostResumeBehavior: "same-host",
|
|
233
|
+
targetAccess: "relay",
|
|
234
|
+
secretBrokering: "none",
|
|
235
|
+
egressControl: "none",
|
|
236
|
+
persistence: {
|
|
237
|
+
resume: "same-sandbox",
|
|
238
|
+
capture: "none",
|
|
239
|
+
clone: false,
|
|
240
|
+
captureSource: "not-applicable",
|
|
241
|
+
retention: "provider-managed",
|
|
242
|
+
restoreMount: "same-resource"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
docker: {
|
|
247
|
+
id: "docker",
|
|
248
|
+
label: sandboxDriverLabels.docker,
|
|
249
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
250
|
+
metadata: {
|
|
251
|
+
driverRunsIn: ["local"],
|
|
252
|
+
hostStopBehavior: "terminates-host",
|
|
253
|
+
hostResumeBehavior: "same-host",
|
|
254
|
+
targetAccess: "loopback",
|
|
255
|
+
secretBrokering: "none",
|
|
256
|
+
egressControl: "none",
|
|
257
|
+
persistence: {
|
|
258
|
+
resume: "replacement-restore",
|
|
259
|
+
capture: "same-resource",
|
|
260
|
+
clone: false,
|
|
261
|
+
captureSource: "preserved",
|
|
262
|
+
retention: "provider-managed",
|
|
263
|
+
restoreMount: "same-resource"
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
270
|
+
function isRecord(value) {
|
|
271
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
272
|
+
}
|
|
273
|
+
function asRecord(value) {
|
|
274
|
+
return isRecord(value) ? value : void 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/json.ts
|
|
278
|
+
function num(input) {
|
|
279
|
+
return typeof input === "number" ? input : void 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/drivers/transient-error.ts
|
|
283
|
+
function textOf(input) {
|
|
284
|
+
return typeof input === "string" ? input : "";
|
|
285
|
+
}
|
|
286
|
+
function driverErrorSignals(err) {
|
|
287
|
+
const shaped = isRecord(err) ? err : {};
|
|
288
|
+
const nested = isRecord(shaped.response) ? shaped.response : {};
|
|
289
|
+
return {
|
|
290
|
+
status: num(nested.status) ?? num(shaped.status),
|
|
291
|
+
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function isTransientDriverError(err, markers) {
|
|
295
|
+
const { status, text } = driverErrorSignals(err);
|
|
296
|
+
if (status !== void 0 && status >= 500) return true;
|
|
297
|
+
return markers.some((marker) => text.includes(marker));
|
|
298
|
+
}
|
|
299
|
+
|
|
48
300
|
// src/drivers/vercel.ts
|
|
49
301
|
var DEFAULT_RUNTIME = "node22";
|
|
50
302
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
@@ -53,20 +305,22 @@ var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
|
53
305
|
var DEFAULT_SNAPSHOT_BUILD_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
54
306
|
var DEFAULT_OPERATION_TIMEOUT_MS = 6e4;
|
|
55
307
|
var DEFAULT_KEEP_ALIVE_MS = 5 * 60 * 1e3;
|
|
56
|
-
|
|
57
|
-
|
|
308
|
+
function runtimePackage(version) {
|
|
309
|
+
return `@claxedo/workspace-runtime@${version}`;
|
|
310
|
+
}
|
|
58
311
|
var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
59
312
|
var snapshotCache = /* @__PURE__ */ new Map();
|
|
60
313
|
var snapshotCacheState = /* @__PURE__ */ new WeakMap();
|
|
314
|
+
function isVercelSandboxFactory(value) {
|
|
315
|
+
const row = asRecord(value);
|
|
316
|
+
return typeof row?.create === "function" && typeof row.get === "function";
|
|
317
|
+
}
|
|
61
318
|
function nameFor(workspaceId) {
|
|
62
319
|
return `claxedo-${workspaceId}`;
|
|
63
320
|
}
|
|
321
|
+
var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
|
|
64
322
|
function transientDriverError(err) {
|
|
65
|
-
|
|
66
|
-
const status = shaped.response?.status ?? shaped.status;
|
|
67
|
-
if (typeof status === "number" && status >= 500) return true;
|
|
68
|
-
const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
69
|
-
return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline") || text.includes("pending");
|
|
323
|
+
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
70
324
|
}
|
|
71
325
|
function network(input) {
|
|
72
326
|
if (!input.net || input.net.mode === "allow-all") return void 0;
|
|
@@ -74,23 +328,53 @@ function network(input) {
|
|
|
74
328
|
if (hosts.length === 0) return "deny-all";
|
|
75
329
|
return { allow: hosts };
|
|
76
330
|
}
|
|
77
|
-
function vercelBrokeredNetworkPolicy(secrets) {
|
|
331
|
+
function vercelBrokeredNetworkPolicy(secrets, base) {
|
|
78
332
|
const allow = {};
|
|
333
|
+
for (const [host, rules] of baseAllowEntries(base)) allow[host] = [...rules];
|
|
79
334
|
for (const secret of secrets) {
|
|
80
335
|
if (!secret.header) {
|
|
81
336
|
throw new Error(
|
|
82
337
|
`vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
|
|
83
338
|
);
|
|
84
339
|
}
|
|
340
|
+
if (secret.hosts.length === 0) {
|
|
341
|
+
throw new Error(`vercel brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
|
|
342
|
+
}
|
|
343
|
+
const value = secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
|
|
344
|
+
const methods = [...secret.methods ?? []];
|
|
345
|
+
const pathPrefixes = [...secret.pathPrefixes ?? []];
|
|
85
346
|
for (const host of secret.hosts) {
|
|
86
|
-
;
|
|
87
|
-
(
|
|
347
|
+
const rules = allow[host] ??= [];
|
|
348
|
+
for (const prefix of methods.length ? pathPrefixes : []) {
|
|
349
|
+
rules.push({
|
|
350
|
+
match: { path: { startsWith: prefix }, method: methods },
|
|
351
|
+
transform: [{ headers: { [secret.header]: value } }]
|
|
352
|
+
});
|
|
353
|
+
}
|
|
88
354
|
}
|
|
89
355
|
}
|
|
90
|
-
|
|
356
|
+
const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
|
|
357
|
+
return { allow, ...subnets };
|
|
358
|
+
}
|
|
359
|
+
function baseAllowEntries(base) {
|
|
360
|
+
if (base === void 0 || base === "allow-all") return [["*", []]];
|
|
361
|
+
if (base === "deny-all") return [];
|
|
362
|
+
if (Array.isArray(base.allow)) return base.allow.map((host) => [host, []]);
|
|
363
|
+
return Object.entries(base.allow ?? {});
|
|
91
364
|
}
|
|
92
365
|
function createVercelSandboxDriver(options) {
|
|
93
|
-
|
|
366
|
+
let defaultFactory;
|
|
367
|
+
function resolveFactory() {
|
|
368
|
+
if (options.sandbox) return options.sandbox;
|
|
369
|
+
defaultFactory ??= import("@vercel/sandbox").then((sdk) => {
|
|
370
|
+
const factory = sdk.Sandbox;
|
|
371
|
+
if (!isVercelSandboxFactory(factory)) {
|
|
372
|
+
throw new Error("@vercel/sandbox does not expose the factory surface this driver uses");
|
|
373
|
+
}
|
|
374
|
+
return factory;
|
|
375
|
+
});
|
|
376
|
+
return defaultFactory;
|
|
377
|
+
}
|
|
94
378
|
const runtime = options.runtime ?? DEFAULT_RUNTIME;
|
|
95
379
|
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
96
380
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
@@ -117,22 +401,17 @@ function createVercelSandboxDriver(options) {
|
|
|
117
401
|
};
|
|
118
402
|
}
|
|
119
403
|
function bootEnv(input, hostId) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
132
|
-
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
133
|
-
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
134
|
-
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
135
|
-
return env;
|
|
404
|
+
return workspaceRuntimeBootEnv({
|
|
405
|
+
workspaceId: input.workspaceId,
|
|
406
|
+
hostId,
|
|
407
|
+
directory: workspaceDirectory(input),
|
|
408
|
+
port: runtimePort,
|
|
409
|
+
host: "0.0.0.0",
|
|
410
|
+
source: input.source,
|
|
411
|
+
env: input.env,
|
|
412
|
+
runner: options.runner,
|
|
413
|
+
controlEnv: options.controlEnv
|
|
414
|
+
});
|
|
136
415
|
}
|
|
137
416
|
async function runSetup(sandbox, command) {
|
|
138
417
|
await sandbox.runCommand("bash", ["-c", command], {
|
|
@@ -141,7 +420,8 @@ function createVercelSandboxDriver(options) {
|
|
|
141
420
|
}
|
|
142
421
|
async function ensureSnapshot() {
|
|
143
422
|
if (options.baseSnapshotId) return options.baseSnapshotId;
|
|
144
|
-
const
|
|
423
|
+
const expectedRuntimeVersion = workspaceRuntimeVersion();
|
|
424
|
+
const cacheKey = `${options.teamId}:${options.projectId}:${runtime}:${expectedRuntimeVersion}`;
|
|
145
425
|
const hit = cache.get(cacheKey);
|
|
146
426
|
const expiresAt = cacheState.expires.get(cacheKey);
|
|
147
427
|
if (hit && expiresAt === void 0) {
|
|
@@ -152,7 +432,7 @@ function createVercelSandboxDriver(options) {
|
|
|
152
432
|
const running = cacheState.builds.get(cacheKey);
|
|
153
433
|
if (running) return running;
|
|
154
434
|
const build = (async () => {
|
|
155
|
-
const builder = await
|
|
435
|
+
const builder = await (await resolveFactory()).create({
|
|
156
436
|
...credentials(),
|
|
157
437
|
runtime,
|
|
158
438
|
ports: [runtimePort],
|
|
@@ -183,17 +463,11 @@ function createVercelSandboxDriver(options) {
|
|
|
183
463
|
`sudo chown $(whoami) ${shell(RUNTIME_DIR)}`,
|
|
184
464
|
`cd ${shell(RUNTIME_DIR)}`,
|
|
185
465
|
"npm init -y",
|
|
186
|
-
`npm install ${
|
|
187
|
-
`node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${
|
|
466
|
+
`npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
|
|
467
|
+
`node -e "const fs=require('fs');const actual=JSON.parse(fs.readFileSync('${RUNTIME_DIR}/node_modules/@claxedo/workspace-runtime/package.json','utf8')).version;if(actual!=='${expectedRuntimeVersion}'){throw new Error('workspace-runtime snapshot package version mismatch: expected ${expectedRuntimeVersion}, got '+actual)}"`,
|
|
188
468
|
"test -x node_modules/.bin/workspace-runtime",
|
|
189
|
-
"test -x node_modules/.bin/claude-agent-acp",
|
|
190
|
-
"test -x node_modules/.bin/codex-acp",
|
|
191
469
|
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
|
|
192
|
-
|
|
193
|
-
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
|
|
194
|
-
"command -v workspace-runtime",
|
|
195
|
-
"command -v claude-agent-acp",
|
|
196
|
-
"command -v codex-acp"
|
|
470
|
+
"command -v workspace-runtime"
|
|
197
471
|
].join(" && "));
|
|
198
472
|
const snapshot = await builder.snapshot({
|
|
199
473
|
expiration: 0,
|
|
@@ -247,22 +521,26 @@ function createVercelSandboxDriver(options) {
|
|
|
247
521
|
const hostId = input.hostId ?? nameFor(input.workspaceId);
|
|
248
522
|
const env = {
|
|
249
523
|
...bootEnv(input, hostId),
|
|
250
|
-
...await options.env?.(input, { id: hostId })
|
|
524
|
+
...await options.env?.(input, { id: hostId }),
|
|
525
|
+
// Last, so no caller-supplied variable of the same name can stand in for
|
|
526
|
+
// a placeholder the firewall transform overwrites.
|
|
527
|
+
...brokeredPlaceholderEnv(input.secrets)
|
|
251
528
|
};
|
|
252
|
-
const
|
|
529
|
+
const createPolicy = network(input);
|
|
530
|
+
const sandbox = await (await resolveFactory()).create({
|
|
253
531
|
...credentials(),
|
|
254
532
|
source: await source(input),
|
|
255
533
|
ports: [runtimePort],
|
|
256
534
|
timeout: timeoutMs,
|
|
257
535
|
env,
|
|
258
|
-
...
|
|
536
|
+
...createPolicy ? { networkPolicy: createPolicy } : {}
|
|
259
537
|
}).catch((err) => {
|
|
260
538
|
if (transientDriverError(err)) return void 0;
|
|
261
539
|
throw err;
|
|
262
540
|
});
|
|
263
541
|
if (!sandbox) return { provisioning: true, retryAfterMs: 2e3 };
|
|
264
542
|
if (input.secrets?.length) {
|
|
265
|
-
await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets), {
|
|
543
|
+
await sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(input.secrets, createPolicy), {
|
|
266
544
|
signal: AbortSignal.timeout(operationTimeoutMs)
|
|
267
545
|
});
|
|
268
546
|
}
|
|
@@ -270,7 +548,7 @@ function createVercelSandboxDriver(options) {
|
|
|
270
548
|
return readyTarget(input, sandbox, hostId);
|
|
271
549
|
}
|
|
272
550
|
async function sandboxById(target) {
|
|
273
|
-
return
|
|
551
|
+
return (await resolveFactory()).get({
|
|
274
552
|
...credentials(),
|
|
275
553
|
sandboxId: target.sandboxId
|
|
276
554
|
});
|
|
@@ -282,7 +560,9 @@ function createVercelSandboxDriver(options) {
|
|
|
282
560
|
hostStopBehavior: "terminates-host",
|
|
283
561
|
hostResumeBehavior: "replacement-host",
|
|
284
562
|
targetAccess: "relay",
|
|
285
|
-
secretBrokering: "native"
|
|
563
|
+
secretBrokering: "native",
|
|
564
|
+
egressControl: "hosts",
|
|
565
|
+
persistence: sandboxDriverCatalog.vercel.metadata.persistence
|
|
286
566
|
},
|
|
287
567
|
ensureHost,
|
|
288
568
|
resumeHost: (input) => ensureHost(input.ensure),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The egress allowlist a HOSTED, multi-tenant sandbox is provisioned with.
|
|
3
|
+
*
|
|
4
|
+
* A hosted sandbox runs agent-authored code on the operator's infrastructure,
|
|
5
|
+
* next to other tenants' sandboxes, holding a checkout of the user's private
|
|
6
|
+
* repository and (via `SandboxBrokeredSecret`) the ability to spend the
|
|
7
|
+
* operator's model credits. The default for that shape of workload is
|
|
8
|
+
* restrictive, not allow-all: the interesting attack is not "the agent
|
|
9
|
+
* downloads a package", it is "the agent POSTs the repository to a host the
|
|
10
|
+
* attacker controls". An allowlist is what makes that fail.
|
|
11
|
+
*
|
|
12
|
+
* Every entry below has to earn its place, so the list is assembled from three
|
|
13
|
+
* sources of truth rather than from taste:
|
|
14
|
+
*
|
|
15
|
+
* 1. DERIVED PER REQUEST — the control plane the sandbox reports back to and
|
|
16
|
+
* the git host it clones from. These differ per deployment and per
|
|
17
|
+
* workspace, so they are inputs, not constants.
|
|
18
|
+
* 2. MODEL PROVIDERS — the endpoints an agent harness must reach to do any
|
|
19
|
+
* work at all. Taken from the harness→provider mapping the credential
|
|
20
|
+
* layer already maintains (`PROVIDER_TO_GROUP` /`DEFAULT_ALLOWLIST` in
|
|
21
|
+
* claxedo-server's `network/types.ts`), narrowed to the API hostnames.
|
|
22
|
+
* 3. PACKAGE REGISTRIES — an agent that cannot install a dependency cannot
|
|
23
|
+
* edit a real repository. Same source, again narrowed.
|
|
24
|
+
*
|
|
25
|
+
* What is deliberately NOT here, despite being in that same default list:
|
|
26
|
+
* general-purpose object storage and CDN wildcards (`*.workers.dev`,
|
|
27
|
+
* `r2.cloudflarestorage.com`, `storage.googleapis.com`,
|
|
28
|
+
* `*.blob.core.windows.net`, `*.amazonaws.com`). Every one of those is a
|
|
29
|
+
* bucket an attacker can create in their own account, so allowlisting them
|
|
30
|
+
* hands back exactly the exfiltration channel the policy exists to close. A
|
|
31
|
+
* deployment that genuinely needs one adds it through `extraHosts` as an
|
|
32
|
+
* explicit, auditable decision.
|
|
33
|
+
*
|
|
34
|
+
* This module is data + string handling only, so it runs unchanged inside the
|
|
35
|
+
* hosted Worker (no DNS, no node builtins). That matters: name-based policy is
|
|
36
|
+
* the only kind the Worker can compute, which is why `egressControl` treats
|
|
37
|
+
* name filtering as the capability that qualifies a driver for hosted use.
|
|
38
|
+
*/
|
|
39
|
+
import type { SandboxNetworkPolicy, SandboxSource } from ".";
|
|
40
|
+
/**
|
|
41
|
+
* Model-provider endpoints. Derived from the harness→provider groups in
|
|
42
|
+
* claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
|
|
43
|
+
* anthropic, codex-app-server/cursor-sdk → openai,
|
|
44
|
+
* google → google_ai; plus the aggregator and catalog endpoints the engine
|
|
45
|
+
* itself fetches at startup.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SANDBOX_MODEL_PROVIDER_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai"];
|
|
48
|
+
/**
|
|
49
|
+
* Package registries. Narrowed from the npm / pypi / rust / golang groups of
|
|
50
|
+
* `DEFAULT_ALLOWLIST` to the hostnames a package install actually contacts —
|
|
51
|
+
* the wildcard CDN entries in those groups are excluded on purpose (see the
|
|
52
|
+
* module comment).
|
|
53
|
+
*/
|
|
54
|
+
export declare const SANDBOX_PACKAGE_REGISTRY_HOSTS: readonly ["registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
|
|
55
|
+
/** The deployment-independent floor: providers + registries. */
|
|
56
|
+
export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai", "registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
|
|
57
|
+
/**
|
|
58
|
+
* The git host this workspace clones from.
|
|
59
|
+
*
|
|
60
|
+
* Scoped to the ONE repository host in the request rather than to a forge
|
|
61
|
+
* allowlist, matching how `authenticatedGitHubCloneSource` scopes the clone
|
|
62
|
+
* credential it brokers (`hosts: ["github.com"]`) — the credential and the
|
|
63
|
+
* network policy should describe the same reachable surface.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sandboxSourceHost(source: SandboxSource | undefined): string | undefined;
|
|
66
|
+
export type HostedSandboxNetworkPolicyInput = {
|
|
67
|
+
/**
|
|
68
|
+
* Control-plane and relay endpoints the runtime reports back to: the relay
|
|
69
|
+
* it tunnels through and fetches `WORKSPACE_RUNTIME_RELAY_JWKS_URL` from,
|
|
70
|
+
* and the control-plane origin serving `WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL`
|
|
71
|
+
* plus register/heartbeat. URLs or bare hostnames; blanks are ignored.
|
|
72
|
+
*/
|
|
73
|
+
controlPlane: Array<string | undefined>;
|
|
74
|
+
/** Workspace source — its git host is allowed, nothing else on that forge. */
|
|
75
|
+
source?: SandboxSource;
|
|
76
|
+
/** Operator escape hatch for deployment-specific hosts. */
|
|
77
|
+
extraHosts?: readonly string[];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Build the restricted policy for a hosted sandbox.
|
|
81
|
+
*
|
|
82
|
+
* Always `mode: "restricted"`. There is no input that makes this return
|
|
83
|
+
* allow-all — a hosted sandbox with an empty or unresolvable control-plane URL
|
|
84
|
+
* is a misconfiguration, and answering it with "then allow everything" is how
|
|
85
|
+
* the gap this closes came to exist in the first place.
|
|
86
|
+
*/
|
|
87
|
+
export declare function hostedSandboxNetworkPolicy(input: HostedSandboxNetworkPolicyInput): SandboxNetworkPolicy;
|
|
@@ -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 void 0;
|
|
29
|
+
try {
|
|
30
|
+
const host = new URL(raw).hostname.trim().toLowerCase();
|
|
31
|
+
return host ? host : void 0;
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function sandboxSourceHost(source) {
|
|
37
|
+
if (!source || source.kind !== "git") return void 0;
|
|
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
|
+
};
|