19 KiB
Multi-user Hermes migration — qualification and operations
Web UX refinement — 2026-10-04 (production deployed)
Release ea4e191 deployed successfully via Gitea run244
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. 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 andBlue, redfor a blue-left/red-right PNG.hope-hermes-shane-fc44d140: ready; real authenticated gateway image request at port20001 returned HTTP200 andGreen, 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:20000and: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, API127.0.0.1:20000.hope-hermes-shane-fc44d140: host networking, ready, API127.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; 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
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/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.