diff --git a/src/cli/capability-cli.test.ts b/src/cli/capability-cli.test.ts index 24cfbb27ad3..1f1e210aa4d 100644 --- a/src/cli/capability-cli.test.ts +++ b/src/cli/capability-cli.test.ts @@ -97,6 +97,13 @@ const mocks = vi.hoisted(() => ({ : {}), }), ), + resolveCommandSecretRefsViaGateway: vi.fn(async ({ config }: { config: unknown }) => ({ + resolvedConfig: config, + diagnostics: [], + targetStatesByPath: {}, + hadUnresolvedTargets: false, + })), + getTtsCommandSecretTargetIds: vi.fn(() => new Set(["messages.tts.providers.*.apiKey"])), createEmbeddingProvider: vi.fn(async () => ({ provider: { id: "openai", @@ -188,6 +195,14 @@ vi.mock("../gateway/connection-details.js", () => ({ })), })); +vi.mock("./command-secret-gateway.js", () => ({ + resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway, +})); + +vi.mock("./command-secret-targets.js", () => ({ + getTtsCommandSecretTargetIds: mocks.getTtsCommandSecretTargetIds, +})); + vi.mock("../media-understanding/runtime.js", () => ({ describeImageFile: mocks.describeImageFile as typeof import("../media-understanding/runtime.js").describeImageFile, @@ -311,6 +326,15 @@ describe("capability cli", () => { mocks.generateVideo.mockReset(); mocks.transcribeAudioFile.mockClear(); mocks.textToSpeech.mockClear(); + mocks.resolveCommandSecretRefsViaGateway + .mockReset() + .mockImplementation(async ({ config }: { config: unknown }) => ({ + resolvedConfig: config, + diagnostics: [], + targetStatesByPath: {}, + hadUnresolvedTargets: false, + })); + mocks.getTtsCommandSecretTargetIds.mockClear(); mocks.setTtsProvider.mockClear(); mocks.resolveExplicitTtsOverrides.mockClear(); mocks.buildMediaUnderstandingRegistry.mockReset().mockReturnValue(new Map()); @@ -1057,6 +1081,58 @@ describe("capability cli", () => { expect(mocks.setTtsProvider).not.toHaveBeenCalled(); }); + it("resolves static TTS SecretRefs before local conversion", async () => { + const sourceConfig = { + messages: { + tts: { + providers: { + minimax: { + apiKey: { source: "exec", provider: "mockexec", id: "minimax/tts/apiKey" }, + }, + }, + }, + }, + }; + const resolvedConfig = { + messages: { + tts: { + providers: { + minimax: { + apiKey: "resolved-minimax-key", + }, + }, + }, + }, + }; + mocks.loadConfig.mockReturnValueOnce(sourceConfig); + mocks.resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({ + resolvedConfig, + diagnostics: [], + targetStatesByPath: { + "messages.tts.providers.minimax.apiKey": "resolved_local", + }, + hadUnresolvedTargets: false, + }); + + await runRegisteredCli({ + register: registerCapabilityCli as (program: Command) => void, + argv: ["capability", "tts", "convert", "--text", "hello", "--json"], + }); + + expect(mocks.resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith({ + config: sourceConfig, + commandName: "infer tts convert", + targetIds: new Set(["messages.tts.providers.*.apiKey"]), + mode: "enforce_resolved", + }); + expect(mocks.resolveExplicitTtsOverrides).toHaveBeenCalledWith( + expect.objectContaining({ cfg: resolvedConfig }), + ); + expect(mocks.textToSpeech).toHaveBeenCalledWith( + expect.objectContaining({ cfg: resolvedConfig }), + ); + }); + it("disables TTS fallback when explicit provider or voice/model selection is requested", async () => { await runRegisteredCli({ register: registerCapabilityCli as (program: Command) => void, diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index a5ba86618b7..4c12e1cf385 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -79,6 +79,8 @@ import { runWebSearch, } from "../web-search/runtime.js"; import { runCommandWithRuntime } from "./cli-utils.js"; +import { resolveCommandSecretRefsViaGateway } from "./command-secret-gateway.js"; +import { getTtsCommandSecretTargetIds } from "./command-secret-targets.js"; import { createDefaultDeps } from "./deps.js"; import { removeCommandByName } from "./program/command-tree.js"; import { collectOption } from "./program/helpers.js"; @@ -1111,7 +1113,12 @@ async function runTtsConvert(params: { } satisfies CapabilityEnvelope; } - const cfg = loadConfig(); + const { resolvedConfig: cfg } = await resolveCommandSecretRefsViaGateway({ + config: loadConfig(), + commandName: "infer tts convert", + targetIds: getTtsCommandSecretTargetIds(), + mode: "enforce_resolved", + }); const overrides = resolveExplicitTtsOverrides({ cfg, provider: params.provider, diff --git a/src/cli/command-secret-resolution.coverage.test.ts b/src/cli/command-secret-resolution.coverage.test.ts index 9da2c0f322b..3dd9e440675 100644 --- a/src/cli/command-secret-resolution.coverage.test.ts +++ b/src/cli/command-secret-resolution.coverage.test.ts @@ -4,6 +4,7 @@ import { readCommandSource } from "./command-source.test-helpers.js"; const SECRET_TARGET_CALLSITES = [ bundledPluginFile("memory-core", "src/cli.runtime.ts"), + "src/cli/capability-cli.ts", "src/cli/qr-cli.ts", "src/agents/agent-runtime-config.ts", "src/commands/agent.ts", diff --git a/src/cli/command-secret-targets.test.ts b/src/cli/command-secret-targets.test.ts index cabf3028da7..8515a295536 100644 --- a/src/cli/command-secret-targets.test.ts +++ b/src/cli/command-secret-targets.test.ts @@ -58,6 +58,7 @@ import { getQrRemoteCommandSecretTargetIds, getScopedChannelsCommandSecretTargets, getSecurityAuditCommandSecretTargetIds, + getTtsCommandSecretTargetIds, } from "./command-secret-targets.js"; describe("command secret target ids", () => { @@ -73,6 +74,11 @@ describe("command secret target ids", () => { expect(ids.has("channels.discord.token")).toBe(false); }); + it("keeps static TTS targets out of the registry path", () => { + const ids = getTtsCommandSecretTargetIds(); + expect(ids).toEqual(new Set(["messages.tts.providers.*.apiKey"])); + }); + it("includes memorySearch remote targets for agent runtime commands", () => { const ids = getAgentRuntimeCommandSecretTargetIds(); expect(ids.has("agents.defaults.memorySearch.remote.apiKey")).toBe(true); diff --git a/src/cli/command-secret-targets.ts b/src/cli/command-secret-targets.ts index 9bbed1c5707..1309d290d10 100644 --- a/src/cli/command-secret-targets.ts +++ b/src/cli/command-secret-targets.ts @@ -23,12 +23,13 @@ const STATIC_MODEL_TARGET_IDS = [ "models.providers.*.request.tls.key", "models.providers.*.request.tls.passphrase", ] as const; +const STATIC_TTS_TARGET_IDS = ["messages.tts.providers.*.apiKey"] as const; const STATIC_AGENT_RUNTIME_BASE_TARGET_IDS = [ ...STATIC_MODEL_TARGET_IDS, "agents.defaults.memorySearch.remote.apiKey", "agents.list[].memorySearch.remote.apiKey", "agents.list[].tts.providers.*.apiKey", - "messages.tts.providers.*.apiKey", + ...STATIC_TTS_TARGET_IDS, "skills.entries.*.apiKey", "tools.web.search.apiKey", ] as const; @@ -221,6 +222,10 @@ export function getModelsCommandSecretTargetIds(): Set { return toTargetIdSet(STATIC_MODEL_TARGET_IDS); } +export function getTtsCommandSecretTargetIds(): Set { + return toTargetIdSet(STATIC_TTS_TARGET_IDS); +} + export function getAgentRuntimeCommandSecretTargetIds(params?: { includeChannelTargets?: boolean; }): Set {