Skip to content

talk.config still throws on SecretRef apiKey when configured under messages.tts.providers.<id>.apiKey (gap left by #72496) #73109

@omarshahine

Description

@omarshahine

Summary

#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:

return {
  apiKey: normalizeResolvedSecretInputString({
    value: raw?.apiKey,
    path: "messages.tts.providers.elevenlabs.apiKey",
  }),
  // ...
};

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.

Why #72496 didn't catch it

#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.

Repro environment (no PII)

  • OpenClaw running a build that contains 8ce4f8fc84 (so talk.config RPC fails with "unresolved SecretRef" for talk.providers.*.apiKey, breaks Talk Mode discovery #72496's talk.providers path is fixed).
  • 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.

Metadata

Metadata

Assignees

Labels

maintainerMaintainer-authored PR

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions