fix(plugins): reject invalid inferred package runtimes

This commit is contained in:
Vincent Koc
2026-05-04 01:55:50 -07:00
parent 7482754aca
commit 88b21427f8
3 changed files with 45 additions and 7 deletions

View File

@@ -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.

View File

@@ -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({

View File

@@ -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 (