@claxedo/sandbox-manager 0.7.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 +245 -42
- 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 +43 -58
- package/dist/driver-catalog.mjs +192 -229
- 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 +5 -8
- package/dist/drivers/cloudflare.mjs +364 -213
- package/dist/drivers/docker.d.ts +4 -3
- package/dist/drivers/docker.mjs +244 -165
- package/dist/drivers/local-brokering.d.ts +14 -0
- package/dist/drivers/local-brokering.mjs +1029 -0
- package/dist/drivers/modal.d.ts +4 -7
- package/dist/drivers/modal.mjs +220 -155
- package/dist/drivers/transient-error.d.ts +13 -0
- package/dist/drivers/vercel.d.ts +21 -23
- package/dist/drivers/vercel.mjs +279 -169
- 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 +11 -4
- package/dist/hosted-network-policy.mjs +221 -4
- package/dist/image-name.d.ts +12 -2
- package/dist/image-name.mjs +43 -0
- package/dist/image.d.ts +1 -20
- package/dist/image.mjs +11 -66
- package/dist/index.d.ts +8 -533
- package/dist/index.mjs +1420 -315
- package/dist/json.d.ts +13 -0
- 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 +15 -5
- package/dist/manager.d.ts +2 -0
- package/dist/runtime-env.d.ts +15 -2
- package/dist/runtime-env.mjs +50 -1
- 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 +1 -3
- package/dist/service-url-exposure.mjs +1 -26
- 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 +65 -54
- package/package.json +67 -81
- 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 -499
- package/dist/drivers/cloudflare-egress.d.ts +0 -40
- package/dist/drivers/cloudflare-egress.mjs +0 -113
- package/dist/drivers/daytona.d.ts +0 -127
- package/dist/drivers/daytona.mjs +0 -541
- package/dist/drivers/exe.d.ts +0 -26
- package/dist/drivers/exe.mjs +0 -469
- package/dist/drivers/fetch-bridge.d.ts +0 -10
- package/dist/drivers/fetch-bridge.mjs +0 -111
- package/dist/lease-policy.d.ts +0 -48
- package/dist/lease-policy.mjs +0 -280
package/dist/drivers/vercel.mjs
CHANGED
|
@@ -1,7 +1,55 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
1
|
+
// src/egress-policy.ts
|
|
2
|
+
function brokeredSecretPlaceholder(name) {
|
|
3
|
+
return `claxedo-broker:${name}`;
|
|
4
|
+
}
|
|
5
|
+
function brokeredPlaceholderEnv(secrets) {
|
|
6
|
+
return Object.fromEntries((secrets ?? []).map((secret) => [secret.name, brokeredSecretPlaceholder(secret.name)]));
|
|
7
|
+
}
|
|
3
8
|
|
|
4
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
|
+
function workspaceRuntimeBootEnv(input) {
|
|
40
|
+
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
41
|
+
const env = {
|
|
42
|
+
...workspaceRuntimeTargetEnv(input),
|
|
43
|
+
...workspaceRuntimeSourceEnv(input),
|
|
44
|
+
...input.env
|
|
45
|
+
};
|
|
46
|
+
if (input.nativeHarness) env.WORKSPACE_RUNTIME_NATIVE_HARNESS = input.nativeHarness;
|
|
47
|
+
if (input.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = input.controlEnv.relayJwksUrl;
|
|
48
|
+
if (input.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = input.controlEnv.relayVerifyPem;
|
|
49
|
+
if (input.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = input.controlEnv.managementJwksUrl;
|
|
50
|
+
if (input.controlEnv?.sessionAuthorityUrl) env.WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL = input.controlEnv.sessionAuthorityUrl;
|
|
51
|
+
return env;
|
|
52
|
+
}
|
|
5
53
|
function workspaceRuntimeTargetEnv(input) {
|
|
6
54
|
return {
|
|
7
55
|
WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
|
|
@@ -37,95 +85,77 @@ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
|
37
85
|
// src/defaults.ts
|
|
38
86
|
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
39
87
|
|
|
40
|
-
// src/
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
|
|
117
|
+
// ../sandbox-contract/dist/index.mjs
|
|
118
|
+
var sandboxDriverCredentialFields = {
|
|
119
|
+
modal: [
|
|
120
|
+
{ key: "token_id", label: "Token ID", secret: true },
|
|
121
|
+
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
122
|
+
],
|
|
123
|
+
vercel: [
|
|
124
|
+
{ key: "access_token", label: "Access Token", secret: true },
|
|
125
|
+
{ key: "team_id", label: "Team ID" },
|
|
126
|
+
{ key: "project_id", label: "Project ID" }
|
|
127
|
+
],
|
|
128
|
+
cloudflare: [
|
|
129
|
+
{ key: "api_token", label: "API Token", secret: true },
|
|
130
|
+
{ key: "worker_url", label: "Worker URL" }
|
|
131
|
+
],
|
|
132
|
+
boat: [{ key: "api_key", label: "API Key", secret: true }],
|
|
133
|
+
docker: [{ key: "image", label: "Image" }]
|
|
134
|
+
};
|
|
135
|
+
var sandboxDriverLabels = {
|
|
136
|
+
modal: "Modal",
|
|
137
|
+
vercel: "Vercel",
|
|
138
|
+
cloudflare: "Cloudflare",
|
|
139
|
+
boat: "Boat",
|
|
140
|
+
docker: "Docker"
|
|
141
|
+
};
|
|
65
142
|
|
|
66
143
|
// src/driver-catalog.ts
|
|
67
144
|
var sandboxDriverCatalog = {
|
|
68
|
-
exe: {
|
|
69
|
-
id: "exe",
|
|
70
|
-
label: "exe.dev",
|
|
71
|
-
credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
|
|
72
|
-
metadata: {
|
|
73
|
-
driverRunsIn: ["worker", "node"],
|
|
74
|
-
hostStopBehavior: "not-supported",
|
|
75
|
-
hostResumeBehavior: "same-host",
|
|
76
|
-
targetAccess: "relay",
|
|
77
|
-
secretBrokering: "none",
|
|
78
|
-
egressControl: "none",
|
|
79
|
-
persistence: {
|
|
80
|
-
resume: "same-sandbox",
|
|
81
|
-
capture: "none",
|
|
82
|
-
clone: true,
|
|
83
|
-
captureSource: "not-applicable",
|
|
84
|
-
retention: "provider-managed",
|
|
85
|
-
restoreMount: "same-resource"
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
daytona: {
|
|
90
|
-
id: "daytona",
|
|
91
|
-
label: "Daytona",
|
|
92
|
-
credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
|
|
93
|
-
metadata: {
|
|
94
|
-
driverRunsIn: ["worker", "node"],
|
|
95
|
-
hostStopBehavior: "suspends-host",
|
|
96
|
-
hostResumeBehavior: "same-host",
|
|
97
|
-
targetAccess: "relay",
|
|
98
|
-
secretBrokering: "native",
|
|
99
|
-
// Daytona is the only driver that can filter egress by NAME as well as
|
|
100
|
-
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
101
|
-
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
102
|
-
egressControl: "hosts-and-cidrs",
|
|
103
|
-
persistence: {
|
|
104
|
-
resume: "same-sandbox",
|
|
105
|
-
capture: "filesystem",
|
|
106
|
-
clone: false,
|
|
107
|
-
captureSource: "preserved",
|
|
108
|
-
retention: "provider-managed",
|
|
109
|
-
restoreMount: "new-resource"
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
145
|
modal: {
|
|
114
146
|
id: "modal",
|
|
115
|
-
label:
|
|
116
|
-
credentialFields:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
label: sandboxDriverLabels.modal,
|
|
148
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
149
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
150
|
+
runtimeEnv: "callback",
|
|
151
|
+
preparedImage: true,
|
|
120
152
|
metadata: {
|
|
121
153
|
driverRunsIn: ["node"],
|
|
122
154
|
hostStopBehavior: "terminates-host",
|
|
123
155
|
hostResumeBehavior: "replacement-host",
|
|
124
156
|
targetAccess: "relay",
|
|
125
157
|
secretBrokering: "none",
|
|
126
|
-
//
|
|
127
|
-
// an allowlist, and its driver throws on a host policy. A blackout is
|
|
128
|
-
// not containment for a workspace that has to clone and reach a model.
|
|
158
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
129
159
|
egressControl: "none",
|
|
130
160
|
persistence: {
|
|
131
161
|
resume: "replacement-restore",
|
|
@@ -139,20 +169,18 @@ var sandboxDriverCatalog = {
|
|
|
139
169
|
},
|
|
140
170
|
vercel: {
|
|
141
171
|
id: "vercel",
|
|
142
|
-
label:
|
|
143
|
-
credentialFields:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
],
|
|
172
|
+
label: sandboxDriverLabels.vercel,
|
|
173
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
174
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
175
|
+
runtimeEnv: "callback",
|
|
176
|
+
preparedImage: false,
|
|
148
177
|
metadata: {
|
|
149
178
|
driverRunsIn: ["node"],
|
|
150
179
|
hostStopBehavior: "terminates-host",
|
|
151
180
|
hostResumeBehavior: "replacement-host",
|
|
152
181
|
targetAccess: "relay",
|
|
153
182
|
secretBrokering: "native",
|
|
154
|
-
//
|
|
155
|
-
// never CIDRs.
|
|
183
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
156
184
|
egressControl: "hosts",
|
|
157
185
|
persistence: {
|
|
158
186
|
resume: "replacement-restore",
|
|
@@ -166,40 +194,42 @@ var sandboxDriverCatalog = {
|
|
|
166
194
|
},
|
|
167
195
|
cloudflare: {
|
|
168
196
|
id: "cloudflare",
|
|
169
|
-
label:
|
|
170
|
-
credentialFields:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
label: sandboxDriverLabels.cloudflare,
|
|
198
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
199
|
+
runtimeNetwork: { controlPlane: "direct", relay: "configured" },
|
|
200
|
+
runtimeEnv: "callback",
|
|
201
|
+
preparedImage: false,
|
|
202
|
+
billing: cloudflareBilling,
|
|
174
203
|
metadata: {
|
|
175
204
|
driverRunsIn: ["worker"],
|
|
176
|
-
hostStopBehavior: "
|
|
205
|
+
hostStopBehavior: "terminates-host",
|
|
177
206
|
hostResumeBehavior: "same-host",
|
|
207
|
+
hostWatchesIdle: true,
|
|
178
208
|
targetAccess: "relay",
|
|
179
|
-
secretBrokering: "
|
|
180
|
-
//
|
|
181
|
-
// (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
|
|
182
|
-
// to route brokered-credential requests through, not a boundary. The
|
|
183
|
-
// driver therefore drops `net` entirely — which is exactly why this
|
|
184
|
-
// must be declared, so the manager refuses instead of provisioning a
|
|
185
|
-
// sandbox the caller believes is contained.
|
|
209
|
+
secretBrokering: "native",
|
|
210
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
186
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.
|
|
187
213
|
persistence: {
|
|
188
214
|
resume: "replacement-restore",
|
|
189
|
-
capture: "
|
|
215
|
+
capture: "filesystem",
|
|
190
216
|
clone: false,
|
|
191
217
|
captureSource: "preserved",
|
|
192
218
|
retention: "provider-managed",
|
|
193
|
-
restoreMount: "
|
|
219
|
+
restoreMount: "new-resource"
|
|
194
220
|
}
|
|
195
221
|
}
|
|
196
222
|
},
|
|
197
|
-
|
|
198
|
-
id: "
|
|
199
|
-
label:
|
|
200
|
-
credentialFields:
|
|
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,
|
|
201
231
|
metadata: {
|
|
202
|
-
driverRunsIn: ["node"],
|
|
232
|
+
driverRunsIn: ["worker", "node"],
|
|
203
233
|
hostStopBehavior: "suspends-host",
|
|
204
234
|
hostResumeBehavior: "same-host",
|
|
205
235
|
targetAccess: "relay",
|
|
@@ -212,13 +242,17 @@ var sandboxDriverCatalog = {
|
|
|
212
242
|
captureSource: "not-applicable",
|
|
213
243
|
retention: "provider-managed",
|
|
214
244
|
restoreMount: "same-resource"
|
|
215
|
-
}
|
|
245
|
+
},
|
|
246
|
+
machineClasses: ["small", "default", "large"]
|
|
216
247
|
}
|
|
217
248
|
},
|
|
218
249
|
docker: {
|
|
219
250
|
id: "docker",
|
|
220
|
-
label:
|
|
221
|
-
credentialFields:
|
|
251
|
+
label: sandboxDriverLabels.docker,
|
|
252
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
253
|
+
runtimeNetwork: { controlPlane: "docker-host", relay: "configured" },
|
|
254
|
+
runtimeEnv: "callback",
|
|
255
|
+
preparedImage: true,
|
|
222
256
|
metadata: {
|
|
223
257
|
driverRunsIn: ["local"],
|
|
224
258
|
hostStopBehavior: "terminates-host",
|
|
@@ -238,8 +272,60 @@ var sandboxDriverCatalog = {
|
|
|
238
272
|
}
|
|
239
273
|
};
|
|
240
274
|
|
|
275
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
276
|
+
function isRecord(value) {
|
|
277
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
278
|
+
}
|
|
279
|
+
function asRecord(value) {
|
|
280
|
+
return isRecord(value) ? value : void 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/json.ts
|
|
284
|
+
function num(input) {
|
|
285
|
+
return typeof input === "number" ? input : void 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/drivers/transient-error.ts
|
|
289
|
+
function textOf(input) {
|
|
290
|
+
return typeof input === "string" ? input : "";
|
|
291
|
+
}
|
|
292
|
+
function driverErrorSignals(err) {
|
|
293
|
+
const shaped = isRecord(err) ? err : {};
|
|
294
|
+
const nested = isRecord(shaped.response) ? shaped.response : {};
|
|
295
|
+
return {
|
|
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),
|
|
299
|
+
text: `${textOf(shaped.code)} ${textOf(shaped.name)} ${textOf(shaped.message)}`.toLowerCase()
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function isTransientDriverError(err, markers) {
|
|
303
|
+
const { status, text } = driverErrorSignals(err);
|
|
304
|
+
if (status !== void 0 && status >= 500) return true;
|
|
305
|
+
return markers.some((marker) => text.includes(marker));
|
|
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
|
+
}
|
|
319
|
+
|
|
241
320
|
// src/drivers/vercel.ts
|
|
242
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"`;
|
|
243
329
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
244
330
|
var DEFAULT_WORKSPACE_DIR = "/workspace";
|
|
245
331
|
var DEFAULT_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
@@ -252,15 +338,16 @@ function runtimePackage(version) {
|
|
|
252
338
|
var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
253
339
|
var snapshotCache = /* @__PURE__ */ new Map();
|
|
254
340
|
var snapshotCacheState = /* @__PURE__ */ new WeakMap();
|
|
341
|
+
function isVercelSandboxFactory(value) {
|
|
342
|
+
const row = asRecord(value);
|
|
343
|
+
return typeof row?.create === "function" && typeof row.get === "function";
|
|
344
|
+
}
|
|
255
345
|
function nameFor(workspaceId) {
|
|
256
346
|
return `claxedo-${workspaceId}`;
|
|
257
347
|
}
|
|
348
|
+
var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
|
|
258
349
|
function transientDriverError(err) {
|
|
259
|
-
|
|
260
|
-
const status = shaped.response?.status ?? shaped.status;
|
|
261
|
-
if (typeof status === "number" && status >= 500) return true;
|
|
262
|
-
const text = `${shaped.code ?? ""} ${shaped.name ?? ""} ${shaped.message ?? ""}`.toLowerCase();
|
|
263
|
-
return text.includes("timeout") || text.includes("unavailable") || text.includes("deadline") || text.includes("pending");
|
|
350
|
+
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
264
351
|
}
|
|
265
352
|
function network(input) {
|
|
266
353
|
if (!input.net || input.net.mode === "allow-all") return void 0;
|
|
@@ -277,14 +364,42 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
|
|
|
277
364
|
`vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
|
|
278
365
|
);
|
|
279
366
|
}
|
|
367
|
+
if (secret.hosts.length === 0) {
|
|
368
|
+
throw new Error(`vercel brokered secret "${secret.name}" requires at least one host in its egress allowlist`);
|
|
369
|
+
}
|
|
370
|
+
const value = secret.scheme ? `${secret.scheme} ${secret.value}` : secret.value;
|
|
371
|
+
const methods = [...secret.methods ?? []];
|
|
372
|
+
const pathPrefixes = [...secret.pathPrefixes ?? []];
|
|
280
373
|
for (const host of secret.hosts) {
|
|
281
|
-
;
|
|
282
|
-
(
|
|
374
|
+
const rules = allow[host] ??= [];
|
|
375
|
+
for (const prefix of methods.length ? pathPrefixes : []) {
|
|
376
|
+
const path = credentialPathPattern(prefix);
|
|
377
|
+
if (path === void 0) continue;
|
|
378
|
+
rules.push({
|
|
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
|
+
},
|
|
384
|
+
transform: [{ headers: { [secret.header]: value } }]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
283
387
|
}
|
|
284
388
|
}
|
|
285
389
|
const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
|
|
286
390
|
return { allow, ...subnets };
|
|
287
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
|
+
}
|
|
288
403
|
function baseAllowEntries(base) {
|
|
289
404
|
if (base === void 0 || base === "allow-all") return [["*", []]];
|
|
290
405
|
if (base === "deny-all") return [];
|
|
@@ -292,7 +407,18 @@ function baseAllowEntries(base) {
|
|
|
292
407
|
return Object.entries(base.allow ?? {});
|
|
293
408
|
}
|
|
294
409
|
function createVercelSandboxDriver(options) {
|
|
295
|
-
|
|
410
|
+
let defaultFactory;
|
|
411
|
+
function resolveFactory() {
|
|
412
|
+
if (options.sandbox) return options.sandbox;
|
|
413
|
+
defaultFactory ??= import("@vercel/sandbox").then((sdk) => {
|
|
414
|
+
const factory = sdk.Sandbox;
|
|
415
|
+
if (!isVercelSandboxFactory(factory)) {
|
|
416
|
+
throw new Error("@vercel/sandbox does not expose the factory surface this driver uses");
|
|
417
|
+
}
|
|
418
|
+
return factory;
|
|
419
|
+
});
|
|
420
|
+
return defaultFactory;
|
|
421
|
+
}
|
|
296
422
|
const runtime = options.runtime ?? DEFAULT_RUNTIME;
|
|
297
423
|
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
298
424
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
@@ -319,27 +445,20 @@ function createVercelSandboxDriver(options) {
|
|
|
319
445
|
};
|
|
320
446
|
}
|
|
321
447
|
function bootEnv(input, hostId) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
};
|
|
333
|
-
if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
|
|
334
|
-
if (options.controlEnv?.relayJwksUrl) env.WORKSPACE_RUNTIME_RELAY_JWKS_URL = options.controlEnv.relayJwksUrl;
|
|
335
|
-
if (options.controlEnv?.relayVerifyPem) env.WORKSPACE_RUNTIME_RELAY_HOST_VERIFY_PEM = options.controlEnv.relayVerifyPem;
|
|
336
|
-
if (options.controlEnv?.managementJwksUrl) env.WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL = options.controlEnv.managementJwksUrl;
|
|
337
|
-
return env;
|
|
448
|
+
return workspaceRuntimeBootEnv({
|
|
449
|
+
workspaceId: input.workspaceId,
|
|
450
|
+
hostId,
|
|
451
|
+
directory: workspaceDirectory(input),
|
|
452
|
+
port: runtimePort,
|
|
453
|
+
host: "0.0.0.0",
|
|
454
|
+
source: input.source,
|
|
455
|
+
env: input.env,
|
|
456
|
+
nativeHarness: options.nativeHarness,
|
|
457
|
+
controlEnv: options.controlEnv
|
|
458
|
+
});
|
|
338
459
|
}
|
|
339
460
|
async function runSetup(sandbox, command) {
|
|
340
|
-
await sandbox.runCommand("bash", ["-c", command], {
|
|
341
|
-
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
342
|
-
});
|
|
461
|
+
await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.runCommand("bash", ["-c", command], { signal }));
|
|
343
462
|
}
|
|
344
463
|
async function ensureSnapshot() {
|
|
345
464
|
if (options.baseSnapshotId) return options.baseSnapshotId;
|
|
@@ -355,7 +474,7 @@ function createVercelSandboxDriver(options) {
|
|
|
355
474
|
const running = cacheState.builds.get(cacheKey);
|
|
356
475
|
if (running) return running;
|
|
357
476
|
const build = (async () => {
|
|
358
|
-
const builder = await
|
|
477
|
+
const builder = await (await resolveFactory()).create({
|
|
359
478
|
...credentials(),
|
|
360
479
|
runtime,
|
|
361
480
|
ports: [runtimePort],
|
|
@@ -363,7 +482,8 @@ function createVercelSandboxDriver(options) {
|
|
|
363
482
|
});
|
|
364
483
|
await runSetup(builder, "sudo dnf install -y git make gcc-c++ python3 > /dev/null 2>&1");
|
|
365
484
|
await runSetup(builder, [
|
|
366
|
-
|
|
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",
|
|
367
487
|
"curl https://cursor.com/install -fsS | bash",
|
|
368
488
|
"curl -fsSL https://ampcode.com/install.sh | bash",
|
|
369
489
|
"curl -fsSL https://app.factory.ai/cli | sh",
|
|
@@ -374,12 +494,10 @@ function createVercelSandboxDriver(options) {
|
|
|
374
494
|
"command -v opencode",
|
|
375
495
|
"command -v claude",
|
|
376
496
|
"command -v codex",
|
|
377
|
-
"command -v gemini",
|
|
378
497
|
"command -v agent",
|
|
379
498
|
"command -v cursor-agent",
|
|
380
499
|
"command -v amp",
|
|
381
|
-
"command -v droid"
|
|
382
|
-
"command -v pi"
|
|
500
|
+
"command -v droid"
|
|
383
501
|
].join(" && "));
|
|
384
502
|
await runSetup(builder, [
|
|
385
503
|
`sudo mkdir -p ${shell(RUNTIME_DIR)}`,
|
|
@@ -389,19 +507,10 @@ function createVercelSandboxDriver(options) {
|
|
|
389
507
|
`npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
|
|
390
508
|
`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)}"`,
|
|
391
509
|
"test -x node_modules/.bin/workspace-runtime",
|
|
392
|
-
"test -x node_modules/.bin/claude-agent-acp",
|
|
393
|
-
"test -x node_modules/.bin/codex-acp",
|
|
394
510
|
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/workspace-runtime /usr/local/bin/workspace-runtime`,
|
|
395
|
-
|
|
396
|
-
`sudo ln -sf ${RUNTIME_DIR}/node_modules/.bin/codex-acp /usr/local/bin/codex-acp`,
|
|
397
|
-
"command -v workspace-runtime",
|
|
398
|
-
"command -v claude-agent-acp",
|
|
399
|
-
"command -v codex-acp"
|
|
511
|
+
"command -v workspace-runtime"
|
|
400
512
|
].join(" && "));
|
|
401
|
-
const snapshot = await builder.snapshot({
|
|
402
|
-
expiration: 0,
|
|
403
|
-
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
404
|
-
});
|
|
513
|
+
const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => builder.snapshot({ expiration: 0, signal }));
|
|
405
514
|
cache.set(cacheKey, snapshot.snapshotId);
|
|
406
515
|
cacheState.expires.set(cacheKey, now() + SNAPSHOT_CACHE_TTL_MS);
|
|
407
516
|
return snapshot.snapshotId;
|
|
@@ -422,14 +531,14 @@ function createVercelSandboxDriver(options) {
|
|
|
422
531
|
}
|
|
423
532
|
async function startRuntime(sandbox, input, env) {
|
|
424
533
|
const directory = workspaceDirectory(input);
|
|
425
|
-
await sandbox.runCommand({
|
|
534
|
+
await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.runCommand({
|
|
426
535
|
cmd: "bash",
|
|
427
536
|
args: ["-lc", `mkdir -p ${shell(directory)}; cd ${shell(directory)}; exec ${runtimeCommand}`],
|
|
428
537
|
cwd: "/vercel/sandbox",
|
|
429
538
|
env,
|
|
430
539
|
detached: true,
|
|
431
|
-
signal
|
|
432
|
-
});
|
|
540
|
+
signal
|
|
541
|
+
}));
|
|
433
542
|
}
|
|
434
543
|
async function readyTarget(input, sandbox, hostId) {
|
|
435
544
|
const url = sandbox.domain(runtimePort);
|
|
@@ -450,10 +559,13 @@ function createVercelSandboxDriver(options) {
|
|
|
450
559
|
const hostId = input.hostId ?? nameFor(input.workspaceId);
|
|
451
560
|
const env = {
|
|
452
561
|
...bootEnv(input, hostId),
|
|
453
|
-
...await options.env?.(input, { id: hostId })
|
|
562
|
+
...await options.env?.(input, { id: hostId }),
|
|
563
|
+
// Last, so no caller-supplied variable of the same name can stand in for
|
|
564
|
+
// a placeholder the firewall transform overwrites.
|
|
565
|
+
...brokeredPlaceholderEnv(input.secrets)
|
|
454
566
|
};
|
|
455
567
|
const createPolicy = network(input);
|
|
456
|
-
const sandbox = await
|
|
568
|
+
const sandbox = await (await resolveFactory()).create({
|
|
457
569
|
...credentials(),
|
|
458
570
|
source: await source(input),
|
|
459
571
|
ports: [runtimePort],
|
|
@@ -464,17 +576,17 @@ function createVercelSandboxDriver(options) {
|
|
|
464
576
|
if (transientDriverError(err)) return void 0;
|
|
465
577
|
throw err;
|
|
466
578
|
});
|
|
467
|
-
if (!sandbox) return { provisioning: true, retryAfterMs:
|
|
579
|
+
if (!sandbox) return { provisioning: true, retryAfterMs: TRANSIENT_DRIVER_RETRY_MS };
|
|
580
|
+
await input.onImageReady?.();
|
|
468
581
|
if (input.secrets?.length) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
});
|
|
582
|
+
const secrets = input.secrets;
|
|
583
|
+
await withDeadlineSignal(operationTimeoutMs, (signal) => sandbox.updateNetworkPolicy(vercelBrokeredNetworkPolicy(secrets, createPolicy), { signal }));
|
|
472
584
|
}
|
|
473
585
|
await startRuntime(sandbox, input, env);
|
|
474
586
|
return readyTarget(input, sandbox, hostId);
|
|
475
587
|
}
|
|
476
588
|
async function sandboxById(target) {
|
|
477
|
-
return
|
|
589
|
+
return (await resolveFactory()).get({
|
|
478
590
|
...credentials(),
|
|
479
591
|
sandboxId: target.sandboxId
|
|
480
592
|
});
|
|
@@ -493,7 +605,7 @@ function createVercelSandboxDriver(options) {
|
|
|
493
605
|
ensureHost,
|
|
494
606
|
resumeHost: (input) => ensureHost(input.ensure),
|
|
495
607
|
async touch(target) {
|
|
496
|
-
await (await sandboxById(target)).extendTimeout(keepAliveMs)
|
|
608
|
+
await (await sandboxById(target)).extendTimeout(keepAliveMs);
|
|
497
609
|
},
|
|
498
610
|
async suspend(target) {
|
|
499
611
|
await (await sandboxById(target)).stop({ blocking: false });
|
|
@@ -505,10 +617,8 @@ function createVercelSandboxDriver(options) {
|
|
|
505
617
|
await (await sandboxById(target)).stop({ blocking: true });
|
|
506
618
|
},
|
|
507
619
|
async snapshot(target) {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
signal: AbortSignal.timeout(snapshotBuildTimeoutMs)
|
|
511
|
-
});
|
|
620
|
+
const sandbox = await sandboxById(target);
|
|
621
|
+
const snapshot = await withDeadlineSignal(snapshotBuildTimeoutMs, (signal) => sandbox.snapshot({ expiration: 0, signal }));
|
|
512
622
|
return { snapshotId: snapshot.snapshotId };
|
|
513
623
|
}
|
|
514
624
|
};
|