fix(plugins): guard config contract metadata

This commit is contained in:
Vincent Koc
2026-06-03 17:40:36 +02:00
parent ec47d1cdd5
commit c49ffbc034
2 changed files with 128 additions and 8 deletions
+91
View File
@@ -265,6 +265,97 @@ describe("resolvePluginConfigContractsById", () => {
);
});
it("keeps healthy config contract records after unreadable registry plugin metadata", () => {
const unreadableRecord = {
get id() {
throw new Error("config contract plugin id getter exploded");
},
origin: "config",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.broken.config"],
},
} as never;
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue(
createRegistry([
unreadableRecord,
createPluginRecord({
id: "healthy",
origin: "config",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.healthy.config"],
},
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["healthy"],
fallbackToBundledMetadata: false,
}),
).toEqual(
new Map([
[
"healthy",
{
origin: "config",
configContracts: {
compatibilityMigrationPaths: ["plugins.entries.healthy.config"],
},
},
],
]),
);
});
it("keeps bundled config contract fallback after unreadable bundled plugin metadata", () => {
const unreadableRecord = {
get id() {
throw new Error("bundled config contract plugin id getter exploded");
},
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "broken.secret", expected: "string" }],
},
},
} as never;
mocks.loadBundledManifestRegistry.mockReturnValue(
createRegistry([
unreadableRecord,
createPluginRecord({
id: "healthy-bundled",
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "healthy.secret", expected: "string" }],
},
},
}),
]),
);
expect(
resolvePluginConfigContractsById({
pluginIds: ["healthy-bundled"],
}),
).toEqual(
new Map([
[
"healthy-bundled",
{
origin: "bundled",
configContracts: {
secretInputs: {
paths: [{ path: "healthy.secret", expected: "string" }],
},
},
},
],
]),
);
});
it("can skip bundled metadata fallback for registry-scoped callers", () => {
expect(
resolvePluginConfigContractsById({
+37 -8
View File
@@ -1,7 +1,7 @@
import { normalizeSortedUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { discoverOpenClawPlugins, type PluginDiscoveryResult } from "./discovery.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
import type { PluginManifestConfigContracts } from "./manifest.js";
import type { PluginOrigin } from "./plugin-origin.types.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
@@ -15,6 +15,30 @@ export type PluginConfigContractMetadata = {
configContracts: PluginManifestConfigContracts;
};
type PluginConfigContractRecordRead =
| {
ok: true;
id: string;
origin: PluginOrigin;
configContracts?: PluginManifestConfigContracts;
}
| { ok: false };
function readPluginConfigContractRecord(
plugin: PluginManifestRegistry["plugins"][number],
): PluginConfigContractRecordRead {
try {
return {
ok: true,
id: plugin.id,
origin: plugin.origin,
configContracts: plugin.configContracts,
};
} catch {
return { ok: false };
}
}
export function resolvePluginConfigContractsById(params: {
config?: OpenClawConfig;
workspaceDir?: string;
@@ -54,7 +78,11 @@ export function resolvePluginConfigContractsById(params: {
diagnostics: discovery.diagnostics,
});
for (const plugin of registry.plugins) {
bundledContractFallbacks.set(plugin.id, plugin.configContracts);
const record = readPluginConfigContractRecord(plugin);
if (!record.ok) {
continue;
}
bundledContractFallbacks.set(record.id, record.configContracts);
}
if (!bundledContractFallbacks.has(pluginId)) {
bundledContractFallbacks.set(pluginId, undefined);
@@ -70,16 +98,17 @@ export function resolvePluginConfigContractsById(params: {
includeDisabled: true,
});
for (const plugin of registry.plugins) {
if (!pluginIds.includes(plugin.id)) {
const record = readPluginConfigContractRecord(plugin);
if (!record.ok || !pluginIds.includes(record.id)) {
continue;
}
resolvedPluginOrigins.set(plugin.id, plugin.origin);
if (!plugin.configContracts) {
resolvedPluginOrigins.set(record.id, record.origin);
if (!record.configContracts) {
continue;
}
matches.set(plugin.id, {
origin: plugin.origin,
configContracts: plugin.configContracts,
matches.set(record.id, {
origin: record.origin,
configContracts: record.configContracts,
});
}