9.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; initialadmin_password/john_passwordare 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; verifiedhealthy. 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
- The authenticated Hope settings sheet loads the Manifest upstream
/v1/modelscatalog 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/modeloperate only on the signed-in account. Preferences persist in a separate SQLite table with account-deletion cascade. Existing accounts default toauto/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
/modelsession 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 SSEmodelechoes 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.saywhich 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
br0and loopback with node IP192.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.