@claxedo/sandbox-manager 0.9.0 → 0.10.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 +233 -26
- package/dist/checkpoint-manager.d.ts +29 -4
- package/dist/checkpoint-manager.mjs +133 -24
- package/dist/command.d.ts +9 -2
- package/dist/command.mjs +8 -8
- package/dist/contract.d.ts +687 -0
- package/dist/driver-billing.d.ts +29 -0
- package/dist/driver-catalog.d.ts +42 -7
- package/dist/driver-catalog.mjs +115 -161
- package/dist/drivers/boat-client.d.ts +95 -0
- package/dist/drivers/boat-named-snapshots.d.ts +31 -0
- package/dist/drivers/boat-template.d.ts +49 -0
- package/dist/drivers/boat-template.mjs +1196 -0
- package/dist/drivers/boat.d.ts +64 -0
- package/dist/drivers/boat.mjs +990 -0
- package/dist/drivers/cloudflare.d.ts +3 -3
- package/dist/drivers/cloudflare.mjs +208 -147
- package/dist/drivers/docker.d.ts +4 -3
- package/dist/drivers/docker.mjs +154 -126
- package/dist/drivers/local-brokering.d.ts +14 -0
- package/dist/drivers/local-brokering.mjs +1029 -0
- package/dist/drivers/modal.d.ts +2 -2
- package/dist/drivers/modal.mjs +121 -111
- package/dist/drivers/transient-error.d.ts +2 -0
- package/dist/drivers/vercel.d.ts +3 -2
- package/dist/drivers/vercel.mjs +151 -115
- package/dist/egress-policy.d.ts +134 -0
- package/dist/egress-warnings.d.ts +22 -0
- package/dist/garbage-collect.d.ts +14 -0
- package/dist/hosted-network-policy.d.ts +9 -2
- package/dist/hosted-network-policy.mjs +218 -1
- package/dist/image-name.d.ts +12 -2
- package/dist/image-name.mjs +14 -5
- package/dist/image.d.ts +1 -20
- package/dist/image.mjs +11 -88
- package/dist/index.d.ts +8 -578
- package/dist/index.mjs +1379 -320
- package/dist/lease-failure.d.ts +173 -0
- package/dist/lease-lifecycle.d.ts +65 -0
- package/dist/lease-retry-wait.d.ts +8 -0
- package/dist/lease-start.d.ts +21 -0
- package/dist/lease-types.d.ts +4 -3
- package/dist/manager.d.ts +2 -0
- package/dist/runtime-env.d.ts +4 -2
- package/dist/runtime-env.mjs +37 -2
- package/dist/runtime-snapshot.d.ts +49 -0
- package/dist/runtime-version.d.ts +9 -0
- package/dist/runtime-version.mjs +2 -0
- package/dist/service-url-exposure.d.ts +0 -1
- package/dist/service-url-exposure.mjs +0 -9
- package/dist/start-fresh.d.ts +17 -0
- package/dist/start-telemetry.d.ts +37 -0
- package/dist/stores/memory.d.ts +1 -1
- package/dist/stores/memory.mjs +63 -52
- package/package.json +26 -42
- package/dist/daytona-allow-list.d.ts +0 -16
- package/dist/daytona-allow-list.mjs +0 -11
- package/dist/drivers/box.d.ts +0 -52
- package/dist/drivers/box.mjs +0 -574
- package/dist/drivers/daytona.d.ts +0 -163
- package/dist/drivers/daytona.mjs +0 -683
- package/dist/drivers/exe.d.ts +0 -26
- package/dist/drivers/exe.mjs +0 -492
- package/dist/drivers/fetch-bridge.d.ts +0 -10
- package/dist/drivers/fetch-bridge.mjs +0 -126
- package/dist/lease-policy.d.ts +0 -52
- package/dist/lease-policy.mjs +0 -290
package/dist/drivers/vercel.mjs
CHANGED
|
@@ -1,33 +1,4 @@
|
|
|
1
|
-
// src/
|
|
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
|
|
1
|
+
// src/egress-policy.ts
|
|
31
2
|
function brokeredSecretPlaceholder(name) {
|
|
32
3
|
return `claxedo-broker:${name}`;
|
|
33
4
|
}
|
|
@@ -36,13 +7,43 @@ function brokeredPlaceholderEnv(secrets) {
|
|
|
36
7
|
}
|
|
37
8
|
|
|
38
9
|
// src/runtime-env.ts
|
|
10
|
+
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
11
|
+
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
12
|
+
"WORKSPACE_RUNTIME_HOST_ID",
|
|
13
|
+
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
14
|
+
];
|
|
15
|
+
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
16
|
+
if (!env) return [];
|
|
17
|
+
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
18
|
+
}
|
|
19
|
+
function assertWorkspaceRuntimeIdentityEnv(env) {
|
|
20
|
+
const conflicts = workspaceRuntimeIdentityEnvConflicts(env);
|
|
21
|
+
if (conflicts.length === 0) return;
|
|
22
|
+
throw new Error(
|
|
23
|
+
`sandbox env cannot set runtime identity ${conflicts.join(", ")}: the driver's placement owns the identity its target reports and the lease records`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
27
|
+
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
28
|
+
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
29
|
+
"WORKSPACE_RUNTIME_PORT",
|
|
30
|
+
"WORKSPACE_RUNTIME_HOST",
|
|
31
|
+
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
32
|
+
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
33
|
+
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
34
|
+
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
35
|
+
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
36
|
+
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
37
|
+
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
38
|
+
]);
|
|
39
39
|
function workspaceRuntimeBootEnv(input) {
|
|
40
|
+
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
40
41
|
const env = {
|
|
41
42
|
...workspaceRuntimeTargetEnv(input),
|
|
42
43
|
...workspaceRuntimeSourceEnv(input),
|
|
43
44
|
...input.env
|
|
44
45
|
};
|
|
45
|
-
if (input.
|
|
46
|
+
if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
|
|
46
47
|
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
47
48
|
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
48
49
|
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
@@ -78,13 +79,43 @@ function shell(input) {
|
|
|
78
79
|
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// src/constants.ts
|
|
83
|
+
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
84
|
+
|
|
81
85
|
// src/defaults.ts
|
|
82
86
|
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
83
87
|
|
|
88
|
+
// src/driver-billing.ts
|
|
89
|
+
var HOUR_SECONDS = 3600;
|
|
90
|
+
var boatBilling = {
|
|
91
|
+
source: { url: "https://docs.boat.dev/pricing", readOn: "2026-10-12" },
|
|
92
|
+
meter: "Boat bills machine time per second while a sandbox runs, at an hourly rate per size; a stopped sandbox costs nothing.",
|
|
93
|
+
rates: {
|
|
94
|
+
small: { size: "small", vcpu: 2, memoryGb: 4, usdPerHour: 0.018 },
|
|
95
|
+
default: { size: "default", vcpu: 4, memoryGb: 8, usdPerHour: 0.036 },
|
|
96
|
+
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
100
|
+
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
101
|
+
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
102
|
+
var CLOUDFLARE_STANDARD_1 = { size: "standard-1", vcpu: 0.5, memoryGb: 4, diskGb: 8 };
|
|
103
|
+
var cloudflareBilling = {
|
|
104
|
+
source: { url: "https://developers.cloudflare.com/containers/pricing/", readOn: "2026-10-12" },
|
|
105
|
+
meter: "Cloudflare bills a container for every 10 ms it runs: memory at $0.0000025 per GiB-second and disk at $0.00000007 per GB-second for the whole run, plus CPU at $0.00002 per vCPU-second of active use.",
|
|
106
|
+
rates: {
|
|
107
|
+
default: {
|
|
108
|
+
size: CLOUDFLARE_STANDARD_1.size,
|
|
109
|
+
vcpu: CLOUDFLARE_STANDARD_1.vcpu,
|
|
110
|
+
memoryGb: CLOUDFLARE_STANDARD_1.memoryGb,
|
|
111
|
+
usdPerHour: (CLOUDFLARE_STANDARD_1.memoryGb * CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY + CLOUDFLARE_STANDARD_1.diskGb * CLOUDFLARE_USD_PER_GB_SECOND_DISK) * HOUR_SECONDS
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
excludes: `active CPU, billed at $${CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE} per vCPU-second and not measured by Claxedo, and the monthly allowance a Workers Paid plan includes`
|
|
115
|
+
};
|
|
116
|
+
|
|
84
117
|
// ../sandbox-contract/dist/index.mjs
|
|
85
118
|
var sandboxDriverCredentialFields = {
|
|
86
|
-
exe: [{ key: "api_token", label: "API Token", secret: true }],
|
|
87
|
-
daytona: [{ key: "api_key", label: "API Key", secret: true }],
|
|
88
119
|
modal: [
|
|
89
120
|
{ key: "token_id", label: "Token ID", secret: true },
|
|
90
121
|
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
@@ -98,68 +129,26 @@ var sandboxDriverCredentialFields = {
|
|
|
98
129
|
{ key: "api_token", label: "API Token", secret: true },
|
|
99
130
|
{ key: "worker_url", label: "Worker URL" }
|
|
100
131
|
],
|
|
101
|
-
|
|
132
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
102
133
|
docker: [{ key: "image", label: "Image" }]
|
|
103
134
|
};
|
|
104
135
|
var sandboxDriverLabels = {
|
|
105
|
-
exe: "exe.dev",
|
|
106
|
-
daytona: "Daytona",
|
|
107
136
|
modal: "Modal",
|
|
108
137
|
vercel: "Vercel",
|
|
109
138
|
cloudflare: "Cloudflare",
|
|
110
|
-
|
|
139
|
+
boat: "Boat",
|
|
111
140
|
docker: "Docker"
|
|
112
141
|
};
|
|
113
142
|
|
|
114
143
|
// src/driver-catalog.ts
|
|
115
144
|
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
145
|
modal: {
|
|
160
146
|
id: "modal",
|
|
161
147
|
label: sandboxDriverLabels.modal,
|
|
162
148
|
credentialFields: sandboxDriverCredentialFields.modal,
|
|
149
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
150
|
+
runtimeEnv: "callback",
|
|
151
|
+
preparedImage: true,
|
|
163
152
|
metadata: {
|
|
164
153
|
driverRunsIn: ["node"],
|
|
165
154
|
hostStopBehavior: "terminates-host",
|
|
@@ -182,6 +171,9 @@ var sandboxDriverCatalog = {
|
|
|
182
171
|
id: "vercel",
|
|
183
172
|
label: sandboxDriverLabels.vercel,
|
|
184
173
|
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
174
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
175
|
+
runtimeEnv: "callback",
|
|
176
|
+
preparedImage: false,
|
|
185
177
|
metadata: {
|
|
186
178
|
driverRunsIn: ["node"],
|
|
187
179
|
hostStopBehavior: "terminates-host",
|
|
@@ -204,30 +196,40 @@ var sandboxDriverCatalog = {
|
|
|
204
196
|
id: "cloudflare",
|
|
205
197
|
label: sandboxDriverLabels.cloudflare,
|
|
206
198
|
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
199
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
200
|
+
runtimeEnv: "callback",
|
|
201
|
+
preparedImage: false,
|
|
202
|
+
billing: cloudflareBilling,
|
|
207
203
|
metadata: {
|
|
208
204
|
driverRunsIn: ["worker"],
|
|
209
|
-
hostStopBehavior: "
|
|
205
|
+
hostStopBehavior: "terminates-host",
|
|
210
206
|
hostResumeBehavior: "same-host",
|
|
207
|
+
hostWatchesIdle: true,
|
|
211
208
|
targetAccess: "relay",
|
|
212
209
|
secretBrokering: "native",
|
|
213
210
|
// Credential handlers do not restrict unrelated destinations.
|
|
214
211
|
egressControl: "none",
|
|
212
|
+
// A container snapshot holds the whole root filesystem; the platform keeps it 30 days past its last restore and offers no deletion.
|
|
215
213
|
persistence: {
|
|
216
214
|
resume: "replacement-restore",
|
|
217
|
-
capture: "
|
|
215
|
+
capture: "filesystem",
|
|
218
216
|
clone: false,
|
|
219
217
|
captureSource: "preserved",
|
|
220
218
|
retention: "provider-managed",
|
|
221
|
-
restoreMount: "
|
|
219
|
+
restoreMount: "new-resource"
|
|
222
220
|
}
|
|
223
221
|
}
|
|
224
222
|
},
|
|
225
|
-
|
|
226
|
-
id: "
|
|
227
|
-
label: sandboxDriverLabels.
|
|
228
|
-
credentialFields: sandboxDriverCredentialFields.
|
|
223
|
+
boat: {
|
|
224
|
+
id: "boat",
|
|
225
|
+
label: sandboxDriverLabels.boat,
|
|
226
|
+
credentialFields: sandboxDriverCredentialFields.boat,
|
|
227
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
228
|
+
runtimeEnv: "callback",
|
|
229
|
+
preparedImage: true,
|
|
230
|
+
billing: boatBilling,
|
|
229
231
|
metadata: {
|
|
230
|
-
driverRunsIn: ["node"],
|
|
232
|
+
driverRunsIn: ["worker", "node"],
|
|
231
233
|
hostStopBehavior: "suspends-host",
|
|
232
234
|
hostResumeBehavior: "same-host",
|
|
233
235
|
targetAccess: "relay",
|
|
@@ -240,13 +242,17 @@ var sandboxDriverCatalog = {
|
|
|
240
242
|
captureSource: "not-applicable",
|
|
241
243
|
retention: "provider-managed",
|
|
242
244
|
restoreMount: "same-resource"
|
|
243
|
-
}
|
|
245
|
+
},
|
|
246
|
+
machineClasses: ["small", "default", "large"]
|
|
244
247
|
}
|
|
245
248
|
},
|
|
246
249
|
docker: {
|
|
247
250
|
id: "docker",
|
|
248
251
|
label: sandboxDriverLabels.docker,
|
|
249
252
|
credentialFields: sandboxDriverCredentialFields.docker,
|
|
253
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
254
|
+
runtimeEnv: "callback",
|
|
255
|
+
preparedImage: true,
|
|
250
256
|
metadata: {
|
|
251
257
|
driverRunsIn: ["local"],
|
|
252
258
|
hostStopBehavior: "terminates-host",
|
|
@@ -287,7 +293,9 @@ function driverErrorSignals(err) {
|
|
|
287
293
|
const shaped = isRecord(err) ? err : {};
|
|
288
294
|
const nested = isRecord(shaped.response) ? shaped.response : {};
|
|
289
295
|
return {
|
|
290
|
-
|
|
296
|
+
// `response.status` is the axios shape; `statusCode` is what SDK-native
|
|
297
|
+
// error classes expose instead.
|
|
298
|
+
status: num(nested.status) ?? num(shaped.status) ?? num(shaped.statusCode),
|
|
291
299
|
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
292
300
|
};
|
|
293
301
|
}
|
|
@@ -296,9 +304,28 @@ function isTransientDriverError(err, markers) {
|
|
|
296
304
|
if (status !== void 0 && status >= 500) return true;
|
|
297
305
|
return markers.some((marker) => text.includes(marker));
|
|
298
306
|
}
|
|
307
|
+
var TRANSIENT_DRIVER_RETRY_MS = 2e3;
|
|
308
|
+
|
|
309
|
+
// ../claxedo-helpers/dist/deadline.mjs
|
|
310
|
+
async function withDeadlineSignal(timeoutMs, work, parent) {
|
|
311
|
+
const deadline = new AbortController();
|
|
312
|
+
const timer = setTimeout(() => deadline.abort(new DOMException("The operation timed out.", "TimeoutError")), timeoutMs);
|
|
313
|
+
try {
|
|
314
|
+
return await work(parent ? AbortSignal.any([parent, deadline.signal]) : deadline.signal);
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
299
319
|
|
|
300
320
|
// src/drivers/vercel.ts
|
|
301
321
|
var DEFAULT_RUNTIME = "node22";
|
|
322
|
+
var HARNESS_RELEASE_AGE_EXCLUSIONS = `printf '%s\\n' ${[
|
|
323
|
+
"@agentclientprotocol/sdk",
|
|
324
|
+
"@anthropic-ai/claude-*",
|
|
325
|
+
"@cursor/sdk*",
|
|
326
|
+
"@earendil-works/*",
|
|
327
|
+
"@openai/codex"
|
|
328
|
+
].map((name) => `'min-release-age-exclude[]=${name}'`).join(" ")} >> "$HOME/.npmrc"`;
|
|
302
329
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
303
330
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
304
331
|
var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
@@ -346,8 +373,14 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
|
|
|
346
373
|
for (const host of secret.hosts) {
|
|
347
374
|
const rules = allow[host] ??= [];
|
|
348
375
|
for (const prefix of methods.length ? pathPrefixes : []) {
|
|
376
|
+
const path = credentialPathPattern(prefix);
|
|
377
|
+
if (path === void 0) continue;
|
|
349
378
|
rules.push({
|
|
350
|
-
match: {
|
|
379
|
+
match: {
|
|
380
|
+
path: { regex: path },
|
|
381
|
+
method: methods,
|
|
382
|
+
headers: [{ key: { exact: secret.header.toLowerCase() }, value: { exact: `${secret.scheme ? `${secret.scheme} ` : ""}claxedo-broker:${secret.name}` } }]
|
|
383
|
+
},
|
|
351
384
|
transform: [{ headers: { [secret.header]: value } }]
|
|
352
385
|
});
|
|
353
386
|
}
|
|
@@ -356,6 +389,17 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
|
|
|
356
389
|
const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
|
|
357
390
|
return { allow, ...subnets };
|
|
358
391
|
}
|
|
392
|
+
var RE2_SPECIAL = /[\\.+*?()|[\]{}^$]/g;
|
|
393
|
+
var PATH_CHAR = "(?:[^/\\\\%?#]|%(?:2[0-46-9a-dA-D]|5[0-9abd-fABD-F]|[013-46-9a-fA-F][0-9a-fA-F]))";
|
|
394
|
+
var PATH_CHAR_NOT_DOT = "(?:[^./\\\\%?#]|%(?:2[0-46-9a-dA-D]|5[0-9abd-fABD-F]|[013-46-9a-fA-F][0-9a-fA-F]))";
|
|
395
|
+
var SEGMENT = `(?:(?:${PATH_CHAR_NOT_DOT}|\\.${PATH_CHAR_NOT_DOT}|\\.\\.${PATH_CHAR})${PATH_CHAR}*)?`;
|
|
396
|
+
function credentialPathPattern(prefix) {
|
|
397
|
+
if (!new RegExp(`^(?:/${SEGMENT})+$`).test(prefix)) return void 0;
|
|
398
|
+
const base = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
399
|
+
const literal = base.replace(RE2_SPECIAL, "\\$&");
|
|
400
|
+
const rest = prefix.endsWith("/") ? `/${SEGMENT}(?:/${SEGMENT})*` : `(?:/${SEGMENT})*`;
|
|
401
|
+
return `^${literal}${rest}(?:\\?.*)?$`;
|
|
402
|
+
}
|
|
359
403
|
function baseAllowEntries(base) {
|
|
360
404
|
if (base === void 0 || base === "allow-all") return [["*", []]];
|
|
361
405
|
if (base === "deny-all") return [];
|
|
@@ -409,14 +453,12 @@ function createVercelSandboxDriver(options) {
|
|
|
409
453
|
host: "0.0.0.0",
|
|
410
454
|
source: input.source,
|
|
411
455
|
env: input.env,
|
|
412
|
-
|
|
456
|
+
nativeHarness: options.nativeHarness,
|
|
413
457
|
controlEnv: options.controlEnv
|
|
414
458
|
});
|
|
415
459
|
}
|
|
416
460
|
async function runSetup(sandbox, command) {
|
|
417
|
-
await sandbox.runCommand("bash", ["-c", command], {
|
|
418
|
-
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
419
|
-
});
|
|
461
|
+
await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.runCommand("bash", ["-c", command], { signal }));
|
|
420
462
|
}
|
|
421
463
|
async function ensureSnapshot() {
|
|
422
464
|
if (options.baseSnapshotId) return options.baseSnapshotId;
|
|
@@ -440,7 +482,8 @@ function createVercelSandboxDriver(options) {
|
|
|
440
482
|
});
|
|
441
483
|
await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
|
|
442
484
|
await runSetup(builder, [
|
|
443
|
-
|
|
485
|
+
HARNESS_RELEASE_AGE_EXCLUSIONS,
|
|
486
|
+
"npm i -g --min-release-age=2 tsx@4.22.3 opencode-ai@1.15.10 @anthropic-ai/claude-code@2.1.285 @openai/codex@0.159.2",
|
|
444
487
|
"curl https://cursor.com/install -fsS | bash",
|
|
445
488
|
"curl -fsSL https://ampcode.com/install.sh | bash",
|
|
446
489
|
"curl -fsSL https://app.factory.ai/cli | sh",
|
|
@@ -451,12 +494,10 @@ function createVercelSandboxDriver(options) {
|
|
|
451
494
|
"command -v opencode",
|
|
452
495
|
"command -v claude",
|
|
453
496
|
"command -v codex",
|
|
454
|
-
"command -v gemini",
|
|
455
497
|
"command -v agent",
|
|
456
498
|
"command -v cursor-agent",
|
|
457
499
|
"command -v amp",
|
|
458
|
-
"command -v droid"
|
|
459
|
-
"command -v pi"
|
|
500
|
+
"command -v droid"
|
|
460
501
|
].join(" && "));
|
|
461
502
|
await runSetup(builder, [
|
|
462
503
|
`sudo mkdir -p ${shell(RUNTIME_DIR)}`,
|
|
@@ -469,10 +510,7 @@ function createVercelSandboxDriver(options) {
|
|
|
469
510
|
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
|
|
470
511
|
"command -v workspace-runtime"
|
|
471
512
|
].join(" && "));
|
|
472
|
-
const snapshot = await builder.snapshot({
|
|
473
|
-
expiration: 0,
|
|
474
|
-
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
475
|
-
});
|
|
513
|
+
const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => builder.snapshot({ expiration: 0, signal }));
|
|
476
514
|
cache.set(cacheKey, snapshot.snapshotId);
|
|
477
515
|
cacheState.expires.set(cacheKey, now() + SNAPSHOT_CACHE_TTL_MS);
|
|
478
516
|
return snapshot.snapshotId;
|
|
@@ -493,14 +531,14 @@ function createVercelSandboxDriver(options) {
|
|
|
493
531
|
}
|
|
494
532
|
async function startRuntime(sandbox, input, env) {
|
|
495
533
|
const directory = workspaceDirectory(input);
|
|
496
|
-
await sandbox.runCommand({
|
|
534
|
+
await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.runCommand({
|
|
497
535
|
cmd: "bash",
|
|
498
536
|
args: ["-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
|
|
499
537
|
cwd: "/vercel/sandbox",
|
|
500
538
|
env,
|
|
501
539
|
detached: true,
|
|
502
|
-
signal
|
|
503
|
-
});
|
|
540
|
+
signal
|
|
541
|
+
}));
|
|
504
542
|
}
|
|
505
543
|
async function readyTarget(input, sandbox, hostId) {
|
|
506
544
|
const url = sandbox.domain(runtimePort);
|
|
@@ -538,11 +576,11 @@ function createVercelSandboxDriver(options) {
|
|
|
538
576
|
if (transientDriverError(err)) return void 0;
|
|
539
577
|
throw err;
|
|
540
578
|
});
|
|
541
|
-
if (!sandbox) return { provisioning: true, retryAfterMs:
|
|
579
|
+
if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
|
|
580
|
+
await input.onImageReady?.();
|
|
542
581
|
if (input.secrets?.length) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
});
|
|
582
|
+
const secrets = input.secrets;
|
|
583
|
+
await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(secrets, createPolicy), { signal }));
|
|
546
584
|
}
|
|
547
585
|
await startRuntime(sandbox, input, env);
|
|
548
586
|
return readyTarget(input, sandbox, hostId);
|
|
@@ -567,7 +605,7 @@ function createVercelSandboxDriver(options) {
|
|
|
567
605
|
ensureHost,
|
|
568
606
|
resumeHost: (input) => ensureHost(input.ensure),
|
|
569
607
|
async touch(target) {
|
|
570
|
-
await (await sandboxById(target)).extendTimeout(keepAliveMs)
|
|
608
|
+
await (await sandboxById(target)).extendTimeout(keepAliveMs);
|
|
571
609
|
},
|
|
572
610
|
async suspend(target) {
|
|
573
611
|
await (await sandboxById(target)).stop({ blocking: false });
|
|
@@ -579,10 +617,8 @@ function createVercelSandboxDriver(options) {
|
|
|
579
617
|
await (await sandboxById(target)).stop({ blocking: true });
|
|
580
618
|
},
|
|
581
619
|
async snapshot(target) {
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
585
|
-
});
|
|
620
|
+
const sandbox = await sandboxById(target);
|
|
621
|
+
const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.snapshot({ expiration: 0, signal }));
|
|
586
622
|
return { snapshotId: snapshot.snapshotId };
|
|
587
623
|
}
|
|
588
624
|
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type SandboxNetworkPolicy = {
|
|
2
|
+
mode: "allow-all" | "restricted";
|
|
3
|
+
hosts?: string[];
|
|
4
|
+
cidrs?: string[];
|
|
5
|
+
};
|
|
6
|
+
/** Brokered destinations are the minimum egress needed to use the credential. */
|
|
7
|
+
export declare function sandboxNetworkPolicyWithBrokeredHosts(policy: SandboxNetworkPolicy, secrets: readonly SandboxBrokeredSecret[] | undefined): SandboxNetworkPolicy;
|
|
8
|
+
/** @see SandboxDriverMetadata.egressControl */
|
|
9
|
+
export type SandboxEgressControl = "none" | "hosts" | "hosts-and-cidrs";
|
|
10
|
+
/**
|
|
11
|
+
* What the manager does with a caller's `net` for a given driver: enforce
|
|
12
|
+
* where the driver can, and say so loudly rather than refuse where it cannot.
|
|
13
|
+
*
|
|
14
|
+
* - `"enforce"` — the driver gets the policy verbatim and contains the
|
|
15
|
+
* sandbox. Also the answer when the caller asked for no containment
|
|
16
|
+
* (absent / `allow-all`), which is nothing to enforce.
|
|
17
|
+
* - `"withhold"` — the driver declares `egressControl: "none"`, so there is no
|
|
18
|
+
* containment to be had. The policy is NOT handed down: half the `"none"`
|
|
19
|
+
* drivers throw on a restricted policy (docker, modal, boat) and the
|
|
20
|
+
* other half silently drop it (cloudflare, the fetch bridge). Withholding at
|
|
21
|
+
* the manager means the throwing drivers never see one — their throws stay
|
|
22
|
+
* in place as their own last line of defence — and the silently-dropping
|
|
23
|
+
* ones stop pretending. The sandbox comes up with UNRESTRICTED egress, which
|
|
24
|
+
* is a real exposure, so the manager warns (see `onEgressUnenforced`).
|
|
25
|
+
* - `"refuse"` — the driver HAS egress control but cannot express this
|
|
26
|
+
* particular policy's encoding (a hosts-only driver handed an address-only
|
|
27
|
+
* allowlist). Deliberately still fail-closed: this is a composition mistake
|
|
28
|
+
* on an otherwise-enforcing driver, and degrading it to "withhold" would
|
|
29
|
+
* weaken the enforcing path. `hostedSandboxNetworkPolicy` only ever emits
|
|
30
|
+
* hosts, so no production path reaches it.
|
|
31
|
+
*
|
|
32
|
+
* Pure, like `validateSandboxPersistenceCapabilities`, so the manager, the
|
|
33
|
+
* composition layer, and tests can all ask the same question.
|
|
34
|
+
*/
|
|
35
|
+
export type SandboxEgressDisposition = {
|
|
36
|
+
action: "enforce";
|
|
37
|
+
} | {
|
|
38
|
+
action: "withhold";
|
|
39
|
+
reason: "sandbox_egress_uncontained";
|
|
40
|
+
} | {
|
|
41
|
+
action: "refuse";
|
|
42
|
+
reason: "sandbox_egress_policy_unenforceable";
|
|
43
|
+
};
|
|
44
|
+
/** @see SandboxEgressDisposition */
|
|
45
|
+
export declare function sandboxEgressDisposition(control: SandboxEgressControl, net: SandboxNetworkPolicy | undefined): SandboxEgressDisposition;
|
|
46
|
+
/**
|
|
47
|
+
* Emitted whenever a restricted egress policy is withheld because the composed
|
|
48
|
+
* driver declares no egress control: an unrestricted sandbox that nobody was
|
|
49
|
+
* told about is the exposure, so the degrade is never silent.
|
|
50
|
+
*
|
|
51
|
+
* Two phases, because a per-create line is easy to miss in request logs:
|
|
52
|
+
* - `"composition"` — once per `createSandboxManager` per driver, at boot.
|
|
53
|
+
* - `"ensure"` — every time a caller's policy is actually withheld, naming the
|
|
54
|
+
* workspace and the allowlist that did not take effect.
|
|
55
|
+
*/
|
|
56
|
+
export type SandboxEgressUnenforcedEvent = {
|
|
57
|
+
phase: "composition" | "ensure";
|
|
58
|
+
reason: "sandbox_egress_uncontained";
|
|
59
|
+
/** `SandboxDriver.id` of the composed driver. */
|
|
60
|
+
driver: string;
|
|
61
|
+
/** Always `"none"` today — carried so a sink can group without re-deriving. */
|
|
62
|
+
egressControl: SandboxEgressControl;
|
|
63
|
+
/** Ready-to-log sentence. Sinks that only forward text can use this alone. */
|
|
64
|
+
message: string;
|
|
65
|
+
/** Present on `"ensure"`. */
|
|
66
|
+
workspaceId?: string;
|
|
67
|
+
/** The start's placement labels. Present on `"ensure"`. */
|
|
68
|
+
labels?: Record<string, string>;
|
|
69
|
+
/** The allowlist the caller asked for and did NOT get. Present on `"ensure"`. */
|
|
70
|
+
requested?: SandboxNetworkPolicy;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* A credential the SANDBOX must be able to USE for outbound requests but must
|
|
74
|
+
* NEVER be able to READ. Unlike `env` (plaintext, readable — reserved for
|
|
75
|
+
* credentials the agent is trusted with, e.g. the user's own model
|
|
76
|
+
* subscription), a brokered secret's `value` never enters the sandbox: the
|
|
77
|
+
* provider injects it on egress to `hosts` only. Non-model credentials
|
|
78
|
+
* (connection tokens, deploy tokens) belong here.
|
|
79
|
+
*
|
|
80
|
+
* Guarantees enforced by the manager: brokered secrets are never written to
|
|
81
|
+
* labels, never logged, and never captured in a driver snapshot. A driver
|
|
82
|
+
* whose `metadata.secretBrokering` is not `"native"` cannot honor them and the
|
|
83
|
+
* manager refuses to provision (fail-closed).
|
|
84
|
+
*/
|
|
85
|
+
export type SandboxBrokeredSecret = {
|
|
86
|
+
/** Env var name the sandbox references. */
|
|
87
|
+
name: string;
|
|
88
|
+
/** The secret material. Never enters the sandbox in plaintext. */
|
|
89
|
+
value: string;
|
|
90
|
+
/** Egress allowlist: the only hosts for which the value is substituted/injected. */
|
|
91
|
+
hosts: string[];
|
|
92
|
+
/**
|
|
93
|
+
* The HTTP header the value is injected as on egress to `hosts`. Every
|
|
94
|
+
* native driver needs one and refuses a secret without it.
|
|
95
|
+
*/
|
|
96
|
+
header?: string;
|
|
97
|
+
/**
|
|
98
|
+
* The authentication scheme `header` carries, when it has one (`Bearer`).
|
|
99
|
+
*
|
|
100
|
+
* A driver that WRITES the whole header composes `"<scheme> <value>"`; one
|
|
101
|
+
* that SUBSTITUTES a placeholder the sandbox already wrote after the scheme
|
|
102
|
+
* ignores it, because the scheme is in the request before the value is.
|
|
103
|
+
*/
|
|
104
|
+
scheme?: string;
|
|
105
|
+
/**
|
|
106
|
+
* The methods and path prefixes the credential may be attached to, on top of
|
|
107
|
+
* `hosts`. A vendor host serves far more than the routes a turn needs —
|
|
108
|
+
* api.anthropic.com also answers the organization-admin API — and everything
|
|
109
|
+
* sharing the sandbox reaches the same host.
|
|
110
|
+
*
|
|
111
|
+
* Optional so a producer that has not been taught to state a policy still
|
|
112
|
+
* type-checks, and fails closed instead: a driver that can express them
|
|
113
|
+
* attaches the credential only within them, so an absent or empty policy
|
|
114
|
+
* names a host and nothing else, and nothing is spendable at a host alone. A
|
|
115
|
+
* driver whose provider cannot express them documents that it drops them,
|
|
116
|
+
* and the host allowlist is all the containment there is.
|
|
117
|
+
*/
|
|
118
|
+
methods?: readonly string[];
|
|
119
|
+
pathPrefixes?: readonly string[];
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* What the sandbox presents so a header-injecting driver recognizes the request
|
|
123
|
+
* as one asking for that secret.
|
|
124
|
+
*
|
|
125
|
+
* Carries no authority: the value is attached at the edge, keyed by the host
|
|
126
|
+
* and this string, and the string itself is never accepted by a vendor. The
|
|
127
|
+
* Cloudflare sandbox Worker is deployed on its own and cannot depend on this
|
|
128
|
+
* package, so it matches on its own copy of the prefix; a driver minting
|
|
129
|
+
* anything else sends every brokered request upstream bare, which is what
|
|
130
|
+
* `brokered-placeholder.test.ts` exists to catch.
|
|
131
|
+
*/
|
|
132
|
+
export declare function brokeredSecretPlaceholder(name: string): string;
|
|
133
|
+
/** The environment a header-injecting driver must add so the sandbox can present each secret. */
|
|
134
|
+
export declare function brokeredPlaceholderEnv(secrets: readonly SandboxBrokeredSecret[] | undefined): Record<string, string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SandboxEgressControl, SandboxEgressUnenforcedEvent, SandboxNetworkPolicy } from "./egress-policy";
|
|
2
|
+
/**
|
|
3
|
+
* The two halves of the warning that a driver runs sandboxes with unrestricted
|
|
4
|
+
* egress. The composition half is raised here, once per driver per isolate: a
|
|
5
|
+
* per-create line lands in request logs and is easy to miss, this one lands
|
|
6
|
+
* wherever the process starts, so an operator learns of it BEFORE the first
|
|
7
|
+
* workspace exists. `withheld` is the start half, raised once per driver per
|
|
8
|
+
* isolate from the first start whose policy the driver could not enforce.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createEgressWarnings(input: {
|
|
11
|
+
driver: {
|
|
12
|
+
id: string;
|
|
13
|
+
egressControl: SandboxEgressControl;
|
|
14
|
+
};
|
|
15
|
+
sink?: (event: SandboxEgressUnenforcedEvent) => void;
|
|
16
|
+
}): {
|
|
17
|
+
withheld(start: {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
labels?: Record<string, string>;
|
|
20
|
+
requested: SandboxNetworkPolicy;
|
|
21
|
+
}): void;
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SandboxDriver, type SandboxGarbageCollectResult, type SandboxLeaseStore } from "./contract";
|
|
2
|
+
/**
|
|
3
|
+
* Destroys every sandbox the driver lists under `appLabel` that no lease owns.
|
|
4
|
+
* The lease store is authoritative for which provider resource a workspace
|
|
5
|
+
* owns, so the sweep reads the provider first and the leases second: a driver
|
|
6
|
+
* that cannot enumerate answers "we could not look", never "nothing is
|
|
7
|
+
* orphaned", because an empty listing reaching the sweep would make every live
|
|
8
|
+
* sandbox look like an orphan and destroy the fleet.
|
|
9
|
+
*/
|
|
10
|
+
export declare function sweepSandboxes(input: {
|
|
11
|
+
driver: SandboxDriver;
|
|
12
|
+
leaseStore: SandboxLeaseStore;
|
|
13
|
+
appLabel: string;
|
|
14
|
+
}): Promise<SandboxGarbageCollectResult>;
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
* the only kind the Worker can compute, which is why `egressControl` treats
|
|
37
37
|
* name filtering as the capability that qualifies a driver for hosted use.
|
|
38
38
|
*/
|
|
39
|
-
import type {
|
|
39
|
+
import type { SandboxSource } from "./contract";
|
|
40
|
+
import type { SandboxNetworkPolicy } from "./egress-policy";
|
|
40
41
|
/**
|
|
41
42
|
* Model-provider endpoints. Derived from the harness→provider groups in
|
|
42
43
|
* claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
|
|
@@ -58,9 +59,15 @@ export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com
|
|
|
58
59
|
* The git host this workspace clones from.
|
|
59
60
|
*
|
|
60
61
|
* Scoped to the ONE repository host in the request rather than to a forge
|
|
61
|
-
* allowlist, matching how `
|
|
62
|
+
* allowlist, matching how `githubCloneSecret` scopes the clone
|
|
62
63
|
* credential it brokers (`hosts: ["github.com"]`) — the credential and the
|
|
63
64
|
* network policy should describe the same reachable surface.
|
|
65
|
+
*
|
|
66
|
+
* The host is caller-selected, so only a public spelling earns an allowlist
|
|
67
|
+
* entry: `169.254.169.254` or `localhost` named as a repoUrl would otherwise
|
|
68
|
+
* open the sandbox's egress to the provisioning network's own addresses. A
|
|
69
|
+
* DNS name cannot be seen through here (no resolver in this module); a name
|
|
70
|
+
* that hides a private answer is clone admission's refusal, not this one's.
|
|
64
71
|
*/
|
|
65
72
|
export declare function sandboxSourceHost(source: SandboxSource | undefined): string | undefined;
|
|
66
73
|
export type HostedSandboxNetworkPolicyInput = {
|