fix(plugins): reject blank runtime entries

This commit is contained in:
Vincent Koc
2026-05-04 01:41:03 -07:00
parent 23950b5664
commit a9282f3571
4 changed files with 88 additions and 5 deletions

View File

@@ -61,6 +61,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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.
- Plugins/runtime state: keep the key being registered when namespace eviction runs in the same millisecond as existing entries, so `register` and `registerIfAbsent` do not report success while evicting their own fresh value. Thanks @vincentkoc.
- Control UI/Talk: make failed Talk startup errors dismissable and clear the stale Talk error state when dismissed, so missing realtime voice provider configuration does not leave a permanent chat banner. Fixes #77071. Thanks @ijoshdavis.

View File

@@ -928,6 +928,34 @@ describe("discoverOpenClawPlugins", () => {
).toBe(true);
});
it("rejects blank package runtimeExtensions before falling back to inferred entries", async () => {
const stateDir = makeTempDir();
const pluginDir = path.join(stateDir, "extensions", "runtime-blank-pack");
mkdirSafe(path.join(pluginDir, "src"));
mkdirSafe(path.join(pluginDir, "dist"));
writePluginPackageManifest({
packageDir: pluginDir,
packageName: "@openclaw/runtime-blank-pack",
extensions: ["./src/index.ts"],
runtimeExtensions: [" "],
});
writePluginEntry(path.join(pluginDir, "src", "index.ts"));
writePluginEntry(path.join(pluginDir, "dist", "index.js"));
const result = await discoverWithStateDir(stateDir, {});
expectCandidatePresence(result, { absent: ["runtime-blank-pack"] });
expect(
result.diagnostics.some(
(entry) =>
entry.level === "error" &&
entry.message.includes("openclaw.runtimeExtensions[0]") &&
entry.message.includes("non-empty string"),
),
).toBe(true);
});
it("infers built dist entries for installed TypeScript package plugins", async () => {
const stateDir = makeTempDir();
const pluginDir = path.join(stateDir, "extensions", "built-peer-pack");

View File

@@ -985,6 +985,37 @@ describe("installPluginFromArchive", () => {
}
});
it("rejects package installs when runtimeExtensions contains a blank entry", async () => {
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true });
fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true });
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: "runtime-blank-plugin",
version: "1.0.0",
openclaw: {
extensions: ["./src/index.ts"],
runtimeExtensions: [" "],
},
}),
);
fs.writeFileSync(path.join(pluginDir, "src", "index.ts"), "export {};\n");
fs.writeFileSync(path.join(pluginDir, "dist", "index.js"), "export {};\n");
const result = await installPluginFromDir({
dirPath: pluginDir,
extensionsDir,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.INVALID_OPENCLAW_EXTENSIONS);
expect(result.error).toContain("openclaw.runtimeExtensions[0]");
expect(result.error).toContain("non-empty string");
}
});
it("rejects package installs when runtimeSetupEntry is missing", async () => {
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
fs.mkdirSync(path.join(pluginDir, "src"), { recursive: true });

View File

@@ -21,6 +21,8 @@ type RuntimeExtensionsResolution =
| { ok: true; runtimeExtensions: string[] }
| { ok: false; error: string };
type PackageManifestStringList = { ok: true; entries: string[] } | { ok: false; error: string };
function runtimeExtensionsLengthMismatchMessage(params: {
runtimeExtensionsLength: number;
extensionsLength: number;
@@ -31,11 +33,25 @@ function runtimeExtensionsLengthMismatchMessage(params: {
);
}
function normalizePackageManifestStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
function readPackageManifestStringList(params: {
fieldName: string;
value: unknown;
}): PackageManifestStringList {
if (!Array.isArray(params.value)) {
return { ok: true, entries: [] };
}
return value.map((entry) => normalizeOptionalString(entry) ?? "").filter(Boolean);
const entries: string[] = [];
for (const [index, entry] of params.value.entries()) {
const normalized = normalizeOptionalString(entry);
if (!normalized) {
return {
ok: false,
error: `package.json ${params.fieldName}[${index}] must be a non-empty string`,
};
}
entries.push(normalized);
}
return { ok: true, entries };
}
function resolvePackageRuntimeExtensionEntries(params: {
@@ -43,7 +59,14 @@ function resolvePackageRuntimeExtensionEntries(params: {
extensions: readonly string[];
}): RuntimeExtensionsResolution {
const packageManifest = getPackageManifestMetadata(params.manifest ?? undefined);
const runtimeExtensions = normalizePackageManifestStringList(packageManifest?.runtimeExtensions);
const runtimeExtensionsResult = readPackageManifestStringList({
fieldName: "openclaw.runtimeExtensions",
value: packageManifest?.runtimeExtensions,
});
if (!runtimeExtensionsResult.ok) {
return runtimeExtensionsResult;
}
const runtimeExtensions = runtimeExtensionsResult.entries;
if (runtimeExtensions.length === 0) {
return { ok: true, runtimeExtensions: [] };
}