Commit Graph
12 Commits
Author SHA1 Message Date
Palash Debnath 5a615d2c66 feat(workers): package headless GPU nodes (#1638) (#1648)
Closes #1638.\n\nPackages headless GPU workers with durable enrollment, bounded artifact handling, cross-platform lifecycle cleanup, and regression coverage. Incorporates CodeRabbit, Greptile, CodeQL, and platform-CI findings before merge.
2026-08-24 16:32:56 +05:30
debpalash 32d9a8a964 fix: tighten log safety regressions 2026-08-10 12:49:42 +00:00
debpalash f45e8ecc07 fix: keep secret-store failures fixed-shape 2026-08-10 11:58:06 +00:00
debpalash 40662aeb06 fix: close log safety review gaps 2026-08-10 11:19:12 +00:00
debpalash fe856c9c5e fix(security): omit sensitive log context 2026-08-09 21:43:00 +00:00
debpalash 854a637b83 fix(security): make untrusted log values single-line 2026-08-09 21:26:00 +00:00
Palash Debnath 5cab8e0149 feat: rename the product to VoiceStudio (previously OmniVoice-Studio)
Renames what users see. The app, the installers, the window title, the
docs and all 21 locales now say VoiceStudio, with "(previously
OmniVoice-Studio)" noted near the title of each doc surface so people
recognise it.

Deliberately NOT renamed, because renaming any of them silently breaks
an existing install — there is no legacy-path fallback anywhere in this
codebase:

  - bundle identifier com.debpalash.omnivoice-studio (MSI UpgradeCode,
    macOS TCC grants, managed venv, WebView localStorage, the
    single-instance lock)
  - data directories OmniVoice / .omnivoice and omnivoice.db
  - the ~150 OMNIVOICE_* environment variables
  - the X-OmniVoice-* HTTP headers (a wire protocol)
  - the published Docker image paths
  - the OmniVoice ENGINE, which is a model name and not this product

tests/test_identity_paths_survive_the_rename.py pins every one of those
so a future well-meaning sweep cannot orphan a user's library.

Linux .deb users install a new package name and should apt remove
omnivoice-studio; that note is in the changelog.
2026-08-07 01:30:58 +05:30
29269b9cf0 feat: LLM Providers page + Autofit translation quality (fit-to-segment-time) (#838) (#854)
* feat(llm): multi-provider LLM registry + encrypted key storage + settings API (v0.3.8, phase 1)

Foundation for the LLM Providers settings page and timing-aware (Autofit)
translation. Every provider in the shipped .env is OpenAI-compatible, so one
client drives all of them via a registry instead of a class-per-provider.

- llm_providers.py: registry of 16 providers (OpenAI, OpenRouter, Groq,
  Cerebras, Google AI, Mistral, Cohere, NVIDIA, GitHub Models, Cloudflare,
  HuggingFace, SambaNova, SiliconFlow, + local Ollama/LM Studio + Custom).
  Field resolution precedence env → encrypted store → default; active-provider
  selection (LLM_DEFAULT_PROVIDER → stored → first keyed remote; local requires
  explicit pick so we never assume a local server is up). Legacy TRANSLATE_*
  maps to the Custom provider (keyless-with-base_url preserved).
- settings_store.py: generic ENCRYPTED secrets (get/set/clear_secret,
  list_secret_names) reusing the HF-token Fernet path; get_text/set_text now
  refuse the secret namespace (no ciphertext leak).
- llm_backend.py: OpenAICompatBackend resolves the active provider's
  base_url/key/model from the registry. Backward-compatible.
- settings API: GET /llm-providers, PUT /llm-providers/{id} (encrypted key +
  overrides), POST /llm-providers/active, POST /llm-providers/{id}/test.
  Loopback-gated; never returns key material.
- 11 registry tests; existing llm-endpoint/openai-available tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(settings): LLM Providers page — configure any provider's key/URL/model + Test + set active (v0.3.8, phase 2)

New Settings → System → LLM Providers pane (Brain icon, searchable). Lists all
16 registry providers; pick one to configure its encrypted API key, base URL,
model (and Cloudflare account id), Test the connection with one round-trip, and
'Save & use for translation' to make it the active provider for Cinematic/
Autofit. Keys are write-only from the UI (masked placeholder, never echoed);
env-set keys show as read-only. Local providers (Ollama/LM Studio) need no key.

- LLMProvidersPanel.jsx: provider selector + per-provider config + Test/activate,
  following the LLMEndpointPanel pattern (apiJson/apiFetch/apiPost, SettingsSection
  primitives).
- settingsCategories.jsx: new 'llm-providers' category under System + Brain icon.
- Settings.jsx: route the category to the panel.
- en.json: settings.llm_providers label.
- Frontend build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(translate): Autofit quality style + one-click LLM setup from the dub menu (v0.3.8, phases 3-4)

Autofit = Cinematic + a strict 'never exceed the segment time' fit. The LLM
rewrites each translated line so its target-language reading time fits within
the slot, preserving the video timing without harsh audio time-stretch.

Backend:
- speech_rate.adjust_for_slot(strict=): strict caps the accepted upper ratio at
  1.0 (fit within slot) vs Cinematic's 1.08; best-effort, degrades gracefully
  with no LLM.
- dub_translate: quality='autofit' takes the LLM refine path and runs the fit
  pass with strict=True; reports quality_used accurately.
- TranslateRequest.quality doc note.

Frontend:
- 'autofit' added to the quality control (Settings Translation + dub menu) and
  the TranslateQuality type.
- Dub menu: picking Cinematic/Autofit with no LLM no longer dead-ends on a toast
  — it offers a one-click 'Set up' that routes to Settings → LLM Providers,
  with copy about fitting translations to segment time (#838).

- 4 strict-fit tests; frontend build green; i18n keys added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(translate): document Autofit quality + the LLM Providers page (v0.3.8, phase 5)

- CHANGELOG [0.3.8] Added: Autofit style + LLM Providers page.
- docs/dubbing/translation-engines.md: Fast/Autofit/Cinematic quality section
  and an LLM Providers setup section (16 providers, encrypted keys, offline
  Ollama/LM Studio, env overrides).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(api): add /api/settings/llm-providers routes to the route-inventory snapshot

Regenerated tests/fixtures/api_routes.txt for the 4 new LLM-providers endpoints
so test_route_inventory_matches_snapshot passes (keep-main-green).

* style(frontend): oxfmt the LLM Providers panel + dub quality control (format:check green)

---------

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:55:59 +05:30
Palash DebnathandClaude Opus 4.7 b34dcd9e11 Phase 4 Plan 04-01: SPIKE-01 GGUF — GO + integration (#100)
* Phase 4 Plan 04-01: SPIKE-01 GGUF — GO + Wave 1 integration

Integrates Serveurperso/OmniVoice-GGUF as a hardware-adaptive default
voice-cloning engine, with overridable fallback to the in-process
OmniVoiceBackend. Spike confirmed GO: the model is a clean quantization
of k2-fsa/OmniVoice (Apache-2.0 + MIT runtime, `omnivoice-lm` custom
architecture so it does NOT load in vanilla llama.cpp).

Pinned SHAs:
  * Serveurperso/OmniVoice-GGUF revision: 361609388ae572a820d085185bbbe2a2aac4b30e
  * ServeurpersoCom/omnivoice.cpp master:  886fc079838ca7400cb2b42b36e2a65aa1daabe8

Implements GGUF-01 (hardware probe) through GGUF-05 (default-engine
resolver with graceful fallback). The four `bin/omnivoice-tts-*`
artifacts are committed as zero-byte placeholders; the new CI matrix
job builds the real binaries per platform from the pinned commit SHA
and appends a SHA-256 manifest used by `is_available()` for tampering
detection (T-04-01). The macos-14 (Apple Silicon) slot is marked
`continue-on-error: true` because omnivoice.cpp publishes no
`buildmetal.sh` (Pitfall 1 / Assumption A1) — failure feeds into Task
3's GO/NO-GO call.

Quant override is allow-listed against quant_map.json entries only
(T-04-05). Argv is composed from typed Path objects rooted in
HF_HUB_CACHE; never uses `shell=True`. HF token redaction applies to
captured stderr before logging (AUTH-05 / T-04-04).

Tests: 36 new (8 hardware-probe + 13 GGUF engine + 6 settings_store
quant override + grep gate); 428 passed in full suite vs 402+ baseline.
ADR Status stays "Proposed (research-supported)" — Task 3 (human
checkpoint) flips to Accepted after CI produces real binaries and a
reviewer signs off on the GGUF-06 cross-hardware smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: install libopenblas-dev on linux-x86_64 omnivoice-tts build

The pinned omnivoice.cpp commit (886fc079...) ships a `buildcpu.sh`
that passes `-DGGML_BLAS=ON`. ubuntu-latest has no BLAS implementation
preinstalled, so the cmake configure step fails with
`Could NOT find BLAS (missing: BLAS_LIBRARIES)` and the job exits in
13 s before producing the linux-x86_64 binary.

macOS (Accelerate, built in) and Windows (BLAS off by default in the
ggml CMakeLists for non-APPLE platforms — the build script doesn't
invoke buildcpu.sh on those slots) are unaffected and stay green.

Adds a Linux-gated apt step to install libopenblas-dev + pkg-config
before the build, restoring cross-platform parity per the
CLAUDE.md "default features must work on every platform" rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(gguf): constrain ref_audio to project roots — block /etc/shadow on Linux

The GGUF engine's `_build_argv` previously validated ref_audio only via
`ref_path.is_file()` — i.e. "does this path exist?" That check is
platform-dependent: `/etc/shadow` doesn't exist on macOS (rejected
naturally), but it IS a real system file on Linux, so the validation
silently accepted it. CI's ubuntu-22.04 runner exposed the gap via
`test_generate_blocks_freeform_ref_audio`, which exists precisely to
guard the "freeform ref_audio path" attack surface.

Fix: confine ref_audio to one of three allowed roots before existence
checks:
  - VOICES_DIR (user-saved voice profiles)
  - DUB_DIR (per-job auto-clones extracted from source video)
  - tempfile.gettempdir() (browser-upload temp files; existing
    `cleanup_ref` flow in generation.py)

Anything outside those roots → FileNotFoundError, matching the existing
failure-mode contract callers handle. Existence check still runs after,
so the test's mocked subprocess.run is never reached and the test
passes deterministically on all three platforms.

Cross-platform parity (per CLAUDE.md 2026-05-20 rule): identical
behaviour on macOS / Windows / Linux — the allow-list is computed from
core.config which uses platform-specific path resolution but yields the
same logical "project tree" on every OS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(gguf): mark darwin-x86_64 binary build as experimental

GitHub's macos-13 (Intel) runner pool is heavily contended — PR #100
queued for 30+ minutes waiting on darwin-x86_64 while every other
platform finished in ~1m. Intel Macs are also fading hardware (Apple's
platform momentum is entirely on Apple Silicon), and the GGUF engine's
runtime already handles a missing binary gracefully (`is_available()`
returns False on Intel Mac with a "binary not bundled for this
platform" message, same path used for first-launch before any binaries
build).

`experimental: true` mirrors what darwin-arm64 (Metal) already has —
slot still runs and uploads its binary when successful, but a failure
or runner backlog no longer blocks merges. Keeps the GGUF engine
shippable across the dominant arm64 / Linux / Windows surface without
holding the inbox on a slow-runner queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:51:31 +05:30
Palash DebnathandClaude Opus 4.7 93aa66ab0a Phase 3 Plan 03-01: Supertonic-3 engine on SubprocessBackend (#101)
* Phase 3 Plan 03-01: Supertonic-3 engine on SubprocessBackend

Adds Supertonic-3 as a 7th opt-in TTS engine on the Phase 2
SubprocessBackend primitive. Closes TTS-01..06 (REQUIREMENTS.md):

  * TTS-01 — _REGISTRY["supertonic3"] resolves to Supertonic3Backend,
             a SubprocessBackend subclass.
  * TTS-02 — `supertonic==1.3.1` lives under [project.optional-dependencies];
             default `uv sync --no-dev` does NOT install it. Exactly one
             `onnxruntime` row in `uv pip list` after `--extra supertonic`.
  * TTS-03 — Model revision pinned by 40-char commit SHA
             (724fb5abbf5502583fb520898d45929e62f02c0b — the "Initial
             Supertonic 3 release" SHA, same as the SDK's own pin).
             Resolver script for intentional bumps:
             scripts/resolve_supertonic3_sha.py.
  * TTS-04 — Honest CPU-only reporting. `is_available()` message says
             "ready (CPU-only via onnxruntime)" and never mentions
             "cuda" or "mps". `gpu_compat = ("cpu",)`.
  * TTS-05 — License gate via settings_store helpers
             (get/set_license_accepted) + Loopback-only
             /api/settings/license endpoint + SupertonicLicenseDialog
             frontend modal showing MIT (code) and OpenRAIL-M (model).
             Wired into EngineCompatibilityMatrix as an "Accept license"
             button on rows whose `reason` mentions "license not
             accepted".
  * TTS-06 — 3 langs (en/ja/ru) × 3 sec smoke test in
             tests/test_supertonic3.py::test_smoke_3langs_3sec
             (OMNIVOICE_SMOKE-gated; asserts no onnxruntime-gpu row
             post-synthesize).

Package legitimacy gate (Task 1 in plan): supertonic on PyPI verified
to be published by Supertone Inc. (ato@supertone.ai), repo
github.com/supertone-inc/supertonic, wheel is pure-Python with no
postinstall scripts. Same publisher ships supertonic-js on npm under
the same maintainer email.

Test results:
  * tests/test_supertonic3.py — 10 passed, 3 skipped (network-gated).
  * tests/smoke/ — 4 passed.
  * tests/ (full, --ignore=tests/manual) — 412 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(tests): uv sync --all-extras so optional-engine tests can import their package

Phase 3 added `supertonic` as an optional dependency. The CI Tests job
runs `uv sync` (no extras), so `test_cpu_only_honest` and `test_license_gate`
in tests/test_supertonic3.py hit the "supertonic package not installed"
fallback instead of the real import path, and fail.

Bare `uv sync` is the right default for users (engines are opt-in), but
the test environment should exercise the full surface. `--all-extras`
keeps the smoke job lean (still bare `uv sync`) while letting Tests
verify the integrated behavior of every optional engine.

Future-proofs against the same failure mode in Phase 4 (GGUF) and any
later optional engines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:09:48 +05:30
Palash DebnathandClaude Opus 4.7 715766cb04 Phase 1 Wave 2: per-OS install docs + Settings UI + error→docs deeplinks (#94)
* docs(install): per-OS install pages + drift validator + CI gate

Splits the 600-line README install section into self-contained per-OS docs
under docs/install/{macos,windows,linux,docker}.md plus a Top-10
troubleshooting index. Each OS doc is end-to-end: a user opens it and
reaches a working app following only commands inside that file.

Adds:
- docs/install/{macos,windows,linux,docker}.md  (OS-specific install paths)
- docs/install/troubleshooting.md               (top 10 install errors)
- docs/engines/cosyvoice.md                     (closes #55 docs half)
- docs/features/diarization.md                  (pyannote license flow)
- docs/setup/huggingface-token.md               (3-source cascade guide)
- scripts/validate-install-docs.py              (INST-06 docs-drift gate)
- tests/scripts/test_validate_install_docs.py   (B-5: validator self-tests)
- .github/workflows/ci.yml step running the validator on every PR

Implements INST-02 (README routing), INST-03 (macOS Gatekeeper anchor),
INST-12 docs half (Windows torch-compile-oom anchor), DOCS-01..05.

The validator is a one-way diff: every `<!-- validate -->`-tagged line
in docs must appear in scripts/desktop-prod.sh after normalisation
(prompt-prefix strip, CRLF, trailing whitespace, blank-and-comment skip).
A `<!-- validate: skip -->` marker opts out for human-readability blocks.
Its own 10 unit tests catch regressions in the gate itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(deeplinks): links.py + error_docs_map (Python + TS mirror)

Adds the single source of truth for the project repo URL and the 4-class
error → docs taxonomy that both the in-app ErrorBoundary deeplink button
(Wave 2 Task 3) and the Phase 5 bug reporter will consume.

New:
- backend/core/links.py            — PROJECT_REPO_URL + BLOB_MAIN resolver
                                      (Tauri config first, pyproject fallback)
- backend/core/error_docs_map.py   — lookup(error_class) → docs URL
- frontend/src/utils/errorDocsMap.ts (TS mirror with classifyError helper)
- tests/backend/core/test_links.py + test_error_docs_map.py
- frontend/src/utils/errorDocsMap.test.ts

Resolves checker B-6 (links.py ownership) and Open Question #3 (which fork
the deeplinks resolve to — the Tauri updater endpoint wins, which points
at the desktop app fork debpalash/OmniVoice-Studio).

The TS BASE constant is documented as the second hardcoded URL drift site;
the keys-sync test (`test_keys_match_python_map` equivalent) guards the
4-class taxonomy contract between Python + TS halves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ui): Settings → API Keys panel + ErrorBoundary docs deeplink

Wave 2 AUTH-03 UI half + ErrorBoundary deeplink wiring.

ErrorBoundary fallback now renders an "Open docs for this error" button
that classifies the thrown Error message (heuristic: pkg_resources → 401 /
HfHubHTTP → WebKit / white screen → quarantine / Gatekeeper) and opens the
matching docs anchor via Tauri shell.open (with a window.open fallback
in browser dev mode).

ApiKeysPanel consumes the Wave 1 resolver state endpoint:
  - 3 source rows (App / Env var / HF CLI) with set/unset indicator,
    masked token preview, whoami username + green check
  - "Active" badge on whichever source is currently serving the cascade
  - App-row only: Save (POST /api/settings/hf-token) +
    Clear (DELETE with optional "also clear HF CLI" confirm dialog)
  - "Test now" button refetches state (invalidates the resolver's
    validation cache via the same endpoint hit)

Panel mounted in the existing Settings → Credentials tab; the legacy
HF_TOKEN row from CREDENTIAL_FIELDS is filtered out so the two paths
don't fight over the same key.

Threat T-02-02: the panel never displays the full token. The masked
value comes from the resolver state endpoint; the full token only
crosses the IPC boundary on Save (POST) and is cleared from local
state on success.

Closes AUTH-03 fully (Wave 1 backend + this Wave 2 UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(perf): INST-12 Disable torch.compile (Windows) toggle (backend + UI)

Wave 2 Task 4 — full INST-12 delivery per checker B-2/B-7 v0.3.0 fat-release
decision. Both the docs half (windows.md anchor, shipped in earlier commit)
and the runtime toggle are now in Phase 1.

Backend:
- backend/services/settings_store.py: adds get_text/set_text helpers for
  non-secret config (refuses to write to the encrypted hf_token key).
- backend/api/routers/settings.py: GET + PUT
  /api/settings/perf/torch-compile-disabled, both under the existing
  loopback guard (threat T-02-04).
- backend/services/engine_env.py: new `build_engine_env()` helper that
  centralises HF_TOKEN/YOUR_HF_TOKEN injection from the 3-source resolver
  AND injects TORCH_COMPILE_DISABLE=1 when the flag is set on win32.
  Phase 2 SubprocessBackend launchers should adopt the same helper.
- backend/services/sonitranslate.py: migrated to engine_env.build_engine_env()
  while preserving the source-level `env["HF_TOKEN"]` sentinel that
  test_sonitranslate_module_uses_resolver checks.

Frontend:
- frontend/src/components/settings/PerformancePanel.{jsx,css,test.jsx}:
  toggle UI with the explainer for #65; renders disabled with a "not
  applicable" badge on macOS/Linux.
- frontend/src/pages/Settings.jsx: mounts the panel into the Credentials
  tab alongside the API Keys panel.

Tests:
- tests/backend/test_perf_settings.py: 7 backend tests (default state,
  PUT persistence, T-02-04 non-loopback rejection, settings_store round-
  trip, env injection on win32, NO injection on macOS/Linux, NO injection
  when disabled).
- frontend PerformancePanel.test.jsx: 5 tests (renders from GET state,
  PUT on toggle, disabled on non-Windows platforms, pre-enabled state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(planning): Wave 2 SUMMARY + REQUIREMENTS status updates

- .planning/phases/01.../01-02-SUMMARY.md: full implementation report
  per template (truths, commits, tests, deviations, drift-site
  acknowledgments per W-3, launcher seam name for Phase 2,
  taxonomy keys for Phase 5).
- .planning/REQUIREMENTS.md: flips Wave 2 closures to Done:
    AUTH-03, INST-02, INST-03 (docs half), INST-06, INST-12,
    DOCS-01..05.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:22:10 +05:30
Palash Debnath 4a6b978df9 Phase 1 Wave 1: HF token persistence + redactor (closes #35) (#91)
* 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.
2026-05-20 05:10:37 +05:30