@claxedo/sandbox-manager 0.7.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 +10 -14
- package/dist/driver-catalog.d.ts +3 -53
- package/dist/driver-catalog.mjs +137 -128
- package/dist/drivers/box.mjs +143 -68
- package/dist/drivers/cloudflare.d.ts +4 -7
- package/dist/drivers/cloudflare.mjs +198 -108
- package/dist/drivers/daytona.d.ts +49 -13
- package/dist/drivers/daytona.mjs +225 -83
- package/dist/drivers/docker.mjs +118 -67
- package/dist/drivers/exe.mjs +74 -51
- package/dist/drivers/fetch-bridge.mjs +19 -4
- package/dist/drivers/modal.d.ts +2 -5
- package/dist/drivers/modal.mjs +130 -75
- package/dist/drivers/transient-error.d.ts +11 -0
- package/dist/drivers/vercel.d.ts +19 -22
- package/dist/drivers/vercel.mjs +178 -104
- package/dist/hosted-network-policy.d.ts +2 -2
- package/dist/hosted-network-policy.mjs +3 -3
- package/dist/image-name.mjs +34 -0
- package/dist/image.d.ts +2 -2
- package/dist/image.mjs +33 -11
- package/dist/index.d.ts +62 -17
- package/dist/index.mjs +68 -22
- package/dist/json.d.ts +13 -0
- package/dist/lease-policy.d.ts +6 -2
- package/dist/lease-policy.mjs +50 -40
- package/dist/lease-types.d.ts +11 -2
- package/dist/runtime-env.d.ts +12 -1
- package/dist/runtime-env.mjs +14 -0
- package/dist/service-url-exposure.d.ts +1 -2
- package/dist/service-url-exposure.mjs +1 -17
- package/dist/stores/memory.mjs +2 -2
- package/package.json +57 -55
- 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,
|
|
@@ -31,44 +78,45 @@ function shell(input) {
|
|
|
31
78
|
return `'${input.replace(/'/g, `'"'"'`)}'`;
|
|
32
79
|
}
|
|
33
80
|
|
|
34
|
-
// src/constants.ts
|
|
35
|
-
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
36
|
-
|
|
37
81
|
// src/defaults.ts
|
|
38
82
|
var RUNTIME_DIR = "/opt/workspace-runtime";
|
|
39
83
|
|
|
40
|
-
//
|
|
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
|
-
|
|
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
|
+
};
|
|
65
113
|
|
|
66
114
|
// src/driver-catalog.ts
|
|
67
115
|
var sandboxDriverCatalog = {
|
|
68
116
|
exe: {
|
|
69
117
|
id: "exe",
|
|
70
|
-
label:
|
|
71
|
-
credentialFields:
|
|
118
|
+
label: sandboxDriverLabels.exe,
|
|
119
|
+
credentialFields: sandboxDriverCredentialFields.exe,
|
|
72
120
|
metadata: {
|
|
73
121
|
driverRunsIn: ["worker", "node"],
|
|
74
122
|
hostStopBehavior: "not-supported",
|
|
@@ -88,17 +136,15 @@ var sandboxDriverCatalog = {
|
|
|
88
136
|
},
|
|
89
137
|
daytona: {
|
|
90
138
|
id: "daytona",
|
|
91
|
-
label:
|
|
92
|
-
credentialFields:
|
|
139
|
+
label: sandboxDriverLabels.daytona,
|
|
140
|
+
credentialFields: sandboxDriverCredentialFields.daytona,
|
|
93
141
|
metadata: {
|
|
94
142
|
driverRunsIn: ["worker", "node"],
|
|
95
143
|
hostStopBehavior: "suspends-host",
|
|
96
144
|
hostResumeBehavior: "same-host",
|
|
97
145
|
targetAccess: "relay",
|
|
98
146
|
secretBrokering: "native",
|
|
99
|
-
//
|
|
100
|
-
// by address: `domainAllowList` (names) alongside `networkAllowList`
|
|
101
|
-
// (CIDRs), with `networkBlockAll` as the deny-all floor.
|
|
147
|
+
// The driver translates names to domainAllowList, or addresses to networkAllowList.
|
|
102
148
|
egressControl: "hosts-and-cidrs",
|
|
103
149
|
persistence: {
|
|
104
150
|
resume: "same-sandbox",
|
|
@@ -112,20 +158,15 @@ var sandboxDriverCatalog = {
|
|
|
112
158
|
},
|
|
113
159
|
modal: {
|
|
114
160
|
id: "modal",
|
|
115
|
-
label:
|
|
116
|
-
credentialFields:
|
|
117
|
-
{ key: "token_id", label: "Token ID", secret: true },
|
|
118
|
-
{ key: "token_secret", label: "Token Secret", secret: true }
|
|
119
|
-
],
|
|
161
|
+
label: sandboxDriverLabels.modal,
|
|
162
|
+
credentialFields: sandboxDriverCredentialFields.modal,
|
|
120
163
|
metadata: {
|
|
121
164
|
driverRunsIn: ["node"],
|
|
122
165
|
hostStopBehavior: "terminates-host",
|
|
123
166
|
hostResumeBehavior: "replacement-host",
|
|
124
167
|
targetAccess: "relay",
|
|
125
168
|
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.
|
|
169
|
+
// The driver does not install Modal's domain allowlist or alpha sidecar.
|
|
129
170
|
egressControl: "none",
|
|
130
171
|
persistence: {
|
|
131
172
|
resume: "replacement-restore",
|
|
@@ -139,20 +180,15 @@ var sandboxDriverCatalog = {
|
|
|
139
180
|
},
|
|
140
181
|
vercel: {
|
|
141
182
|
id: "vercel",
|
|
142
|
-
label:
|
|
143
|
-
credentialFields:
|
|
144
|
-
{ key: "access_token", label: "Access Token", secret: true },
|
|
145
|
-
{ key: "team_id", label: "Team ID" },
|
|
146
|
-
{ key: "project_id", label: "Project ID" }
|
|
147
|
-
],
|
|
183
|
+
label: sandboxDriverLabels.vercel,
|
|
184
|
+
credentialFields: sandboxDriverCredentialFields.vercel,
|
|
148
185
|
metadata: {
|
|
149
186
|
driverRunsIn: ["node"],
|
|
150
187
|
hostStopBehavior: "terminates-host",
|
|
151
188
|
hostResumeBehavior: "replacement-host",
|
|
152
189
|
targetAccess: "relay",
|
|
153
190
|
secretBrokering: "native",
|
|
154
|
-
//
|
|
155
|
-
// never CIDRs.
|
|
191
|
+
// The driver translates hosts only; the SDK's subnet rules are not wired to net.cidrs.
|
|
156
192
|
egressControl: "hosts",
|
|
157
193
|
persistence: {
|
|
158
194
|
resume: "replacement-restore",
|
|
@@ -166,23 +202,15 @@ var sandboxDriverCatalog = {
|
|
|
166
202
|
},
|
|
167
203
|
cloudflare: {
|
|
168
204
|
id: "cloudflare",
|
|
169
|
-
label:
|
|
170
|
-
credentialFields:
|
|
171
|
-
{ key: "api_token", label: "API Token", secret: true },
|
|
172
|
-
{ key: "worker_url", label: "Worker URL" }
|
|
173
|
-
],
|
|
205
|
+
label: sandboxDriverLabels.cloudflare,
|
|
206
|
+
credentialFields: sandboxDriverCredentialFields.cloudflare,
|
|
174
207
|
metadata: {
|
|
175
208
|
driverRunsIn: ["worker"],
|
|
176
209
|
hostStopBehavior: "not-supported",
|
|
177
210
|
hostResumeBehavior: "same-host",
|
|
178
211
|
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.
|
|
212
|
+
secretBrokering: "native",
|
|
213
|
+
// Credential handlers do not restrict unrelated destinations.
|
|
186
214
|
egressControl: "none",
|
|
187
215
|
persistence: {
|
|
188
216
|
resume: "replacement-restore",
|
|
@@ -196,8 +224,8 @@ var sandboxDriverCatalog = {
|
|
|
196
224
|
},
|
|
197
225
|
box: {
|
|
198
226
|
id: "box",
|
|
199
|
-
label:
|
|
200
|
-
credentialFields:
|
|
227
|
+
label: sandboxDriverLabels.box,
|
|
228
|
+
credentialFields: sandboxDriverCredentialFields.box,
|
|
201
229
|
metadata: {
|
|
202
230
|
driverRunsIn: ["node"],
|
|
203
231
|
hostStopBehavior: "suspends-host",
|
|
@@ -217,8 +245,8 @@ var sandboxDriverCatalog = {
|
|
|
217
245
|
},
|
|
218
246
|
docker: {
|
|
219
247
|
id: "docker",
|
|
220
|
-
label:
|
|
221
|
-
credentialFields:
|
|
248
|
+
label: sandboxDriverLabels.docker,
|
|
249
|
+
credentialFields: sandboxDriverCredentialFields.docker,
|
|
222
250
|
metadata: {
|
|
223
251
|
driverRunsIn: ["local"],
|
|
224
252
|
hostStopBehavior: "terminates-host",
|
|
@@ -238,6 +266,37 @@ var sandboxDriverCatalog = {
|
|
|
238
266
|
}
|
|
239
267
|
};
|
|
240
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
|
+
|
|
241
300
|
// src/drivers/vercel.ts
|
|
242
301
|
var DEFAULT_RUNTIME = "node22";
|
|
243
302
|
var DEFAULT_RUNTIME_COMMAND = "/usr/local/bin/workspace-runtime";
|
|
@@ -252,15 +311,16 @@ function runtimePackage(version) {
|
|
|
252
311
|
var SNAPSHOT_CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
253
312
|
var snapshotCache = /* @__PURE__ */ new Map();
|
|
254
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
|
+
}
|
|
255
318
|
function nameFor(workspaceId) {
|
|
256
319
|
return `claxedo-${workspaceId}`;
|
|
257
320
|
}
|
|
321
|
+
var TRANSIENT_MARKERS = ["timeout", "unavailable", "deadline", "pending"];
|
|
258
322
|
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");
|
|
323
|
+
return isTransientDriverError(err, TRANSIENT_MARKERS);
|
|
264
324
|
}
|
|
265
325
|
function network(input) {
|
|
266
326
|
if (!input.net || input.net.mode === "allow-all") return void 0;
|
|
@@ -277,9 +337,20 @@ function vercelBrokeredNetworkPolicy(secrets, base) {
|
|
|
277
337
|
`vercel brokered secret "${secret.name}" requires a header \u2014 the firewall injects the value as an HTTP header on egress`
|
|
278
338
|
);
|
|
279
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 ?? []];
|
|
280
346
|
for (const host of secret.hosts) {
|
|
281
|
-
;
|
|
282
|
-
(
|
|
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
|
+
}
|
|
283
354
|
}
|
|
284
355
|
}
|
|
285
356
|
const subnets = typeof base === "object" && base?.subnets ? { subnets: base.subnets } : {};
|
|
@@ -292,7 +363,18 @@ function baseAllowEntries(base) {
|
|
|
292
363
|
return Object.entries(base.allow ?? {});
|
|
293
364
|
}
|
|
294
365
|
function createVercelSandboxDriver(options) {
|
|
295
|
-
|
|
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
|
+
}
|
|
296
378
|
const runtime = options.runtime ?? DEFAULT_RUNTIME;
|
|
297
379
|
const runtimePort = options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
|
|
298
380
|
const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
|
|
@@ -319,22 +401,17 @@ function createVercelSandboxDriver(options) {
|
|
|
319
401
|
};
|
|
320
402
|
}
|
|
321
403
|
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;
|
|
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
|
+
});
|
|
338
415
|
}
|
|
339
416
|
async function runSetup(sandbox, command) {
|
|
340
417
|
await sandbox.runCommand("bash", ["-c", command], {
|
|
@@ -355,7 +432,7 @@ function createVercelSandboxDriver(options) {
|
|
|
355
432
|
const running = cacheState.builds.get(cacheKey);
|
|
356
433
|
if (running) return running;
|
|
357
434
|
const build = (async () => {
|
|
358
|
-
const builder = await
|
|
435
|
+
const builder = await (await resolveFactory()).create({
|
|
359
436
|
...credentials(),
|
|
360
437
|
runtime,
|
|
361
438
|
ports: [runtimePort],
|
|
@@ -389,14 +466,8 @@ function createVercelSandboxDriver(options) {
|
|
|
389
466
|
`npm install ${runtimePackage(expectedRuntimeVersion)} --min-release-age=2`,
|
|
390
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)}"`,
|
|
391
468
|
"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
469
|
`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"
|
|
470
|
+
"command -v workspace-runtime"
|
|
400
471
|
].join(" && "));
|
|
401
472
|
const snapshot = await builder.snapshot({
|
|
402
473
|
expiration: 0,
|
|
@@ -450,10 +521,13 @@ function createVercelSandboxDriver(options) {
|
|
|
450
521
|
const hostId = input.hostId ?? nameFor(input.workspaceId);
|
|
451
522
|
const env = {
|
|
452
523
|
...bootEnv(input, hostId),
|
|
453
|
-
...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)
|
|
454
528
|
};
|
|
455
529
|
const createPolicy = network(input);
|
|
456
|
-
const sandbox = await
|
|
530
|
+
const sandbox = await (await resolveFactory()).create({
|
|
457
531
|
...credentials(),
|
|
458
532
|
source: await source(input),
|
|
459
533
|
ports: [runtimePort],
|
|
@@ -474,7 +548,7 @@ function createVercelSandboxDriver(options) {
|
|
|
474
548
|
return readyTarget(input, sandbox, hostId);
|
|
475
549
|
}
|
|
476
550
|
async function sandboxById(target) {
|
|
477
|
-
return
|
|
551
|
+
return (await resolveFactory()).get({
|
|
478
552
|
...credentials(),
|
|
479
553
|
sandboxId: target.sandboxId
|
|
480
554
|
});
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
import type { SandboxNetworkPolicy, SandboxSource } from ".";
|
|
40
40
|
/**
|
|
41
41
|
* Model-provider endpoints. Derived from the harness→provider groups in
|
|
42
|
-
* claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-
|
|
43
|
-
*
|
|
42
|
+
* claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
|
|
43
|
+
* anthropic, codex-app-server/cursor-sdk → openai,
|
|
44
44
|
* google → google_ai; plus the aggregator and catalog endpoints the engine
|
|
45
45
|
* itself fetches at startup.
|
|
46
46
|
*/
|
|
@@ -25,16 +25,16 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
|
|
|
25
25
|
];
|
|
26
26
|
function hostOf(value) {
|
|
27
27
|
const raw = value?.trim();
|
|
28
|
-
if (!raw) return;
|
|
28
|
+
if (!raw) return void 0;
|
|
29
29
|
try {
|
|
30
30
|
const host = new URL(raw).hostname.trim().toLowerCase();
|
|
31
31
|
return host ? host : void 0;
|
|
32
32
|
} catch {
|
|
33
|
-
return;
|
|
33
|
+
return void 0;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
function sandboxSourceHost(source) {
|
|
37
|
-
if (!source || source.kind !== "git") return;
|
|
37
|
+
if (!source || source.kind !== "git") return void 0;
|
|
38
38
|
return hostOf(source.repoUrl);
|
|
39
39
|
}
|
|
40
40
|
function hostedSandboxNetworkPolicy(input) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/runtime-version.ts
|
|
2
|
+
var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
|
|
3
|
+
function workspaceRuntimeVersion() {
|
|
4
|
+
return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/image-name.ts
|
|
8
|
+
var SNAPSHOT_SCHEMA_VERSION = 8;
|
|
9
|
+
var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
|
|
10
|
+
function snapshotVersion(value) {
|
|
11
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
12
|
+
}
|
|
13
|
+
function sandboxImageRepository(env = process.env) {
|
|
14
|
+
const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
|
|
15
|
+
return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
|
|
16
|
+
}
|
|
17
|
+
function buildIdSuffix(buildId, env) {
|
|
18
|
+
const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
|
|
19
|
+
return id ? `-${snapshotVersion(id)}` : "";
|
|
20
|
+
}
|
|
21
|
+
function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
|
|
22
|
+
return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
23
|
+
}
|
|
24
|
+
function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
|
|
25
|
+
return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
DEFAULT_SANDBOX_IMAGE_REPOSITORY,
|
|
29
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
30
|
+
defaultSandboxImage,
|
|
31
|
+
defaultSnapshotName,
|
|
32
|
+
sandboxImageRepository,
|
|
33
|
+
snapshotVersion
|
|
34
|
+
};
|
package/dist/image.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Daytona, Image } from "@daytona/sdk";
|
|
1
|
+
import type { Daytona, Image } from "@daytona/sdk";
|
|
2
2
|
export { RUNTIME_DIR, WORKSPACE_RUNTIME_PORT, WORKSPACE_DIR } from "./defaults";
|
|
3
3
|
export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, defaultSandboxImage, defaultSnapshotName, } from "./image-name";
|
|
4
4
|
/**
|
|
@@ -19,5 +19,5 @@ export type SandboxImageOptions = {
|
|
|
19
19
|
export declare const SANDBOX_IMAGE_REPOSITORY: string;
|
|
20
20
|
export declare const SANDBOX_IMAGE: string;
|
|
21
21
|
export declare const SNAPSHOT_NAME: string;
|
|
22
|
-
export declare function buildSandboxImage(): Image
|
|
22
|
+
export declare function buildSandboxImage(): Promise<Image>;
|
|
23
23
|
export declare function ensureSnapshot(daytona: Daytona, options?: SandboxImageOptions): Promise<string>;
|
package/dist/image.mjs
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/image.ts
|
|
2
|
-
import { Image, DaytonaNotFoundError } from "@daytona/sdk";
|
|
3
|
-
|
|
4
1
|
// src/constants.ts
|
|
5
2
|
var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
|
|
6
3
|
|
|
@@ -36,6 +33,19 @@ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env =
|
|
|
36
33
|
return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
|
|
37
34
|
}
|
|
38
35
|
|
|
36
|
+
// ../claxedo-helpers/dist/guards.mjs
|
|
37
|
+
function isRecord(value) {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
function asRecord(value) {
|
|
41
|
+
return isRecord(value) ? value : void 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/json.ts
|
|
45
|
+
function text(input) {
|
|
46
|
+
return typeof input === "string" && input.length > 0 ? input : void 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
// src/image.ts
|
|
40
50
|
function defaultImageLogSink(message, context) {
|
|
41
51
|
if (context) console.info(message, context);
|
|
@@ -44,16 +54,26 @@ function defaultImageLogSink(message, context) {
|
|
|
44
54
|
var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
|
|
45
55
|
var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
|
|
46
56
|
var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
|
|
47
|
-
function buildSandboxImage() {
|
|
57
|
+
async function buildSandboxImage() {
|
|
58
|
+
const { Image } = await import("@daytona/sdk");
|
|
48
59
|
return Image.base(SANDBOX_IMAGE);
|
|
49
60
|
}
|
|
50
61
|
function snapshotUsable(state) {
|
|
51
62
|
return state === "ready" || state === "active";
|
|
52
63
|
}
|
|
64
|
+
function snapshotView(input) {
|
|
65
|
+
const row = asRecord(input) ?? {};
|
|
66
|
+
return { id: text(row.id), state: text(row.state), errorReason: text(row.errorReason) };
|
|
67
|
+
}
|
|
68
|
+
async function activateSnapshot(snapshots, id) {
|
|
69
|
+
const activate = asRecord(snapshots)?.activate;
|
|
70
|
+
if (typeof activate !== "function") throw new Error("Daytona SDK does not support snapshot activation");
|
|
71
|
+
await activate.call(snapshots, { id });
|
|
72
|
+
}
|
|
53
73
|
async function createSnapshot(daytona, log) {
|
|
54
74
|
log("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
|
|
55
75
|
await daytona.snapshot.create(
|
|
56
|
-
{ name: SNAPSHOT_NAME, image: buildSandboxImage() },
|
|
76
|
+
{ name: SNAPSHOT_NAME, image: await buildSandboxImage() },
|
|
57
77
|
{
|
|
58
78
|
timeout: 120,
|
|
59
79
|
onLogs: (chunk) => log("[snapshot]", { text: chunk })
|
|
@@ -64,14 +84,15 @@ async function createSnapshot(daytona, log) {
|
|
|
64
84
|
async function ensureSnapshot(daytona, options = {}) {
|
|
65
85
|
const log = options.log ?? defaultImageLogSink;
|
|
66
86
|
try {
|
|
67
|
-
const existing = await daytona.snapshot.get(SNAPSHOT_NAME);
|
|
87
|
+
const existing = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
|
|
68
88
|
if (existing.state === "inactive") {
|
|
69
89
|
log("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
|
|
70
|
-
|
|
90
|
+
if (!existing.id) throw new Error("Daytona snapshot is inactive but reports no id to activate");
|
|
91
|
+
await activateSnapshot(daytona.snapshot, existing.id);
|
|
71
92
|
for (let i = 0; i < 120; i++) {
|
|
72
|
-
const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
|
|
93
|
+
const snap = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
|
|
73
94
|
if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
|
|
74
|
-
if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason}`);
|
|
95
|
+
if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason ?? "unknown"}`);
|
|
75
96
|
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
76
97
|
}
|
|
77
98
|
throw new Error("Snapshot activation timed out");
|
|
@@ -79,9 +100,9 @@ async function ensureSnapshot(daytona, options = {}) {
|
|
|
79
100
|
if (existing.state === "building") {
|
|
80
101
|
log("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
|
|
81
102
|
for (let i = 0; i < 300; i++) {
|
|
82
|
-
const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
|
|
103
|
+
const snap = snapshotView(await daytona.snapshot.get(SNAPSHOT_NAME));
|
|
83
104
|
if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
|
|
84
|
-
if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason}`);
|
|
105
|
+
if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason ?? "unknown"}`);
|
|
85
106
|
if (i % 15 === 0) log("Still waiting for snapshot...", { state: snap.state, elapsed: `${i * 2}s` });
|
|
86
107
|
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
87
108
|
}
|
|
@@ -89,6 +110,7 @@ async function ensureSnapshot(daytona, options = {}) {
|
|
|
89
110
|
}
|
|
90
111
|
return SNAPSHOT_NAME;
|
|
91
112
|
} catch (err) {
|
|
113
|
+
const { DaytonaNotFoundError } = await import("@daytona/sdk");
|
|
92
114
|
if (!(err instanceof DaytonaNotFoundError)) throw err;
|
|
93
115
|
}
|
|
94
116
|
return createSnapshot(daytona, log);
|