@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/exe.mjs
DELETED
|
@@ -1,469 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { SandboxDriver } from "..";
|
|
2
|
-
export type FetchBridgeSandboxDriverOptions = {
|
|
3
|
-
id: string;
|
|
4
|
-
baseUrl: string;
|
|
5
|
-
token?: string;
|
|
6
|
-
fetch?: typeof fetch;
|
|
7
|
-
autoStopMs: number;
|
|
8
|
-
autoDeleteMs: number;
|
|
9
|
-
};
|
|
10
|
-
export declare function createFetchBridgeSandboxDriver(options: FetchBridgeSandboxDriverOptions): SandboxDriver;
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
// src/drivers/fetch-bridge.ts
|
|
2
|
-
function cleanUrl(input) {
|
|
3
|
-
return input.replace(/\/+$/, "");
|
|
4
|
-
}
|
|
5
|
-
function assertLifecycleSettings(options) {
|
|
6
|
-
if (!Number.isFinite(options.autoStopMs) || options.autoStopMs <= 0) {
|
|
7
|
-
throw new Error("Fetch bridge sandbox driver requires a finite auto-stop/sleep policy");
|
|
8
|
-
}
|
|
9
|
-
if (!Number.isFinite(options.autoDeleteMs) || options.autoDeleteMs <= 0) {
|
|
10
|
-
throw new Error("Fetch bridge sandbox driver requires a finite auto-delete policy");
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function headers(token) {
|
|
14
|
-
return {
|
|
15
|
-
"content-type": "application/json",
|
|
16
|
-
...token ? { authorization: `Bearer ${token}` } : {}
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function target(input) {
|
|
20
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Fetch bridge sandbox driver returned an invalid response");
|
|
21
|
-
const row = input;
|
|
22
|
-
if (row.status === "provisioning") {
|
|
23
|
-
return {
|
|
24
|
-
provisioning: true,
|
|
25
|
-
retryAfterMs: typeof row.retryAfterMs === "number" ? row.retryAfterMs : 2e3
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
if (typeof row.sandboxId !== "string" || typeof row.url !== "string" || typeof row.hostId !== "string") {
|
|
29
|
-
throw new Error("Fetch bridge sandbox driver returned an incomplete target");
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
workspaceId: typeof row.workspaceId === "string" ? row.workspaceId : void 0,
|
|
33
|
-
sandboxId: row.sandboxId,
|
|
34
|
-
url: row.url,
|
|
35
|
-
hostId: row.hostId,
|
|
36
|
-
driverResourceId: typeof row.driverResourceId === "string" ? row.driverResourceId : void 0,
|
|
37
|
-
labels: row.labels && typeof row.labels === "object" && !Array.isArray(row.labels) ? row.labels : void 0
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function bridgeTarget(input) {
|
|
41
|
-
const result = target(input);
|
|
42
|
-
if ("provisioning" in result) throw new Error("Fetch bridge sandbox driver returned provisioning status in a target list");
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
function targetList(input) {
|
|
46
|
-
const rows = Array.isArray(input) ? input : input && typeof input === "object" && !Array.isArray(input) && Array.isArray(input.targets) ? input.targets : void 0;
|
|
47
|
-
if (!rows) throw new Error("Fetch bridge sandbox driver returned an invalid target list");
|
|
48
|
-
return rows.map(bridgeTarget);
|
|
49
|
-
}
|
|
50
|
-
async function post(options, path, body) {
|
|
51
|
-
const res = await (options.fetch ?? fetch)(`${cleanUrl(options.baseUrl)}${path}`, {
|
|
52
|
-
method: "POST",
|
|
53
|
-
headers: headers(options.token),
|
|
54
|
-
body: JSON.stringify(body)
|
|
55
|
-
});
|
|
56
|
-
if (!res.ok) throw new Error(`Fetch bridge sandbox driver ${path} failed with ${res.status}`);
|
|
57
|
-
return res.json();
|
|
58
|
-
}
|
|
59
|
-
function createFetchBridgeSandboxDriver(options) {
|
|
60
|
-
assertLifecycleSettings(options);
|
|
61
|
-
async function ensureHost(input) {
|
|
62
|
-
return target(await post(options, "/runtime/ensure", {
|
|
63
|
-
workspaceId: input.workspaceId,
|
|
64
|
-
homeRegion: input.homeRegion,
|
|
65
|
-
epoch: input.epoch,
|
|
66
|
-
labels: input.labels,
|
|
67
|
-
hostControl: {
|
|
68
|
-
autoStopMs: options.autoStopMs,
|
|
69
|
-
autoDeleteMs: options.autoDeleteMs
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
id: options.id,
|
|
75
|
-
metadata: {
|
|
76
|
-
driverRunsIn: ["worker", "node"],
|
|
77
|
-
hostStopBehavior: "suspends-host",
|
|
78
|
-
hostResumeBehavior: "same-host",
|
|
79
|
-
targetAccess: "relay",
|
|
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
|
-
}
|
|
90
|
-
},
|
|
91
|
-
ensureHost,
|
|
92
|
-
async list() {
|
|
93
|
-
return targetList(await post(options, "/runtime/list", {}));
|
|
94
|
-
},
|
|
95
|
-
async touch(input) {
|
|
96
|
-
await post(options, "/runtime/touch", input);
|
|
97
|
-
},
|
|
98
|
-
async stop(input) {
|
|
99
|
-
await post(options, "/runtime/stop", input);
|
|
100
|
-
},
|
|
101
|
-
async suspend(input) {
|
|
102
|
-
await post(options, "/runtime/stop", input);
|
|
103
|
-
},
|
|
104
|
-
async destroy(input) {
|
|
105
|
-
await post(options, "/runtime/destroy", input);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
export {
|
|
110
|
-
createFetchBridgeSandboxDriver
|
|
111
|
-
};
|
package/dist/lease-policy.d.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { SandboxLeaseRow } from "./lease-types";
|
|
2
|
-
import { type SandboxDriverID } from "./driver-catalog";
|
|
3
|
-
export type SandboxDecision = {
|
|
4
|
-
action: "skip";
|
|
5
|
-
reason: string;
|
|
6
|
-
} | {
|
|
7
|
-
action: "wait";
|
|
8
|
-
until: number;
|
|
9
|
-
reason: string;
|
|
10
|
-
} | {
|
|
11
|
-
action: "resume";
|
|
12
|
-
reason: string;
|
|
13
|
-
} | {
|
|
14
|
-
action: "restore_snapshot";
|
|
15
|
-
snapshot_id: string;
|
|
16
|
-
reason: string;
|
|
17
|
-
} | {
|
|
18
|
-
action: "start_prepared";
|
|
19
|
-
image_id: string;
|
|
20
|
-
reason: string;
|
|
21
|
-
} | {
|
|
22
|
-
action: "start_fresh";
|
|
23
|
-
reason: string;
|
|
24
|
-
} | {
|
|
25
|
-
action: "stop_idle";
|
|
26
|
-
reason: string;
|
|
27
|
-
} | {
|
|
28
|
-
action: "mark_failed";
|
|
29
|
-
reason: string;
|
|
30
|
-
};
|
|
31
|
-
export type SandboxDecisionConfig = {
|
|
32
|
-
maxRetries: number;
|
|
33
|
-
idleMs: number;
|
|
34
|
-
backoffMaxMs: number;
|
|
35
|
-
healthTimeoutMs: number;
|
|
36
|
-
};
|
|
37
|
-
export type SandboxDriverPlacement = {
|
|
38
|
-
canPauseAndRestartSameResource: boolean;
|
|
39
|
-
canCreateFilesystemSnapshot: boolean;
|
|
40
|
-
canStartFromPreparedImage: boolean;
|
|
41
|
-
canStopExplicitly: boolean;
|
|
42
|
-
hasHealthProbe: boolean;
|
|
43
|
-
};
|
|
44
|
-
export declare const DEFAULT_WORKSPACE_HOST_DECISION_CONFIG: SandboxDecisionConfig;
|
|
45
|
-
export declare function sandboxDriverPlacement(driver: SandboxDriverID): SandboxDriverPlacement;
|
|
46
|
-
export declare function decideSandboxStart(lease: SandboxLeaseRow, placement: SandboxDriverPlacement, now: number, config?: SandboxDecisionConfig): SandboxDecision;
|
|
47
|
-
export declare function decideSandboxHealthFailure(lease: SandboxLeaseRow, _placement: SandboxDriverPlacement, now: number, config?: SandboxDecisionConfig): SandboxDecision;
|
|
48
|
-
export declare function nextSandboxRetryAt(retryCount: number, now: number, config?: SandboxDecisionConfig): number | null;
|