openclaw - ✅(Solved) Fix talk.config still throws on SecretRef apiKey when configured under messages.tts.providers.<id>.apiKey (gap left by #72496) [2 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#73109Fetched 2026-04-28 06:27:28
View on GitHub
Comments
0
Participants
1
Timeline
10
Reactions
0
Participants
Assignees
Timeline (top)
referenced ×5cross-referenced ×2assigned ×1closed ×1

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

Error Message

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

Root Cause

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

Fix Action

Fix / Workaround

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.

PR fix notes

PR #73111: fix(gateway): strip SecretRef apiKey from messages.tts.providers before talk.config hands it to speech providers

Description (problem / solution / changelog)

Summary

Closes the gap left by #72496 on the parallel messages.tts.providers.<id>.apiKey site. After #72496 landed, talk.config still throws unresolved SecretRef whenever an operator pins their TTS apiKey as a SecretRef on the messages.tts side — visible in production with the exact same user-facing symptom #72496 was filed for (iOS / macOS / Control UI Talk overlays silently fall back to local AVSpeechSynthesizer because the discovery handshake errors out).

Reproduction on a build that contains 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 throwing call site is the strict-resolver normalizeResolvedSecretInputString invocation inside (e.g.) extensions/elevenlabs/speech-provider.ts::normalizeElevenLabsProviderConfig, which reads raw?.apiKey straight off baseTtsConfig.providers.elevenlabs and calls the strict normalizer on it — exactly the same shape as the bug #72496 fixed for talk.providers.

Detailed write-up at #73109.

Fix

Mirror #72496's approach. Add stripUnresolvedSecretApiKeysFromBaseTtsProviders in src/gateway/server-methods/talk.ts, walking each entry of baseTtsConfig.providers and applying the existing stripUnresolvedSecretApiKey helper. Apply at the resolveTalkResponseFromConfig call site so the base TTS config handed down to speechProvider.resolveTalkConfig({ baseTtsConfig }) no longer carries unresolved SecretRef wrappers on apiKey.

The strip is conservative — it only mutates when at least one provider entry's apiKey was a non-string, non-undefined value (i.e. a SecretRef-shaped object). All other entries pass through unchanged, including ones that already carry resolved string keys.

Files

  • src/gateway/server-methods/talk.ts — new stripUnresolvedSecretApiKeysFromBaseTtsProviders(base) helper plus the call at the existing resolveTalkResponseFromConfig site (line 376), upstream of speechProvider.resolveTalkConfig({ baseTtsConfig }). Doc comment cross-links #72496 so the relationship between the two patches is visible at the seam.
  • src/gateway/server.talk-config.test.ts — new it(\"does not throw when SecretRef apiKey on messages.tts.providers flows through a strict provider resolver\", ...) regression. Mirrors #72496's strict-resolver fixture but configures the SecretRef on messages.tts.providers.<id>.apiKey instead of talk.providers.<id>.apiKey. Verified the test fails on the parent commit (the production-reported error) and passes on this branch.

Test plan

  • pnpm exec vitest run src/gateway/server.talk-config.test.ts — 10/10 pass on this branch (including the new regression)
  • Confirmed the new test fails on origin/main with the exact production-reported unresolved SecretRef \"file:secrets:/skills/elevenlabs\" error
  • Manual: a gateway running this build with messages.tts.providers.elevenlabs.apiKey: { source, provider, id } SecretRef returns a clean talk.config response (provider: \"elevenlabs\", full resolved config, redacted apiKey for read-scope callers)
  • Reviewer: verify the strip pattern doesn't accidentally mutate plain-string apiKeys (the helper short-circuits when mutated === false, returning the original base reference)
  • Reviewer: confirm there are no other strict resolvers reading apiKey out of baseTtsConfig.providers via a different shape that would need an additional strip pass

Related

  • #72496 — fixed the talk.providers side; this PR closes the parallel messages.tts.providers gap.
  • #72506 — separate BlueBubbles voice-memo issue; not affected by either patch.

🤖 Generated with Claude Code

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/gateway/server-methods/talk.ts (modified, +55/-1)
  • src/gateway/server.talk-config.test.ts (modified, +167/-0)

PR #73286: fix(gateway): use runtime config for secret-backed talk

Description (problem / solution / changelog)

Summary

  • Problem: talk.config and channel logout could hand provider/channel callbacks the source config snapshot, leaving SecretRef wrappers unresolved on runtime paths.
  • Why it matters: Talk overlays can miss configured SecretRef-backed speech providers and fall back to local speech; channel logout can fail for SecretRef-backed account tokens.
  • What changed: pass active runtime config to provider/channel execution, expose the shared runtime snapshot selector to speech-core, and keep readback payloads source-shaped/redacted for UI.
  • What did NOT change (scope boundary): no config migration, no new provider behavior, no secret disclosure in talk.config read scope.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #73109
  • Related #73111
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: source/file snapshots preserve SecretRef markers for UI/editing, but provider/channel callbacks need the active runtime snapshot with secrets already materialized.
  • Missing detection / guardrail: existing Talk websocket coverage only proved the request did not throw after stripping unresolved wrappers; it did not prove strict providers received runtime-resolved messages.tts credentials.
  • Contributing context: speech-core helpers can be called with either the active source snapshot or runtime snapshot, so the runtime selection logic belongs in the shared runtime snapshot seam.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/gateway/server-methods/talk.test.ts, src/gateway/server-methods/channels.start.test.ts, extensions/speech-core/src/tts.test.ts, src/gateway/server.talk-config.test.ts
  • Scenario the test should lock in: strict Talk provider resolvers receive runtime-resolved messages.tts.providers.<id>.apiKey and runtime timeout; channel logout receives runtime config; read-scope talk.config stays redacted.
  • Why this is the smallest reliable guardrail: handler-level tests exercise the exact callback payloads without the slower websocket harness, while the websocket file keeps protocol/readback coverage.
  • Existing test that already covers this (if any): prior websocket Talk tests covered redaction/schema shape, but not runtime-resolved base TTS config.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Talk Mode now discovers SecretRef-backed speech providers through talk.config instead of falling back to local speech when the provider resolver requires a concrete runtime API key.

Diagram (if applicable)

Before:
Talk overlay -> talk.config -> source snapshot SecretRef -> strict provider rejects/unconfigured

After:
Talk overlay -> talk.config -> runtime snapshot credential -> provider resolves -> redacted UI payload

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? Yes
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: provider callbacks now receive the runtime-resolved config they already receive on execution paths; talk.config read-scope responses restore the source SecretRef shape and pass through existing redaction so credential material is not returned to clients.

Repro + Verification

Environment

  • OS: macOS local, Blacksmith Testbox Linux runner
  • Runtime/container: Node/pnpm repo wrapper
  • Model/provider: N/A
  • Integration/channel (if any): Talk TTS, channel logout
  • Relevant config (redacted): messages.tts.providers.<id>.apiKey as SecretRef

Steps

  1. Configure Talk + messages.tts.providers.<id>.apiKey through a SecretRef.
  2. Call talk.config with read scope against a strict provider resolver.
  3. Call channel logout for a plugin account whose token is runtime-resolved.

Expected

  • Provider/channel callbacks see runtime-resolved config.
  • talk.config response remains redacted for read scope.

Actual

  • Before this patch, source snapshots could hand unresolved SecretRef wrappers into provider/channel callbacks.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: pnpm test src/gateway/server-methods/talk.test.ts src/gateway/server.talk-config.test.ts; Blacksmith Testbox pnpm test src/gateway/server-methods/talk.test.ts src/gateway/server.talk-config.test.ts; Blacksmith Testbox pnpm check:changed.
  • Edge cases checked: read-scope redaction, runtime timeout selection, strict SecretRef normalization, channel logout runtime config, __proto__ provider-key hardening.
  • What you did not verify: live third-party TTS provider call with a real API key.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: provider callbacks receive resolved runtime credentials on a config-discovery path.
    • Mitigation: this matches execution-path expectations, and response construction restores source-shaped SecretRef values before existing redaction runs.

Changed files

  • CHANGELOG.md (modified, +1/-1)
  • docs/plugins/sdk-runtime.md (modified, +2/-0)
  • extensions/speech-core/src/tts.ts (modified, +8/-34)
  • src/gateway/server-methods/channels.start.test.ts (modified, +75/-1)
  • src/gateway/server-methods/channels.ts (modified, +1/-1)
  • src/gateway/server-methods/talk.test.ts (modified, +106/-0)
  • src/gateway/server-methods/talk.ts (modified, +19/-36)
  • src/gateway/server.talk-config.test.ts (modified, +2/-93)
  • src/plugin-sdk/runtime-config-snapshot.ts (modified, +1/-0)

Code Example

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

---

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

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

extent analysis

TL;DR

The issue can be fixed by stripping SecretRef wrappers from messages.tts.providers.<id>.apiKey before passing it to speechProvider.resolveTalkConfig.

Guidance

  • The problem arises from the normalizeElevenLabsProviderConfig function throwing an error when raw?.apiKey is a SecretRef wrapper.
  • To verify the issue, run the openclaw gateway call talk.config command and check if it throws a GatewayClientRequestError.
  • A temporary workaround is to replace messages.tts.providers.elevenlabs.apiKey with a literal string or an environment variable, as shown in the repro environment section.
  • The suggested fix involves defensively stripping SecretRef wrappers from messages.tts.providers.<id>.apiKey before handing the base TTS config down to speechProvider.resolveTalkConfig.

Example

// Example of stripping SecretRef wrappers
const stripUnresolvedSecretApiKey = (apiKey) => {
  if (apiKey && apiKey.source === 'file' && apiKey.provider === 'secrets') {
    return null; // or throw an error, depending on the desired behavior
  }
  return apiKey;
};

// Usage
const baseTtsConfig = {
  providers: {
    elevenlabs: {
      apiKey: stripUnresolvedSecretApiKey(messages.tts.providers.elevenlabs.apiKey),
    },
  },
};

Notes

The fix should be applied to the messages.tts.providers.<id>.apiKey path, which is currently untouched by the #72496 fix. This will ensure that the talk.config RPC works correctly even when the messages.tts.providers.<id>.apiKey is configured as a SecretRef wrapper.

Recommendation

Apply the workaround by replacing messages.tts.providers.elevenlabs.apiKey with a literal string or an environment variable, until the suggested fix is implemented. This will allow the talk.config RPC to

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…

Still need to ship something?

×6

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

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix talk.config still throws on SecretRef apiKey when configured under messages.tts.providers.<id>.apiKey (gap left by #72496) [2 pull requests, 1 participants]