fix(release): backport worker verifier bounds

Backport the final worker artifact parsing bounds from main onto the frozen 2026.8.1-beta.3 candidate.

(cherry picked from commit 53fb8220ce)
This commit is contained in:
Peter Steinberger
2026-08-20 04:01:47 -07:00
committed by Vincent Koc
parent 9a6b1d63ff
commit a906b1fdcb
2 changed files with 159 additions and 58 deletions
+55 -42
View File
@@ -19,6 +19,10 @@ import { pathToFileURL } from "node:url";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { ALWAYS_ALLOWED_RUNTIME_DIR_NAMES } from "../src/plugin-sdk/facade-activation-contract.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
import {
WORKER_BUNDLE_ENTRY_PATH,
WORKER_BUNDLE_RSYNC_RECEIVER_PATH,
} from "../src/shared/worker-bundle-hash.js";
import { readBoundedResponseText } from "./lib/bounded-response.mjs";
import { listBundledPluginPackArtifacts } from "./lib/bundled-plugin-build-entries.mjs";
import { formatErrorMessage } from "./lib/error-format.mts";
@@ -65,9 +69,15 @@ const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.fi
const NODE_BUILTIN_MODULES = new Set(builtinModules.map((name) => name.replace(/^node:/u, "")));
const MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES = 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 6 * 1024 * 1024;
const MAX_INSTALLED_WORKER_DEPLOY_DIST_JS_BYTES = 80 * 1024 * 1024;
// Keep the dependency scan bounded while allowing headroom for generated root chunks.
const MAX_INSTALLED_ROOT_DIST_JS_FILES = 10_000;
const ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE = /\.(?:c|m)?js$/u;
// The ~69 MB self-contained worker needs extra headroom, but synchronous read/parse stays bounded.
const SELF_CONTAINED_WORKER_DEPLOY_DIST_PATHS = new Set([
`worker/${WORKER_BUNDLE_ENTRY_PATH}`,
`worker/${WORKER_BUNDLE_RSYNC_RECEIVER_PATH}`,
]);
const OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS = new Set([
// Optional A2UI markdown renderer. The Canvas host bundle catches the missing
// package and falls back when the optional renderer is unavailable.
@@ -95,6 +105,10 @@ type DistJavaScriptFileListResult =
| { files: string[]; limitExceeded: false }
| { files: string[]; limit: number; limitExceeded: true };
type InstalledRootDistJavaScriptReadResult =
| { error: string; ok: false }
| { ok: true; relativePath: string; source: string };
type PublishedInstallScenario = {
name: string;
installSpecs: string[];
@@ -464,7 +478,7 @@ export function collectInstalledPackageErrors(params: {
errors.push(...collectInstalledPluginSdkDeclarationErrors(params.packageRoot));
errors.push(...collectInstalledRootDependencyManifestErrors(params.packageRoot));
return errors;
return [...new Set(errors)];
}
export function collectInstalledAlwaysAllowedRuntimeFacadeErrors(packageRoot: string): string[] {
@@ -524,13 +538,7 @@ export function normalizeInstalledBinaryVersion(output: string): string {
return versionMatch?.[0] ?? trimmed;
}
function listDistJavaScriptFiles(
packageRoot: string,
opts: {
maxFiles?: number;
skipRelativePath?: (relativePath: string) => boolean;
} = {},
): DistJavaScriptFileListResult {
function listInstalledRootDistJavaScriptFiles(packageRoot: string): DistJavaScriptFileListResult {
const distDir = join(packageRoot, "dist");
if (!existsSync(distDir)) {
return { files: [], limitExceeded: false };
@@ -553,7 +561,7 @@ function listDistJavaScriptFiles(
const entryPath = join(currentDir, entry.name);
const relativePath = relative(distDir, entryPath).replaceAll("\\", "/");
if (opts.skipRelativePath?.(relativePath)) {
if (relativePath === "extensions" || relativePath.startsWith("extensions/")) {
continue;
}
if (entry.isDirectory()) {
@@ -562,10 +570,10 @@ function listDistJavaScriptFiles(
}
if (entry.isFile() && ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE.test(entry.name)) {
files.push(entryPath);
if (opts.maxFiles !== undefined && files.length > opts.maxFiles) {
if (files.length > MAX_INSTALLED_ROOT_DIST_JS_FILES) {
return {
files,
limit: opts.maxFiles,
limit: MAX_INSTALLED_ROOT_DIST_JS_FILES,
limitExceeded: true,
};
}
@@ -583,25 +591,43 @@ function formatInstalledDistFileScanLimitError(scope: string, limit: number): st
return `installed package ${scope} contains more than ${limit} JavaScript files; refusing to scan unbounded package contents.`;
}
function readInstalledRootDistJavaScriptFile(
packageRoot: string,
filePath: string,
): InstalledRootDistJavaScriptReadResult {
const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
const maxBytes = SELF_CONTAINED_WORKER_DEPLOY_DIST_PATHS.has(relativePath)
? MAX_INSTALLED_WORKER_DEPLOY_DIST_JS_BYTES
: MAX_INSTALLED_ROOT_DIST_JS_BYTES;
const fileStat = lstatSync(filePath);
if (!fileStat.isFile() || fileStat.size > maxBytes) {
return {
error: `installed package root dist file '${relativePath}' is invalid or exceeds ${maxBytes} bytes.`,
ok: false,
};
}
return { ok: true, relativePath, source: readFileSync(filePath, "utf8") };
}
export function collectInstalledContextEngineRuntimeErrors(packageRoot: string): string[] {
const errors: string[] = [];
const distFiles = listDistJavaScriptFiles(packageRoot, {
maxFiles: MAX_INSTALLED_ROOT_DIST_JS_FILES,
});
const distFiles = listInstalledRootDistJavaScriptFiles(packageRoot);
if (distFiles.limitExceeded) {
return [formatInstalledDistFileScanLimitError("dist", distFiles.limit)];
return [formatInstalledDistFileScanLimitError("root dist", distFiles.limit)];
}
// The legacy marker is a root runtime bundling contract; extension assets are plugin-owned.
for (const filePath of distFiles.files) {
const contents = readFileSync(filePath, "utf8");
if (contents.includes(LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER)) {
errors.push(
const file = readInstalledRootDistJavaScriptFile(packageRoot, filePath);
if (!file.ok) {
return [file.error];
}
if (file.source.includes(LEGACY_CONTEXT_ENGINE_UNRESOLVED_RUNTIME_MARKER)) {
return [
"installed package includes unresolved legacy context engine runtime loader; rebuild with a bundler-traceable LegacyContextEngine import.",
);
break;
];
}
}
return errors;
return [];
}
function collectInstalledPluginSdkDeclarationErrors(packageRoot: string): string[] {
@@ -632,14 +658,6 @@ function collectInstalledPluginSdkDeclarationErrors(packageRoot: string): string
return errors;
}
function listInstalledRootDistJavaScriptFiles(packageRoot: string): DistJavaScriptFileListResult {
return listDistJavaScriptFiles(packageRoot, {
maxFiles: MAX_INSTALLED_ROOT_DIST_JS_FILES,
skipRelativePath: (relativePath) =>
relativePath === "extensions" || relativePath.startsWith("extensions/"),
});
}
type ParsedImportSpecifiersResult =
| { ok: true; specifiers: Set<string> }
| { ok: false; error: string };
@@ -747,19 +765,14 @@ export function collectInstalledRootDependencyManifestErrors(packageRoot: string
collectBundledExtensionRuntimeDependencyOwners(packageRoot);
for (const filePath of distFiles.files) {
const fileStat = lstatSync(filePath);
if (!fileStat.isFile() || fileStat.size > MAX_INSTALLED_ROOT_DIST_JS_BYTES) {
const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
return [
`installed package root dist file '${relativePath}' is invalid or exceeds ${MAX_INSTALLED_ROOT_DIST_JS_BYTES} bytes.`,
];
const file = readInstalledRootDistJavaScriptFile(packageRoot, filePath);
if (!file.ok) {
return [file.error];
}
const source = readFileSync(filePath, "utf8");
const relativePath = relative(join(packageRoot, "dist"), filePath).replaceAll("\\", "/");
const parsedSpecifiers = extractJavaScriptImportSpecifiers(source);
const parsedSpecifiers = extractJavaScriptImportSpecifiers(file.source);
if (!parsedSpecifiers.ok) {
return [
`installed package root dist file '${relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
`installed package root dist file '${file.relativePath}' could not be parsed for runtime dependency verification: ${parsedSpecifiers.error}.`,
];
}
for (const specifier of parsedSpecifiers.specifiers) {
@@ -772,13 +785,13 @@ export function collectInstalledRootDependencyManifestErrors(packageRoot: string
isBundledExtensionOwnedRuntimeImport({
dependencyName,
ownersByDependency: bundledExtensionRuntimeDependencyOwners,
source,
source: file.source,
})
) {
continue;
}
const importers = missingImporters.get(dependencyName) ?? new Set<string>();
importers.add(relativePath);
importers.add(file.relativePath);
missingImporters.set(dependencyName, importers);
}
}
+104 -16
View File
@@ -1,7 +1,7 @@
import { spawnSync } from "node:child_process";
import { generateKeyPairSync, sign } from "node:crypto";
// OpenClaw npm postpublish tests validate postpublish verification behavior.
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, truncateSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { describe, expect, it, vi } from "vitest";
@@ -25,6 +25,10 @@ import {
verifyNpmProvenanceAttestation,
verifyNpmRegistrySignatures,
} from "../scripts/openclaw-npm-postpublish-verify.ts";
import {
WORKER_BUNDLE_ENTRY_PATH,
WORKER_BUNDLE_RSYNC_RECEIVER_PATH,
} from "../src/shared/worker-bundle-hash.js";
import { withEnv } from "../src/test-utils/env.js";
const INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT = 10_000;
@@ -596,6 +600,32 @@ describe("collectInstalledPackageErrors", () => {
);
});
it("rejects an oversized worker before the full verifier reads its contents", () => {
const packageRoot = makeInstalledPackageRoot();
try {
writeFileSync(join(packageRoot, "package.json"), '{"version":"2026.3.23"}\n', "utf8");
const workerPath = join(packageRoot, "dist", "worker", WORKER_BUNDLE_ENTRY_PATH);
mkdirSync(dirname(workerPath), { recursive: true });
writeFileSync(workerPath, "/* Failed to load legacy context engine runtime. */\n", "utf8");
truncateSync(workerPath, 80 * 1024 * 1024 + 1);
const errors = collectInstalledPackageErrors({
expectedVersion: "2026.3.23",
installedVersion: "2026.3.23",
packageRoot,
});
const sizeError = `installed package root dist file 'worker/${WORKER_BUNDLE_ENTRY_PATH}' is invalid or exceeds 83886080 bytes.`;
expect(errors.filter((error) => error === sizeError)).toEqual([sizeError]);
expect(errors).not.toContain(
"installed package includes unresolved legacy context engine runtime loader; rebuild with a bundler-traceable LegacyContextEngine import.",
);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it.each(["ollama", "lmstudio"])(
"rejects a missing installed bundled %s provider directory",
(providerId) => {
@@ -889,6 +919,31 @@ describe("collectInstalledContextEngineRuntimeErrors", () => {
}
});
it("ignores extension-owned JavaScript assets", () => {
const packageRoot = makeInstalledPackageRoot();
try {
const viewerPath = join(
packageRoot,
"dist",
"extensions",
"diffs",
"assets",
"viewer-runtime.js",
);
mkdirSync(dirname(viewerPath), { recursive: true });
writeFileSync(
viewerPath,
'throw new Error("Failed to load legacy context engine runtime.");\n',
"utf8",
);
expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toStrictEqual([]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}
});
it("refuses unbounded packaged dist scans", () => {
const packageRoot = makeInstalledPackageRoot();
@@ -896,7 +951,7 @@ describe("collectInstalledContextEngineRuntimeErrors", () => {
writeDistJavaScriptFiles(packageRoot, INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT + 1);
expect(collectInstalledContextEngineRuntimeErrors(packageRoot)).toEqual([
`installed package dist contains more than ${INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT} JavaScript files; refusing to scan unbounded package contents.`,
`installed package root dist contains more than ${INSTALLED_ROOT_DIST_JS_FILE_SCAN_LIMIT} JavaScript files; refusing to scan unbounded package contents.`,
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
@@ -1059,7 +1114,7 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
}
});
it("flags undeclared imports from mjs and cjs root dist files", () => {
it("flags undeclared imports from nested mjs and direct cjs root dist files", () => {
const packageRoot = makeInstalledPackageRoot();
try {
@@ -1067,9 +1122,9 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
mkdirSync(join(packageRoot, "dist", "runtime"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "esm-entry.mjs"),
join(packageRoot, "dist", "runtime", "esm-entry.mjs"),
'export { value } from "mjs-only";\n',
"utf8",
);
@@ -1081,7 +1136,7 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root is missing declared runtime dependency 'cjs-only' for dist importers: cjs-entry.cjs. Add it to package.json dependencies/optionalDependencies.",
"installed package root is missing declared runtime dependency 'mjs-only' for dist importers: esm-entry.mjs. Add it to package.json dependencies/optionalDependencies.",
"installed package root is missing declared runtime dependency 'mjs-only' for dist importers: runtime/esm-entry.mjs. Add it to package.json dependencies/optionalDependencies.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
@@ -1155,7 +1210,40 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
}
});
it("refuses oversized root dist files", () => {
it.each([
{
expected: [
"installed package root dist file 'oversized.js' is invalid or exceeds 6291456 bytes.",
],
name: "rejects oversized direct dist files",
relativePath: "oversized.js",
},
{
expected: [
"installed package root dist file 'runtime/oversized.js' is invalid or exceeds 6291456 bytes.",
],
name: "rejects oversized arbitrary nested dist files",
relativePath: "runtime/oversized.js",
},
{
expected: [],
name: "accepts the oversized worker deploy entrypoint",
relativePath: `worker/${WORKER_BUNDLE_ENTRY_PATH}`,
},
{
expected: [],
name: "accepts the oversized worker rsync receiver",
relativePath: `worker/${WORKER_BUNDLE_RSYNC_RECEIVER_PATH}`,
},
{
expected: [
`installed package root dist file 'worker/${WORKER_BUNDLE_ENTRY_PATH}' is invalid or exceeds 83886080 bytes.`,
],
name: "rejects the worker deploy entrypoint above its dedicated parser bound",
relativePath: `worker/${WORKER_BUNDLE_ENTRY_PATH}`,
sparseSize: 80 * 1024 * 1024 + 1,
},
])("$name", ({ expected, relativePath, sparseSize }) => {
const packageRoot = makeInstalledPackageRoot();
try {
@@ -1163,16 +1251,16 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
version: "2026.4.22",
dependencies: {},
});
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "oversized.js"),
"x".repeat(6 * 1024 * 1024 + 1),
"utf8",
);
const filePath = join(packageRoot, "dist", relativePath);
mkdirSync(dirname(filePath), { recursive: true });
if (sparseSize) {
writeFileSync(filePath, "/*", "utf8");
truncateSync(filePath, sparseSize);
} else {
writeFileSync(filePath, `/* ${"x".repeat(6 * 1024 * 1024)} */\n`, "utf8");
}
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root dist file 'oversized.js' is invalid or exceeds 6291456 bytes.",
]);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual(expected);
} finally {
rmSync(packageRoot, { recursive: true, force: true });
}