fix(gateway): talk.config no longer throws on SecretRef apiKey (#72496)#72584
Conversation
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Resolved talk provider apiKey passed to plugins during talk.config for read-scope callers (confused deputy secret exposure)
DescriptionIn This creates a confused-deputy secret exposure / secret-usage issue:
Vulnerable flow (simplified):
Vulnerable code: const runtimeProviderConfig = runtimeResolved?.config ?? {};
// Prefer runtime-resolved provider config (already-substituted secrets)
const providerInputConfig = stripUnresolvedSecretApiKey(
Object.keys(runtimeProviderConfig).length > 0 ? runtimeProviderConfig : sourceProviderConfig,
);
const resolvedConfig =
speechProvider?.resolveTalkConfig?.({
...,
talkProviderConfig: providerInputConfig,
}) ?? providerInputConfig;Although RecommendationAvoid passing resolved secrets to provider plugins when servicing Options (choose based on intended trust model):
const providerInputConfig = {
...(Object.keys(runtimeProviderConfig).length > 0 ? runtimeProviderConfig : sourceProviderConfig),
apiKey: undefined,
};
In all cases, keep redaction for responses, but also prevent secret propagation into plugin code on unprivileged requests. Analyzed PR: #72584 at commit Last updated on: 2026-04-27T16:01:45Z |
Greptile SummaryThis PR fixes a bug where Confidence Score: 5/5Safe to merge — targeted fix for a well-understood discovery-path bug with a solid regression test. The change is narrow, well-commented, and does not touch any write paths or actual secret-resolution logic. The regression test drives the exact failure scenario (strict resolver + SecretRef apiKey) for both permission levels. No issues found. No files require special attention. Reviews (1): Last reviewed commit: "fix(gateway): redact SecretRef apiKey th..." | Re-trigger Greptile |
Live verification on Lobster (2026-04-27)Fast-patched the PR build onto Lobster's gateway and walked the full reproduction → fix → smoke → restore cycle against the issue's
Pre-fix error matches issue #72496 verbatim; post-fix |
…owing
The talk.config discovery RPC was handing the source-snapshot's
talkProviderConfig (with the unresolved SecretRef wrapper still on
apiKey) to speechProvider.resolveTalkConfig. ElevenLabs/OpenAI's
strict normalizeResolvedSecretInputString helper threw 'unresolved
SecretRef' there, so iOS / macOS / Control UI Talk overlays never
learned the configured provider and silently fell back to local
AVSpeechSynthesizer ('robot voice') even though talk.realtime.session
and talk.speak both worked end-to-end with the same SecretRef.
Prefer the runtime-resolved provider config when calling
resolveTalkConfig, strip the apiKey field if it's still a SecretRef
wrapper at the call site, and restore the source-shaped apiKey onto
the response so the UI keeps the SecretRef context. Redaction strips
the value when includeSecrets=false.
Adds a regression test using a strict resolver speech provider that
mirrors ElevenLabs/OpenAI behavior so the path stays covered for
SecretRef apiKeys.
Fixes #72496
Thanks @omarshahine
733f8e5 to
783f44c
Compare
Summary
Fixes #72496.
talk.configwas throwingunresolved SecretRefwhenevertalk.providers.<id>.apiKeywas 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.sessionandtalk.speakboth worked with the same SecretRef — so this was purely a discovery-path bug.Root cause:
resolveTalkResponseFromConfiginsrc/gateway/server-methods/talk.tshanded the source-snapshot'stalkProviderConfig(with the SecretRef wrapper still onapiKey) tospeechProvider.resolveTalkConfig({...}). ElevenLabs's resolver (and OpenAI's, and any provider that uses the strict secret-input helpers) then callednormalizeResolvedSecretInputStringon the wrapper, which threw on the unresolved SecretRef.Fix
resolveTalkConfig(substituted strings in production paths).apiKeyfield if it's still a SecretRef wrapper before handing it to the provider, so strict secret-input helpers never see an unresolved wrapper.apiKey(the SecretRef) ontoresolved.configin the response so the UI keeps the SecretRef context. Existing redaction sentinels the value whenincludeSecrets: false.Adds a regression test that registers a speech provider mirroring ElevenLabs/OpenAI's strict resolver behavior and drives a SecretRef apiKey through
talk.configfor both read-only andincludeSecrets: truecallers.Test plan
pnpm test src/gateway/server.talk-config.test.ts— 9/9 pass; new test fails onunresolved SecretRefwithout the fixpnpm check:changed(typecheck core/core-tests, lint, import cycles, guards) — greenpnpm test:changed— greenopenclaw gateway call talk.configreturns provider snapshot → iOS/macOS Talk overlays use ElevenLabs voice instead of AVSpeechSynthesizer🤖 Generated with Claude Code