diff --git a/CHANGELOG.md b/CHANGELOG.md index 821777455c6..6d46b8bafb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/runtime-deps: verify staged package entry files before reusing mirrored runtime roots, so browser-control repairs incomplete `ajv`/MCP SDK installs after update instead of failing after restart on a missing `ajv/dist/ajv.js`. Refs #74630. Thanks @spickeringlr. - Channels/Feishu: retry file-typed iOS video resource downloads as `media` after a Feishu/Lark HTTP 502 and preserve the original 502 when the fallback also fails. Fixes #49855; carries forward #50164 and #73986. Thanks @alex-xuweilong. - Providers/Amazon Bedrock: expose the full Claude Opus 4.7 thinking profile (`xhigh`, `adaptive`, and `max`) for Bedrock model refs, while keeping Opus/Sonnet 4.6 on adaptive-by-default, so `/think` menus and validation match the Anthropic transport behavior. Fixes #74701. Thanks @prasad-yashdeep, @sparkleHazard, @Sanjays2402, and @hclsys. - Plugins/tokenjuice: compile the bundled plugin against tokenjuice 0.7.0's published OpenClaw host types instead of a local compatibility shim, so package contract drift fails in OpenClaw validation before release. Thanks @vincentkoc. diff --git a/src/plugins/bundled-runtime-deps-materialization.ts b/src/plugins/bundled-runtime-deps-materialization.ts index c5bf92c35b0..4ec44a3f08d 100644 --- a/src/plugins/bundled-runtime-deps-materialization.ts +++ b/src/plugins/bundled-runtime-deps-materialization.ts @@ -52,27 +52,49 @@ function sameRuntimeDepSpecs(left: readonly string[], right: readonly string[]): ); } -function readInstalledRuntimeDepVersion(rootDir: string, depName: string): string | null { +function readInstalledRuntimeDepPackage( + rootDir: string, + depName: string, +): { packageDir: string; packageJson: JsonObject } | null { try { - const parsed = JSON.parse( - fs.readFileSync(resolveDependencySentinelAbsolutePath(rootDir, depName), "utf8"), - ) as unknown; + const packageJsonPath = resolveDependencySentinelAbsolutePath(rootDir, depName); + const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return null; } - const version = (parsed as JsonObject).version; - return typeof version === "string" && version.trim() ? version.trim() : null; + return { packageDir: path.dirname(packageJsonPath), packageJson: parsed as JsonObject }; } catch { return null; } } +function hasInstalledRuntimeDepEntryFiles(packageDir: string, packageJson: JsonObject): boolean { + const main = packageJson.main; + if (typeof main !== "string" || main.trim() === "") { + return true; + } + const mainPath = path.resolve(packageDir, main); + if (mainPath !== packageDir && !mainPath.startsWith(`${packageDir}${path.sep}`)) { + return false; + } + return fs.existsSync(mainPath); +} + export function isRuntimeDepSatisfied( rootDir: string, dep: { name: string; version: string }, ): boolean { - const installedVersion = readInstalledRuntimeDepVersion(rootDir, dep.name); - return Boolean(installedVersion && satisfies(installedVersion, dep.version)); + const installed = readInstalledRuntimeDepPackage(rootDir, dep.name); + if (!installed) { + return false; + } + const version = installed.packageJson.version; + return Boolean( + typeof version === "string" && + version.trim() && + satisfies(version.trim(), dep.version) && + hasInstalledRuntimeDepEntryFiles(installed.packageDir, installed.packageJson), + ); } export function isRuntimeDepSatisfiedInAnyRoot( diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index 9344d42dd3c..5985248173f 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -3198,6 +3198,58 @@ describe("ensureBundledPluginRuntimeDeps", () => { expect(installRoot).not.toBe(pluginRoot); }); + it("repairs package-level mirrors when an installed package entry file is missing", () => { + const packageRoot = makeTempDir(); + const stageDir = makeTempDir(); + fs.writeFileSync( + path.join(packageRoot, "package.json"), + JSON.stringify({ + name: "openclaw", + version: "2026.4.27", + dependencies: { ajv: "8.20.0" }, + openclaw: { + bundle: { + mirroredRootRuntimeDependencies: ["ajv"], + }, + }, + }), + ); + const pluginRoot = writeBundledPluginPackage({ + packageRoot, + pluginId: "browser", + deps: {}, + enabledByDefault: true, + }); + const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir }; + const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, { env }); + writeGeneratedRuntimeDepsManifest(installRoot, ["ajv@8.20.0"]); + const ajvRoot = path.join(installRoot, "node_modules", "ajv"); + fs.mkdirSync(ajvRoot, { recursive: true }); + fs.writeFileSync( + path.join(ajvRoot, "package.json"), + JSON.stringify({ name: "ajv", version: "8.20.0", main: "dist/ajv.js" }), + ); + + const calls: BundledRuntimeDepsInstallParams[] = []; + const result = ensureBundledPluginRuntimeDeps({ + env, + pluginId: "browser", + pluginRoot, + installDeps: (params) => { + calls.push(params); + }, + }); + + expect(result.installedSpecs).toEqual(["ajv@8.20.0"]); + expect(calls).toEqual([ + { + installRoot, + missingSpecs: ["ajv@8.20.0"], + installSpecs: ["ajv@8.20.0"], + }, + ]); + }); + it("mirrors sqlite-vec into the packaged default memory runtime deps", () => { const packageRoot = makeTempDir(); fs.writeFileSync(