From 88b21427f8ad8657a86e68209215a729af0f538a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 4 May 2026 01:55:50 -0700 Subject: [PATCH] fix(plugins): reject invalid inferred package runtimes --- CHANGELOG.md | 1 + src/plugins/discovery.test.ts | 33 +++++++++++++++++++++++++ src/plugins/package-entry-resolution.ts | 18 ++++++++------ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 613ce4bc301..4126f1be6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/packages: reject inferred built runtime entries that exist but fail package-boundary checks instead of falling back to TypeScript source for installed packages. Thanks @vincentkoc. - Plugins/loader: do not retry native-loaded JavaScript plugin modules through the source transformer after native evaluation has already reached a missing dependency, avoiding duplicate top-level side effects. Thanks @vincentkoc. - Plugins/packages: reject blank `openclaw.runtimeExtensions` entries instead of silently ignoring them and falling back to inferred TypeScript runtime entries. Thanks @vincentkoc. - Doctor/plugins: remove stale managed npm plugin shadow entries from the managed package lock as well as `package.json` and `node_modules`, so future npm operations do not keep referencing repaired bundled-plugin shadows. Thanks @vincentkoc. diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index 7536b0d8c31..293dd15a6ef 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -1505,6 +1505,39 @@ describe("discoverOpenClawPlugins", () => { return true; }, }, + { + name: "rejects hardlinked inferred built runtime entries instead of falling back to source", + expectedDiagnostic: "escapes" as const, + expectedId: "pack", + setup: (stateDir: string) => { + if (process.platform === "win32") { + return false; + } + const globalExt = path.join(stateDir, "extensions", "pack"); + const outsideDir = path.join(stateDir, "outside"); + const outsideFile = path.join(outsideDir, "index.js"); + const linkedFile = path.join(globalExt, "dist", "index.js"); + mkdirSafe(path.join(globalExt, "src")); + mkdirSafe(path.dirname(linkedFile)); + mkdirSafe(outsideDir); + fs.writeFileSync(path.join(globalExt, "src", "index.ts"), "export default {}", "utf-8"); + fs.writeFileSync(outsideFile, "export default {}", "utf-8"); + try { + fs.linkSync(outsideFile, linkedFile); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "EXDEV") { + return false; + } + throw err; + } + writePluginPackageManifest({ + packageDir: globalExt, + packageName: "@openclaw/pack", + extensions: ["./src/index.ts"], + }); + return true; + }, + }, ] as const)("$name", async ({ setup, expectedDiagnostic, expectedId }) => { const stateDir = makeTempDir(); await expectRejectedPackageExtensionEntry({ diff --git a/src/plugins/package-entry-resolution.ts b/src/plugins/package-entry-resolution.ts index b92f55ea393..1d9160c5f91 100644 --- a/src/plugins/package-entry-resolution.ts +++ b/src/plugins/package-entry-resolution.ts @@ -434,19 +434,20 @@ function resolveSafePackageEntry(params: { return { relativePath: path.relative(params.packageDir, absolutePath).replace(/\\/g, "/") }; } -function resolveExistingPackageEntrySource(params: { +function resolveOptionalExistingPackageEntrySource(params: { packageDir: string; packageRootRealPath?: string; entryPath: string; sourceLabel: string; diagnostics: PluginDiagnostic[]; rejectHardlinks?: boolean; -}): string | null { +}): { status: "missing" } | { status: "invalid" } | { status: "resolved"; source: string } { const source = path.resolve(params.packageDir, params.entryPath); if (!fs.existsSync(source)) { - return null; + return { status: "missing" }; } - return resolvePackageEntrySource(params); + const resolved = resolvePackageEntrySource(params); + return resolved ? { status: "resolved", source: resolved } : { status: "invalid" }; } function resolvePackageRuntimeEntrySource(params: { @@ -499,7 +500,7 @@ function resolvePackageRuntimeEntrySource(params: { if (shouldInferBuiltRuntimeEntry(params.origin)) { const builtEntryCandidates = listBuiltRuntimeEntryCandidates(safeEntry.relativePath); for (const candidate of builtEntryCandidates) { - const runtimeSource = resolveExistingPackageEntrySource({ + const runtimeSource = resolveOptionalExistingPackageEntrySource({ packageDir: params.packageDir, ...(params.packageRootRealPath !== undefined ? { packageRootRealPath: params.packageRootRealPath } @@ -509,8 +510,11 @@ function resolvePackageRuntimeEntrySource(params: { diagnostics: params.diagnostics, rejectHardlinks: params.rejectHardlinks, }); - if (runtimeSource) { - return runtimeSource; + if (runtimeSource.status === "resolved") { + return runtimeSource.source; + } + if (runtimeSource.status === "invalid") { + return null; } } if (