@workbench-ai/workbench-core 0.0.68 → 0.0.69

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,17 @@
1
+ import { type WorkbenchRemoteKind } from "@workbench-ai/workbench-contract";
2
+ export type { WorkbenchRemoteKind };
3
+ export interface ParsedWorkbenchCloudRemoteUrl {
4
+ kind: "workbench-cloud";
5
+ url: string;
6
+ baseUrl: string;
7
+ owner: string;
8
+ skill: string;
9
+ }
10
+ export interface ParsedWorkbenchFileRemoteUrl {
11
+ kind: "file";
12
+ url: string;
13
+ path: string;
14
+ }
15
+ export type ParsedWorkbenchRemoteUrl = ParsedWorkbenchCloudRemoteUrl | ParsedWorkbenchFileRemoteUrl;
16
+ export declare function parseWorkbenchRemoteUrl(rawUrl: string): ParsedWorkbenchRemoteUrl;
17
+ //# sourceMappingURL=remote-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-model.d.ts","sourceRoot":"","sources":["../src/remote-model.ts"],"names":[],"mappings":"AAEA,OAAO,EAA+B,KAAK,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAIzG,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,iBAAiB,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,wBAAwB,GAAG,6BAA6B,GAAG,4BAA4B,CAAC;AAEpG,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,CAoEhF"}
@@ -0,0 +1,86 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { normalizeWorkbenchSkillName } from "@workbench-ai/workbench-contract";
3
+ import { WorkbenchCodedError } from "./coded-errors.js";
4
+ export function parseWorkbenchRemoteUrl(rawUrl) {
5
+ const trimmed = rawUrl.trim();
6
+ if (!trimmed) {
7
+ throw new WorkbenchCodedError("remote_invalid_url", "Workbench remote URL must be non-empty.", {
8
+ remediation: "Run workbench remote add --name NAME --url file:///absolute/path or workbench remote add --name NAME --url https://HOST/skills/OWNER/SKILL.",
9
+ exitCode: 2,
10
+ });
11
+ }
12
+ let url;
13
+ try {
14
+ url = new URL(trimmed);
15
+ }
16
+ catch {
17
+ throw new WorkbenchCodedError("remote_invalid_url", `Invalid Workbench remote URL: ${trimmed}`, {
18
+ remediation: "Use file:///absolute/path for local remotes or https://HOST/skills/OWNER/SKILL for Workbench Cloud remotes.",
19
+ subject: { url: trimmed },
20
+ exitCode: 2,
21
+ });
22
+ }
23
+ if (url.protocol === "file:") {
24
+ let filePath;
25
+ try {
26
+ filePath = fileURLToPath(url);
27
+ }
28
+ catch {
29
+ throw new WorkbenchCodedError("remote_invalid_url", `Invalid file Workbench remote URL: ${trimmed}`, {
30
+ remediation: "Use an absolute file URL such as file:///tmp/workbench-remote.",
31
+ subject: { url: trimmed },
32
+ exitCode: 2,
33
+ });
34
+ }
35
+ if (!filePath.startsWith("/")) {
36
+ throw new WorkbenchCodedError("remote_invalid_url", `File Workbench remote must use an absolute path: ${trimmed}`, {
37
+ remediation: "Use file:///absolute/path for local Workbench remotes.",
38
+ subject: { url: trimmed },
39
+ exitCode: 2,
40
+ });
41
+ }
42
+ return { kind: "file", url: url.toString(), path: filePath };
43
+ }
44
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
45
+ throw new WorkbenchCodedError("remote_unsupported_scheme", `Unsupported Workbench remote scheme: ${url.protocol.replace(/:$/u, "")}`, {
46
+ remediation: "Use file:///absolute/path for local remotes or https://HOST/skills/OWNER/SKILL for Workbench Cloud remotes.",
47
+ subject: { url: trimmed, scheme: url.protocol.replace(/:$/u, "") },
48
+ exitCode: 2,
49
+ });
50
+ }
51
+ const segments = url.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
52
+ if (segments.length !== 3 || segments[0] !== "skills") {
53
+ throw new WorkbenchCodedError("remote_invalid_skill_slug", `Workbench Cloud remote must use /skills/OWNER/SKILL: ${trimmed}`, {
54
+ remediation: "Run workbench remote add --name NAME --url https://HOST/skills/OWNER/SKILL.",
55
+ subject: { url: trimmed },
56
+ exitCode: 2,
57
+ });
58
+ }
59
+ const owner = validateRemoteSlug(segments[1], "owner", trimmed);
60
+ const skill = validateRemoteSlug(segments[2], "skill", trimmed);
61
+ url.hash = "";
62
+ url.search = "";
63
+ url.pathname = `/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`;
64
+ return {
65
+ kind: "workbench-cloud",
66
+ url: url.toString().replace(/\/$/u, ""),
67
+ baseUrl: url.origin,
68
+ owner,
69
+ skill,
70
+ };
71
+ }
72
+ function validateRemoteSlug(value, label, displayUrl) {
73
+ const normalized = normalizeWorkbenchSkillName(value);
74
+ if (!normalized || normalized !== value) {
75
+ throw new WorkbenchCodedError("remote_invalid_skill_slug", `Workbench Cloud remote ${label} must be a URL-safe slug: ${value}`, {
76
+ remediation: "Use lowercase letters, numbers, and single hyphens in Workbench Cloud remote URLs.",
77
+ subject: { url: displayUrl, segment: label, value },
78
+ exitCode: 2,
79
+ });
80
+ }
81
+ return normalized;
82
+ }
83
+ function isLoopbackHost(hostname) {
84
+ const normalized = hostname.toLowerCase().replace(/^\[(.*)\]$/u, "$1");
85
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
86
+ }
@@ -18,6 +18,6 @@ export declare function resolveDockerRuntimeImageRef(imageRef: string, options:
18
18
  }): string;
19
19
  export declare function normalizeRuntimeRegistry(value: string): string;
20
20
  export declare function normalizeWorkbenchWorkerId(value: unknown): string | undefined;
21
- export declare function resolveWorkbenchWorkerId(skills: readonly unknown[], fallback: string): string;
21
+ export declare function resolveWorkbenchWorkerId(candidates: readonly unknown[], fallback: string): string;
22
22
  export declare function quoteShellArg(value: string): string;
23
23
  //# sourceMappingURL=runtime-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,mBAAmB,EACpB,MAAM,kCAAkC,CAAC;AAE1C,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAEvE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIvE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAI/D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU9D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,mBAAmB,EAAE,GACpC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAAO,GACvD,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA+ChC;AAwBD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,kCAAkC,EAAE,IAAI,CAWtG;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAChF,MAAM,CASR;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAeD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAS7E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS7F;AAoBD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD"}
1
+ {"version":3,"file":"runtime-utils.d.ts","sourceRoot":"","sources":["../src/runtime-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EACJ,mBAAmB,EACpB,MAAM,kCAAkC,CAAC;AAE1C,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAEvE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIvE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAI/D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAE9D;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU9D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,mBAAmB,EAAE,GACpC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAAO,GACvD,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA+ChC;AAwBD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,kCAAkC,EAAE,IAAI,CAWtG;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAChF,MAAM,CASR;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAeD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAS7E;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASjG;AAoBD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD"}
@@ -167,9 +167,9 @@ export function normalizeWorkbenchWorkerId(value) {
167
167
  }
168
168
  return NON_IDENTIFYING_WORKER_IDS.has(normalized.toLowerCase()) ? undefined : normalized;
169
169
  }
170
- export function resolveWorkbenchWorkerId(skills, fallback) {
171
- for (const skill of skills) {
172
- const normalized = normalizeWorkbenchWorkerId(skill);
170
+ export function resolveWorkbenchWorkerId(candidates, fallback) {
171
+ for (const candidate of candidates) {
172
+ const normalized = normalizeWorkbenchWorkerId(candidate);
173
173
  if (normalized) {
174
174
  return normalized;
175
175
  }
@@ -1 +1 @@
1
- {"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/docker.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EACV,8BAA8B,EAC/B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,KAAK,wBAAwB,EAE7B,KAAK,yBAAyB,EAE9B,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AA6C7B,wBAAgB,oCAAoC,IACjD,wBAAwB,CAY1B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,8BAA8B,EACpC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,yBAAyB,GACnC,YAAY,CAmDd"}
1
+ {"version":3,"file":"docker.d.ts","sourceRoot":"","sources":["../../src/sandbox-backends/docker.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EACV,8BAA8B,EAC/B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,KAAK,wBAAwB,EAE7B,KAAK,yBAAyB,EAE9B,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AA8C7B,wBAAgB,oCAAoC,IACjD,wBAAwB,CAY1B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,8BAA8B,EACpC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,yBAAyB,GACnC,YAAY,CAmDd"}
@@ -17,6 +17,7 @@ const BUILT_IN_ENVIRONMENT_IMAGES = {
17
17
  };
18
18
  const DOCKER_RUNTIME_MOUNT = "/workbench-runtime";
19
19
  const DOCKER_DEFAULT_WORKSPACE = "/workspace";
20
+ const mutableDockerTemplateImageBuilds = new Map();
20
21
  export function createDockerSandboxBackendDescriptor() {
21
22
  return {
22
23
  name: DOCKER_SANDBOX_BACKEND,
@@ -98,18 +99,33 @@ async function prepareDockerTemplateImage(execution, args, execFileAsync) {
98
99
  ]);
99
100
  const sourceHash = args.environmentVersion?.sourceHash?.trim() || createHash("sha256").update(dockerfile).digest("hex");
100
101
  const image = `workbench/sandbox-${safeDockerImageSegment(args.environmentVersion?.id ?? "env")}:${sourceHash.slice(0, 16)}`;
101
- const imageExists = await execFileAsync("docker", ["image", "inspect", image], { maxBuffer: 1024 * 1024 })
102
- .then(() => true, () => false);
103
- if (imageExists) {
102
+ const pending = mutableDockerTemplateImageBuilds.get(image);
103
+ if (pending) {
104
+ return await pending;
105
+ }
106
+ const build = (async () => {
107
+ const imageExists = await execFileAsync("docker", ["image", "inspect", image], { maxBuffer: 1024 * 1024 })
108
+ .then(() => true, () => false);
109
+ if (imageExists) {
110
+ return image;
111
+ }
112
+ const contextRoot = path.join(args.workdir ?? os.tmpdir(), "workbench-docker-environments", sourceHash.slice(0, 32));
113
+ const dockerfilePath = path.join(contextRoot, "Dockerfile");
114
+ await fs.rm(contextRoot, { recursive: true, force: true }).catch(() => undefined);
115
+ await fs.mkdir(contextRoot, { recursive: true });
116
+ await fs.writeFile(dockerfilePath, `${dockerfile}\n`, { mode: 0o600 });
117
+ await execFileAsync("docker", ["build", "-t", image, "-f", dockerfilePath, contextRoot], { maxBuffer: 20 * 1024 * 1024 });
104
118
  return image;
119
+ })();
120
+ mutableDockerTemplateImageBuilds.set(image, build);
121
+ try {
122
+ return await build;
123
+ }
124
+ finally {
125
+ if (mutableDockerTemplateImageBuilds.get(image) === build) {
126
+ mutableDockerTemplateImageBuilds.delete(image);
127
+ }
105
128
  }
106
- const contextRoot = path.join(args.workdir ?? os.tmpdir(), "workbench-docker-environments", sourceHash.slice(0, 32));
107
- const dockerfilePath = path.join(contextRoot, "Dockerfile");
108
- await fs.rm(contextRoot, { recursive: true, force: true }).catch(() => undefined);
109
- await fs.mkdir(contextRoot, { recursive: true });
110
- await fs.writeFile(dockerfilePath, `${dockerfile}\n`, { mode: 0o600 });
111
- await execFileAsync("docker", ["build", "-t", image, "-f", dockerfilePath, contextRoot], { maxBuffer: 20 * 1024 * 1024 });
112
- return image;
113
129
  }
114
130
  async function prepareDockerSandboxWorkspace(args, request, startedAt) {
115
131
  const [{ execFile }, fs, os, path, { promisify }] = await Promise.all([
@@ -168,7 +184,7 @@ async function runDockerSandboxExecution(args, sandbox, execution) {
168
184
  const runtimeImport = readRequiredMetadataString(metadata, "runtimeImport", DOCKER_SANDBOX_BACKEND);
169
185
  const sandboxUid = readRequiredMetadataNumber(metadata, "sandboxUid", DOCKER_SANDBOX_BACKEND);
170
186
  const sandboxGid = readRequiredMetadataNumber(metadata, "sandboxGid", DOCKER_SANDBOX_BACKEND);
171
- const progressTarget = readOptionalProgressTarget(metadata.progressTarget);
187
+ const progressTarget = args.progress ?? readOptionalProgressTarget(metadata.progressTarget);
172
188
  const network = asRuntimeRecord(metadata.network);
173
189
  const resources = execution.policy.resources;
174
190
  const tmpfsSize = dockerSize(resources.diskGb);
@@ -566,17 +582,17 @@ function findDockerSourceRoot() {
566
582
  }
567
583
  const cwd = process.cwd();
568
584
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
569
- const skills = [
585
+ const roots = [
570
586
  cwd,
571
587
  path.resolve(cwd, ".."),
572
588
  path.resolve(cwd, "../.."),
573
589
  path.resolve(cwd, "../../.."),
574
590
  path.resolve(moduleDir, "../../../../../.."),
575
591
  ];
576
- for (const skill of skills) {
577
- if (existsSync(path.join(skill, "products/workbench/packages/core/worker/sandbox-adapter-runner.cjs")) &&
578
- existsSync(path.join(skill, "products/workbench/packages/core/src/index.ts"))) {
579
- return skill;
592
+ for (const root of roots) {
593
+ if (existsSync(path.join(root, "products/workbench/packages/core/worker/sandbox-adapter-runner.cjs")) &&
594
+ existsSync(path.join(root, "products/workbench/packages/core/src/index.ts"))) {
595
+ return root;
580
596
  }
581
597
  }
582
598
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-ai/workbench-core",
3
- "version": "0.0.68",
3
+ "version": "0.0.69",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/workbench-ai/workbench.git",
@@ -22,8 +22,8 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "yaml": "^2.8.2",
25
- "@workbench-ai/workbench-contract": "0.0.68",
26
- "@workbench-ai/workbench-protocol": "0.0.68"
25
+ "@workbench-ai/workbench-contract": "0.0.69",
26
+ "@workbench-ai/workbench-protocol": "0.0.69"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.3.1",