d91beef0fd314250d8d9b94de86dfea019a8bd96
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
19ae20111a |
fix(security): replace persistent admin keys with scoped sessions (#1528)
* fix(security): replace persistent admin keys with sessions Exchange the remote administrator key once for bounded, revocable credentials. Canonicalize backend principals, enforce cookie CSRF and exact origins, and use path-bound one-use WebSocket tickets. Migrate the bundled UI away from durable master-key storage and credential-bearing URLs. Add unit, integration, static-hygiene, and production-browser regressions plus synchronized operator documentation. * docs: link session hardening to PR 1528 * fix(security): key session indexes with process pepper Use HMAC-SHA-256 instead of an unkeyed digest for in-memory session and WebSocket-ticket indexes. This preserves constant-size lookup identifiers, makes copied records unusable without the process pepper, and resolves CodeQL's weak sensitive-data hash finding. * fix(auth): align empty bearer migration precedence Centralize the Authorization-channel presence decision with canonical principal parsing. Bearer followed only by spaces now remains an empty channel during legacy cookie migration, while unsupported or invalid explicit credentials stay authoritative and fail closed. * fix(security): harden admin session review boundaries * fix(security): derive key generations with HKDF * fix(auth): anchor the admin-session store so module reloads cannot fork it test_master_exchange_does_not_bypass_pin_on_normal_routes failed in full-suite runs: test_mcp_bindings' client fixture purges the services.* tree from sys.modules and reloads main, so api.routers.auth re-imported a fresh services.admin_sessions (new AdminSessionStore) while core.auth kept its import-time reference to the old one — the exchange issued the cookie into one store and the middleware resolved it against another, turning the expected "PIN required" into "API key required". Root cause is the class of bug, not the one test: a process-global auth store defined as a bare module-level singleton forks under importlib.reload or purge-and-reimport. Fix at the source: admin_session_store now resolves through a synthetic sys.modules anchor (_omnivoice_admin_session_store_anchor) that reloads never re-execute and package-prefix purges never match, so every copy of the module shares the one per-process store. No consumer or behavior changes. Regression test reproduces both fork vectors (in-place reload and sys.modules purge + fresh import) and asserts previously issued sessions still resolve and the store identity is preserved; it fails before this fix and passes after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): honor X-Forwarded-Proto for CSRF origin and Secure cookies behind TLS proxies Behind Tailscale Serve (docs/remote-gpu.md) or any TLS-terminating proxy, the browser talks https while the backend hop stays http, so exact-origin CSRF compared an https Origin against an http expectation and rejected every legitimate request, and the session cookie shipped without Secure. uvicorn's ProxyHeadersMiddleware only rewrites the scope for loopback peers, which misses Docker and any non-loopback proxy topology. New core.csrf.effective_scheme derives the client-facing scheme: resolved scope first (uvicorn's trusted-proxy rewrite wins), then an upgrade-only read of X-Forwarded-Proto's first value — https/wss promotes http to https, everything else is ignored, and a genuine TLS hop can never be downgraded. Used by both the destination-origin comparison and auth._secure_cookie so the WS-ticket/logout CSRF paths and the cookie Secure flag agree. Spoofing gains nothing: the host:port half of the origin tuple is untouched, browsers cannot attach the header cross-site without a preflight this API never grants, and forging it on plain http only adds Secure (the browser then drops the cookie — self-harm only). Regression tests: proxied https origin accepted (origin check, Secure flag, logout), comma-separated chains, scope-fallback path, spoofed header still rejects cross-origin, cannot downgrade real https, junk values ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): consume the stored admin key only after a successful exchange A remote-backend user upgrading with their backend unreachable lost the only stored copy of OMNIVOICE_API_KEY: every migration path deleted the durable ov_api_key BEFORE the session exchange settled, stranding them until they recovered the key from the server box. Close the whole class: - client.ts bootstrap: read the legacy key, exchange first, and remove the durable copy only after the exchange succeeds; on failure the key stays so the next launch retries the migration (auth gate still rises). - authSession.ts exchangeApiKey: move removeLegacyMaster from before the fetch to the cookie/bearer success paths — the key never coexists with a live session, but a rejected or hung exchange no longer consumes it. - remoteBackendProbe.ts configuredRemoteBackend: stop wiping the key on every app mount. - RemoteBackendPanel: a connection test or an aborted save no longer wipes the pending key; only disabling the remote backend discards it. - prefKeys.js: ov_api_key moves from PREF_KEYS to PRESERVED_KEYS — factory reset preserves the pending connection credential exactly like ov_backend_url; the successful migration is what deletes it. Tighten the credential-hygiene static guard to match: it accepted sessionStorage.setItem('ov_api_key', …) — the exact class it exists to close. The guard now flags .setItem(<master key>) on any storage receiver, quote style, or injected-store alias, with a self-test pinning what it catches and what stays legal. Fail-before/pass-after regression tests: backend unreachable retains the key and the next bootstrap retries it; a successful exchange removes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(auth): make session validation occupancy-independent * test(auth): catch optional master-key storage calls * feat(docs): add PR control document for bultodepapas in VoiceStudio * docs: keep the PR tracking board in the fork; credit the changelog line The pr-control document is excellent process discipline, but it is the contributor's own operational board (their inventory, their update commands) — it lives naturally in their fork, and docs/agents/ here is context every repo agent loads. Removed with appreciation; the changelog line gains its contributor credit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: debpalash <4178343+debpalash@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
43de1c794c |
feat(workers): remote GPU workers over a versioned gRPC protocol
Send individual jobs to GPUs on your other machines while everything else stays local. Opt-in, off by default: with the toggle off there is no listening socket, no certificate and no background loop. Design follows remote/goal_v2.md, the council-revised goal doc. The decisions that shaped the code, and why: * A disconnect is an unknown outcome, not a failure. The original design reassigned on disconnect while also describing the case where the worker had already finished — following both guarantees duplicate execution. An attempt now holds a grace window; a worker returning inside it commits its result and no second attempt is ever made. * At-least-once execution, exactly-once result commit. The result is persisted BEFORE it is acknowledged, so a crash between the two cannot silently lose a finished render. * Deadlines are phased (accept -> model load -> execute -> deliver) and liveness is a progress lease. The old fixed 30s execution budget was two orders of magnitude below what this product actually does; silence is the failure signal, not slowness. * Capacity is derived from free VRAM, never configured: a static value corrupts output under torch.compile thread affinity (#315) and aborts the process on small cards (#567). * A circuit breaker replaces the reliability-score/quarantine machinery, which had no recovery path (no probation workload exists in a TTS product) and penalised consumer networks for existing. * Identity is a keypair the worker generates and never sends. A server-assigned id is a name, not an authenticator, so revocation of one would be theatre. Enrollment tokens are single-use and carry the control plane's certificate fingerprint for pin-on-first-use. Adds the domain core, scheduler, durable task store, gRPC transport, worker agent, management API, Settings panel, and docs. Protobuf reserves the tenant/trace/usage fields a hosted control plane would need, since adding them later means upgrading a whole fleet. Includes tests for the failure paths that matter: duplicate delivery, stale-session fencing, reconnect reconciliation, grace expiry, breaker attribution, and a real end-to-end TLS round trip. |
||
|
|
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.
|
||
|
|
eeffe6c2d1 |
fix(gguf): a source-built runtime must actually run (#1348) (#1384)
A meticulous report from an LXC/CPU-only source install surfaced three real defects: the build script deleted the libggml shared libraries a dynamically-linked build needs (first spawn died with exit 127), the hardcoded 120s per-spawn kill switch reaped legitimate CPU-only renders, and OMNIVOICE_ALLOWED_ORIGINS — the only fix for cross-origin browser access — was documented nowhere. All platform branches of scripts/build-omnivoice-tts.sh now copy the shared libs next to the binary, the CI artifact glob uploads them, and the backend puts bin/ on the loader path for every spawn of the engine binary. The timeout defaults to 600s (above the pool guard's well-diagnosed 300s deadline), is tunable via OMNIVOICE_GGUF_GENERATE_TIMEOUT_S with non-finite values rejected, and the timeout error names the knob. CORS documented in api-auth.md with a pointer from remote-gpu.md. Regression tests pin the spawn-env rule, the per-branch copy rule, the artifact glob, and the timeout behavior. |
||
|
|
f3c2d745c0 |
Merge #1212: PIN + API-key auth guide for the local API
# Conflicts: # docs/api-auth.md |
||
|
|
204eff2be1 |
fix(security): #1213 review — the share PIN must not gate RCE-class admin
CodeRabbit: the 6-digit share PIN is brute-forceable (10^6, no lockout), so letting it unlock admin over the network was still weak. Admin now requires the API key (a long operator secret) or loopback; the PIN is consumption-only and never gates /system/* or /api/settings/*. A PIN-only deployment keeps admin loopback-only. Docs (api-auth.md, remote-gpu.md) aligned with the conditional gate (no credential -> admin open; API key -> admin) and the PIN exclusion. Test inverted: presenting the PIN over the network is now 403 on admin. |
||
|
|
3b879f298e |
fix(auth): keep admin gate independent of trusted-network trust under server mode (#1213)
OMNIVOICE_SERVER_MODE=1 made require_loopback an unconditional no-op, so with OMNIVOICE_TRUSTED_NETWORKS also set a trusted-CIDR client — a consumption-only exemption that bypasses the PIN/API-key middleware via is_local_host — could reach the RCE-class admin surface (/system/set-env, /api/settings/*) with no credential. That collapsed the documented two-tier privilege model (consumption trust != admin trust) in exactly the "lock the backend with a key, exempt a LAN proxy for TTS" configuration. Server mode still can't require true loopback (Docker NAT, #261), but it now applies the credential rule to admin routes: open only when NO credential is configured; otherwise the request must present the API key or share PIN. Trusted-network membership alone never satisfies it. Loopback, credential holders, and no-credential Docker deployments are unchanged; consumption routes (require_local / middleware) keep exempting trusted networks. Regression tests cover the server-mode x trusted-network x credential matrix (fail-before/pass-after). Docs: new docs/api-auth.md two-tier model + quick reference; docs/remote-gpu.md corrected (previously documented the hole as accepted behavior). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
668962133a |
docs(api): PIN + API-key auth guide for the local API (#1210)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f669687ed1 |
feat(backend): trust a local network/proxy via OMNIVOICE_TRUSTED_NETWORKS (#1170)
Self-hosting behind a reverse proxy or on a LAN used to force a blunt choice: OMNIVOICE_SERVER_MODE (trust every non-loopback source) or the API-key/PIN gates — which a proxy that strips the Authorization header breaks for browser clients entirely. Add OMNIVOICE_TRUSTED_NETWORKS (comma-separated CIDRs) whose addresses are treated as trusted by the CONSUMPTION gates (PIN/API-key middleware, dictation WebSocket) via is_local_host — a LAN/proxy client is exempted from consumption auth. Admin routes (require_loopback → /system/set-env, /api/settings/*) stay true-loopback-only (is_loopback, not is_local_host) to preserve the two-tier privilege model: consumption trust ≠ admin trust (RCE-class surface). Opt-in, default empty → zero behavior change. The granular companion to server-mode (#261). Tests: is_loopback / is_local_host / require_loopback contract for trusted CIDRs, adjacent subnets, malformed entries, the two-tier split (trusted-network rejected by the admin gate), and the default (no-trust) case. Docs + CHANGELOG. |
||
|
|
55c852c6f2 |
fix(remote-auth): show an API-key gate (not PIN) for API-key 401s in remote-backend mode (#1154)
* fix(remote-auth): route API-key 401 to an API-key gate, not the PIN form
When OMNIVOICE_API_KEY is set (remote-backend mode), a non-loopback browser
gets 401 "API key required" from BearerKeyMiddleware. But client.ts fired
`ov:pin-required` on every 401, surfacing the PIN gate — whose payload
(sessionStorage ov_pin / X-OmniVoice-Pin) can never satisfy the API-key
middleware. A remote user was stuck on a PIN form they could not pass.
Read the 401 `detail` and dispatch a single `ov:auth-required` CustomEvent
carrying the mode; RemoteAuthGate renders the matching PIN or API-key form.
Adds a `?api_key=` deep-link bootstrap (one-shot — scrubbed from the URL so a
reload can't re-clobber a corrected key) and a guarded saveApiKey helper.
Backend is unchanged — the two 401s are distinguishable by their `detail`
body ("API key required" vs "PIN required"). Docs: remote-gpu.md gains a
"From a browser" subsection for the new ?api_key= deep link.
* fix(remote-auth): preserve URL hash when scrubbing credentials
The replaceState that scrubs ?api_key=/?pin= rebuilt the URL from pathname
(+ optional query) and dropped url.hash, nuking any deep-link fragment
(e.g. #settings). Rebuild with pathname + (?query) + hash.
Addresses greptile + coderabbit review feedback on #1154.
* fix(remote-auth): guard 401 routing against a non-string/malformed detail
String(detail) can itself throw on a 401 detail whose toString is broken
(e.g. { toString: null }), aborting the auth-event dispatch. Match only real
strings with typeof; anything else falls back to PIN mode.
Addresses coderabbit's 17:03 re-review finding on #1154.
* fix(remote-auth): read the deep-link API key from the URL fragment (#api_key=)
Move the remote-backend deep link from ?api_key= (query) to #api_key=
(fragment): fragments are never sent to the server, so the durable key stays
out of the GPU box's and any reverse proxy's request logs on the page load
(greptile P1). ?pin= stays on the query (QR flow, session PIN).
The bootstrap is extracted into a pure, unit-tested _parseDeepLinkCredentials
helper (pin from the query, api_key from the fragment, one-shot scrub of both,
plus a legacy ?api_key= scrubbed-without-reading so a stray query key never
lingers). Docs document #api_key= with encoding guidance for keys containing
+ / & / # / =.
|
||
|
|
fd083749c6 |
fix(settings): network & privacy panels — clearable proxy after reload, HF-mirror panel never vanishes, guarded remote-backend save, honest privacy claims (#1063)
Settings → Network / Models / Sharing / Privacy / OpenAPI fixes:
- NetworkTab: a proxy persisted in a previous session can now be cleared —
the Clear button and "Set" badge derive from the backend-persisted value
(sysInfo.proxy_url), not only from a save in the current session. Proxy row
copy now matches its real semantics ("Applies now" badge; desc/toast no
longer claim a restart is needed or leak yt-dlp jargon — reworded in all
21 locales). FFmpeg path placeholder is platform-appropriate instead of
Windows-only on every OS.
- HFMirrorPanel: the panel no longer disappears when the initial GET fails —
the section shell always renders, with a loading state and an error +
Retry affordance. Saving now toasts, the active preset is marked
(aria-pressed), and the custom-URL row is labelled "Custom mirror URL"
instead of raw HF_ENDPOINT jargon (env var moved to the row note).
- RemoteBackendPanel: full i18n (was 100% hardcoded English); Save & reload
now validates the URL (http/https, parseable) and asks for confirmation
before saving a URL that hasn't passed a connection test — a typo'd base
no longer bricks every API call after reload. Dropped the contradictory
"Restart required" badge (saving reloads the app itself; description says
so). docs/remote-gpu.md updated to match (docs-sync).
- PrivacyTab: the "Network calls" row no longer shows the green "Offline
translator" assurance when the backend is down or reports 'unknown' —
green is reserved for confirmed-offline providers (nllb/argos/
libretranslate), everything unconfirmed shows a neutral "Unknown" badge.
The online-translator warning now deep-links to Translation settings.
- OpenApiPanel: a failed clipboard copy toasts an error instead of silence.
- a11y: all five text inputs across these panels now carry accessible names
(aria-label), previously announced only by their vanishing placeholders.
Tests: new colocated suites for NetworkTab, HFMirrorPanel,
RemoteBackendPanel, PrivacyTab; OpenApiPanel suite extended with copy
success/failure. Frontend suite 140 files / 1061 tests green; i18n parity
probes green (new keys en-only with defaultValue, reworded keys updated in
every locale).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
22ba348f17 |
feat(remote): backend URL + bearer key + Tailscale docs (Wave 2.3) (#364)
Run inference on a remote GPU box, drive it from the desktop app — opt-in,
off by default (loopback-only is unchanged when no key is set).
Backend:
- BearerKeyMiddleware (main.py): when OMNIVOICE_API_KEY is set, every
non-loopback HTTP + WebSocket request must present it (Authorization:
Bearer, ?api_key=, or the ov_key cookie set on first auth). Pure ASGI
(no response buffering), loopback always bypasses, SPA shell stays
reachable. Constant-time compare, never logged.
- ws_remote_authorized() in dependencies; capture_ws lets a keyed
non-loopback client through its inline loopback guard (the thin-client
dictation case: mic local, GPU remote).
Frontend:
- api/client.ts: ov_backend_url (localStorage) is the top-precedence base
override; new wsUrl() derives ws scheme + host from the API base (not
window.location, which lies in the Tauri webview) and appends ?api_key.
apiFetch attaches the bearer header. Both WS call sites (dictation,
events) routed through wsUrl; the HTTP transcribe fallback through
apiFetch.
- Settings > Sharing > Remote backend panel: URL + key fields, a
test-connection probe against {url}/health, save-and-reload.
Docs: docs/remote-gpu.md — the Tailscale recipe (MagicDNS + Serve, never
Funnel, headscale note, plain-HTTP-is-sniffable warning, PIN-vs-key split).
Tests: 10 bearer-middleware cases (inert without env, loopback bypass,
401 without/pass with key via header+query, wrong key, shell exemption,
plain-ASGI guard, WS handshake reject/accept). Validated in CI.
Spec: parity program Wave 2.3 / competitive-analysis §R2 rungs 1-3.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|