Files
hope-voice-api/QUALIFICATION.md
T

7.8 KiB

Multi-user Hermes migration — qualification and operations

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.
  • 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.

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.
  • Long requests emit progress after three seconds/tool start. Spoken wait notices use independent audio, not AgentSession.say which queues behind the response. First answer cancels notice playback; notice is not added to conversation history.
  • 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.