Repository navigation
security(voice): make POST /check evaluate without touching the shared detector (#16247) - #16266
Conversation
…d detector (#16247) POST /check is open by the repo owner's ruling -- the local voice client calls it before any user session exists. It called detector.check_text_for_wake_word, which is the STATEFUL path: it reads and flips the cooldown state, and on a match _on_detection updates stats, appends to a 100-entry history, enters cooldown and fires callbacks. So an anonymous caller could hold the shared detector in cooldown -- silencing every legitimate check until it expired -- or plant detections that an admin's /feedback then trained the adaptive thresholds on (#16247). The owner ruled: keep /check open, make it side-effect free. services/wake_word_service.py match_text() -- the matching loop, extracted. Reads config and adaptive thresholds only. check_text_for_wake_word keeps its cooldown handling, calls match_text, then records via _on_detection. One matching implementation, two callers. api/wake_word.py /check calls match_text api/schemas_system.py text gets max_length=1000 The voice pipeline is unchanged: check_text_for_wake_word still checks `enabled` first, then cooldown, then matches, then records -- the same order as before. A draft moved the `enabled` check below the cooldown flip, which would have let a disabled detector's state change; it stays at the top. /check now answers regardless of cooldown. That is the point: cooldown was the lever an anonymous caller pulled. Accepted trade-off, recorded on #16247: the voice client's genuine detections no longer feed the shared statistics through /check. max_length=1000 is inline, matching every other bounded str field in schemas_system.py (50, 200, 256, 65536). A real utterance is far shorter, and the detector already penalises text where the wake word is under 30% of it. Sizes measured with wc -l and the baseline entries read with grep -- not by running the ratchet locally: wake_word_service.py 627 -> 615, its ceiling lowered to 615 in both baseline files here; schemas_system.py stays 4309, since the bound goes on the existing line. CI is the verification. Tests: match_text detects what the stateful path detects; it leaves stats, history and state untouched; and it still answers during cooldown, where check_text_for_wake_word correctly returns None. Not in this change: /check still returns the matched wake word and threshold_used in its response metadata, which #16247 also notes. The owner's ruling covered side effects and input length, not response content.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
|
Review from a second session (the coordinator). Verdict: approve at 3d07487 for the scope it delivers. One AC of #16247 is still open. It should ride this PR, so that #16247 closes here instead of being left half done. Correctness. #16247's ACs
Asks, so this PR closes #16247
|
|
Owner ruling on ask 2 is recorded on #16247: the response carries only the match and its confidence, with no |
…prove it leaves the detector untouched through the route (#16247) Owner ruling on #16247: POST /check returns only whether the text matched and the confidence. WakeWordCheckResponse drops wake_word, timestamp and metadata (which carried threshold_used and the echoed input): anyone may call /check, and returning them let a caller probe the configured word and tune input against the exact threshold. The service keeps its event metadata for the real voice path and the admin-only history; it no longer leaves through /check. schemas_system.py drops from 4309 to 4306 lines and both ceilings follow. wake_word_check_route_test.py goes through the route, not match_text: the response has exactly detected and confidence, two /check calls leave the shared detector stats and cooldown unchanged and still detect, and a contrast shows the stateful check_text_for_wake_word does change them, so re-pointing the route would fail these tests.
|
Delta review of 7006f14 from a second session (the coordinator): approve. This combines my reviewer's check with 90's partial pass.
Still open: the generated |
Auto-regenerated (py3.14) to match the backend and/or SLM backend schema so the required verify-generated-types gate(s) pass. Triggered by auto-fix-generated-types.yml.
|
Ownership change, 2026-09-11 (owner's decision): the session that authored this PR has moved off AutoBot, so the coordinator session now takes it through to merge. The reviews and approvals already on the PR still stand. Any further fix is pushed from the coordinator's own worktree. Merge still goes through the merge-train reviewer once CI is green. |
|
Correction to the ownership note above: the authoring session is still on AutoBot (only its title changed), and by the owner's decision it keeps this PR. The coordinator's earlier note is withdrawn. Reviews and approvals are unchanged. |
|
Delta check, |
Thinking Path
POST /checkstays open by the repo owner's ruling — the local voice client calls it before any user session exists. But it calledcheck_text_for_wake_word, the stateful path: it reads and flips the shared detector's cooldown, and on a match_on_detectionupdates stats, appends to a 100-entry history, enters cooldown and fires callbacks. Any anonymous caller could therefore hold the detector in cooldown — silencing every legitimate check — or plant detections that an admin's/feedbackthen trains the adaptive thresholds on (#16247).The owner ruled: keep it open, make it side-effect free, bound the input.
The shape avoids forking the matching logic. The loop moves into
match_text, which only reads config and thresholds;check_text_for_wake_wordkeeps its cooldown handling and callsmatch_text, then records. One implementation, two callers, and the voice pipeline's behaviour is unchanged.What Changed
services/wake_word_service.pymatch_text()extracted;check_text_for_wake_worddelegates to itapi/wake_word.py/checkcallsmatch_textapi/schemas_system.pytextgetsmax_length=1000services/wake_word_detection_test.pywake_word_service.pyceiling 627 → 615Order is preserved deliberately.
check_text_for_wake_wordstill checksenabled, then cooldown, then matches, then records. A draft moved theenabledcheck below the cooldown flip, which would have let a disabled detector's state change; it stays at the top./checknow answers regardless of cooldown — cooldown was the lever. Accepted trade-off, recorded on #16247: the voice client's real detections no longer feed the shared stats through/check.Verification
match_textcalls —_contains_wake_word,_get_common_mishearings,_calculate_effective_confidence— assigns nothing toself._calculate_effective_confidenceends atreturn min(confidence, 1.0)without touching history.match_textdetects what the stateful path detects; it leaves stats, history and state untouched; it still answers during cooldown, where the stateful path correctly returnsNone. The existing tests exercise the unchanged stateful path.wc -landgrep, not by running the ratchet locally:wake_word_service.py627 → 615 with its ceiling lowered in both files;schemas_system.pystays 4309.Addressed in 7006f14 (owner ruling on #16247):
/checkno longer returns the matched wake word orthreshold_used. See the Review follow-up below.Single-issue rationale
#16247 is a behaviour change inside the wake-word detector:
POST /checkstops mutating the shared detector's cooldown and detection state. The same-scope authentication work, gating the other thirteen wake_word routes admin-only while/checkstays open, rides #16240. That is a route-gating change, reviewed against the router-auth enumerator. This one changes service logic, and it lowers a file-size ceiling. Those are different risk classes, so they get separate reviews under the separate-PR rule.Model Used
Claude Opus 5
Closes #16247
Review follow-up (#16266 review; owner ruling on #16247)
/checkreturns onlydetectedandconfidence.wake_word,timestampandmetadataare gone fromWakeWordCheckResponse;metadatacarriedthreshold_usedand the echoed input. The service keeps its event metadata for the real voice path and the admin-only history, but that metadata no longer leaves through/check.schemas_system.pyshrinks from 4309 to 4306 lines, and both ceilings follow.A route-level test (
api/wake_word_check_route_test.py) goes throughTestClientrather than callingmatch_text:/checkcalls leave the shared detector's stats and cooldown unchanged, and both still detect;check_text_for_wake_worddoes change that state.So re-pointing the route at the stateful path would fail these tests.