diff --git a/src/cli/command-secret-targets.import.test.ts b/src/cli/command-secret-targets.import.test.ts index 168100aa9f3..921ca1ed12a 100644 --- a/src/cli/command-secret-targets.import.test.ts +++ b/src/cli/command-secret-targets.import.test.ts @@ -20,14 +20,34 @@ describe("command secret targets module import", () => { expect(listSecretTargetRegistryEntries).not.toHaveBeenCalled(); expect(mod.getModelsCommandSecretTargetIds().has("models.providers.*.apiKey")).toBe(true); expect(mod.getQrRemoteCommandSecretTargetIds().has("gateway.remote.token")).toBe(true); - expect( - mod.getAgentRuntimeCommandSecretTargetIds().has("agents.defaults.memorySearch.remote.apiKey"), - ).toBe(true); expect(listSecretTargetRegistryEntries).not.toHaveBeenCalled(); expect(() => mod.getChannelsCommandSecretTargetIds()).toThrow("registry touched too early"); expect(listSecretTargetRegistryEntries).toHaveBeenCalledTimes(1); }); + it("loads registry lazily for agent runtime plugin credential targets", async () => { + const listSecretTargetRegistryEntries = vi.fn(() => [ + { id: "plugins.entries.example.config.webSearch.apiKey" }, + { id: "plugins.entries.example.config.other.apiKey" }, + { id: "channels.telegram.botToken" }, + ]); + + vi.doMock("../secrets/target-registry.js", () => ({ + discoverConfigSecretTargetsByIds: vi.fn(() => []), + listSecretTargetRegistryEntries, + })); + + const mod = await import("./command-secret-targets.js"); + + expect(listSecretTargetRegistryEntries).not.toHaveBeenCalled(); + const ids = mod.getAgentRuntimeCommandSecretTargetIds(); + expect(ids.has("agents.defaults.memorySearch.remote.apiKey")).toBe(true); + expect(ids.has("plugins.entries.example.config.webSearch.apiKey")).toBe(true); + expect(ids.has("plugins.entries.example.config.other.apiKey")).toBe(false); + expect(ids.has("channels.telegram.botToken")).toBe(false); + expect(listSecretTargetRegistryEntries).toHaveBeenCalledTimes(1); + }); + it("can resolve configured-channel status targets without the full registry", async () => { const listSecretTargetRegistryEntries = vi.fn(() => { throw new Error("registry touched too early"); diff --git a/src/cli/command-secret-targets.ts b/src/cli/command-secret-targets.ts index 758a09c367e..45a22ab0dd2 100644 --- a/src/cli/command-secret-targets.ts +++ b/src/cli/command-secret-targets.ts @@ -30,16 +30,6 @@ const STATIC_AGENT_RUNTIME_BASE_TARGET_IDS = [ "messages.tts.providers.*.apiKey", "skills.entries.*.apiKey", "tools.web.search.apiKey", - "plugins.entries.brave.config.webSearch.apiKey", - "plugins.entries.google.config.webSearch.apiKey", - "plugins.entries.exa.config.webSearch.apiKey", - "plugins.entries.xai.config.webSearch.apiKey", - "plugins.entries.moonshot.config.webSearch.apiKey", - "plugins.entries.perplexity.config.webSearch.apiKey", - "plugins.entries.firecrawl.config.webSearch.apiKey", - "plugins.entries.firecrawl.config.webFetch.apiKey", - "plugins.entries.tavily.config.webSearch.apiKey", - "plugins.entries.minimax.config.webSearch.apiKey", ] as const; const STATIC_STATUS_TARGET_IDS = [ "agents.defaults.memorySearch.remote.apiKey", @@ -67,6 +57,7 @@ type CommandSecretTargets = { }; let cachedCommandSecretTargets: CommandSecretTargets | undefined; +let cachedAgentRuntimeBaseTargetIds: string[] | undefined; let cachedChannelSecretTargetIds: string[] | undefined; function getChannelSecretTargetIds(): string[] { @@ -74,6 +65,26 @@ function getChannelSecretTargetIds(): string[] { return cachedChannelSecretTargetIds; } +function isPluginWebCredentialTargetId(id: string): boolean { + const segments = id.split("."); + if (segments[0] !== "plugins" || segments[1] !== "entries" || segments[3] !== "config") { + return false; + } + const configPath = segments.slice(4).join("."); + return configPath === "webSearch.apiKey" || configPath === "webFetch.apiKey"; +} + +function getAgentRuntimeBaseTargetIds(): string[] { + cachedAgentRuntimeBaseTargetIds ??= [ + ...STATIC_AGENT_RUNTIME_BASE_TARGET_IDS, + ...listSecretTargetRegistryEntries() + .map((entry) => entry.id) + .filter(isPluginWebCredentialTargetId) + .toSorted(), + ]; + return cachedAgentRuntimeBaseTargetIds; +} + function isScopedChannelSecretTargetEntry(params: { entry: { id: string; @@ -120,7 +131,7 @@ function buildCommandSecretTargets(): CommandSecretTargets { const channelTargetIds = getChannelSecretTargetIds(); return { channels: channelTargetIds, - agentRuntime: [...STATIC_AGENT_RUNTIME_BASE_TARGET_IDS, ...channelTargetIds], + agentRuntime: [...getAgentRuntimeBaseTargetIds(), ...channelTargetIds], status: [...STATIC_STATUS_TARGET_IDS, ...channelTargetIds], securityAudit: [...STATIC_SECURITY_AUDIT_TARGET_IDS, ...channelTargetIds], }; @@ -213,7 +224,7 @@ export function getAgentRuntimeCommandSecretTargetIds(params?: { includeChannelTargets?: boolean; }): Set { if (params?.includeChannelTargets !== true) { - return toTargetIdSet(STATIC_AGENT_RUNTIME_BASE_TARGET_IDS); + return toTargetIdSet(getAgentRuntimeBaseTargetIds()); } return toTargetIdSet(getCommandSecretTargets().agentRuntime); } diff --git a/src/secrets/channel-contract-surface-guardrails.test.ts b/src/secrets/channel-contract-surface-guardrails.test.ts index 7ca4d9a691e..d145112d587 100644 --- a/src/secrets/channel-contract-surface-guardrails.test.ts +++ b/src/secrets/channel-contract-surface-guardrails.test.ts @@ -26,6 +26,12 @@ const CORE_SECRET_SURFACE_GUARDS = [ /plugins\.entries\.(?:brave|google|exa|xai|moonshot|perplexity|firecrawl|tavily|minimax)\.config\.web(?:Search|Fetch)\.apiKey/, ], }, + { + path: "src/cli/command-secret-targets.ts", + forbiddenPatterns: [ + /plugins\.entries\.(?:brave|google|exa|xai|moonshot|perplexity|firecrawl|tavily|minimax)\.config\.web(?:Search|Fetch)\.apiKey/, + ], + }, { path: "src/config/markdown-tables.ts", forbiddenPatterns: [/["']signal["']/, /["']whatsapp["']/, /["']mattermost["']/],