@claxedo/sandbox-manager 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,469 @@
1
+ // src/command.ts
2
+ function shell(input) {
3
+ return `'${input.replace(/'/g, `'"'"'`)}'`;
4
+ }
5
+
6
+ // src/constants.ts
7
+ var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
8
+
9
+ // src/hosted-network-policy.ts
10
+ var SANDBOX_MODEL_PROVIDER_HOSTS = [
11
+ "api.anthropic.com",
12
+ "api.openai.com",
13
+ "generativelanguage.googleapis.com",
14
+ "openrouter.ai",
15
+ // Model catalog + engine config, fetched on runner startup.
16
+ "models.dev",
17
+ "opencode.ai"
18
+ ];
19
+ var SANDBOX_PACKAGE_REGISTRY_HOSTS = [
20
+ "registry.npmjs.org",
21
+ "nodejs.org",
22
+ "pypi.org",
23
+ "files.pythonhosted.org",
24
+ "crates.io",
25
+ "static.crates.io",
26
+ "index.crates.io",
27
+ "proxy.golang.org",
28
+ "sum.golang.org"
29
+ ];
30
+ var SANDBOX_BASELINE_EGRESS_HOSTS = [
31
+ ...SANDBOX_MODEL_PROVIDER_HOSTS,
32
+ ...SANDBOX_PACKAGE_REGISTRY_HOSTS
33
+ ];
34
+
35
+ // src/runtime-version.ts
36
+ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
37
+ function workspaceRuntimeVersion() {
38
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
39
+ }
40
+
41
+ // src/image-name.ts
42
+ var SNAPSHOT_SCHEMA_VERSION = 8;
43
+ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
44
+ function snapshotVersion(value) {
45
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
46
+ }
47
+ function sandboxImageRepository(env = process.env) {
48
+ const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
49
+ return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
50
+ }
51
+ function buildIdSuffix(buildId, env) {
52
+ const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
53
+ return id ? `-${snapshotVersion(id)}` : "";
54
+ }
55
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
56
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
57
+ }
58
+ function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
59
+ return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
60
+ }
61
+
62
+ // src/driver-catalog.ts
63
+ var sandboxDriverCatalog = {
64
+ exe: {
65
+ id: "exe",
66
+ label: "exe.dev",
67
+ credentialFields: [{ key: "api_token", label: "API Token", secret: true }],
68
+ metadata: {
69
+ driverRunsIn: ["worker", "node"],
70
+ hostStopBehavior: "not-supported",
71
+ hostResumeBehavior: "same-host",
72
+ targetAccess: "relay",
73
+ secretBrokering: "none",
74
+ egressControl: "none",
75
+ persistence: {
76
+ resume: "same-sandbox",
77
+ capture: "none",
78
+ clone: true,
79
+ captureSource: "not-applicable",
80
+ retention: "provider-managed",
81
+ restoreMount: "same-resource"
82
+ }
83
+ }
84
+ },
85
+ daytona: {
86
+ id: "daytona",
87
+ label: "Daytona",
88
+ credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
89
+ metadata: {
90
+ driverRunsIn: ["worker", "node"],
91
+ hostStopBehavior: "suspends-host",
92
+ hostResumeBehavior: "same-host",
93
+ targetAccess: "relay",
94
+ secretBrokering: "native",
95
+ // Daytona is the only driver that can filter egress by NAME as well as
96
+ // by address: `domainAllowList` (names) alongside `networkAllowList`
97
+ // (CIDRs), with `networkBlockAll` as the deny-all floor.
98
+ egressControl: "hosts-and-cidrs",
99
+ persistence: {
100
+ resume: "same-sandbox",
101
+ capture: "filesystem",
102
+ clone: false,
103
+ captureSource: "preserved",
104
+ retention: "provider-managed",
105
+ restoreMount: "new-resource"
106
+ }
107
+ }
108
+ },
109
+ modal: {
110
+ id: "modal",
111
+ label: "Modal",
112
+ credentialFields: [
113
+ { key: "token_id", label: "Token ID", secret: true },
114
+ { key: "token_secret", label: "Token Secret", secret: true }
115
+ ],
116
+ metadata: {
117
+ driverRunsIn: ["node"],
118
+ hostStopBehavior: "terminates-host",
119
+ hostResumeBehavior: "replacement-host",
120
+ targetAccess: "relay",
121
+ secretBrokering: "none",
122
+ // Modal can cut the network entirely (`blockNetwork`) but cannot express
123
+ // an allowlist, and its driver throws on a host policy. A blackout is
124
+ // not containment for a workspace that has to clone and reach a model.
125
+ egressControl: "none",
126
+ persistence: {
127
+ resume: "replacement-restore",
128
+ capture: "filesystem",
129
+ clone: false,
130
+ captureSource: "preserved",
131
+ retention: "provider-managed",
132
+ restoreMount: "new-resource"
133
+ }
134
+ }
135
+ },
136
+ vercel: {
137
+ id: "vercel",
138
+ label: "Vercel",
139
+ credentialFields: [
140
+ { key: "access_token", label: "Access Token", secret: true },
141
+ { key: "team_id", label: "Team ID" },
142
+ { key: "project_id", label: "Project ID" }
143
+ ],
144
+ metadata: {
145
+ driverRunsIn: ["node"],
146
+ hostStopBehavior: "terminates-host",
147
+ hostResumeBehavior: "replacement-host",
148
+ targetAccess: "relay",
149
+ secretBrokering: "native",
150
+ // Vercel's sandbox firewall takes a hostname allow list (or "deny-all"),
151
+ // never CIDRs.
152
+ egressControl: "hosts",
153
+ persistence: {
154
+ resume: "replacement-restore",
155
+ capture: "filesystem",
156
+ clone: false,
157
+ captureSource: "stopped",
158
+ retention: "explicit",
159
+ restoreMount: "new-resource"
160
+ }
161
+ }
162
+ },
163
+ cloudflare: {
164
+ id: "cloudflare",
165
+ label: "Cloudflare",
166
+ credentialFields: [
167
+ { key: "api_token", label: "API Token", secret: true },
168
+ { key: "worker_url", label: "Worker URL" }
169
+ ],
170
+ metadata: {
171
+ driverRunsIn: ["worker"],
172
+ hostStopBehavior: "not-supported",
173
+ hostResumeBehavior: "same-host",
174
+ targetAccess: "relay",
175
+ secretBrokering: "proxy",
176
+ // The Cloudflare Sandbox Worker exposes no egress filter: its broker
177
+ // (drivers/cloudflare-egress.ts) is an OPT-IN proxy the sandbox chooses
178
+ // to route brokered-credential requests through, not a boundary. The
179
+ // driver therefore drops `net` entirely — which is exactly why this
180
+ // must be declared, so the manager refuses instead of provisioning a
181
+ // sandbox the caller believes is contained.
182
+ egressControl: "none",
183
+ persistence: {
184
+ resume: "replacement-restore",
185
+ capture: "directories",
186
+ clone: false,
187
+ captureSource: "preserved",
188
+ retention: "provider-managed",
189
+ restoreMount: "copy-on-write"
190
+ }
191
+ }
192
+ },
193
+ box: {
194
+ id: "box",
195
+ label: "Box",
196
+ credentialFields: [{ key: "api_key", label: "API Key", secret: true }],
197
+ metadata: {
198
+ driverRunsIn: ["node"],
199
+ hostStopBehavior: "suspends-host",
200
+ hostResumeBehavior: "same-host",
201
+ targetAccess: "relay",
202
+ secretBrokering: "none",
203
+ egressControl: "none",
204
+ persistence: {
205
+ resume: "same-sandbox",
206
+ capture: "none",
207
+ clone: false,
208
+ captureSource: "not-applicable",
209
+ retention: "provider-managed",
210
+ restoreMount: "same-resource"
211
+ }
212
+ }
213
+ },
214
+ docker: {
215
+ id: "docker",
216
+ label: "Docker",
217
+ credentialFields: [{ key: "image", label: "Image" }],
218
+ metadata: {
219
+ driverRunsIn: ["local"],
220
+ hostStopBehavior: "terminates-host",
221
+ hostResumeBehavior: "same-host",
222
+ targetAccess: "loopback",
223
+ secretBrokering: "none",
224
+ egressControl: "none",
225
+ persistence: {
226
+ resume: "replacement-restore",
227
+ capture: "same-resource",
228
+ clone: false,
229
+ captureSource: "preserved",
230
+ retention: "provider-managed",
231
+ restoreMount: "same-resource"
232
+ }
233
+ }
234
+ }
235
+ };
236
+
237
+ // src/image.ts
238
+ import { Image, DaytonaNotFoundError } from "@daytona/sdk";
239
+ var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
240
+ var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
241
+ var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
242
+
243
+ // src/runtime-env.ts
244
+ function workspaceRuntimeTargetEnv(input) {
245
+ return {
246
+ WORKSPACE_RUNTIME_WORKSPACE_ID: input.workspaceId,
247
+ ...input.hostId ? { WORKSPACE_RUNTIME_HOST_ID: input.hostId } : {},
248
+ WORKSPACE_RUNTIME_DIRECTORY: input.directory,
249
+ WORKSPACE_RUNTIME_PORT: String(input.port),
250
+ ...input.host ? { WORKSPACE_RUNTIME_HOST: input.host } : {}
251
+ };
252
+ }
253
+ function workspaceRuntimeSourceEnv(input) {
254
+ if (!input.source || input.source.kind === "empty") return { WORKSPACE_RUNTIME_SOURCE_KIND: "empty" };
255
+ return {
256
+ WORKSPACE_RUNTIME_SOURCE_KIND: "git",
257
+ WORKSPACE_RUNTIME_GIT_REPO_URL: input.source.repoUrl,
258
+ ...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
259
+ };
260
+ }
261
+
262
+ // src/drivers/exe.ts
263
+ var DEFAULT_ENDPOINT = "https://exe.dev/exec";
264
+ var DEFAULT_RUNTIME_COMMAND = "workspace-runtime";
265
+ var DEFAULT_WORKSPACE_DIR = "/workspace";
266
+ var DEFAULT_OPERATION_TIMEOUT_MS = 12e4;
267
+ var DEFAULT_EXE_APP_LABEL = "claxedo";
268
+ var DEFAULT_HEALTH_TIMEOUT_MS = 6e4;
269
+ var DEFAULT_HEALTH_INTERVAL_MS = 1e3;
270
+ function exeWorkspaceName(workspaceId, epoch) {
271
+ const slug = workspaceId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30) || "workspace";
272
+ const hash = [...workspaceId].reduce((value, char) => Math.imul(value ^ char.charCodeAt(0), 16777619), 2166136261);
273
+ return `claxedo-ws-${slug}-${(hash >>> 0).toString(36).slice(0, 8)}-g${epoch}`.slice(0, 63).replace(/-+$/g, "");
274
+ }
275
+ function createExeSandboxDriver(options) {
276
+ const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;
277
+ const image = options.image ?? SANDBOX_IMAGE;
278
+ const runtimeCommand = options.runtimeCommand ?? DEFAULT_RUNTIME_COMMAND;
279
+ const workspaceDir = options.workspaceDir ?? DEFAULT_WORKSPACE_DIR;
280
+ const operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
281
+ const healthTimeoutMs = options.healthTimeoutMs ?? DEFAULT_HEALTH_TIMEOUT_MS;
282
+ const healthIntervalMs = options.healthIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
283
+ const fetchImpl = options.fetchImpl ?? fetch;
284
+ const labels = /* @__PURE__ */ new Map();
285
+ async function api(command) {
286
+ const response = await fetchImpl(endpoint, {
287
+ method: "POST",
288
+ headers: {
289
+ Authorization: `Bearer ${options.apiToken}`,
290
+ "Content-Type": "text/plain; charset=utf-8"
291
+ },
292
+ body: command,
293
+ signal: AbortSignal.timeout(operationTimeoutMs)
294
+ });
295
+ const text = await response.text();
296
+ const body = text ? JSON.parse(text) : {};
297
+ if (!response.ok || body.ok === false || typeof body.error === "string") {
298
+ throw new Error(`exe.dev command failed (${response.status}): ${String(body.error ?? body.message ?? response.statusText)}`);
299
+ }
300
+ return body;
301
+ }
302
+ function vm(input) {
303
+ if (!input || typeof input !== "object") return;
304
+ const value = input;
305
+ if (typeof value.vm_name !== "string") return;
306
+ return {
307
+ vm_name: value.vm_name,
308
+ ...typeof value.https_url === "string" ? { https_url: value.https_url } : {},
309
+ ...typeof value.ssh_dest === "string" ? { ssh_dest: value.ssh_dest } : {},
310
+ ...typeof value.status === "string" ? { status: value.status } : {},
311
+ ...Array.isArray(value.tags) ? { tags: value.tags.filter((tag) => typeof tag === "string") } : {}
312
+ };
313
+ }
314
+ function durableLabels(item) {
315
+ const cached = labels.get(item.vm_name);
316
+ if (cached) return cached;
317
+ const tags = item.tags ?? [];
318
+ if (!tags.includes("claxedo")) return;
319
+ const workspaceId = tags.find((tag) => tag.startsWith("workspace-"))?.slice("workspace-".length);
320
+ if (!workspaceId) return;
321
+ const epoch = item.vm_name.match(/-g(\d+)$/)?.[1];
322
+ if (!epoch) return;
323
+ return { app: DEFAULT_EXE_APP_LABEL, workspaceId, epoch };
324
+ }
325
+ async function inspect(name) {
326
+ const result = await api(`ls ${shell(name)}`);
327
+ return (Array.isArray(result.vms) ? result.vms : []).map(vm).find((candidate) => candidate?.vm_name === name);
328
+ }
329
+ function target(input) {
330
+ const url = input.vm.https_url ?? `https://${input.vm.vm_name}.exe.xyz`;
331
+ return {
332
+ ...input.workspaceId ? { workspaceId: input.workspaceId } : {},
333
+ sandboxId: input.vm.vm_name,
334
+ url,
335
+ hostId: input.hostId ?? input.vm.vm_name,
336
+ driverResourceId: input.vm.vm_name,
337
+ labels: input.labels ?? labels.get(input.vm.vm_name),
338
+ driver: { id: "exe", resourceId: input.vm.vm_name }
339
+ };
340
+ }
341
+ async function execute(name, command) {
342
+ const body = await api(`ssh ${shell(name)} ${shell(command)}`);
343
+ const result = body.result && typeof body.result === "object" ? body.result : body;
344
+ return {
345
+ stdout: typeof result.stdout === "string" ? result.stdout : "",
346
+ stderr: typeof result.stderr === "string" ? result.stderr : "",
347
+ exitCode: typeof result.exit_code === "number" ? result.exit_code : typeof result.exitCode === "number" ? result.exitCode : 0
348
+ };
349
+ }
350
+ async function executeOrThrow(name, command, label) {
351
+ const result = await execute(name, command);
352
+ if (result.exitCode !== 0) {
353
+ throw new Error(`exe.dev ${name} ${label} failed (${result.exitCode}): ${result.stderr || result.stdout}`);
354
+ }
355
+ return result;
356
+ }
357
+ function runtimePort(input) {
358
+ return input.workspaceRuntimePort ?? options.runtimePort ?? DEFAULT_WORKSPACE_RUNTIME_PORT;
359
+ }
360
+ function directory(input) {
361
+ return input.workspaceRoot ?? workspaceDir;
362
+ }
363
+ async function boot(vm2, input, hostId) {
364
+ const env = {
365
+ ...workspaceRuntimeTargetEnv({
366
+ workspaceId: input.workspaceId,
367
+ hostId,
368
+ directory: directory(input),
369
+ port: runtimePort(input),
370
+ host: "0.0.0.0"
371
+ }),
372
+ ...workspaceRuntimeSourceEnv({ source: input.source }),
373
+ ...input.env,
374
+ ...await options.env?.(input, vm2)
375
+ };
376
+ if (options.runner) env.WORKSPACE_RUNTIME_RUNNER = options.runner;
377
+ const envArgs = Object.entries(env).map(([key, value]) => `${key}=${shell(value)}`).join(" ");
378
+ await executeOrThrow(
379
+ vm2.vm_name,
380
+ `mkdir -p ${shell(directory(input))} && (pkill -f ${shell(runtimeCommand)} || true) && cd ${shell(directory(input))} && nohup env ${envArgs} ${runtimeCommand} > .claxedo-workspace-runtime.log 2>&1 < /dev/null &`,
381
+ "runtime start"
382
+ );
383
+ const until = Date.now() + healthTimeoutMs;
384
+ while (Date.now() <= until) {
385
+ const health = await execute(
386
+ vm2.vm_name,
387
+ `curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:${runtimePort(input)}/global/health || true`
388
+ );
389
+ if (health.stdout.trim() === "200") {
390
+ labels.set(vm2.vm_name, input.labels);
391
+ return target({ vm: vm2, workspaceId: input.workspaceId, hostId, labels: input.labels });
392
+ }
393
+ await new Promise((resolve) => setTimeout(resolve, healthIntervalMs));
394
+ }
395
+ throw new Error(`exe.dev ${vm2.vm_name} workspace runtime did not become healthy`);
396
+ }
397
+ function assertNetwork(input) {
398
+ if (input.net && input.net.mode !== "allow-all") {
399
+ throw new Error("exe.dev SandboxDriver cannot enforce host-based network policy");
400
+ }
401
+ }
402
+ async function ensureHost(input) {
403
+ assertNetwork(input);
404
+ const name = exeWorkspaceName(input.workspaceId, input.epoch);
405
+ const existing = await inspect(name);
406
+ const created = existing ?? vm(await api(
407
+ `new --name=${shell(name)} --image=${shell(input.bootSource?.kind === "image" ? input.bootSource.image : image)} --tag=claxedo --tag=${shell(`workspace-${input.workspaceId}`)} ${input.hostId ? `--tag=${shell(`host-${input.hostId}`)} ` : ""}--no-email`
408
+ ).then((result) => result.vm ?? result));
409
+ if (!created) throw new Error(`exe.dev did not return VM metadata for ${name}`);
410
+ return boot(created, input, input.hostId ?? name);
411
+ }
412
+ async function resumeHost(input) {
413
+ assertNetwork(input.ensure);
414
+ const name = input.lease.sandboxId ?? exeWorkspaceName(input.ensure.workspaceId, input.lease.epoch);
415
+ const existing = await inspect(name);
416
+ if (!existing) return ensureHost(input.ensure);
417
+ return boot(existing, input.ensure, input.ensure.hostId ?? input.lease.hostId ?? name);
418
+ }
419
+ return {
420
+ id: "exe",
421
+ metadata: sandboxDriverCatalog.exe.metadata,
422
+ ensureHost,
423
+ resumeHost,
424
+ // Provider-state enumeration for `garbageCollect()`. Labels come from
425
+ // DURABLE provider state (tags + the VM name's epoch suffix), not the
426
+ // in-process cache — see `durableLabels`. A VM whose labels cannot be
427
+ // recovered is returned WITHOUT them so GC skips it as unlabeled rather
428
+ // than acting on a guess; it is never destroyed on partial identity.
429
+ async list() {
430
+ const result = await api("ls claxedo-ws-*");
431
+ return (Array.isArray(result.vms) ? result.vms : []).map(vm).filter((item) => !!item && item.vm_name.startsWith("claxedo-ws-")).map((item) => {
432
+ const recovered = durableLabels(item);
433
+ const hostId = item.tags?.find((tag) => tag.startsWith("host-"))?.slice("host-".length);
434
+ return target({
435
+ vm: item,
436
+ ...recovered?.workspaceId ? { workspaceId: recovered.workspaceId } : {},
437
+ ...recovered ? { labels: recovered } : {},
438
+ ...hostId ? { hostId } : {}
439
+ });
440
+ });
441
+ },
442
+ async inspect(input) {
443
+ const current = await inspect(input.sandboxId);
444
+ return current ? target({ vm: current, workspaceId: input.workspaceId, hostId: input.hostId }) : void 0;
445
+ },
446
+ exec: (input, command) => execute(input.sandboxId, command),
447
+ async stop(input) {
448
+ await executeOrThrow(input.sandboxId, `pkill -f ${shell(runtimeCommand)} || true`, "runtime stop");
449
+ },
450
+ async destroy(input) {
451
+ await api(`rm ${shell(input.sandboxId)}`);
452
+ labels.delete(input.sandboxId);
453
+ },
454
+ async clone(input, clone) {
455
+ const name = exeWorkspaceName(clone.name, 1);
456
+ const copied = vm(await api(`cp ${shell(input.sandboxId)} ${shell(name)} --copy-tags`).then(
457
+ (result) => result.vm ?? result
458
+ ));
459
+ if (!copied) throw new Error(`exe.dev did not return cloned VM metadata for ${name}`);
460
+ return target({ vm: copied });
461
+ },
462
+ async touch() {
463
+ }
464
+ };
465
+ }
466
+ export {
467
+ createExeSandboxDriver,
468
+ exeWorkspaceName
469
+ };
@@ -77,7 +77,16 @@ function createFetchBridgeSandboxDriver(options) {
77
77
  hostStopBehavior: "suspends-host",
78
78
  hostResumeBehavior: "same-host",
79
79
  targetAccess: "relay",
80
- secretBrokering: "none"
80
+ secretBrokering: "none",
81
+ egressControl: "none",
82
+ persistence: {
83
+ resume: "same-sandbox",
84
+ capture: "none",
85
+ clone: false,
86
+ captureSource: "not-applicable",
87
+ retention: "not-applicable",
88
+ restoreMount: "not-applicable"
89
+ }
81
90
  },
82
91
  ensureHost,
83
92
  async list() {