Kid Mode: content filter on the live voice path (#157) + plumb DOTTY_ADMIN_TOKEN end-to-end#158
Merged
Merged
Conversation
…docs) The X-Admin-Token chain (#149-#152) shipped code-complete but operationally dead: five services read DOTTY_ADMIN_TOKEN, zero config surfaces provided it, so every deploy silently stayed permissive and the admin routes remained unauthenticated. - make setup: create .env from .env.example if missing; generate a 32-byte hex token when no DOTTY_ADMIN_TOKEN line exists (idempotent; an existing line — even deliberately empty — is respected) - docker-compose.yml.template + compose.all-in-one.yml: pass ${DOTTY_ADMIN_TOKEN:-} to xiaozhi-server - dotty-pi/docker-compose.yml: add optional env_file (the container had no env mechanism at all, so adminFetch could never receive the token) - bridge/ + dotty-behaviour/ compose: document the token among the deploy-dir .env secrets - .env.example + SETUP.md §10: enable-everywhere-or-nowhere semantics - CHANGELOG: record the #149-#152 chain (previously absent) + this fix Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…157) Lift the pure blocked-content core (three tier regexes + the kid-safe replacement) out of bridge/text.py into the shared custom-providers/textUtils.py — already the canonical safety-constants home both containers import — and add content_filter_match() plus a sentence-buffered filter_tts_stream() wrapper. The bridge keeps its metrics/safety-ring/logging wrapper on top, behaviour unchanged. Both live providers now run TTS-bound output through the filter, gated on kid_mode: - streaming decision (per the issue's open question): buffer to _SENTENCE_BOUNDARY — the tier patterns are single-word regexes, so a blocked term can never straddle a sentence boundary; nothing reaches TTS before its sentence is checked, and spoken latency is unchanged (the TTS layer buffers to sentences anyway) - on a hit the rest of the turn is replaced with the cheerful redirect; mid-turn replacements drop the redirect's leading emoji to preserve the one-emoji-per-reply firmware contract - kid_mode off is a transparent passthrough (zero behaviour change) - OpenAICompat's emoji-prefix enforcement runs before the filter, so the leading-glyph contract survives filtering Tests: tests/test_voice_content_filter.py (16 cases — core matcher, stream wrapper incl. chunk-straddling terms, bridge wrapper parity, and both providers hit/clean/kid-off, mirroring the #146 pattern); plus a guard that the tier regexes exist in only one file. docs/faq.md updated to describe the filter honestly as a weak, bypassable backstop. Still open on #157 before release sign-off: on-device red-team verification (bench-pending). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fd2fe41 to
d083f59
Compare
Open
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two commits, both pre-release items from the 2026-06-11 architecture review:
1.
feat(auth)— plumbDOTTY_ADMIN_TOKENend-to-endThe #149–#152 X-Admin-Token chain shipped code-complete but operationally dead: five services read
DOTTY_ADMIN_TOKEN, but no config surface provided it (not.env.example, not any compose file, notmake setup, not the docs — anddotty-pi's compose had no env mechanism at all). Every deploy silently stayed permissive, i.e. the admin routes remained unauthenticated.make setupnow generates a 32-byte hex token into.env(idempotent — an existingDOTTY_ADMIN_TOKENline, even deliberately empty, is respected)${DOTTY_ADMIN_TOKEN:-}to xiaozhi-server;dotty-pigains an optionalenv_file; bridge/behaviour compose comments name the token among their deploy-dir.envsecrets.env.example, newSETUP.md§10, and a CHANGELOG entry that also retroactively records the feat(xiaozhi-auth): gate /xiaozhi/admin/* behind X-Admin-Token (permissive) #149–feat(bridge-auth): send X-Admin-Token on all /xiaozhi/admin/* calls #152 chain2.
feat(kid-mode)— implement #157 per its speccontent_filter_match, tier regexes, replacement) lifted frombridge/text.pyinto the sharedcustom-providers/textUtils.py; the bridge keeps its metrics/ring/logging wrapper on top, behaviour unchanged — no regex duplication (test-enforced)filter_tts_stream(): nothing reaches TTS before its sentence is checked (word-level patterns can't straddle a sentence boundary; chunk-straddling terms are caught); a hit replaces the rest of the turn with the cheerful redirect, minus its leading emoji mid-turn to keep the one-emoji firmware contract;kid_modeoff is a transparent passthroughPiVoiceLLM,OpenAICompat) wrap their TTS-bound streams; OpenAICompat's emoji enforcement runs first so the leading-glyph contract survivesdocs/faq.mdupdated: the filter is described honestly as a weak, bypassable backstop — prompt steering stays the primary defenceAcceptance (#157)
textUtils.py;bridge/text.pyimports it; no regex duplicationpi_voiceblocks onkid_mode+ tier hit; clean text passes untouchedopenai_compatsamekid_modeoff → zero behaviour changetests/test_dashboard_say_filter.pyTest plan
ruff check .cleanpytest tests/ custom-providers/pi_voice/tests/(119) +pytest dotty-behaviour/tests/ --cov-append(213) — all pass, combined coverage 69.4% (gate 56%)tests/test_voice_content_filter.py(core matcher, stream wrapper, bridge parity, both providers)Refs #157 (leaves the on-device red-team box open — close the issue after bench verification, not on merge). Also advances #138.
🤖 Generated with Claude Code