You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#72496 fixed talk.config RPC throwing unresolved SecretRef for talk.providers.<id>.apiKey. The same redaction-throws bug still triggers when the SecretRef is on the parallel messages.tts.providers.<id>.apiKey site, which talk.config also walks when it builds the resolved provider config.
End-to-end repro on a v2026.4.26+ build that contains the #72496 fix at 8ce4f8fc84:
$ openclaw gateway call talk.config
Gateway call failed: GatewayClientRequestError: Error: messages.tts.providers.elevenlabs.apiKey: unresolved SecretRef "file:secrets:/skills/elevenlabs". Resolve this command against an active gateway runtime snapshot before reading it.
The exact path that throws is in extensions/elevenlabs/speech-provider.ts::normalizeElevenLabsProviderConfig (and the same shape in extensions/openai/... and other strict resolvers), called from resolveTalkConfig:
raw?.apiKey is the messages.tts.providers.elevenlabs.apiKey value — when configured as a SecretRef wrapper rather than a plain string, the strict normalizer throws. talk.config never makes it past that throw, so iOS / macOS / Control UI Talk overlays fall back to local AVSpeechSynthesizer the same way #72496 was filed for.
#72496 introduced stripUnresolvedSecretApiKey and applied it to talkProviderConfig (the per-active-provider talk config) before passing the value into speechProvider.resolveTalkConfig({ talkProviderConfig }). The same RPC also passes baseTtsConfig: messages.tts down the same call, and the strict resolver reads baseTtsConfig.providers[id].apiKey from inside that. That path was untouched.
In the operator-config layout that bites users in production, talk.providers.<id>.apiKey and messages.tts.providers.<id>.apiKey typically point at the same SecretRef — Talk Mode and the agent tts tool sharing one credential. Once #72496 lands, the workaround moves from "literal apiKey on both" to "literal apiKey on messages.tts only" — visibly partial, and confusing for the next operator who hits it.
File-backed secrets at ~/.openclaw/secrets.json with an ElevenLabs (or OpenAI) apiKey at /skills/elevenlabs.
Both talk.providers.elevenlabs.apiKey and messages.tts.providers.elevenlabs.apiKey configured as { "source": "file", "provider": "secrets", "id": "/skills/elevenlabs" }.
openclaw gateway call talk.config → throws as quoted above.
Replacing only messages.tts.providers.elevenlabs.apiKey with ${ELEVENLABS_API_KEY} (or a literal string) makes the RPC succeed.
Suggested fix
Same shape as #72496: defensively strip SecretRef wrappers from messages.tts.providers.<id>.apiKey before handing the base TTS config down to speechProvider.resolveTalkConfig. PR follows shortly.
Summary
#72496 fixed
talk.configRPC throwingunresolved SecretReffortalk.providers.<id>.apiKey. The same redaction-throws bug still triggers when the SecretRef is on the parallelmessages.tts.providers.<id>.apiKeysite, whichtalk.configalso walks when it builds the resolved provider config.End-to-end repro on a v2026.4.26+ build that contains the #72496 fix at
8ce4f8fc84:The exact path that throws is in
extensions/elevenlabs/speech-provider.ts::normalizeElevenLabsProviderConfig(and the same shape inextensions/openai/...and other strict resolvers), called fromresolveTalkConfig:raw?.apiKeyis themessages.tts.providers.elevenlabs.apiKeyvalue — when configured as a SecretRef wrapper rather than a plain string, the strict normalizer throws.talk.confignever makes it past that throw, so iOS / macOS / Control UI Talk overlays fall back to local AVSpeechSynthesizer the same way #72496 was filed for.Why #72496 didn't catch it
#72496 introduced
stripUnresolvedSecretApiKeyand applied it totalkProviderConfig(the per-active-provider talk config) before passing the value intospeechProvider.resolveTalkConfig({ talkProviderConfig }). The same RPC also passesbaseTtsConfig: messages.ttsdown the same call, and the strict resolver readsbaseTtsConfig.providers[id].apiKeyfrom inside that. That path was untouched.In the operator-config layout that bites users in production,
talk.providers.<id>.apiKeyandmessages.tts.providers.<id>.apiKeytypically point at the same SecretRef — Talk Mode and the agentttstool sharing one credential. Once #72496 lands, the workaround moves from "literal apiKey on both" to "literal apiKey onmessages.ttsonly" — visibly partial, and confusing for the next operator who hits it.Repro environment (no PII)
8ce4f8fc84(so talk.config RPC fails with "unresolved SecretRef" for talk.providers.*.apiKey, breaks Talk Mode discovery #72496'stalk.providerspath is fixed).~/.openclaw/secrets.jsonwith an ElevenLabs (or OpenAI) apiKey at/skills/elevenlabs.talk.providers.elevenlabs.apiKeyandmessages.tts.providers.elevenlabs.apiKeyconfigured as{ "source": "file", "provider": "secrets", "id": "/skills/elevenlabs" }.openclaw gateway call talk.config→ throws as quoted above.messages.tts.providers.elevenlabs.apiKeywith${ELEVENLABS_API_KEY}(or a literal string) makes the RPC succeed.Suggested fix
Same shape as #72496: defensively strip SecretRef wrappers from
messages.tts.providers.<id>.apiKeybefore handing the base TTS config down tospeechProvider.resolveTalkConfig. PR follows shortly.