Files
hope-voice-api/QUALIFICATION.md
T
snowspeeder 8905dc2c2a
Rebuild & Deploy / rebuild (push) Successful in 3m7s
fix: give realtime voice a dedicated conversation view
2026-10-04 10:35:34 -04:00

335 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Multi-user Hermes migration — qualification and operations
## Dedicated web voice session refinement — 2026-10-04 (local only; not deployed)
Local browser qualification exercised the actual web app's voice start, mute,
unmute, transcript-packet handling and end lifecycle with fake LiveKit at
1440×900, 390×844, 320×568 and 844×390. No JavaScript errors or viewport overflow
were observed. The transcript owns its scroll region, session controls remain
persistent, the text composer is hidden during voice, and its draft is restored
when the session ends. Independent actual-app fake-transport screenshots passed
in light and dark themes at all four viewports after the final changes; the LED
overflow issue was fixed.
Parent-reported static checks passed again after the final changes:
`node --check web/app.js`, `node --check web/chat.js`, `git diff --check`, and
theme generator `--check`. The final seven-module offline browser suite passed:
**47 passed in 97.40s** (parent receipt `bash-1727`). Coverage includes cancellation
while connecting, token failure, microphone denial, disconnect, logout, scroll
position, selected durable-history isolation, and the HTTP-only compatibility
composer. Fake transport and simulated microphone failures are browser lifecycle
evidence, not physical audio qualification.
No credentials, live APIs or NAS were accessed for this refinement. No production
deployment, physical microphone, real TTS or native qualification is claimed.
Existing dirty iOS, AGENTS and design changes were left untouched. No commit or
push was performed.
## Web UX refinement — 2026-10-04 (production deployed)
Release `ea4e191` deployed successfully via [Gitea run244](https://git.srmr.xyz/snowspeeder/hope-voice-api/actions/runs/244)
following exact Python3.12 CI qualification:148 passed plus theme verification.
Initial runner bind failure and a real SQLite cancellation/close race were fixed,
without disabling qualification or changing protected runtime configuration.
Docker speech/web and control containers restarted with newly built images and
reported healthy/OK. All seven live web entrypoint assets matched release bytes;
anonymous auth/token remained401. Live mobile login loaded without JS errors or
overflow. Account data was not read/mutated; no authenticated CRUD, audio or native
installation claim is made. Detailed receipts are in the linked report below.
Web-only sidebar actions, Library upload/reuse and Saved drafts, scheduled-task
layout, memory revision recovery, and contextual Security/status/signout improved.
Final 43 offline browser tests passed; 85 final full-interface/internal-scroll
screenshots captured at 1440/390/320 widths with no JS or unhandled/external
requests. Independent source review findings fixed and final review approved.
See [detailed scope and evidence](docs/WEB_UX_REFINEMENT.md). Live public asset,
anonymous authorization and container-health verification is documented above;
native parity and physical audio remain outside this refinement.
## iPhone certificate onboarding follow-up — 2026-10-01
User reported certificate error at iOS sign-in. Current pinned native HTTPS probe
passed on the physical phone, while deleted John Doe account correctly returned401.
User confirmed deleting the disposable account; do not recreate it. Prior receipts
below describe historical qualification, not current John account existence.
User approved public certificate trust onboarding. Exact nginx route
`/hope-server.mobileconfig` serves only `/etc/voice/certs/hope-server.mobileconfig`
from persistent certificate mount with `application/x-apple-aspen-config` and
`Cache-Control: no-store`. It is a verified public certificate profile, no keys or
credentials; requires manual iOS profile installation + full trust for WSS. Private
key URL remains404. Do not expose the entire cert directory. Initial container-copy
profile disappeared during intervening deployment; persistent alias fixed it.
## Hermes auto image input — live receipt 2026-10-02
Verified upstream `/v1/responses` with model `auto`, an inline PNG containing
red-left/blue-right panels, and reasoning effort `none`: HTTP200, served model
`Qwen3.8-27b`, answer `Red Blue`, zero reasoning tokens. The initial control-exec
probe was interrupted with exit137 during a control replacement; the successful
probe ran from the stable administrator Hermes container.
The shared provisioner now declares only
`providers.hope-upstream.models.auto.supports_vision: true`. Existing managed
model lists migrate to metadata mappings, preserving other settings. A durable
private reload marker ensures restart retries survive control interruption.
No global vision override was added for other selectable models.
Deployed control image `hope-voice-control:vision-auto-v1`, also tagged `latest`,
image `sha256:570319b3fd60c8161741a6c46cfc61f16a214383b8f8114c5692e00ca506197d`.
Startup migrated and restarted both currently deployed accounts in place:
- `hope-hermes-admin-69101358`: ready; real authenticated gateway image request
at port20000 returned HTTP200 and `Blue, red` for a blue-left/red-right PNG.
- `hope-hermes-shane-fc44d140`: ready; real authenticated gateway image request
at port20001 returned HTTP200 and `Green, red.` for a green-left/red-right PNG.
Both runtime capability lookups returned true for the custom provider's `auto`;
both retained configs have the declaration and no pending reload marker. Account
settings, credentials, container identities, ports and bind mounts were retained;
voice remained healthy and control health returned OK. No unrelated deployment
was changed. This qualifies gateway image input, not a browser upload workflow.
Protected config backups/control inspect receipt are under
`/mnt/user/appdata/hope-voice/rollback/vision-20261002T201506Z`; original control is
retained stopped as `hope-control-pre-vision-20261002T201506Z`. Clean source was
staged separately; the NAS development checkout was not overwritten. Offline
account/provisioning/bootstrap/deployment rollback regression:44 passed.
## Per-account Hermes WebUI — live receipt 2026-10-02
Installed pinned nesquena/hermes-webui `62e4d7b9e2560f9d49c0c29c2dc1414fbd4e2d12`
inside both existing account containers using `hope-hermes-webui:62e4d7b`
(image `sha256:9324675849a485d4f6b851045d065bd4ebf5c3b7b5d85c0b112e9f91e64571b3`).
WebUI and the existing pinned Hermes gateway run as supervised peer processes.
Control image `sha256:e24aaa314d2e261da3693fd14ebe14dfbd62dbcf2d046e9181399ac033b3c7a8`
is running as `hope-voice-control:webui-upgrade`, also tagged `latest`.
- Admin WebUI: `https://192.168.86.2:30000`.
- Shane WebUI: `https://192.168.86.2:30001`.
- Gateway APIs remain private on `127.0.0.1:20000` and `:20001`.
- Distinct persistent random browser passwords, cookie names, WebUI state and
self-signed TLS certificates per account. Passwords are stored root-only under
`<runtime-root>/hermes/<full-account-id>/webui-password`, not Hope login passwords.
- Protected runtime environment now enables WebUI and its packaged image for new
accounts, using LAN port allocation range `30000-30999`. CI builds the image.
Verified from a separate LAN client: both HTTPS login pages returned200; no host
firewall/router changes were necessary. Authenticated checks on each WebUI:
health200, anonymous sessions401, password login200, authenticated sessions200.
Each authenticated WebUI created a qualification session, submitted a real
Gateway-backed turn and received an SSE stream containing `WEBUI_LAN_OK`.
Both pinned gateways advertise runs, SSE, cancellation and approval capabilities.
Full browser approval/cancellation workflow has not been independently qualified.
Offline regression:81 passed. Voice remains healthy, control healthOK, accounts
ready. No public-internet port forwarding or exposure configured.
Persistent state and runtime env backed up under
`/mnt/user/appdata/hope-voice/rollback/webui-20261002T173832Z` with protected inspect
receipts; previous control retained stopped as `hope-control-pre-webui-20261002T173832Z`.
Homes/workspaces and private gateway credentials retained. Source is still
local/unpushed; an older CI checkout could undo this change. Self-signed browser
certificate trust must be accepted/installed by clients. Different ports on the
same IP are not a browser cookie isolation boundary; distinct account DNS names
provide stronger isolation. Runtime instructions in `control/README.md`.
## Host-network LAN migration — live receipt 2026-10-02
Deployed control image `sha256:cc5cc7dd470e590ffc0c0b8084f4b2c2496074d42028695dde23644e2d4a576f`
as `hope-voice-control:lan-upgrade`, also tagged `hope-voice-control:latest`.
Control startup migrated both existing accounts without changing account names,
credentials or persistent home/workspace mount paths:
- `hope-hermes-admin-69101358`: host networking, ready, API `127.0.0.1:20000`.
- `hope-hermes-shane-fc44d140`: host networking, ready, API `127.0.0.1:20001`.
Authenticated Hermes readiness passed for both. API key SHA256 digests matched
pre-migration backup records. Actual `/proc/net/tcp` listeners were loopback-only.
From inside **each** container, TCP connections succeeded to `192.168.86.5:2099`
(model), `192.168.86.2:9178` (Hope UI), and `192.168.86.2:8094` (Hope Music).
These are reachability checks, not a new end-to-end conversation qualification.
Owned legacy networks were removed. Voice remained healthy and public UI HTTP200;
control `/health` returned OK. Unrelated containers were not redeployed.
Before replacement, control and both Hermes containers were stopped and account
DB/Hermes persistent state archived root-only under
`/mnt/user/appdata/hope-voice/rollback/lan-20261002T172231Z`, alongside protected
Docker inspection receipts. Stale Unix sockets were intentionally skipped by tar.
Original stopped control retained as `hope-control-pre-lan-20261002T172231Z`.
Existing NAS source checkout was not overwritten; clean control source staged
separately. Source changes are still local/unpushed: future CI deployments from
older source could regress this networking change until the update is committed
and deployed through the repository pipeline.
Runtime and migration instructions are in `control/README.md`. Host networking
removes per-user network namespace isolation; the Hermes API is loopback-only,
but arbitrary terminal tools can expose their own ports unless a separate host
ingress policy prevents it. Historical deployment receipts below predate this
migration.
## Deployment
Target: Unraid `192.168.86.2`, public UI `https://192.168.86.2:9178`.
- `voice`: nginx, LiveKit, Azure speech agent and authenticated token bootstrap.
- `hope-control`: account service, private SQLite and trusted Docker provisioning.
- Administrator container: `hope-hermes-admin-69101358`.
- John Doe container: **`hope-hermes-john-doe-bac897a4`**, private and running.
- User-facing test login username: `john-doe`. Initial passwords are generated,
not committed or printed; initial `admin_password` / `john_password` are in
root-only `/mnt/user/appdata/hope-voice/bootstrap-credentials.json` (SSH),
or use administrator password reset in the website. This initial receipt
does not update after password resets.
- Final voice image: `sha256:ea86ac10377c553f32a70d24f46dea1413c949673ed1b9850a4aabbe8574e8d8`;
verified `healthy`. Final control HTTP health returned `{"status":"ok"}`.
- No push performed. The existing dirty NAS checkout was not overwritten; clean
build source was staged separately. Hope Music and unrelated services untouched.
Runtime root: `/mnt/user/appdata/hope-voice`. `runtime.env`, `voice.env` and
`livekit.yaml` are root-only. Account persistence is in `accounts/`; private
Hermes homes/workspaces in `hermes/`. Bootstrap password is only used on an empty
DB. Never print these files, Docker environment inspection or SQLite key fields.
The runtime Hermes image is pinned to:
`nousresearch/hermes-agent@sha256:d4da4a40cd7a28aba983775d9fd31d94cbf153eeb0cb9e844d6d0f612b7c24db`.
## Real-model result
The supplied Responses endpoint `http://192.168.86.5:2099/v1/responses` accepted
model `auto` and `reasoning.effort: none`. It reported **Qwen3.8-27b**, HTTP 200,
and **zero reasoning tokens** for the direct probe. Hermes custom provider uses
Responses transport and defaults reasoning effort to `none`. The speech bridge
also passes `model_options.reasoning.enabled: false` and forwards content only.
## Verified live
- Browser HTTPS login, HttpOnly cookie session and administrator user listing.
- John Doe created through actual administrative API: durable provisioning →
`ready` → named private Docker container, without manual Docker creation.
- John private Hermes returned `Hello John Doe!` from the real upstream.
- Real browser → authenticated token → WSS/LiveKit → Hermes → final transcript:
`Hello, John Doe — glad to have you here!` (another run: `Hey John! Good to see you.`).
- Chromium reported no page exceptions in the successful browser run.
- Long tool task: Hermes ran Python sleeping eight seconds then printed
`HERMES_LONG_TASK_OK`. Activity/status displayed working before completion;
eventual assistant reply reported the exact marker and exit code zero.
- Disposable account auto-provisioned to ready. Non-admin administration denied
403; attempted client-selected account/room ignored by token service.
- Administrator deletion immediately invalidated session/bootstrap; lifecycle
removed the account and its named container. Private data retained deliberately.
- Logout invalidated the token; room grant revocation also covered offline.
- Actual Azure out-of-band wait-notice and final answer audio received over
WebRTC on two independent tracks (measured nonzero peak levels 0.904 / 0.786).
Probe used authenticated LiveKit text input with agent voice output and muted
synthetic microphone; this verifies synthesis/transport, not physical STT.
- Actual speech worker registered and final container health passed. Health now
checks HTTPS, LiveKit, control and PID-bound worker endpoint, not just web shell.
## Offline evidence
- Account/API tests: **15 passed**, including password-reset/login race and
narrowly trusted proxy identity/failed-login throttling regressions.
- Provisioner tests: **9 passed**.
- Authenticated bootstrap tests: **2 passed**.
- Offline deployment rollback tests: **3 passed**; injected second-service stop,
rename and replacement-run failures restore both original immutable IDs and
running names, without removing originals.
- LiveKit Hermes stream tests: **16 passed** (no reasoning fallback or replay,
approvals fail safely, total timeout, cancellation, visible timer progress,
out-of-band notice/first-answer callback contracts).
- iOS commit **`bebaf59`**, authenticated bearer + device-only Keychain,
origin/account-scoped history, HTTPS/WSS, no model picker, progress UI.
- iOS simulator build and final **52 unit tests passed**. Earlier combined
51 unit + 5 UI passed. Expanded UI run exposed two pre-existing asynchronous
assertion races; synchronization fixed, focused settings/large-type/account
switch tests passed. No claim of one fully green expanded UI run.
## Account model and thinking settings
Deployment receipt: commit `6cb4427`, Gitea run `238` succeeded; running voice
image `sha256:b31198e34ca053e390321254b4237549cad04c2803dd4870ff1d8cabed3e4953`
is healthy. Authenticated mobile Chromium loaded fourteen live models, saved
`openai/gpt-6.1-sol-subscription` / `high`, verified both after reload, then sent
an actual Hope text turn and received `MODEL_SETTINGS_OK` through Hermes.
The administrator's original preferences were restored after qualification.
No page exceptions were observed. This verifies saved settings and a successful
real turn, not independent upstream execution telemetry (see limitation below).
Offline: 76 API/store/browser/deployment tests and 20 Hermes stream tests passed.
- The authenticated Hope settings sheet loads the Manifest upstream `/v1/models`
catalog through the trusted control plane. Only public IDs/names and application
effort choices are returned; the browser never receives provider credentials.
- `GET` / `PUT /api/settings/model` operate only on the signed-in account. Preferences
persist in a separate SQLite table with account-deletion cascade. Existing accounts
default to `auto` / `none`. Cookie mutations require a trusted Origin.
- The speech bridge resolves authorized room settings at every request, including
existing conversations, and snapshots them for that turn. No interruption/replay.
It sends the chosen model with explicit `provider: custom:hope-upstream`; a bare
model would be ignored by the deployed Hermes API's default policy.
- Thinking choices are requested efforts, not guarantees of model support. Manifest
currently provides no capability metadata; Hermes may clamp levels or omit a
disabled reasoning field where an upstream model cannot represent `none`.
- Reasoning content is still excluded from transcripts/speech. Discovery failures
preserve saved settings and disable changes rather than silently selecting a model.
- Hermes gateway `/model` session overrides have higher precedence than ordinary
OpenAI-compatible request overrides. Hope does not expose that session command.
The deployed configuration inspected has no fallback chain. OpenAI SSE `model`
echoes the request and is not independent proof of the upstream executing model.
## Operational behavior and boundaries
- Each user has separate persistent state, API credential, bridge network,
nonroot UID10000 runtime, capability drop, no-new-privileges and resource caps.
- Hermes API is loopback-published, not LAN-published; homes contain no Docker
socket or account DB. Speech has neither Docker socket nor upstream provider key.
- Docker is the tenant boundary, not protection against a kernel exploit. Hermes
tools intentionally access network/model/browser/terminal within that container;
this is not a no-network hostile-code sandbox. The account service is privileged
and must remain trusted/loopback-only. LAN SSH/root administrators can access
all runtime secrets and user data.
- Hermes approvals have no interactive voice approval transport: fail explicitly
rather than hanging or executing/retrying silently. Tool actions are not replayed.
- Status/tool events provide visual progress immediately. Spoken wait notices
occur only after twelve seconds without answer content, once per request, with
varied wording that never repeats back-to-back within a conversation. Status
and tool-start events alone do not trigger speech. Notices use independent
audio, not `AgentSession.say` which queues behind the response. First answer
cancels notice playback; notice is not added to conversation history.
This revised threshold is covered offline; the live receipts above predate it.
- Runtime LiveKit ICE candidates are restricted to Unraid LAN `br0` and loopback
with node IP `192.168.86.2`, avoiding dozens of unreachable Docker bridge candidates.
Adjust the private runtime YAML for a different host/interface.
- Account reset/logout/delete revoke existing grants; agent checks access every
two seconds. Transport JWT alone is not authorization to issue Hermes requests.
- Account deletion removes owned container/network, **not retained home data**.
- Historical MCP/router/memory/dispatch source and tests remain for reference,
but are excluded from runtime packaging. No shared Hermes home migration occurs.
## Deployment / rollback
Build both images, then use `scripts/deploy-unraid.sh` on Unraid. It requires
pre-existing protected environment/state/config/cert paths. It preserves original
containers by timestamped name and root-only inspection receipt; rollback is
installed before any stop/rename. Do not remove rollback containers or retained
homes until explicitly deciding retention policy.
Previously committed credentials (old CI and historical user registry) are no
longer used/packaged. Source cleanup cannot erase Git history; rotate those old
external credentials separately if they remain valid. The supplied new upstream
credential stays protected runtime-only.
No secrets belong in CI source; the workflow builds both images then calls the
protected-host deployment script. LiveKit keys are runtime-only; committed YAML
contains no usable key. Keep API origins synchronized for new DNS names.
## Remaining human/hardware qualification
Real physical microphone recognition, perceived speech quality, barge-in and
an installed iPhone against the NAS have **not** been claimed. Self-signed SDK WSS
trust may require installing a trusted private CA on the iPhone; native HTTP pinning
is not a guarantee the SDK socket trusts that certificate. The deployed public
leaf certificate SHA256 was compared with the native HTTP pin and matched exactly
(`07907abde3bb9c685265909927627d9ea4705d7e7b27056eefe06e10b192af46`). No trust-all/plaintext
fallback was added. Browser automation's certificate exception is a test setting,
not production TLS policy. See iOS project handoff for test artifacts.