HuggingFace token (ApiKeysPanel):
- "Test now" actually re-runs whoami: GET /api/settings/hf-token/state gains
?fresh=1 which drops the resolver's 300s validation cache (the invalidate
hook existed but was never wired to any endpoint), so a fixed network or
rotated token no longer shows a stale verdict for up to 5 minutes. Plain
panel mounts keep the cache.
- Initial load renders a "Checking token sources…" placeholder instead of
flashing a false amber "not set" for all three sources.
- Source rows are now a valid ARIA list (the old role="table" had rows with
no cells, hiding the status from screen readers).
- Enter in the token input respects the in-flight guard the Save button
already had (no duplicate POSTs).
LLM Providers:
- Test / Fetch models abort when the implicit save fails, instead of probing
the previously-stored config and pairing a green "Test ok" badge with a
save error.
- A failed initial load now offers a Retry button instead of dead-ending
until the panel remounts.
LLM Skills: the per-skill provider Select carries an accessible name
("Provider for <skill>") instead of announcing as an unlabeled combobox.
MCP voice bindings:
- All user-facing strings go through i18n (the panel was the only Settings
surface with hardcoded English throughout).
- First-run guidance moved out of per-row hints (which never rendered with
zero bindings and duplicated per row) into the section header + an
InfoHint that links to docs/mcp.md; an empty state invites the first add.
- Delete asks for confirmation via the shared askConfirm, disables the row's
button while in flight, and re-syncs the list even when the DELETE fails
(a 404 row no longer lingers on screen).
- The add row exposes the optional label the API already accepted (the row
title rendered b.label without any way to set it); default_engine stays
MCP-side-only and is documented as such.
- First component test file for the panel (load/empty/add/delete/error/a11y).
Tests: backend fail-before/pass-after for the fresh=1 cache bust; new
frontend coverage for every behavioral change above.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(01-01): encrypted settings store + alembic migration (AUTH-02, T-01-01)
Adds the SQLite-backed encrypted settings store that Phase 1 token resolver
will read from. Closes the at-rest plaintext risk for HF tokens (T-01-01).
- backend/services/settings_store.py: get_hf_token / set_hf_token /
clear_hf_token using Fernet symmetric AEAD. Stored value column never
contains the literal "hf_" substring.
- backend/services/_secret_key.py: per-install Fernet key derived via
scrypt(machine-id + 16-byte random salt). machine-id resolution covers
macOS (ioreg IOPlatformUUID), Linux (/etc/machine-id and dbus fallback),
Windows (HKLM Cryptography MachineGuid via winreg). Final fallback to
hostname+user with a warn log.
- backend/migrations/versions/0001_phase1_settings_table.py: alembic
migration adding `settings(key, value, updated_at)`. Idempotent — checks
for an existing table so fresh installs (where _BASE_SCHEMA already
created it) and v0.2.7 upgrades both succeed.
- backend/core/db.py: _BASE_SCHEMA grows the settings table for fresh
installs; init_db() now runs `alembic upgrade head` after the CREATE.
- backend/migrations/env.py: honours an externally-set sqlalchemy.url so
tests can point alembic at a fixture DB; falls back to core.config
DB_PATH for production.
- pyproject.toml: cryptography>=41 added explicitly (RESEARCH.md
Assumption A1 was checked at execute-time and proved false; the dep was
not present transitively, so the install would fail without this).
Tests (10 cases, all green):
- Round-trip encryption + plaintext-leakage check (T-01-01 invariant)
- Salt persistence across clear/set cycles
- InvalidToken decrypt path returns None (Open Question #5 resolution)
- Concurrent reads consistent under sqlite WAL
- Alembic upgrade on a hand-built v0.2.7 fixture DB preserves all
existing tables + seeded rows (CLAUDE.md backward-compat constraint)
- Alembic downgrade -1 drops only the settings table
Refs #35.
* feat(01-01): 3-source HF token resolver + log redactor + 5 read sites patched
Closes the #35 bug class (bare os.environ.get('HF_TOKEN') reads) by routing
every backend HF-token consumer through one resolver, and mitigates
T-01-02 (info disclosure via logs) by stripping `hf_[A-Za-z0-9]{30,}`
substrings from every log record at the root logger.
backend/services/token_resolver.py:
- resolve(skip) — 3-source cascade (App → Env → HF-CLI), each source
validated via huggingface_hub.whoami(); first valid wins.
- on_401(active) — invalidate cache and re-resolve skipping the source
that just 401'd (AUTH-06).
- state() — three SourceState rows for the Settings UI: set,
masked preview (hf_…<last 3>), whoami_user, whoami_ok.
- save_app_token / clear_app_token — wraps settings_store + calls
huggingface_hub.login(add_to_git_credential=False) per Pitfall #2.
- 300-second whoami cache so repeated Settings-page renders don't hit
the HF API.
backend/core/logging_filter.py:
- HFTokenRedactor(logging.Filter) — regex `hf_[A-Za-z0-9]{30,}` so real
tokens are masked but `hf_hub` / `hf_token` literals survive.
- install_redaction_filter() — idempotent attach to root + every handler.
backend/main.py: install the redactor at startup, BEFORE the file
handler is added. Re-installed after the file handler attaches so the
handler-attached filter list includes it too.
Read-side call sites patched (per Pitfall #1 — every HF token read must
flow through token_resolver.resolve()):
- backend/api/routers/dub_core.py:540 (the original #35 site)
- backend/api/routers/system.py:38 (_has_hf_token notification)
- backend/services/model_manager.py:480 (diarization pipeline auth)
- backend/services/sonitranslate.py:143 (Popen env for SoniTranslate child)
- backend/services/sonitranslate.py:217 (gradio_client predict call)
New endpoint:
- GET /system/hf-token/state — returns the 3-source cascade state with
masked tokens for the Wave 2 Settings UI panel.
Grep gate confirmed clean: zero `os.environ.get("HF_TOKEN")` reads remain
outside token_resolver.py.
Tests (17 new cases, all green):
- tests/backend/services/test_token_resolver.py: priority cascade, 401
skip mid-resolve, on_401 fallback, state() shape, save+login
invariant (add_to_git_credential=False), HUGGING_FACE_HUB_TOKEN
alias acceptance.
- tests/backend/core/test_logging_filter.py: msg + args redaction,
multi-token redaction, non-string args pass-through, short-token
literals preserved, install_redaction_filter idempotence.
Refs #35.
* feat(01-01): Settings hf-token API endpoints + subprocess env injection (AUTH-03/04)
Backend half of the Wave 2 Settings → API Keys UI plus the AUTH-04
subprocess env-injection invariant.
backend/api/routers/settings.py:
- POST /api/settings/hf-token — body {token: str} → save_app_token
- DELETE /api/settings/hf-token — also_clear_hf_cli query → clear_app_token
- GET /api/settings/hf-token/state — same shape as token_resolver.state()
All three are gated by `Depends(require_loopback)` at the router level
(threat T-01-03 mitigation; non-loopback Host → 403).
backend/main.py: router mounted alongside existing API routers.
Subprocess env injection (AUTH-04, threat T-01-04 disposition=accept):
- backend/services/sonitranslate.py already updated in Task 2 to read
via token_resolver.resolve() and inject HF_TOKEN + YOUR_HF_TOKEN into
the SoniTranslate child env block.
- backend/services/gpu_sandbox.py: NOT patched — the GPU sandbox runs
in-process TTS generation that uses the parent's already-loaded HF
state. Adding env injection there is a no-op (parent and child share
state via multiprocessing.Pipe before any HF API call).
- backend/services/model_manager.py:480 (Task 2): resolves in-process,
no subprocess crosses here.
- backend/api/routers/exports.py: subprocess.Popen calls only spawn
`open` / `explorer` / `xdg-open` — file-manager launchers with no
HF needs. Skipped per Task 3 conservative-patching rule.
So the canonical AUTH-04 site for this milestone is sonitranslate.py.
Future SubprocessBackend work in Phase 2 will inherit the same pattern.
Tests (8 new cases, all green):
- tests/backend/test_engine_spawn_token.py
* POST /hf-token loopback → 200 + state.active == "app"
* POST /hf-token non-loopback → 403 ("loopback origin required")
* DELETE /hf-token clears settings_store + state.active == None
* GET /hf-token/state returns 3 source rows in priority order
* GET /hf-token/state non-loopback → 403
* env block contains HF_TOKEN + YOUR_HF_TOKEN when resolver returns one
* env block does NOT contain an injected empty HF_TOKEN when resolver
returns None
* source-level check that backend/services/sonitranslate.py still
reads via token_resolver.resolve() (regression guard against
silent reverts of the AUTH-04 wiring)
Full Wave 1 test suite: 35/35 green. Phase 0 smoke tests still green.
Refs #35.
* docs(01-01): SUMMARY + STATE update for Phase 1 Wave 1 completion
Records execution outcome of the 3-task plan: 10 files created, 9 modified,
35 new test cases, 5 read sites patched, grep gate clean. Documents the
two Rule-3/Rule-2 deviations applied (cryptography dep, env.py URL
override), the subprocess-launcher inventory for Phase 2, and the
known stray edit to the main repo's pyproject.toml that needs a one-
line user action to revert.
Updates STATE.md current-position table, progress bar, and open TODOs to
point at Wave 2 (Plan 01-02) and Wave 3 (Plan 01-03) as the next steps.