refactor: validate provider plugin metadata

This commit is contained in:
Peter Steinberger
2026-03-13 01:15:00 +00:00
parent 87ad1ce9b1
commit c80da4e72f
4 changed files with 414 additions and 13 deletions

View File

@@ -0,0 +1,127 @@
import { describe, expect, it } from "vitest";
import { normalizeRegisteredProvider } from "./provider-validation.js";
import type { PluginDiagnostic, ProviderPlugin } from "./types.js";
function collectDiagnostics() {
const diagnostics: PluginDiagnostic[] = [];
return {
diagnostics,
pushDiagnostic: (diag: PluginDiagnostic) => {
diagnostics.push(diag);
},
};
}
function makeProvider(overrides: Partial<ProviderPlugin>): ProviderPlugin {
return {
id: "demo",
label: "Demo",
auth: [],
...overrides,
};
}
describe("normalizeRegisteredProvider", () => {
it("drops invalid and duplicate auth methods, and clears bad wizard method bindings", () => {
const { diagnostics, pushDiagnostic } = collectDiagnostics();
const provider = normalizeRegisteredProvider({
pluginId: "demo-plugin",
source: "/tmp/demo/index.ts",
provider: makeProvider({
id: " demo ",
label: " Demo Provider ",
aliases: [" alias-one ", "alias-one", ""],
envVars: [" DEMO_API_KEY ", "DEMO_API_KEY"],
auth: [
{
id: " primary ",
label: " Primary ",
kind: "custom",
run: async () => ({ profiles: [] }),
},
{
id: "primary",
label: "Duplicate",
kind: "custom",
run: async () => ({ profiles: [] }),
},
{ id: " ", label: "Missing", kind: "custom", run: async () => ({ profiles: [] }) },
],
wizard: {
onboarding: {
choiceId: " demo-choice ",
methodId: " missing ",
},
modelPicker: {
label: " Demo models ",
methodId: " missing ",
},
},
}),
pushDiagnostic,
});
expect(provider).toMatchObject({
id: "demo",
label: "Demo Provider",
aliases: ["alias-one"],
envVars: ["DEMO_API_KEY"],
auth: [{ id: "primary", label: "Primary" }],
wizard: {
onboarding: {
choiceId: "demo-choice",
},
modelPicker: {
label: "Demo models",
},
},
});
expect(diagnostics.map((diag) => ({ level: diag.level, message: diag.message }))).toEqual([
{
level: "error",
message: 'provider "demo" auth method duplicated id "primary"',
},
{
level: "error",
message: 'provider "demo" auth method missing id',
},
{
level: "warn",
message:
'provider "demo" onboarding method "missing" not found; falling back to available methods',
},
{
level: "warn",
message:
'provider "demo" model-picker method "missing" not found; falling back to available methods',
},
]);
});
it("drops wizard metadata when a provider has no auth methods", () => {
const { diagnostics, pushDiagnostic } = collectDiagnostics();
const provider = normalizeRegisteredProvider({
pluginId: "demo-plugin",
source: "/tmp/demo/index.ts",
provider: makeProvider({
wizard: {
onboarding: {
choiceId: "demo",
},
modelPicker: {
label: "Demo",
},
},
}),
pushDiagnostic,
});
expect(provider?.wizard).toBeUndefined();
expect(diagnostics.map((diag) => diag.message)).toEqual([
'provider "demo" onboarding metadata ignored because it has no auth methods',
'provider "demo" model-picker metadata ignored because it has no auth methods',
]);
});
});

View File

@@ -0,0 +1,232 @@
import type { PluginDiagnostic, ProviderAuthMethod, ProviderPlugin } from "./types.js";
function pushProviderDiagnostic(params: {
level: PluginDiagnostic["level"];
pluginId: string;
source: string;
message: string;
pushDiagnostic: (diag: PluginDiagnostic) => void;
}) {
params.pushDiagnostic({
level: params.level,
pluginId: params.pluginId,
source: params.source,
message: params.message,
});
}
function normalizeText(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function normalizeTextList(values: string[] | undefined): string[] | undefined {
const normalized = Array.from(
new Set((values ?? []).map((value) => value.trim()).filter(Boolean)),
);
return normalized.length > 0 ? normalized : undefined;
}
function normalizeProviderAuthMethods(params: {
providerId: string;
pluginId: string;
source: string;
auth: ProviderAuthMethod[];
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderAuthMethod[] {
const seenMethodIds = new Set<string>();
const normalized: ProviderAuthMethod[] = [];
for (const method of params.auth) {
const methodId = normalizeText(method.id);
if (!methodId) {
pushProviderDiagnostic({
level: "error",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" auth method missing id`,
pushDiagnostic: params.pushDiagnostic,
});
continue;
}
if (seenMethodIds.has(methodId)) {
pushProviderDiagnostic({
level: "error",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" auth method duplicated id "${methodId}"`,
pushDiagnostic: params.pushDiagnostic,
});
continue;
}
seenMethodIds.add(methodId);
normalized.push({
...method,
id: methodId,
label: normalizeText(method.label) ?? methodId,
...(normalizeText(method.hint) ? { hint: normalizeText(method.hint) } : {}),
});
}
return normalized;
}
function normalizeProviderWizard(params: {
providerId: string;
pluginId: string;
source: string;
auth: ProviderAuthMethod[];
wizard: ProviderPlugin["wizard"];
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderPlugin["wizard"] {
if (!params.wizard) {
return undefined;
}
const hasAuthMethods = params.auth.length > 0;
const hasMethod = (methodId: string | undefined) =>
Boolean(methodId && params.auth.some((method) => method.id === methodId));
const normalizeOnboarding = () => {
const onboarding = params.wizard?.onboarding;
if (!onboarding) {
return undefined;
}
if (!hasAuthMethods) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" onboarding metadata ignored because it has no auth methods`,
pushDiagnostic: params.pushDiagnostic,
});
return undefined;
}
const methodId = normalizeText(onboarding.methodId);
if (methodId && !hasMethod(methodId)) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" onboarding method "${methodId}" not found; falling back to available methods`,
pushDiagnostic: params.pushDiagnostic,
});
}
return {
...(normalizeText(onboarding.choiceId)
? { choiceId: normalizeText(onboarding.choiceId) }
: {}),
...(normalizeText(onboarding.choiceLabel)
? { choiceLabel: normalizeText(onboarding.choiceLabel) }
: {}),
...(normalizeText(onboarding.choiceHint)
? { choiceHint: normalizeText(onboarding.choiceHint) }
: {}),
...(normalizeText(onboarding.groupId) ? { groupId: normalizeText(onboarding.groupId) } : {}),
...(normalizeText(onboarding.groupLabel)
? { groupLabel: normalizeText(onboarding.groupLabel) }
: {}),
...(normalizeText(onboarding.groupHint)
? { groupHint: normalizeText(onboarding.groupHint) }
: {}),
...(methodId && hasMethod(methodId) ? { methodId } : {}),
};
};
const normalizeModelPicker = () => {
const modelPicker = params.wizard?.modelPicker;
if (!modelPicker) {
return undefined;
}
if (!hasAuthMethods) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" model-picker metadata ignored because it has no auth methods`,
pushDiagnostic: params.pushDiagnostic,
});
return undefined;
}
const methodId = normalizeText(modelPicker.methodId);
if (methodId && !hasMethod(methodId)) {
pushProviderDiagnostic({
level: "warn",
pluginId: params.pluginId,
source: params.source,
message: `provider "${params.providerId}" model-picker method "${methodId}" not found; falling back to available methods`,
pushDiagnostic: params.pushDiagnostic,
});
}
return {
...(normalizeText(modelPicker.label) ? { label: normalizeText(modelPicker.label) } : {}),
...(normalizeText(modelPicker.hint) ? { hint: normalizeText(modelPicker.hint) } : {}),
...(methodId && hasMethod(methodId) ? { methodId } : {}),
};
};
const onboarding = normalizeOnboarding();
const modelPicker = normalizeModelPicker();
if (!onboarding && !modelPicker) {
return undefined;
}
return {
...(onboarding ? { onboarding } : {}),
...(modelPicker ? { modelPicker } : {}),
};
}
export function normalizeRegisteredProvider(params: {
pluginId: string;
source: string;
provider: ProviderPlugin;
pushDiagnostic: (diag: PluginDiagnostic) => void;
}): ProviderPlugin | null {
const id = normalizeText(params.provider.id);
if (!id) {
pushProviderDiagnostic({
level: "error",
pluginId: params.pluginId,
source: params.source,
message: "provider registration missing id",
pushDiagnostic: params.pushDiagnostic,
});
return null;
}
const auth = normalizeProviderAuthMethods({
providerId: id,
pluginId: params.pluginId,
source: params.source,
auth: params.provider.auth ?? [],
pushDiagnostic: params.pushDiagnostic,
});
const docsPath = normalizeText(params.provider.docsPath);
const aliases = normalizeTextList(params.provider.aliases);
const envVars = normalizeTextList(params.provider.envVars);
const wizard = normalizeProviderWizard({
providerId: id,
pluginId: params.pluginId,
source: params.source,
auth,
wizard: params.provider.wizard,
pushDiagnostic: params.pushDiagnostic,
});
const {
wizard: _ignoredWizard,
docsPath: _ignoredDocsPath,
aliases: _ignoredAliases,
envVars: _ignoredEnvVars,
...restProvider
} = params.provider;
return {
...restProvider,
id,
label: normalizeText(params.provider.label) ?? id,
...(docsPath ? { docsPath } : {}),
...(aliases ? { aliases } : {}),
...(envVars ? { envVars } : {}),
auth,
...(wizard ? { wizard } : {}),
};
}

View File

@@ -13,6 +13,7 @@ import { resolveUserPath } from "../utils.js";
import { registerPluginCommand } from "./commands.js";
import { normalizePluginHttpPath } from "./http-path.js";
import { findOverlappingPluginHttpRoute } from "./http-route-overlap.js";
import { normalizeRegisteredProvider } from "./provider-validation.js";
import type { PluginRuntime } from "./runtime/types.js";
import {
isPluginHookName,
@@ -428,16 +429,16 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
};
const registerProvider = (record: PluginRecord, provider: ProviderPlugin) => {
const id = typeof provider?.id === "string" ? provider.id.trim() : "";
if (!id) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "provider registration missing id",
});
const normalizedProvider = normalizeRegisteredProvider({
pluginId: record.id,
source: record.source,
provider,
pushDiagnostic,
});
if (!normalizedProvider) {
return;
}
const id = normalizedProvider.id;
const existing = registry.providers.find((entry) => entry.provider.id === id);
if (existing) {
pushDiagnostic({
@@ -451,7 +452,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
record.providerIds.push(id);
registry.providers.push({
pluginId: record.id,
provider,
provider: normalizedProvider,
source: record.source,
});
};