feat(bridge-auth): send X-Admin-Token on all /xiaozhi/admin/* calls#152
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR completes the bridge-side wiring for the admin-auth epic by conditionally attaching an X-Admin-Token header (sourced from DOTTY_ADMIN_TOKEN) to bridge→xiaozhi /xiaozhi/admin/* requests, staying a no-op until the coordinated enforcement flip.
Changes:
- Add
_xiaozhi_admin_headers()helpers (backed by module-level_ADMIN_TOKEN) inbridge.pyandbridge/dashboard.py. - Thread the helper through all updated bridge/dashboard
/xiaozhi/admin/*HTTP call sites (requests + urllib). - Add unit tests covering header helper behavior and one integration path through
_dispatch_abort.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
bridge.py |
Adds admin-token helper and applies it to bridge-side admin POST dispatchers. |
bridge/dashboard.py |
Adds admin-token helper and applies it to dashboard admin GET/POST calls (urllib + requests). |
tests/test_admin_headers.py |
Adds tests validating helper behavior and header propagation via _dispatch_abort. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+169
to
+176
| _ADMIN_TOKEN = os.environ.get("DOTTY_ADMIN_TOKEN", "").strip() | ||
|
|
||
|
|
||
| def _xiaozhi_admin_headers() -> dict: | ||
| """X-Admin-Token header for /xiaozhi/admin/* requests when DOTTY_ADMIN_TOKEN | ||
| is set (matches the xiaozhi-server middleware); empty dict otherwise, so the | ||
| bridge is a no-op until the coordinated enforcement flip.""" | ||
| return {"X-Admin-Token": _ADMIN_TOKEN} if _ADMIN_TOKEN else {} |
| _ADMIN_TOKEN = os.environ.get("DOTTY_ADMIN_TOKEN", "").strip() | ||
|
|
||
|
|
||
| def _xiaozhi_admin_headers() -> dict: |
Admin-auth epic, part 4 of 4 (bridge caller — the scattered one). Pairs with the permissive /xiaozhi/admin/* middleware (part 1). Adds a `_xiaozhi_admin_headers()` helper (reads DOTTY_ADMIN_TOKEN into a module-level `_ADMIN_TOKEN`) to both bridge.py and bridge/dashboard.py, threaded through every admin call site: - bridge.py: _dispatch_abort / _dispatch_set_state / _dispatch_set_toggle and the dashboard _dashboard_abort_device / _dashboard_inject_to_device helpers (5 POSTs). - dashboard.py: _xiaozhi_device_count + _xiaozhi_list_songs (urllib GETs, now via urllib.request.Request with headers) and play_song's play-asset POST (3 sites). Calls to dotty-behaviour (vision photo proxy, perception getters) are deliberately NOT given the header. Header only sent when the token is set — no-op until the flip. Tests: new tests/test_admin_headers.py (6 cases) — both helpers set/unset, and an integration through _dispatch_abort capturing the headers kwarg. Full tests/ 71 passed; ruff clean. Completes the foundation: #149 (server) + #150 (behaviour) + #151 (pi-ext) + this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e579b98 to
30706cc
Compare
This was referenced Jun 6, 2026
BrettKinny
added a commit
that referenced
this pull request
Jun 11, 2026
…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>
BrettKinny
added a commit
that referenced
this pull request
Jun 11, 2026
…ADMIN_TOKEN end-to-end (#158) * feat(auth): plumb DOTTY_ADMIN_TOKEN end-to-end (env, compose, setup, 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> * feat(kid-mode): wire shared content_filter into the live voice path (#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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Admin-auth epic — part 4 of 4 (bridge caller, the scattered one). Pairs with the permissive
/xiaozhi/admin/*middleware (#149). Offmain. Completes the foundation.Adds a
_xiaozhi_admin_headers()helper (readsDOTTY_ADMIN_TOKENinto module-level_ADMIN_TOKEN) to bothbridge.pyandbridge/dashboard.py, threaded through every admin call site:_dispatch_abort/_dispatch_set_state/_dispatch_set_toggle, plus the_dashboard_abort_device/_dashboard_inject_to_devicehelpers._xiaozhi_device_count+_xiaozhi_list_songs(urllib GETs →urllib.request.Requestwith headers) andplay_song's play-asset POST.Calls to dotty-behaviour (vision photo proxy, perception getters) deliberately do not get the header. Header only sent when the token is set — no-op until the enforcement flip.
Tests: new
tests/test_admin_headers.py(6 cases) — both helpers set/unset + an integration through_dispatch_abortcapturing the headers kwarg. Fulltests/71 passed; ruff clean.Epic status
Foundation complete across all 4 services: #149 (server middleware) · #150 (behaviour) · #151 (pi-ext) · this (bridge).
Next — the enforcement flip (separate ops step, when you're ready): set the same
DOTTY_ADMIN_TOKENin all 4 container envs, deploy callers first (bridge/behaviour/pi-ext), xiaozhi last. Until then everything is a no-op.🤖 Generated with Claude Code