Files
hope-voice-api/QUALIFICATION.md
T

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