openclaw - ✅(Solved) Fix talk.config RPC fails with "unresolved SecretRef" for talk.providers.*.apiKey, breaks Talk Mode discovery [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#72496Fetched 2026-04-27 05:29:45
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Assignees
Timeline (top)
cross-referenced ×2assigned ×1labeled ×1

talk.config (the Talk-mode discovery RPC that iOS / macOS / Control UI Talk overlays call on startup to learn the configured TTS provider) throws unresolved SecretRef when talk.providers.<id>.apiKey is configured as a SecretRef object. The credential itself resolves correctly — talk.realtime.session and talk.speak both work end-to-end with the same SecretRef — but the discovery handshake fails, so clients never learn what provider is configured and silently fall back to local on-device TTS (AVSpeechSynthesizer / equivalent), producing the classic "robot voice" symptom even though everything is configured correctly.

talk.providers.*.apiKey is officially documented as a supported SecretRef target in docs/reference/secretref-credential-surface.md and listed in docs/.generated/config-baseline.core.json. So this is a discovery / redaction-path bug, not a deliberate restriction.

Error Message

Gateway call failed: GatewayClientRequestError: Error: talk.providers.elevenlabs.apiKey: unresolved SecretRef "file:secrets:/skills/elevenlabs". Resolve this command against an active gateway runtime snapshot before reading it.

Root Cause

talk.config (the Talk-mode discovery RPC that iOS / macOS / Control UI Talk overlays call on startup to learn the configured TTS provider) throws unresolved SecretRef when talk.providers.<id>.apiKey is configured as a SecretRef object. The credential itself resolves correctly — talk.realtime.session and talk.speak both work end-to-end with the same SecretRef — but the discovery handshake fails, so clients never learn what provider is configured and silently fall back to local on-device TTS (AVSpeechSynthesizer / equivalent), producing the classic "robot voice" symptom even though everything is configured correctly.

talk.providers.*.apiKey is officially documented as a supported SecretRef target in docs/reference/secretref-credential-surface.md and listed in docs/.generated/config-baseline.core.json. So this is a discovery / redaction-path bug, not a deliberate restriction.

Fix Action

Workaround

Replace the SecretRef with a literal apiKey string or ${ENV_VAR} interpolation under talk.providers.<id>.apiKey. Both surfaces (iOS + macOS Talk) start working immediately. Downside: literal in openclaw.json, or another env var to manage in the LaunchAgent plist — neither is great for a documented SecretRef target.

PR fix notes

PR #72584: fix(gateway): talk.config no longer throws on SecretRef apiKey (#72496)

Description (problem / solution / changelog)

Summary

Fixes #72496. talk.config was throwing unresolved SecretRef whenever talk.providers.<id>.apiKey was configured as a SecretRef object, so iOS / macOS / Control UI Talk overlays never received a provider snapshot and silently fell back to local on-device TTS (the "robot voice" symptom). The credential itself was healthy — talk.realtime.session and talk.speak both worked with the same SecretRef — so this was purely a discovery-path bug.

Root cause: resolveTalkResponseFromConfig in src/gateway/server-methods/talk.ts handed the source-snapshot's talkProviderConfig (with the SecretRef wrapper still on apiKey) to speechProvider.resolveTalkConfig({...}). ElevenLabs's resolver (and OpenAI's, and any provider that uses the strict secret-input helpers) then called normalizeResolvedSecretInputString on the wrapper, which threw on the unresolved SecretRef.

Fix

  • Prefer the runtime-resolved provider config when calling resolveTalkConfig (substituted strings in production paths).
  • Strip the apiKey field if it's still a SecretRef wrapper before handing it to the provider, so strict secret-input helpers never see an unresolved wrapper.
  • Restore the source-shaped apiKey (the SecretRef) onto resolved.config in the response so the UI keeps the SecretRef context. Existing redaction sentinels the value when includeSecrets: false.

Adds a regression test that registers a speech provider mirroring ElevenLabs/OpenAI's strict resolver behavior and drives a SecretRef apiKey through talk.config for both read-only and includeSecrets: true callers.

Test plan

  • pnpm test src/gateway/server.talk-config.test.ts — 9/9 pass; new test fails on unresolved SecretRef without the fix
  • pnpm check:changed (typecheck core/core-tests, lint, import cycles, guards) — green
  • pnpm test:changed — green
  • Manual: file-backed SecretRef ElevenLabs apiKey → openclaw gateway call talk.config returns provider snapshot → iOS/macOS Talk overlays use ElevenLabs voice instead of AVSpeechSynthesizer

🤖 Generated with Claude Code

Changed files

  • src/gateway/server-methods/talk.ts (modified, +26/-4)
  • src/gateway/server.talk-config.test.ts (modified, +82/-0)

Code Example

{
     "secrets": {
       "providers": {
         "secrets": { "source": "file", "path": "~/.openclaw/secrets.json", "mode": "json" }
       },
       "defaults": { "file": "secrets" }
     }
   }

---

{
     "talk": {
       "provider": "elevenlabs",
       "interruptOnSpeech": true,
       "providers": {
         "elevenlabs": {
           "voiceId": "<voice-id>",
           "modelId": "eleven_v3",
           "outputFormat": "mp3_44100_128",
           "apiKey": {
             "source": "file",
             "provider": "secrets",
             "id": "/skills/elevenlabs"
           }
         }
       }
     }
   }

---

openclaw gateway call talk.config

---

Gateway call failed: GatewayClientRequestError: Error: talk.providers.elevenlabs.apiKey: unresolved SecretRef "file:secrets:/skills/elevenlabs". Resolve this command against an active gateway runtime snapshot before reading it.

---

openclaw gateway call talk.speak --params '{"text":"hello"}'
   # → returns valid audioBase64 (ElevenLabs MP3, ID3 / Lavf encoder header)

   openclaw gateway call talk.realtime.session
   # → returns valid OpenAI Realtime ephemeral key (ek_...) when provider=openai
RAW_BUFFERClick to expand / collapse

Summary

talk.config (the Talk-mode discovery RPC that iOS / macOS / Control UI Talk overlays call on startup to learn the configured TTS provider) throws unresolved SecretRef when talk.providers.<id>.apiKey is configured as a SecretRef object. The credential itself resolves correctly — talk.realtime.session and talk.speak both work end-to-end with the same SecretRef — but the discovery handshake fails, so clients never learn what provider is configured and silently fall back to local on-device TTS (AVSpeechSynthesizer / equivalent), producing the classic "robot voice" symptom even though everything is configured correctly.

talk.providers.*.apiKey is officially documented as a supported SecretRef target in docs/reference/secretref-credential-surface.md and listed in docs/.generated/config-baseline.core.json. So this is a discovery / redaction-path bug, not a deliberate restriction.

Environment

  • OpenClaw: v2026.4.24 (also reproduced on v2026.4.21–v2026.4.24)
  • Platform: macOS (Apple Silicon)
  • Affected surfaces: iOS Talk, macOS native Talk overlay, Control UI Talk (browser/PWA)
  • Secrets backend: file-backed (~/.openclaw/secrets.json, secrets.providers.secrets.source: file)
  • Install: standard npm-installed gateway under macOS LaunchAgent

Reproduction

  1. Configure file-backed secrets with an ElevenLabs (or OpenAI Realtime) API key:

    {
      "secrets": {
        "providers": {
          "secrets": { "source": "file", "path": "~/.openclaw/secrets.json", "mode": "json" }
        },
        "defaults": { "file": "secrets" }
      }
    }
  2. Configure Talk to use a SecretRef for the provider apiKey:

    {
      "talk": {
        "provider": "elevenlabs",
        "interruptOnSpeech": true,
        "providers": {
          "elevenlabs": {
            "voiceId": "<voice-id>",
            "modelId": "eleven_v3",
            "outputFormat": "mp3_44100_128",
            "apiKey": {
              "source": "file",
              "provider": "secrets",
              "id": "/skills/elevenlabs"
            }
          }
        }
      }
    }
  3. Restart the gateway and call the discovery RPC:

    openclaw gateway call talk.config

    Observed:

    Gateway call failed: GatewayClientRequestError: Error: talk.providers.elevenlabs.apiKey: unresolved SecretRef "file:secrets:/skills/elevenlabs". Resolve this command against an active gateway runtime snapshot before reading it.
  4. Confirm the credential itself is healthy:

    openclaw gateway call talk.speak --params '{"text":"hello"}'
    # → returns valid audioBase64 (ElevenLabs MP3, ID3 / Lavf encoder header)
    
    openclaw gateway call talk.realtime.session
    # → returns valid OpenAI Realtime ephemeral key (ek_...) when provider=openai

    So the SecretRef IS resolved by the runtime — only the talk.config discovery / redaction path fails.

  5. Open the iOS app or macOS native Talk overlay → robot voice (local AVSpeechSynthesizer fallback).

Expected behavior

talk.config should successfully redact and return the configured Talk provider snapshot regardless of whether talk.providers.<id>.apiKey is a literal string or a SecretRef object. The redaction path should treat unresolved SecretRef wrappers the same way it treats string secrets — render them as __OPENCLAW_REDACTED__ rather than throwing.

Actual behavior

talk.config fails fast → iOS / macOS / Control UI Talk clients receive no provider snapshot → silent fallback to local on-device TTS. No surfaced error in the client UI; users see the macOS Advanced > Talk Voice panel show the right config but hear the wrong voice.

Likely cause

The talk.config handler in src/gateway/server-methods/talk.ts reads from readConfigFileSnapshot() (raw file, no SecretRef resolution) and then calls redactConfigObject(...) from src/config/redact-snapshot.ts on the talk payload. Somewhere down that path (likely via assertSecretInputResolved in src/config/types.secrets.ts), the redactor encounters the unresolved SecretRef wrapper and throws createUnresolvedSecretInputError instead of redacting the value.

This is the same family of regressions documented in:

  • #68237 (CLOSED) — Slack socket-mode botToken, exact same error string, regressed in 2026.4.15.
  • #68690 (OPEN) — umbrella "SecretRef coverage gaps for several auth/config paths"; lists sibling plugins.entries.voice-call.config.tts.providers.{openai,elevenlabs}.apiKey paths as broken.
  • #72120 (CLOSED) — OpenAI Realtime keychain SecretRef variant (different backend).
  • #14586 (CLOSED, 2026-03) — earlier ElevenLabs talk-mode redaction sentinel bug, fixed for the older flat schema; nested providers.<id> schema regressed.

None of those track talk.config + talk.providers.*.apiKey specifically.

Suggested fix

In the talk.config handler / redactConfigObject path, treat unresolved SecretRef objects the same as resolved string secrets for the purpose of redaction — emit __OPENCLAW_REDACTED__ (or a structured { redacted: true } marker) instead of asserting resolution. The discovery RPC inherently runs on the raw config snapshot, so unresolved SecretRefs are an expected input shape there, not an error condition.

Optional: extend the redaction visitor with explicit SecretRef handling (one branch above the assertion) to keep the strict assertion intact for paths where unresolved refs really are a bug.

Workaround

Replace the SecretRef with a literal apiKey string or ${ENV_VAR} interpolation under talk.providers.<id>.apiKey. Both surfaces (iOS + macOS Talk) start working immediately. Downside: literal in openclaw.json, or another env var to manage in the LaunchAgent plist — neither is great for a documented SecretRef target.

Repro environment notes (no PII)

Voice IDs, API keys, and account-specific paths are placeholders in the snippets above. The bug reproduces with any non-empty SecretRef pointing at a valid ElevenLabs (or OpenAI) key, on a clean LaunchAgent-managed gateway with file-backed secrets.

extent analysis

TL;DR

The talk.config discovery RPC fails with an unresolved SecretRef error when talk.providers.<id>.apiKey is configured as a SecretRef object, despite being a documented supported target.

Guidance

  • Review the redactConfigObject function in src/config/redact-snapshot.ts to ensure it handles unresolved SecretRef objects correctly.
  • Consider adding explicit SecretRef handling to the redaction visitor to treat unresolved refs as __OPENCLAW_REDACTED__ instead of throwing an error.
  • Verify that the talk.config handler is using the correct config snapshot and redaction path.
  • Test the workaround of replacing the SecretRef with a literal apiKey string or ${ENV_VAR} interpolation under talk.providers.<id>.apiKey to confirm it resolves the issue.

Example

No code snippet is provided as the issue is more related to the logic and handling of SecretRef objects rather than a specific code error.

Notes

The issue seems to be related to the redaction path and how it handles unresolved SecretRef objects. The suggested fix is to treat these objects as __OPENCLAW_REDACTED__ instead of throwing an error. However, this may require further investigation and testing to ensure it does not introduce any security vulnerabilities.

Recommendation

Apply the suggested fix by modifying the redactConfigObject function to handle unresolved SecretRef objects correctly, as this is a documented supported target and the current behavior is causing issues with the talk.config discovery RPC.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

talk.config should successfully redact and return the configured Talk provider snapshot regardless of whether talk.providers.<id>.apiKey is a literal string or a SecretRef object. The redaction path should treat unresolved SecretRef wrappers the same way it treats string secrets — render them as __OPENCLAW_REDACTED__ rather than throwing.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING