Skip to content

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

Merged
omarshahine merged 3 commits into
openclaw:mainfrom
omarshahine:fix/messages-tts-secretref
Apr 28, 2026
Merged

fix(gateway): strip SecretRef apiKey from messages.tts.providers before talk.config hands it to speech providers#73111
omarshahine merged 3 commits into
openclaw:mainfrom
omarshahine:fix/messages-tts-secretref

Conversation

@omarshahine

Copy link
Copy Markdown
Contributor

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

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

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S maintainer Maintainer-authored PR labels Apr 28, 2026
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR mirrors the fix from #72496 for the parallel messages.tts.providers.<id>.apiKey path: it adds stripUnresolvedSecretApiKeysFromBaseTtsProviders in talk.ts to strip unresolved SecretRef wrappers before baseTtsConfig is handed to speech-provider resolvers, preventing the unresolved SecretRef throw in normalizeResolvedSecretInputString. A regression test verifies the fix and confirms failure on origin/main.

Confidence Score: 5/5

Safe to merge — targeted fix with no logic regressions and a well-scoped regression test.

The change is a conservative strip-only helper that short-circuits on the no-mutation path (returning the original reference). It precisely mirrors the existing stripUnresolvedSecretApiKey pattern and only affects the baseTtsConfig handed to speech provider resolvers. timeoutMs and all other base TTS fields are untouched. The regression test explicitly reproduces the production error and passes on this branch.

No files require special attention.

Reviews (1): Last reviewed commit: "fix(gateway): strip SecretRef apiKey fro..." | Re-trigger Greptile

@steipete steipete force-pushed the fix/messages-tts-secretref branch 3 times, most recently from 54160f7 to 14a0fea Compare April 28, 2026 02:44
omarshahine and others added 3 commits April 27, 2026 21:00
…re talk.config hands it to speech providers

The fix in openclaw#72496 stripped unresolved SecretRef wrappers from the
talk.providers side of `talk.config` before handing the value into
`speechProvider.resolveTalkConfig({ talkProviderConfig })`, but the same
RPC also forwards `baseTtsConfig: messages.tts` down the same call.
Strict provider resolvers (ElevenLabs/OpenAI) read the active provider's
apiKey out of `baseTtsConfig.providers[id]` and call the same
`normalizeResolvedSecretInputString` helper that throws on an unresolved
wrapper, so `talk.config` still errors out for any operator that pins
`messages.tts.providers.<id>.apiKey: { source, provider, id }` as a
SecretRef — even after upgrading to a build that contains the openclaw#72496 fix.

The user-visible regression is the same as openclaw#72496: iOS / macOS /
Control UI Talk overlays never learn the configured provider through the
discovery handshake and silently fall back to local AVSpeechSynthesizer.
The workaround until this lands is "switch messages.tts.providers.<id>.apiKey
to a literal string or `${ENV_VAR}` while keeping talk.providers.<id>.apiKey
as a SecretRef" — visibly partial.

Apply the same defensive strip pattern to the messages.tts side:
walk `baseTtsConfig.providers` and reuse the existing
`stripUnresolvedSecretApiKey` helper on each provider entry before passing
the base TTS config into `resolveTalkConfig`. The provider resolvers'
strict-secret call sites then see `apiKey: undefined` and skip past
without throwing, mirroring the talk.providers path that openclaw#72496 unblocked.

Adds a regression test that mirrors openclaw#72496's strict-resolver fixture but
configures the SecretRef on `messages.tts.providers.<id>.apiKey` instead
of `talk.providers.<id>.apiKey`. The new test fails without this patch
with the exact `unresolved SecretRef "file:..."` error reported against
production gateways.

Fixes openclaw#73109. Refs openclaw#72496.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…viders to block prototype pollution

Aisle security review on openclaw#73111 flagged a prototype-pollution surface in
`stripUnresolvedSecretInputsFromBaseTtsProviders`: the helper rebuilds
the providers map with `cleaned: Record<string, unknown> = {}` and
assigns dynamic keys from `Object.entries(providers)`. An operator
config payload that carries `messages.tts.providers.__proto__` (or
`constructor`/`prototype`) would mutate Object.prototype on every
following plain-object property lookup downstream — provider ids come
from operator config and are not validated upstream against a safe
character set.

Switch the local map to `Object.create(null)` so the dynamic
`cleaned[providerId] = ...` write becomes a normal own-property
assignment that cannot reach Object.prototype.

Adds a regression test that posts a `messages.tts.providers.__proto__`
config entry with a hostile `polluted` payload and asserts that
`({}).polluted` stays `undefined` after the talk.config call returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@omarshahine omarshahine force-pushed the fix/messages-tts-secretref branch from 15619c4 to aa7941a Compare April 28, 2026 04:03
@omarshahine omarshahine merged commit 4b760be into openclaw:main Apr 28, 2026
66 checks passed
@omarshahine

Copy link
Copy Markdown
Contributor Author

Landed in 4b760be1dd. Full CI green on the rebased SHA aa7941a673 (57 pass / 0 fail). Closes #73109; complementary to #72496.

@omarshahine omarshahine deleted the fix/messages-tts-secretref branch April 28, 2026 04:06
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…rs before talk.config hands them to speech providers (openclaw#73111)

Closes the gap left by openclaw#72496 on the parallel `messages.tts.providers.<id>` site. After openclaw#72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from openclaw#72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes openclaw#73109. Refs openclaw#72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…rs before talk.config hands them to speech providers (openclaw#73111)

Closes the gap left by openclaw#72496 on the parallel `messages.tts.providers.<id>` site. After openclaw#72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from openclaw#72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes openclaw#73109. Refs openclaw#72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…rs before talk.config hands them to speech providers (openclaw#73111)

Closes the gap left by openclaw#72496 on the parallel `messages.tts.providers.<id>` site. After openclaw#72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from openclaw#72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes openclaw#73109. Refs openclaw#72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…rs before talk.config hands them to speech providers (openclaw#73111)

Closes the gap left by openclaw#72496 on the parallel `messages.tts.providers.<id>` site. After openclaw#72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from openclaw#72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes openclaw#73109. Refs openclaw#72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…rs before talk.config hands them to speech providers (openclaw#73111)

Closes the gap left by openclaw#72496 on the parallel `messages.tts.providers.<id>` site. After openclaw#72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from openclaw#72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes openclaw#73109. Refs openclaw#72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…rs before talk.config hands them to speech providers (openclaw#73111)

Closes the gap left by openclaw#72496 on the parallel `messages.tts.providers.<id>` site. After openclaw#72496 landed, `talk.config` still threw `unresolved SecretRef` whenever an operator pinned a TTS apiKey or token as a SecretRef on the messages.tts side — same user-facing symptom (iOS / macOS / Control UI Talk overlays falling back to local AVSpeechSynthesizer).

Adds `stripUnresolvedSecretInputsFromBaseTtsProviders` in `src/gateway/server-methods/talk.ts` that walks each entry in `messages.tts.providers` and strips any unresolved SecretRef wrappers from the configured secret-input keys (`apiKey`, `token`) before handing the base TTS config down to `speechProvider.resolveTalkConfig`. Mirrors the `talk.providers` strip pattern from openclaw#72496.

Hardening: rebuilds the providers map with `Object.create(null)` instead of `{}` so an operator-config payload carrying `messages.tts.providers.__proto__` (or `constructor`/`prototype`) cannot mutate Object.prototype via the dynamic `cleaned[providerId] = ...` assignment. Caught by Aisle security review.

Adds three regression tests covering: SecretRef apiKey on messages.tts (the original bug), SecretRef token on messages.tts (Peter's generalization), and `__proto__`-keyed providers (Aisle hardening). All pass; full CI green (57/57) on the rebased branch.

Fixes openclaw#73109. Refs openclaw#72496.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants