mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
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:
committed by
Vincent Koc
parent
9a6b1d63ff
commit
a906b1fdcb
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user