From 4500f02fe684497ba622443fc2553b0ea3a08d2c Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sat, 6 Jun 2026 22:58:44 +0200 Subject: [PATCH] Fix talk config secret resolution --- src/gateway/server-methods/talk.test.ts | 67 +++++++++++++++++++++++++ src/gateway/server-methods/talk.ts | 6 +-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/gateway/server-methods/talk.test.ts b/src/gateway/server-methods/talk.test.ts index 733fc95e7cfa..c71ff3094d3e 100644 --- a/src/gateway/server-methods/talk.test.ts +++ b/src/gateway/server-methods/talk.test.ts @@ -516,6 +516,73 @@ describe("talk.config handler", () => { expectRecordFields(resolved, { provider: "acme" }); expectRecordFields(resolved?.config, { apiKey: "__OPENCLAW_REDACTED__" }); }); + + it("returns runtime-resolved talk provider secrets to scoped clients", async () => { + const sourceConfig = { + talk: { + provider: "acme", + providers: { + acme: { + apiKey: { source: "env", provider: "default", id: "ACME_SPEECH_API_KEY" }, + speakerVoiceId: "talk-speaker-id", + }, + }, + }, + } as OpenClawConfig; + const runtimeConfig = { + talk: { + provider: "acme", + providers: { + acme: { + apiKey: "env-acme-key", + speakerVoiceId: "talk-speaker-id", + }, + }, + }, + } as OpenClawConfig; + + mocks.readConfigFileSnapshot.mockResolvedValue({ + path: "/tmp/openclaw.json", + hash: "test-hash", + valid: true, + config: sourceConfig, + }); + mocks.getSpeechProvider.mockReturnValue({ + id: "acme", + label: "Acme Strict Speech", + resolveTalkConfig: ({ + talkProviderConfig, + }: { + talkProviderConfig: Record; + }) => { + expectRecordFields(talkProviderConfig, { + apiKey: "env-acme-key", + speakerVoiceId: "talk-speaker-id", + voiceId: "talk-speaker-id", + }); + return talkProviderConfig; + }, + }); + + const respond = vi.fn(); + await talkHandlers["talk.config"]({ + req: { type: "req", id: "1", method: "talk.config" }, + params: { includeSecrets: true }, + client: { connect: { scopes: ["operator.read", "operator.talk.secrets"] } } as never, + isWebchatConnect: () => false, + respond: respond as never, + context: { getRuntimeConfig: () => runtimeConfig } as never, + }); + + const response = expectRespondOk(respond) as { config?: { talk?: Record } }; + const resolved = response.config?.talk?.resolved as Record | undefined; + expectRecordFields(resolved, { provider: "acme" }); + expectRecordFields(resolved?.config, { + apiKey: "env-acme-key", + speakerVoiceId: "talk-speaker-id", + voiceId: "talk-speaker-id", + }); + }); }); describe("talk.session unified handlers", () => { diff --git a/src/gateway/server-methods/talk.ts b/src/gateway/server-methods/talk.ts index f5cd0ba339ff..25187a65d64a 100644 --- a/src/gateway/server-methods/talk.ts +++ b/src/gateway/server-methods/talk.ts @@ -411,10 +411,6 @@ function resolveTalkResponseFromConfig(params: { return undefined; } - if (params.includeSecrets) { - return payload; - } - const sourceResolved = resolveActiveTalkProviderConfig(normalizedTalk); const runtimeResolved = resolveActiveTalkProviderConfig(params.runtimeConfig.talk); const activeProviderId = sourceResolved?.provider ?? runtimeResolved?.provider; @@ -453,7 +449,7 @@ function resolveTalkResponseFromConfig(params: { timeoutMs: typeof selectedBaseTts.timeoutMs === "number" ? selectedBaseTts.timeoutMs : 30_000, }) ?? providerInputConfig; const responseConfig = - sourceProviderConfig.apiKey === undefined + params.includeSecrets || sourceProviderConfig.apiKey === undefined ? resolvedConfig : { ...resolvedConfig, apiKey: sourceProviderConfig.apiKey };