Repository navigation
bug(voice): the TTS stream WebSocket sends no credential, so the authenticated /api/voice/stream refuses every connection #17004
Description
Activity
Chunking triage — a proposal, not an assignment
- Proposed priority:
priority: high— not applied. Setting it is the milestone owner's call. - triage(v0.9.0): release criticality of the 123 open issues — 26 blocking, 36 small, 18 umbrellas, 41 misfiled, 2 undetermined #17639 criterion met: a dead user-facing path — voice output opens its stream socket with no credential, so the authenticated route refuses every connection
- triage(v0.9.0): release criticality of the 123 open issues — 26 blocking, 36 small, 18 umbrellas, 41 misfiled, 2 undetermined #17639 bucket: 1 — genuinely blocking a v0.9.0 release
- Umbrella / container: no.
- Pre-filter: clean — no merged commit on
origin/mainsince 2026-08-14 references this issue. - Basis: triage(v0.9.0): release criticality of the 123 open issues — 26 blocking, 36 small, 18 umbrellas, 41 misfiled, 2 undetermined #17639's per-issue row, reused rather than re-derived — "Voice output (TTS) opens its stream socket with no credential, so the authenticated route refuses every connection (static reading; runtime use unconfirmed)."
Nothing was relabelled, moved or closed by this pass.
- Proposed priority:
B6 spec — two voice surfaces whose failure looks like nothing to do
Batch B6 = #17004 → #17734's client half, one risk class, one review pass. Full spec at
~/b6-spec-2026-09-28.md.#17004 — and the fix is a design choice, not a one-liner
useVoiceOutput.ts:665-666opensnew WebSocket(url)with no credential. The mechanical constraint: the browserWebSocketconstructor cannot set headers, and this app's credential is a Bearer token in anAuthorizationheader (utils/fetchWithAuth.ts:25,:35-36). So the token cannot be attached the way every REST call attaches it, and the fix has to pick a mechanism: a short-lived ticket as a query parameter, theSec-WebSocket-Protocolsubprotocol, or a required first-message handshake.Whichever is chosen, match
open_authenticated_ws— the mechanism the workflow socket already uses (services/workflow_automation/ws_endpoint.py). A second WebSocket auth scheme in this repo is the #17693 shape in a new place.Tests: client half in
useVoiceOutput's own__tests__; server half besideapi/ws_auth_17009_test.py, the established home for "this socket refuses an unauthenticated connection". Mutation: revert to the barenew WebSocket(url)→ the server test shows a refusal and the client test goes red. Negative control: an authenticated connection still succeeds, or the fix has just broken voice output.#17734's client half — and the test that exists today is the half-assertion
AC1 is met (route exists, mounted, authenticated).
_fetchTools(useRealtimeVoice.ts:110-125) returns[]on both!res.okand an exception, so the endpoint failed and there are no tools are the same value.Two assertions, and the second is pinned by nothing today:
- a non-2xx and a thrown exception each produce a failure, not
[]; - a successful fetch of a non-empty toolset produces that toolset.
useRealtimeVoice.test.ts:208treats an empty list as a state — it passes for a real empty toolset and for a failed fetch, so it must be split, not extended. Mutations: return500→ assertion 1 red; return200 []→ assertion 2 red. A single test that accepts[]passes both, which is exactly why this AC is 1/4 and not 2/4.Do not close #17734 on this batch alone — the route half is already done, and
:208's half-assertion is part of the remainder.- a non-2xx and a thrown exception each produce a failure, not
Code and test criteria delivered by #17954 (merge
7f8e7385db). Verified againstorigin/main. The issue stays open for AC3.-
useVoiceOutput.tsauthenticates its WebSocket the same way the other migrated clients do after security(websocket): auth token travels via Sec-WebSocket-Protocol, not the URL (#16457) #16891/security(websocket): every authenticated WebSocket echoes the bearer subprotocol through one helper (#16457) #16939:new WebSocket(url, buildAuthenticatedWsSubprotocols()). No URL token.composables/useVoiceOutput.ts:760:new WebSocket(url, buildAuthenticatedWsSubprotocols() ?? undefined). With no token yet,?? undefinedavoids sending the literal protocol"null".- The URL is still
${getBackendWsUrl()}/api/voice/streamwith no query string.
- A frontend test asserts the socket is opened with the bearer subprotocol.
composables/__tests__/useVoiceOutput.auth.test.ts, caseopens /api/voice/stream with the bearer subprotocol and no URL token(expects['bearer', 'tok-123']).- Same file, case
passes no protocol list, never the string "null", when no token exists yet.
- Runtime evidence: a TTS stream connects and stays open on a running instance, or the feature is confirmed unused and the decision recorded.
- Not met yet. This needs host evidence after the builtin updater deploys
7f8e7385db: enable voice output and confirm the socket stays open with no 4001 close.
- Not met yet. This needs host evidence after the builtin updater deploys
-
What
The frontend's text-to-speech stream opens its WebSocket with no credential at all:
autobot-frontend/src/composables/useVoiceOutput.ts:665-666It sends no
?token=and noSec-WebSocket-Protocol. The backend route (autobot-backend/api/voice_stream.py,voice_stream_ws, mounted at/voice) callsauthenticate_websocketand, when that fails, accepts then closes with 4001 (#15745).authenticate_websockethas no cookie fallback. So by static reading, every real TTS stream connection is refused.Not determined
This is from reading the code, not observed at runtime. The deployment's access logs show no requests to
/api/voice/streamat all (checked read-only on 2026-09-18), so there is no runtime evidence either way. The feature may simply be unused. Confirm on a running instance: enable voice output and watch for a 4001 close.Acceptance criteria
useVoiceOutput.tsauthenticates its WebSocket the same way the other migrated clients do after security(websocket): auth token travels via Sec-WebSocket-Protocol, not the URL (#16457) #16891/security(websocket): every authenticated WebSocket echoes the bearer subprotocol through one helper (#16457) #16939:new WebSocket(url, buildAuthenticatedWsSubprotocols()). No URL token.Found by the independent review of #16939 (WebSocket subprotocol auth). Related: #16457.