# Multi-user Hermes migration — qualification and operations ## 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. ## 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.