diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index 217cdb3e3b6..7537bbe2acc 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -1129,6 +1129,17 @@ describe("loadPluginManifestRegistry", () => { id: "openai", authMethods: ["api-key"], envVars: ["OPENAI_API_KEY"], + authEvidence: [ + { + type: "local-file-with-env", + fileEnvVar: "OPENAI_CREDENTIALS_FILE", + fallbackPaths: ["${HOME}/.config/openai/credentials.json"], + requiresAnyEnv: ["OPENAI_PROJECT", "OPENAI_ORG"], + requiresAllEnv: ["OPENAI_REGION"], + credentialMarker: "openai-local-credentials", + source: "openai local credentials", + }, + ], }, ], cliBackends: ["openai-cli"], @@ -1158,6 +1169,17 @@ describe("loadPluginManifestRegistry", () => { id: "openai", authMethods: ["api-key"], envVars: ["OPENAI_API_KEY"], + authEvidence: [ + { + type: "local-file-with-env", + fileEnvVar: "OPENAI_CREDENTIALS_FILE", + fallbackPaths: ["${HOME}/.config/openai/credentials.json"], + requiresAnyEnv: ["OPENAI_PROJECT", "OPENAI_ORG"], + requiresAllEnv: ["OPENAI_REGION"], + credentialMarker: "openai-local-credentials", + source: "openai local credentials", + }, + ], }, ], cliBackends: ["openai-cli"], diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index 8db699ce414..f4822daac79 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -188,6 +188,29 @@ export type PluginManifestSetupProvider = { authMethods?: string[]; /** Environment variables that can satisfy setup without runtime loading. */ envVars?: string[]; + /** + * Cheap local evidence that a provider can authenticate without loading + * runtime code. Evidence checks must not read secrets, shell out, or call + * provider APIs. + */ + authEvidence?: PluginManifestSetupProviderAuthEvidence[]; +}; + +export type PluginManifestSetupProviderAuthEvidence = { + /** Generic local file evidence gated by required environment metadata. */ + type: "local-file-with-env"; + /** Optional env var containing an explicit credential file path. */ + fileEnvVar?: string; + /** Optional fallback credential file paths. Supports `${HOME}` only. */ + fallbackPaths?: string[]; + /** At least one of these env vars must be non-empty when provided. */ + requiresAnyEnv?: string[]; + /** Every env var listed here must be non-empty when provided. */ + requiresAllEnv?: string[]; + /** Non-secret marker returned when this evidence is present. */ + credentialMarker: string; + /** Human-readable auth source label. */ + source?: string; }; export type PluginManifestSetup = { @@ -982,10 +1005,48 @@ function normalizeManifestSetupProviders( } const authMethods = normalizeTrimmedStringList(entry.authMethods); const envVars = normalizeTrimmedStringList(entry.envVars); + const authEvidence = normalizeManifestSetupProviderAuthEvidence(entry.authEvidence); normalized.push({ id, ...(authMethods.length > 0 ? { authMethods } : {}), ...(envVars.length > 0 ? { envVars } : {}), + ...(authEvidence ? { authEvidence } : {}), + }); + } + return normalized.length > 0 ? normalized : undefined; +} + +function normalizeManifestSetupProviderAuthEvidence( + value: unknown, +): PluginManifestSetupProviderAuthEvidence[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + const normalized: PluginManifestSetupProviderAuthEvidence[] = []; + for (const entry of value) { + if (!isRecord(entry) || entry.type !== "local-file-with-env") { + continue; + } + const credentialMarker = normalizeOptionalString(entry.credentialMarker); + if (!credentialMarker) { + continue; + } + const fileEnvVar = normalizeOptionalString(entry.fileEnvVar); + const fallbackPaths = normalizeTrimmedStringList(entry.fallbackPaths); + if (!fileEnvVar && fallbackPaths.length === 0) { + continue; + } + const requiresAnyEnv = normalizeTrimmedStringList(entry.requiresAnyEnv); + const requiresAllEnv = normalizeTrimmedStringList(entry.requiresAllEnv); + const source = normalizeOptionalString(entry.source); + normalized.push({ + type: "local-file-with-env", + ...(fileEnvVar ? { fileEnvVar } : {}), + ...(fallbackPaths.length > 0 ? { fallbackPaths } : {}), + ...(requiresAnyEnv.length > 0 ? { requiresAnyEnv } : {}), + ...(requiresAllEnv.length > 0 ? { requiresAllEnv } : {}), + credentialMarker, + ...(source ? { source } : {}), }); } return normalized.length > 0 ? normalized : undefined;