@workbench-ai/workbench-core 0.0.46

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.
Files changed (72) hide show
  1. package/dist/adapter-auth.d.ts +63 -0
  2. package/dist/adapter-auth.d.ts.map +1 -0
  3. package/dist/adapter-auth.js +244 -0
  4. package/dist/execution-events.d.ts +53 -0
  5. package/dist/execution-events.d.ts.map +1 -0
  6. package/dist/execution-events.js +195 -0
  7. package/dist/execution-graph.d.ts +27 -0
  8. package/dist/execution-graph.d.ts.map +1 -0
  9. package/dist/execution-graph.js +126 -0
  10. package/dist/execution-jobs.d.ts +70 -0
  11. package/dist/execution-jobs.d.ts.map +1 -0
  12. package/dist/execution-jobs.js +229 -0
  13. package/dist/execution-outputs.d.ts +9 -0
  14. package/dist/execution-outputs.d.ts.map +1 -0
  15. package/dist/execution-outputs.js +393 -0
  16. package/dist/execution-phases.d.ts +21 -0
  17. package/dist/execution-phases.d.ts.map +1 -0
  18. package/dist/execution-phases.js +262 -0
  19. package/dist/execution-runtime-types.d.ts +35 -0
  20. package/dist/execution-runtime-types.d.ts.map +1 -0
  21. package/dist/execution-runtime-types.js +1 -0
  22. package/dist/execution-scheduler.d.ts +31 -0
  23. package/dist/execution-scheduler.d.ts.map +1 -0
  24. package/dist/execution-scheduler.js +241 -0
  25. package/dist/execution-traces.d.ts +16 -0
  26. package/dist/execution-traces.d.ts.map +1 -0
  27. package/dist/execution-traces.js +164 -0
  28. package/dist/execution-usage.d.ts +12 -0
  29. package/dist/execution-usage.d.ts.map +1 -0
  30. package/dist/execution-usage.js +433 -0
  31. package/dist/generic-spec.d.ts +113 -0
  32. package/dist/generic-spec.d.ts.map +1 -0
  33. package/dist/generic-spec.js +656 -0
  34. package/dist/index.d.ts +160 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +2858 -0
  37. package/dist/model-prices-litellm.d.ts +9674 -0
  38. package/dist/model-prices-litellm.d.ts.map +1 -0
  39. package/dist/model-prices-litellm.js +9668 -0
  40. package/dist/runtime-utils.d.ts +18 -0
  41. package/dist/runtime-utils.d.ts.map +1 -0
  42. package/dist/runtime-utils.js +108 -0
  43. package/dist/sandbox-backends/docker.d.ts +5 -0
  44. package/dist/sandbox-backends/docker.d.ts.map +1 -0
  45. package/dist/sandbox-backends/docker.js +568 -0
  46. package/dist/sandbox-backends/index.d.ts +37 -0
  47. package/dist/sandbox-backends/index.d.ts.map +1 -0
  48. package/dist/sandbox-backends/index.js +79 -0
  49. package/dist/sandbox-backends/names.d.ts +6 -0
  50. package/dist/sandbox-backends/names.d.ts.map +1 -0
  51. package/dist/sandbox-backends/names.js +14 -0
  52. package/dist/sandbox-backends/template-images.d.ts +4 -0
  53. package/dist/sandbox-backends/template-images.d.ts.map +1 -0
  54. package/dist/sandbox-backends/template-images.js +48 -0
  55. package/dist/sandbox-inputs.d.ts +27 -0
  56. package/dist/sandbox-inputs.d.ts.map +1 -0
  57. package/dist/sandbox-inputs.js +220 -0
  58. package/dist/sandbox-plane.d.ts +89 -0
  59. package/dist/sandbox-plane.d.ts.map +1 -0
  60. package/dist/sandbox-plane.js +327 -0
  61. package/dist/subject-patch.d.ts +8 -0
  62. package/dist/subject-patch.d.ts.map +1 -0
  63. package/dist/subject-patch.js +63 -0
  64. package/dist/trace-files.d.ts +18 -0
  65. package/dist/trace-files.d.ts.map +1 -0
  66. package/dist/trace-files.js +94 -0
  67. package/environments/libreoffice-agent/Dockerfile +13 -0
  68. package/environments/libreoffice-python/Dockerfile +11 -0
  69. package/environments/node-22/Dockerfile +3 -0
  70. package/environments/python-3.12/Dockerfile +8 -0
  71. package/package.json +42 -0
  72. package/worker/sandbox-adapter-runner.cjs +275 -0
@@ -0,0 +1,568 @@
1
+ import { createHash, } from "node:crypto";
2
+ import { createWriteStream, existsSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath, } from "node:url";
5
+ import { createSandboxAdapterRequest, executionResultFromCompletedSandboxJob, } from "../sandbox-inputs.js";
6
+ import { asRuntimeRecord, importNodeModule, nodeBuiltin, } from "../runtime-utils.js";
7
+ import { createWorkbenchProgressStdoutParser, publishWorkbenchProgressStdoutEnvelope, } from "../execution-events.js";
8
+ import { resolveSandboxTemplateImage, } from "./template-images.js";
9
+ import { DOCKER_SANDBOX_BACKEND, } from "./names.js";
10
+ const BUILT_IN_ENVIRONMENT_IMAGES = {
11
+ "workbench/workbench-node-22:envv_node_22": "products/workbench/environments/node-22/Dockerfile",
12
+ "workbench/workbench-python-3.12:envv_python_3_12": "products/workbench/environments/python-3.12/Dockerfile",
13
+ "workbench/workbench-libreoffice-python:envv_libreoffice_python": "products/workbench/environments/libreoffice-python/Dockerfile",
14
+ "workbench/workbench-libreoffice-agent:envv_libreoffice_agent": "products/workbench/environments/libreoffice-agent/Dockerfile",
15
+ };
16
+ const DOCKER_RUNTIME_MOUNT = "/workbench-runtime";
17
+ const DOCKER_DEFAULT_WORKSPACE = "/workspace";
18
+ export function createDockerSandboxBackendDescriptor() {
19
+ return {
20
+ name: DOCKER_SANDBOX_BACKEND,
21
+ version: "1",
22
+ capabilities: {
23
+ snapshots: true,
24
+ interactiveExec: false,
25
+ filesystemDiff: false,
26
+ networkPolicy: ["none", "open"],
27
+ fileCapabilities: true,
28
+ },
29
+ };
30
+ }
31
+ export function createDockerSandboxPlane(args, startedAt, fileStore) {
32
+ return {
33
+ backend: createDockerSandboxBackendDescriptor(),
34
+ async prepareEnvironment(execution) {
35
+ const [{ execFile }, { promisify }] = await Promise.all([
36
+ importNodeModule(nodeBuiltin("child_process")),
37
+ importNodeModule(nodeBuiltin("util")),
38
+ ]);
39
+ const execFileAsync = promisify(execFile);
40
+ const templateImage = await prepareDockerTemplateImage(execution, args, execFileAsync);
41
+ await ensureDockerExecutionImage(templateImage, execFileAsync);
42
+ return {
43
+ backend: DOCKER_SANDBOX_BACKEND,
44
+ kind: execution.sandbox.kind,
45
+ ref: execution.sandbox.ref,
46
+ metadata: {
47
+ templateImage,
48
+ },
49
+ };
50
+ },
51
+ async createSandbox(request) {
52
+ const metadata = await prepareDockerSandboxWorkspace(args, request, startedAt);
53
+ return {
54
+ sandboxId: request.allocation.sandboxId,
55
+ lifecycleId: request.allocation.lifecycleId,
56
+ backend: request.allocation.backend,
57
+ executionId: request.execution.id,
58
+ template: request.allocation.template,
59
+ metadata: {
60
+ allocation: request.allocation,
61
+ ...metadata,
62
+ },
63
+ };
64
+ },
65
+ async exec(request) {
66
+ const completedJob = await runDockerSandboxExecution(request.sandbox, request.execution);
67
+ return await executionResultFromCompletedSandboxJob({
68
+ completedJob,
69
+ execution: request.execution,
70
+ startedAt,
71
+ backend: DOCKER_SANDBOX_BACKEND,
72
+ allocation: request.allocation,
73
+ capability: request.capability,
74
+ handle: request.sandbox,
75
+ fileStore,
76
+ });
77
+ },
78
+ async destroySandbox(sandbox) {
79
+ await destroyDockerSandbox(sandbox);
80
+ },
81
+ };
82
+ }
83
+ async function prepareDockerTemplateImage(execution, args, execFileAsync) {
84
+ const ref = execution.sandbox.ref;
85
+ if (execution.sandbox.kind !== "oci" || !ref.startsWith("dockerfile://")) {
86
+ return resolveSandboxTemplateImage(execution, args);
87
+ }
88
+ const dockerfile = args.environmentDockerfile?.trim();
89
+ if (!dockerfile) {
90
+ throw new Error(`Execution ${execution.id} uses ${ref}, but the claimed job input omitted environmentDockerfile.`);
91
+ }
92
+ const [fs, os, path] = await Promise.all([
93
+ importNodeModule(nodeBuiltin("fs/promises")),
94
+ importNodeModule(nodeBuiltin("os")),
95
+ importNodeModule(nodeBuiltin("path")),
96
+ ]);
97
+ const sourceHash = args.environmentVersion?.sourceHash?.trim() || createHash("sha256").update(dockerfile).digest("hex");
98
+ const image = `workbench/sandbox-${safeDockerImageSegment(args.environmentVersion?.id ?? "env")}:${sourceHash.slice(0, 16)}`;
99
+ const imageExists = await execFileAsync("docker", ["image", "inspect", image], { maxBuffer: 1024 * 1024 })
100
+ .then(() => true, () => false);
101
+ if (imageExists) {
102
+ return image;
103
+ }
104
+ const contextRoot = path.join(args.workdir ?? os.tmpdir(), "workbench-docker-environments", sourceHash.slice(0, 32));
105
+ const dockerfilePath = path.join(contextRoot, "Dockerfile");
106
+ await fs.rm(contextRoot, { recursive: true, force: true }).catch(() => undefined);
107
+ await fs.mkdir(contextRoot, { recursive: true });
108
+ await fs.writeFile(dockerfilePath, `${dockerfile}\n`, { mode: 0o600 });
109
+ await execFileAsync("docker", ["build", "-t", image, "-f", dockerfilePath, contextRoot], { maxBuffer: 20 * 1024 * 1024 });
110
+ return image;
111
+ }
112
+ async function prepareDockerSandboxWorkspace(args, request, startedAt) {
113
+ const [{ execFile }, fs, os, path, { promisify }] = await Promise.all([
114
+ importNodeModule(nodeBuiltin("child_process")),
115
+ importNodeModule(nodeBuiltin("fs/promises")),
116
+ importNodeModule(nodeBuiltin("os")),
117
+ importNodeModule(nodeBuiltin("path")),
118
+ importNodeModule(nodeBuiltin("util")),
119
+ ]);
120
+ const execFileAsync = promisify(execFile);
121
+ const sandboxUser = dockerSandboxUser();
122
+ const workdir = args.workdir ?? os.tmpdir();
123
+ const sandboxRoot = path.join(workdir, "workbench-docker", request.allocation.sandboxId);
124
+ const requestPath = path.join(sandboxRoot, "request.json");
125
+ const responsePath = path.join(sandboxRoot, "response.json");
126
+ const stdoutPath = path.join(sandboxRoot, "stdout.log");
127
+ const stderrPath = path.join(sandboxRoot, "stderr.log");
128
+ await fs.rm(sandboxRoot, { recursive: true, force: true }).catch(() => undefined);
129
+ await fs.mkdir(sandboxRoot, { recursive: true });
130
+ await alignDockerSandboxPath(fs, sandboxRoot, sandboxUser, 0o700, 0o777);
131
+ await fs.writeFile(requestPath, `${JSON.stringify(createSandboxAdapterRequest(args, request, startedAt), null, 2)}\n`, { mode: 0o600 });
132
+ await alignDockerSandboxPath(fs, requestPath, sandboxUser, 0o600, 0o644);
133
+ const environmentMetadata = asRuntimeRecord(request.environment.metadata);
134
+ const image = typeof environmentMetadata.templateImage === "string"
135
+ ? environmentMetadata.templateImage
136
+ : await prepareDockerTemplateImage(request.execution, args, execFileAsync);
137
+ const network = dockerNetworkConfigForExecution(request.execution);
138
+ const runtimePayload = await prepareDockerRuntimePayload(workdir, execFileAsync, fs, path);
139
+ return {
140
+ root: sandboxRoot,
141
+ request: requestPath,
142
+ response: responsePath,
143
+ stdout: stdoutPath,
144
+ stderr: stderrPath,
145
+ templateImage: image,
146
+ containerName: dockerContainerName(request.allocation.sandboxId),
147
+ runtimeRoot: runtimePayload.mountRoot,
148
+ runnerPath: runtimePayload.runnerPath,
149
+ runtimeImport: runtimePayload.runtimeImport,
150
+ sandboxUid: sandboxUser.uid,
151
+ sandboxGid: sandboxUser.gid,
152
+ network: network,
153
+ };
154
+ }
155
+ async function runDockerSandboxExecution(sandbox, execution) {
156
+ const metadata = asRuntimeRecord(sandbox.metadata);
157
+ const root = readRequiredMetadataString(metadata, "root", DOCKER_SANDBOX_BACKEND);
158
+ const responsePath = readRequiredMetadataString(metadata, "response", DOCKER_SANDBOX_BACKEND);
159
+ const stdoutPath = readRequiredMetadataString(metadata, "stdout", DOCKER_SANDBOX_BACKEND);
160
+ const stderrPath = readRequiredMetadataString(metadata, "stderr", DOCKER_SANDBOX_BACKEND);
161
+ const image = readRequiredMetadataString(metadata, "templateImage", DOCKER_SANDBOX_BACKEND);
162
+ const containerName = readRequiredMetadataString(metadata, "containerName", DOCKER_SANDBOX_BACKEND);
163
+ const runtimeRoot = readRequiredMetadataString(metadata, "runtimeRoot", DOCKER_SANDBOX_BACKEND);
164
+ const runnerPath = readRequiredMetadataString(metadata, "runnerPath", DOCKER_SANDBOX_BACKEND);
165
+ const runtimeImport = readRequiredMetadataString(metadata, "runtimeImport", DOCKER_SANDBOX_BACKEND);
166
+ const sandboxUid = readRequiredMetadataNumber(metadata, "sandboxUid", DOCKER_SANDBOX_BACKEND);
167
+ const sandboxGid = readRequiredMetadataNumber(metadata, "sandboxGid", DOCKER_SANDBOX_BACKEND);
168
+ const network = asRuntimeRecord(metadata.network);
169
+ const resources = execution.policy.resources;
170
+ const tmpfsSize = dockerSize(resources.diskGb);
171
+ const memorySize = dockerSize(resources.memoryGb);
172
+ const workspaceRoot = dockerExecutionWorkspaceRoot(execution);
173
+ const [{ execFile, spawn }, fs, { promisify }] = await Promise.all([
174
+ importNodeModule(nodeBuiltin("child_process")),
175
+ importNodeModule(nodeBuiltin("fs/promises")),
176
+ importNodeModule(nodeBuiltin("util")),
177
+ ]);
178
+ const execFileAsync = promisify(execFile);
179
+ await execFileAsync("docker", ["rm", "-f", containerName], { maxBuffer: 1024 * 1024 }).catch(() => undefined);
180
+ const tmpfsArgs = [
181
+ tmpfsDockerArg(DOCKER_DEFAULT_WORKSPACE, sandboxUid, sandboxGid, tmpfsSize),
182
+ ...(workspaceRoot !== DOCKER_DEFAULT_WORKSPACE
183
+ ? [tmpfsDockerArg(workspaceRoot, sandboxUid, sandboxGid, tmpfsSize)]
184
+ : []),
185
+ ].flatMap((entry) => ["--tmpfs", entry]);
186
+ const dockerArgs = [
187
+ "run",
188
+ "--rm",
189
+ "--name",
190
+ containerName,
191
+ "--network",
192
+ typeof network.mode === "string" ? network.mode : "none",
193
+ "--cpus",
194
+ dockerCpu(resources.cpu),
195
+ "--memory",
196
+ memorySize,
197
+ "--memory-swap",
198
+ memorySize,
199
+ "--user",
200
+ `${sandboxUid}:${sandboxGid}`,
201
+ ...tmpfsArgs,
202
+ "--workdir",
203
+ DOCKER_RUNTIME_MOUNT,
204
+ "-v",
205
+ `${root}:/workbench-execution`,
206
+ "-v",
207
+ `${runtimeRoot}:${DOCKER_RUNTIME_MOUNT}:ro`,
208
+ "--env",
209
+ "HOME=/tmp",
210
+ "--env",
211
+ "USER=workbench",
212
+ "--env",
213
+ `WORKBENCH_WORKSPACE_ROOT=${workspaceRoot}`,
214
+ "--env",
215
+ `WORKBENCH_RUNTIME_IMPORT=${runtimeImport}`,
216
+ image,
217
+ "node",
218
+ runnerPath,
219
+ "/workbench-execution/request.json",
220
+ "/workbench-execution/response.json",
221
+ ];
222
+ const timeoutMs = Math.max(60_000, execution.policy.resources.timeoutMinutes * 60_000 + 30_000);
223
+ let dockerError = null;
224
+ try {
225
+ await runDockerSandboxProcess(spawn, dockerArgs, {
226
+ stdoutPath,
227
+ stderrPath,
228
+ timeoutMs,
229
+ });
230
+ }
231
+ catch (error) {
232
+ dockerError = error instanceof Error ? error.stack ?? error.message : String(error);
233
+ }
234
+ const responseText = await fs.readFile(responsePath, "utf8").catch(async (error) => {
235
+ const [stdout, stderr] = await Promise.all([
236
+ fs.readFile(stdoutPath, "utf8").catch(() => ""),
237
+ fs.readFile(stderrPath, "utf8").catch(() => ""),
238
+ ]);
239
+ const details = [
240
+ dockerError ? `Docker error: ${dockerError}` : null,
241
+ stdout.trim() ? `stdout: ${stdout.trim().slice(0, 4000)}` : null,
242
+ stderr.trim() ? `stderr: ${stderr.trim().slice(0, 4000)}` : null,
243
+ ].filter((entry) => Boolean(entry)).join(" ");
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ throw new Error(`Docker sandbox exited without a response: ${message}${details ? `. ${details}` : ""}`);
246
+ });
247
+ const response = JSON.parse(responseText);
248
+ if (!response.ok) {
249
+ throw new Error(typeof response.error === "string" ? response.error : "Sandbox adapter runner failed.");
250
+ }
251
+ if (!response.job || typeof response.job !== "object") {
252
+ throw new Error("Sandbox adapter runner response omitted job.");
253
+ }
254
+ return response.job;
255
+ }
256
+ function tmpfsDockerArg(pathname, uid, gid, size) {
257
+ return `${pathname}:rw,exec,uid=${uid},gid=${gid},mode=1777,size=${size}`;
258
+ }
259
+ function dockerExecutionWorkspaceRoot(execution) {
260
+ const metadata = asRuntimeRecord(execution.metadata);
261
+ const workdir = typeof metadata.workdir === "string" ? metadata.workdir.trim() : "";
262
+ if (!workdir || workdir === DOCKER_DEFAULT_WORKSPACE) {
263
+ return DOCKER_DEFAULT_WORKSPACE;
264
+ }
265
+ if (!isSafeDockerWorkspaceRoot(workdir)) {
266
+ return DOCKER_DEFAULT_WORKSPACE;
267
+ }
268
+ return workdir;
269
+ }
270
+ function isSafeDockerWorkspaceRoot(value) {
271
+ return value.startsWith("/") &&
272
+ value !== "/" &&
273
+ value !== DOCKER_RUNTIME_MOUNT &&
274
+ value !== "/workbench-execution" &&
275
+ !value.startsWith(`${DOCKER_RUNTIME_MOUNT}/`) &&
276
+ !value.startsWith("/workbench-execution/") &&
277
+ !/[\0\r\n:]/u.test(value);
278
+ }
279
+ async function destroyDockerSandbox(sandbox) {
280
+ const metadata = asRuntimeRecord(sandbox.metadata);
281
+ const root = typeof metadata.root === "string" ? metadata.root : null;
282
+ const containerName = typeof metadata.containerName === "string" ? metadata.containerName : null;
283
+ const [{ execFile }, fs, { promisify }] = await Promise.all([
284
+ importNodeModule(nodeBuiltin("child_process")),
285
+ importNodeModule(nodeBuiltin("fs/promises")),
286
+ importNodeModule(nodeBuiltin("util")),
287
+ ]);
288
+ const execFileAsync = promisify(execFile);
289
+ if (containerName) {
290
+ await execFileAsync("docker", ["rm", "-f", containerName], { maxBuffer: 1024 * 1024 }).catch(() => undefined);
291
+ }
292
+ if (root) {
293
+ await fs.rm(root, { recursive: true, force: true }).catch(() => undefined);
294
+ }
295
+ }
296
+ function runDockerSandboxProcess(spawn, args, options) {
297
+ return new Promise((resolve, reject) => {
298
+ let settled = false;
299
+ let timedOut = false;
300
+ const progressPublishes = [];
301
+ const progressParser = createWorkbenchProgressStdoutParser((envelope) => {
302
+ progressPublishes.push(publishWorkbenchProgressStdoutEnvelope(envelope).catch(() => undefined));
303
+ });
304
+ const stdout = createWriteStream(options.stdoutPath);
305
+ const stderr = createWriteStream(options.stderrPath);
306
+ const child = spawn("docker", args, {
307
+ stdio: ["ignore", "pipe", "pipe"],
308
+ });
309
+ const timer = setTimeout(() => {
310
+ timedOut = true;
311
+ child.kill("SIGKILL");
312
+ }, options.timeoutMs);
313
+ child.stdout?.on("data", (chunk) => {
314
+ stdout.write(chunk);
315
+ progressParser.write(chunk);
316
+ });
317
+ child.stderr?.on("data", (chunk) => {
318
+ stderr.write(chunk);
319
+ });
320
+ const finish = (error) => {
321
+ if (settled) {
322
+ return;
323
+ }
324
+ settled = true;
325
+ clearTimeout(timer);
326
+ progressParser.flush();
327
+ const stdoutClosed = new Promise((closeResolve) => stdout.end(closeResolve));
328
+ const stderrClosed = new Promise((closeResolve) => stderr.end(closeResolve));
329
+ Promise.allSettled([...progressPublishes, stdoutClosed, stderrClosed]).then(() => {
330
+ if (error) {
331
+ reject(error);
332
+ return;
333
+ }
334
+ resolve();
335
+ });
336
+ };
337
+ child.on("error", (error) => finish(error));
338
+ child.on("exit", (code, signal) => {
339
+ if (timedOut) {
340
+ finish(new Error(`Docker sandbox timed out after ${options.timeoutMs}ms.`));
341
+ return;
342
+ }
343
+ if (code === 0) {
344
+ finish();
345
+ return;
346
+ }
347
+ finish(new Error(code === null
348
+ ? `Docker sandbox exited from signal ${signal ?? "unknown"}.`
349
+ : `Docker sandbox exited with code ${code}.`));
350
+ });
351
+ });
352
+ }
353
+ function dockerNetworkConfigForExecution(execution) {
354
+ if (execution.policy.network.egress === "none") {
355
+ return { mode: "none", egress: "none", allowlistEnforced: true };
356
+ }
357
+ if (execution.policy.network.egress === "open") {
358
+ return { mode: "bridge", egress: "open", allowlistEnforced: true };
359
+ }
360
+ return {
361
+ mode: "bridge",
362
+ egress: "allowlist",
363
+ allow: [...(execution.policy.network.allow ?? [])],
364
+ allowlistEnforced: false,
365
+ };
366
+ }
367
+ function dockerContainerName(sandboxId) {
368
+ return `workbench-sandbox-${sandboxId}`.replace(/[^a-z0-9_.-]+/giu, "-").slice(0, 120);
369
+ }
370
+ function dockerCpu(value) {
371
+ return String(value);
372
+ }
373
+ function dockerSize(gib) {
374
+ return `${Math.ceil(gib * 1024)}m`;
375
+ }
376
+ function safeDockerImageSegment(value) {
377
+ return value.toLowerCase().replace(/[^a-z0-9_.-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80) || "env";
378
+ }
379
+ async function prepareDockerRuntimePayload(workdir, execFileAsync, fs, path) {
380
+ const configured = process.env.WORKBENCH_DOCKER_RUNTIME_ROOT?.trim();
381
+ if (configured) {
382
+ return monorepoDockerPayload(configured);
383
+ }
384
+ const runtimeImage = process.env.WORKBENCH_DOCKER_RUNTIME_IMAGE?.trim();
385
+ if (!runtimeImage) {
386
+ return resolveLocalDockerRuntimePayload();
387
+ }
388
+ const runtimeImageId = await dockerImageId(runtimeImage, execFileAsync);
389
+ const cacheRoot = path.join(workdir, "workbench-docker-runtime", `${safeCacheSegment(runtimeImage)}-${safeCacheSegment(runtimeImageId).slice(0, 24)}`);
390
+ const marker = path.join(cacheRoot, ".workbench-core-ready");
391
+ try {
392
+ await fs.access(marker);
393
+ return monorepoDockerPayload(cacheRoot);
394
+ }
395
+ catch {
396
+ // Rebuild the cache below.
397
+ }
398
+ const tmpRoot = `${cacheRoot}.tmp-${Date.now().toString(36)}`;
399
+ const containerName = `workbench-core-${Date.now().toString(36)}`.replace(/[^a-z0-9_.-]+/giu, "-");
400
+ await fs.rm(tmpRoot, { recursive: true, force: true }).catch(() => undefined);
401
+ await fs.mkdir(tmpRoot, { recursive: true });
402
+ try {
403
+ await execFileAsync("docker", ["rm", "-f", containerName], { maxBuffer: 1024 * 1024 }).catch(() => undefined);
404
+ await execFileAsync("docker", ["create", "--name", containerName, runtimeImage, "true"], { maxBuffer: 5 * 1024 * 1024 });
405
+ await execFileAsync("docker", ["cp", `${containerName}:/app/.`, tmpRoot], { maxBuffer: 20 * 1024 * 1024 });
406
+ await fs.writeFile(path.join(tmpRoot, ".workbench-core-ready"), `${runtimeImage}\n${runtimeImageId}\n`);
407
+ await fs.rm(cacheRoot, { recursive: true, force: true }).catch(() => undefined);
408
+ await fs.rename(tmpRoot, cacheRoot);
409
+ return monorepoDockerPayload(cacheRoot);
410
+ }
411
+ finally {
412
+ await execFileAsync("docker", ["rm", "-f", containerName], { maxBuffer: 1024 * 1024 }).catch(() => undefined);
413
+ await fs.rm(tmpRoot, { recursive: true, force: true }).catch(() => undefined);
414
+ }
415
+ }
416
+ async function dockerImageId(image, execFileAsync) {
417
+ const result = await execFileAsync("docker", ["image", "inspect", image, "--format", "{{.Id}}"], { maxBuffer: 1024 * 1024 });
418
+ const id = typeof result.stdout === "string" ? result.stdout.trim() : result.stdout?.toString("utf8").trim();
419
+ if (!id) {
420
+ throw new Error(`Docker image ${image} did not report an image id.`);
421
+ }
422
+ return id;
423
+ }
424
+ function safeCacheSegment(value) {
425
+ return value.replace(/[^a-z0-9_.-]+/giu, "-");
426
+ }
427
+ async function ensureDockerExecutionImage(image, execFileAsync) {
428
+ const imageExists = await execFileAsync("docker", ["image", "inspect", image], { maxBuffer: 1024 * 1024 })
429
+ .then(() => true, () => false);
430
+ if (imageExists) {
431
+ return;
432
+ }
433
+ const builtInDockerfile = localBuiltInDockerfileForImage(image);
434
+ if (builtInDockerfile) {
435
+ await execFileAsync("docker", [
436
+ "build",
437
+ "-t",
438
+ image,
439
+ "-f",
440
+ builtInDockerfile,
441
+ path.dirname(builtInDockerfile),
442
+ ], { maxBuffer: 20 * 1024 * 1024 });
443
+ return;
444
+ }
445
+ await execFileAsync("docker", ["pull", image], { maxBuffer: 20 * 1024 * 1024 });
446
+ }
447
+ function localBuiltInDockerfileForImage(image) {
448
+ if (hasRegistryHost(image)) {
449
+ return null;
450
+ }
451
+ const dockerfile = BUILT_IN_ENVIRONMENT_IMAGES[image];
452
+ if (!dockerfile) {
453
+ return null;
454
+ }
455
+ return path.join(resolveLocalDockerRuntimePayload().builtInDockerfileRoot, dockerfile.replace(/^products\/workbench\/environments\//u, ""));
456
+ }
457
+ function hasRegistryHost(image) {
458
+ const first = image.split("/")[0] ?? "";
459
+ return first === "localhost" || first.includes(".") || first.includes(":");
460
+ }
461
+ function resolveLocalDockerRuntimePayload() {
462
+ const sourceRoot = findDockerSourceRoot();
463
+ if (sourceRoot) {
464
+ return monorepoDockerPayload(sourceRoot);
465
+ }
466
+ const packagePayload = findInstalledPackageDockerPayload();
467
+ if (packagePayload) {
468
+ return packagePayload;
469
+ }
470
+ throw new Error(`Could not resolve Workbench runtime payload from ${process.cwd()}. Run from the monorepo checkout or install the published @workbench-ai/workbench package.`);
471
+ }
472
+ function monorepoDockerPayload(root) {
473
+ return {
474
+ mountRoot: root,
475
+ runnerPath: `${DOCKER_RUNTIME_MOUNT}/products/workbench/packages/core/worker/sandbox-adapter-runner.cjs`,
476
+ runtimeImport: `${DOCKER_RUNTIME_MOUNT}/products/workbench/packages/core/src/index.ts`,
477
+ builtInDockerfileRoot: path.join(root, "products/workbench/environments"),
478
+ };
479
+ }
480
+ function findInstalledPackageDockerPayload() {
481
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
482
+ const packageRoot = path.resolve(moduleDir, "../..");
483
+ const nodeModulesRoot = findAncestorNamed(packageRoot, "node_modules");
484
+ if (!nodeModulesRoot) {
485
+ return null;
486
+ }
487
+ if (existsSync(path.join(packageRoot, "worker/sandbox-adapter-runner.cjs")) &&
488
+ existsSync(path.join(packageRoot, "dist/index.js")) &&
489
+ existsSync(path.join(packageRoot, "environments/node-22/Dockerfile"))) {
490
+ return {
491
+ mountRoot: path.dirname(nodeModulesRoot),
492
+ runnerPath: `${DOCKER_RUNTIME_MOUNT}/node_modules/@workbench-ai/workbench-core/worker/sandbox-adapter-runner.cjs`,
493
+ runtimeImport: `${DOCKER_RUNTIME_MOUNT}/node_modules/@workbench-ai/workbench-core/dist/index.js`,
494
+ builtInDockerfileRoot: path.join(packageRoot, "environments"),
495
+ };
496
+ }
497
+ return null;
498
+ }
499
+ function findAncestorNamed(start, name) {
500
+ let current = start;
501
+ for (;;) {
502
+ if (path.basename(current) === name) {
503
+ return current;
504
+ }
505
+ const parent = path.dirname(current);
506
+ if (parent === current) {
507
+ return null;
508
+ }
509
+ current = parent;
510
+ }
511
+ }
512
+ function findDockerSourceRoot() {
513
+ const configured = process.env.WORKBENCH_DOCKER_SOURCE_ROOT?.trim();
514
+ if (configured) {
515
+ return configured;
516
+ }
517
+ const cwd = process.cwd();
518
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
519
+ const subjects = [
520
+ cwd,
521
+ path.resolve(cwd, ".."),
522
+ path.resolve(cwd, "../.."),
523
+ path.resolve(cwd, "../../.."),
524
+ path.resolve(moduleDir, "../../../../../.."),
525
+ ];
526
+ for (const subject of subjects) {
527
+ if (existsSync(path.join(subject, "products/workbench/packages/core/worker/sandbox-adapter-runner.cjs")) &&
528
+ existsSync(path.join(subject, "products/workbench/packages/core/src/index.ts"))) {
529
+ return subject;
530
+ }
531
+ }
532
+ return null;
533
+ }
534
+ function readRequiredMetadataString(metadata, key, backend) {
535
+ const value = metadata[key];
536
+ if (typeof value !== "string" || value.length === 0) {
537
+ throw new Error(`${backend} sandbox metadata is missing ${key}.`);
538
+ }
539
+ return value;
540
+ }
541
+ function readRequiredMetadataNumber(metadata, key, backend) {
542
+ const value = metadata[key];
543
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
544
+ throw new Error(`${backend} sandbox metadata is missing ${key}.`);
545
+ }
546
+ return value;
547
+ }
548
+ function dockerSandboxUser() {
549
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
550
+ const gid = typeof process.getgid === "function" ? process.getgid() : null;
551
+ if (typeof uid === "number" && Number.isInteger(uid) && uid > 0) {
552
+ return {
553
+ uid,
554
+ gid: typeof gid === "number" && Number.isInteger(gid) && gid >= 0 ? gid : uid,
555
+ };
556
+ }
557
+ return { uid: 1000, gid: 1000 };
558
+ }
559
+ async function alignDockerSandboxPath(fs, targetPath, user, privateMode, fallbackMode) {
560
+ await fs.chmod(targetPath, privateMode).catch(() => undefined);
561
+ const currentUid = typeof process.getuid === "function" ? process.getuid() : null;
562
+ if (currentUid === user.uid) {
563
+ return;
564
+ }
565
+ await fs.chown(targetPath, user.uid, user.gid).catch(async () => {
566
+ await fs.chmod(targetPath, fallbackMode).catch(() => undefined);
567
+ });
568
+ }
@@ -0,0 +1,37 @@
1
+ import type { WorkbenchExecutionRuntimeInput } from "../execution-runtime-types.ts";
2
+ import type { SandboxBackendCapabilities, SandboxExecutionFileStore, SandboxPlane } from "../sandbox-plane.ts";
3
+ import { type WorkbenchSandboxProviderName } from "./names.ts";
4
+ export { DOCKER_SANDBOX_BACKEND, resolveWorkbenchSandboxProviderName, type WorkbenchSandboxProviderName, } from "./names.ts";
5
+ export { createDockerSandboxBackendDescriptor, createDockerSandboxPlane, } from "./docker.ts";
6
+ export interface SandboxHostHealthExpectation {
7
+ provider: WorkbenchSandboxProviderName;
8
+ backend: string;
9
+ capabilities: SandboxBackendCapabilities;
10
+ }
11
+ export interface SandboxProviderRequestedResources {
12
+ cpu: number;
13
+ memoryGb: number;
14
+ diskGb?: number;
15
+ timeoutMinutes?: number;
16
+ }
17
+ export interface SandboxProviderHostCost {
18
+ cpu: number;
19
+ memoryGb: number;
20
+ diskGb: number;
21
+ }
22
+ export interface SandboxProviderLeaseRequest {
23
+ scope: string;
24
+ units: number;
25
+ }
26
+ export interface SandboxProviderAdmission {
27
+ provider: WorkbenchSandboxProviderName;
28
+ hostCost: SandboxProviderHostCost;
29
+ providerLeases: SandboxProviderLeaseRequest[];
30
+ }
31
+ export declare function createSandboxBackendPlaneForProvider(provider: string, args: WorkbenchExecutionRuntimeInput, startedAt: string, fileStore: SandboxExecutionFileStore): SandboxPlane;
32
+ export declare function sandboxHostHealthExpectationForProvider(provider: WorkbenchSandboxProviderName): SandboxHostHealthExpectation;
33
+ export declare function assertSandboxHostHealthForProvider(value: unknown, provider: WorkbenchSandboxProviderName): void;
34
+ export declare function sandboxProviderDefaultMaxConcurrentJobs(_provider: WorkbenchSandboxProviderName): number | null;
35
+ export declare function sandboxProviderAdmissionForResources(provider: WorkbenchSandboxProviderName, resources: SandboxProviderRequestedResources): SandboxProviderAdmission;
36
+ export declare function sandboxProviderLeaseScope(provider: WorkbenchSandboxProviderName): string;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC/B,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EACV,0BAA0B,EAE1B,yBAAyB,EACzB,YAAY,EACb,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EAEL,KAAK,4BAA4B,EAElC,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,sBAAsB,EACtB,mCAAmC,EACnC,KAAK,4BAA4B,GAClC,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oCAAoC,EACpC,wBAAwB,GACzB,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,0BAA0B,CAAC;CAC1C;AAED,MAAM,WAAW,iCAAiC;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,4BAA4B,CAAC;IACvC,QAAQ,EAAE,uBAAuB,CAAC;IAClC,cAAc,EAAE,2BAA2B,EAAE,CAAC;CAC/C;AAED,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,8BAA8B,EACpC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,yBAAyB,GACnC,YAAY,CAMd;AAED,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,4BAA4B,GACrC,4BAA4B,CAS9B;AAED,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,4BAA4B,GACrC,IAAI,CAeN;AAED,wBAAgB,uCAAuC,CACrD,SAAS,EAAE,4BAA4B,GACtC,MAAM,GAAG,IAAI,CAEf;AAED,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,4BAA4B,EACtC,SAAS,EAAE,iCAAiC,GAC3C,wBAAwB,CAkB1B;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,4BAA4B,GACrC,MAAM,CAER"}