fix(plugins): guard model suppression metadata

This commit is contained in:
Vincent Koc
2026-06-03 18:55:13 +02:00
parent 286e5ffe07
commit 3f3d5dd489
2 changed files with 128 additions and 7 deletions
@@ -23,6 +23,38 @@ function createMetadataSnapshot(plugins: Record<string, unknown>[]) {
};
}
function createRawMetadataSnapshot(plugins: Record<string, unknown>[]) {
return {
index: { plugins: [] },
diagnostics: [],
plugins,
};
}
function poisonedAvailabilityPlugin(): Record<string, unknown> {
return Object.defineProperty({ id: "poisoned-availability" }, "origin", {
get() {
throw new Error("model suppression availability exploded");
},
});
}
function poisonedModelCatalogPlugin(): Record<string, unknown> {
return Object.defineProperty(
{
id: "poisoned-model-catalog",
origin: "bundled",
providers: ["poisoned-model-catalog"],
},
"modelCatalog",
{
get() {
throw new Error("model suppression catalog exploded");
},
},
);
}
describe("manifest model suppression", () => {
beforeEach(() => {
mocks.loadPluginMetadataSnapshot.mockReset();
@@ -106,6 +138,63 @@ describe("manifest model suppression", () => {
).toBeUndefined();
});
it("skips unreadable manifest model suppression plugin metadata", () => {
mocks.loadPluginMetadataSnapshot.mockReturnValue(
createRawMetadataSnapshot([
{
id: "openai",
origin: "bundled",
providers: ["openai"],
modelCatalog: {
aliases: {
"azure-openai-responses": {
provider: "openai",
},
},
suppressions: [
{
provider: "azure-openai-responses",
model: "gpt-5.3-codex-spark",
reason: "Use openai/gpt-5.5.",
},
],
},
},
poisonedAvailabilityPlugin(),
poisonedModelCatalogPlugin(),
{
id: "qwen",
origin: "bundled",
providers: ["qwen"],
modelCatalog: {
suppressions: [
{
provider: "qwen",
model: "qwen3.6-plus",
reason: "Use qwen/qwen3.5-plus.",
},
],
},
},
]),
);
expect(
resolveManifestBuiltInModelSuppression({
provider: "azure-openai-responses",
id: "gpt-5.3-codex-spark",
env: process.env,
})?.suppress,
).toBe(true);
expect(
resolveManifestBuiltInModelSuppression({
provider: "qwen",
id: "qwen3.6-plus",
env: process.env,
})?.suppress,
).toBe(true);
});
it("reads planned manifest suppressions fresh per lookup", () => {
const config = { plugins: { entries: { openai: { enabled: true } } } };
+39 -7
View File
@@ -10,6 +10,36 @@ import {
loadManifestMetadataSnapshot,
} from "./manifest-contract-eligibility.js";
type ManifestModelCatalogSuppressionPlugin = Parameters<
typeof planManifestModelCatalogSuppressions
>[0]["registry"]["plugins"][number];
type ManifestMetadataSnapshot = ReturnType<typeof loadManifestMetadataSnapshot>;
function readAvailableManifestModelCatalogSuppressionPlugin(params: {
snapshot: ManifestMetadataSnapshot;
plugin: ManifestMetadataSnapshot["plugins"][number];
config?: OpenClawConfig;
}): ManifestModelCatalogSuppressionPlugin | undefined {
try {
if (
!isManifestPluginAvailableForControlPlane({
snapshot: params.snapshot,
plugin: params.plugin,
config: params.config,
})
) {
return undefined;
}
return {
id: params.plugin.id,
providers: params.plugin.providers,
modelCatalog: params.plugin.modelCatalog,
};
} catch {
return undefined;
}
}
function listManifestModelCatalogSuppressions(params: {
config?: OpenClawConfig;
workspaceDir?: string;
@@ -20,15 +50,17 @@ function listManifestModelCatalogSuppressions(params: {
workspaceDir: params.workspaceDir,
env: params.env,
});
const plugins = snapshot.plugins.flatMap((plugin) => {
const readable = readAvailableManifestModelCatalogSuppressionPlugin({
snapshot,
plugin,
config: params.config,
});
return readable ? [readable] : [];
});
const registry = {
diagnostics: snapshot.diagnostics,
plugins: snapshot.plugins.filter((plugin) =>
isManifestPluginAvailableForControlPlane({
snapshot,
plugin,
config: params.config,
}),
),
plugins,
};
const planned = planManifestModelCatalogSuppressions({ registry });
return planned.suppressions;