diff --git a/scripts/check-openclaw-package-tarball.mjs b/scripts/check-openclaw-package-tarball.mjs index 1d8ceb424641..b632bcdf6558 100644 --- a/scripts/check-openclaw-package-tarball.mjs +++ b/scripts/check-openclaw-package-tarball.mjs @@ -8,6 +8,7 @@ import os from "node:os"; import path from "node:path"; import { performance } from "node:perf_hooks"; import { pathToFileURL } from "node:url"; +import { gte as semverGte, valid as validSemver } from "semver"; import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs"; import { collectPackageDistImports, @@ -323,6 +324,8 @@ const normalized = entries.map((entry) => entry.replace(/^package\//u, "")); const entrySet = new Set(normalized); const errors = []; const warnings = []; +const CODE_MODE_WORKER_PATH = "dist/agents/code-mode.worker.js"; +const FIRST_CODE_MODE_WORKER_VERSION = "2026.5.14-beta.2"; const REQUIRED_TARBALL_ENTRIES = ["dist/control-ui/index.html", ...WORKSPACE_TEMPLATE_PACK_PATHS]; const PACKAGE_INSTALL_GUARD_RELATIVE_PATH = "dist/openclaw-install-guard"; const REQUIRED_TARBALL_ENTRY_PREFIXES = ["dist/control-ui/assets/"]; @@ -474,6 +477,12 @@ if (entrySet.has("package.json")) { packageVersion = ""; } } +const validPackageVersion = validSemver(packageVersion); +const requiresCodeModeWorker = + validPackageVersion !== null && semverGte(validPackageVersion, FIRST_CODE_MODE_WORKER_VERSION); +if (requiresCodeModeWorker && !entrySet.has(CODE_MODE_WORKER_PATH)) { + errors.push(`missing required tar entry ${CODE_MODE_WORKER_PATH}`); +} if (entrySet.has("package-lock.json")) { errors.push("package tarball must not contain package-lock.json"); } @@ -570,6 +579,9 @@ if (entrySet.has("dist/postinstall-inventory.json")) { } else { const normalizedInventory = inventory.map((entry) => entry.replace(/\\/gu, "/")); const normalizedInventorySet = new Set(normalizedInventory); + if (requiresCodeModeWorker && !normalizedInventorySet.has(CODE_MODE_WORKER_PATH)) { + errors.push(`postinstall inventory omits ${CODE_MODE_WORKER_PATH}`); + } if (normalizedInventorySet.has(PACKAGE_INSTALL_GUARD_RELATIVE_PATH)) { errors.push( `package dist inventory must omit install guard ${PACKAGE_INSTALL_GUARD_RELATIVE_PATH}`, diff --git a/test/scripts/check-openclaw-package-tarball-control-ui.test.ts b/test/scripts/check-openclaw-package-tarball-control-ui.test.ts index 989f3281dc5a..3f6760e3420a 100644 --- a/test/scripts/check-openclaw-package-tarball-control-ui.test.ts +++ b/test/scripts/check-openclaw-package-tarball-control-ui.test.ts @@ -16,6 +16,7 @@ import { describe, expect, it } from "vitest"; import { WORKSPACE_TEMPLATE_PACK_PATHS } from "../../scripts/lib/workspace-bootstrap-smoke.mjs"; const CONTROL_UI_INDEX = "dist/control-ui/index.html"; +const CODE_MODE_WORKER_PATH = "dist/agents/code-mode.worker.js"; const CONTROL_UI_ASSETS = [ "dist/control-ui/assets/app.js", "dist/control-ui/assets/app.js.br", @@ -65,8 +66,13 @@ function withPackedPackage( }), }), ); - writeFixtureFile(packageRoot, "dist/postinstall-inventory.json", JSON.stringify(inventory)); + writeFixtureFile( + packageRoot, + "dist/postinstall-inventory.json", + JSON.stringify([...new Set([...inventory, CODE_MODE_WORKER_PATH])]), + ); writeFixtureFile(packageRoot, "dist/index.js", "export {};\n"); + writeFixtureFile(packageRoot, CODE_MODE_WORKER_PATH, "export {};\n"); writeFixtureFile( packageRoot, CONTROL_UI_INDEX, @@ -215,6 +221,7 @@ describe("packaged Control UI postinstall inventory", () => { expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); const installedPackageRoot = installPackedPackage(root, tarball); + expect(existsSync(join(installedPackageRoot, CODE_MODE_WORKER_PATH))).toBe(true); for (const relativePath of CONTROL_UI_FILES) { expect(existsSync(join(installedPackageRoot, relativePath))).toBe(true); } diff --git a/test/scripts/check-openclaw-package-tarball.test.ts b/test/scripts/check-openclaw-package-tarball.test.ts index 1a7a0967c1a3..4ad1c5d3a668 100644 --- a/test/scripts/check-openclaw-package-tarball.test.ts +++ b/test/scripts/check-openclaw-package-tarball.test.ts @@ -11,6 +11,7 @@ import { } from "node:fs"; import { tmpdir } from "node:os"; import { delimiter, dirname, join } from "node:path"; +import { gte as semverGte, valid as validSemver } from "semver"; import { describe, expect, it } from "vitest"; import { LOCAL_BUILD_METADATA_DIST_PATHS } from "../../scripts/lib/local-build-metadata-paths.mjs"; import { PACKAGE_INSTALL_GUARD_RELATIVE_PATH } from "../../scripts/lib/package-dist-inventory.ts"; @@ -18,6 +19,8 @@ import { WORKSPACE_TEMPLATE_PACK_PATHS } from "../../scripts/lib/workspace-boots const CHECK_SCRIPT = "scripts/check-openclaw-package-tarball.mjs"; const NODE_DEFAULT_SPAWN_MAX_BUFFER_BYTES = 1024 * 1024; +const CODE_MODE_WORKER_PATH = "dist/agents/code-mode.worker.js"; +const FIRST_CODE_MODE_WORKER_VERSION = "2026.5.14-beta.2"; const FLAT_PLUGIN_SDK_DECLARATION = "dist/plugin-sdk/provider-entry.d.ts"; const DEEP_PLUGIN_SDK_DECLARATION = "dist/plugin-sdk/src/plugin-sdk/provider-entry.d.ts"; const AI_RUNTIME_PACKAGE_JSON = JSON.stringify({ @@ -55,6 +58,8 @@ function withTarball( testBody: (tarball: string) => void, version = "2026.7.2", options: { + includeCodeModeWorker?: boolean; + includeCodeModeWorkerInInventory?: boolean; includeControlUi?: boolean; includeInstallGuard?: boolean; includeShrinkwrap?: boolean; @@ -64,6 +69,15 @@ function withTarball( ) { const root = mkdtempSync(join(tmpdir(), "openclaw-package-tarball-test-")); try { + const validVersion = validSemver(version); + const includeCodeModeWorker = + options.includeCodeModeWorker ?? + (validVersion !== null && semverGte(validVersion, FIRST_CODE_MODE_WORKER_VERSION)); + const includeCodeModeWorkerInInventory = + options.includeCodeModeWorkerInInventory ?? includeCodeModeWorker; + const packageInventory = includeCodeModeWorkerInInventory + ? [...new Set([...inventory, CODE_MODE_WORKER_PATH])] + : inventory; const packageRoot = join(root, "package"); mkdirSync(join(packageRoot, "dist"), { recursive: true }); writeFileSync( @@ -72,7 +86,7 @@ function withTarball( ); writeFileSync( join(packageRoot, "dist", "postinstall-inventory.json"), - JSON.stringify(inventory), + JSON.stringify(packageInventory), ); const workspaceTemplates = options.includeWorkspaceTemplates === false @@ -113,6 +127,7 @@ function withTarball( ...controlUiFiles, ...installGuardFile, ...shrinkwrapFile, + ...(includeCodeModeWorker ? { [CODE_MODE_WORKER_PATH]: "export {};\n" } : {}), ...files, }; for (const [relativePath, body] of Object.entries(tarFiles)) { @@ -349,6 +364,64 @@ describe("check-openclaw-package-tarball", () => { ); }); + it("accepts historical packages published before the Code Mode worker existed", () => { + withTarball( + ["dist/index.js"], + { "dist/index.js": "export {};\n" }, + (tarball) => { + const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); + }, + "2026.5.14-beta.1", + ); + }); + + it("rejects Code Mode packages that omit the dynamically loaded worker", () => { + withTarball( + ["dist/index.js"], + { "dist/index.js": "export {};\n" }, + (tarball) => { + const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(`missing required tar entry ${CODE_MODE_WORKER_PATH}`); + }, + FIRST_CODE_MODE_WORKER_VERSION, + { includeCodeModeWorker: false }, + ); + }); + + it("rejects Code Mode workers that postinstall would remove", () => { + withTarball( + ["dist/index.js"], + { "dist/index.js": "export {};\n" }, + (tarball) => { + const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(`postinstall inventory omits ${CODE_MODE_WORKER_PATH}`); + }, + FIRST_CODE_MODE_WORKER_VERSION, + { includeCodeModeWorkerInInventory: false }, + ); + }); + + it("accepts Code Mode packages whose worker survives postinstall", () => { + withTarball( + ["dist/index.js"], + { "dist/index.js": "export {};\n" }, + (tarball) => { + const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" }); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("OpenClaw package tarball integrity passed."); + }, + FIRST_CODE_MODE_WORKER_VERSION, + ); + }); + it("rejects dist files that import missing relative chunks", () => { withTarball( ["dist/cli/run-main.js"],