361 lines
22 KiB
Markdown
361 lines
22 KiB
Markdown
# Multi-user Hermes migration — qualification and operations
|
||
|
||
## Dedicated web voice session refinement — 2026-10-04 (local qualification and production follow-up)
|
||
|
||
Local browser qualification exercised the actual web app's voice start, mute,
|
||
unmute, transcript-packet handling and end lifecycle with fake LiveKit at
|
||
1440×900, 390×844, 320×568 and 844×390. No JavaScript errors or viewport overflow
|
||
were observed. The transcript owns its scroll region, session controls remain
|
||
persistent, the text composer is hidden during voice, and its draft is restored
|
||
when the session ends. Independent actual-app fake-transport screenshots passed
|
||
in light and dark themes at all four viewports after the final changes; the LED
|
||
overflow issue was fixed.
|
||
|
||
Parent-reported static checks passed again after the final changes:
|
||
`node --check web/app.js`, `node --check web/chat.js`, `git diff --check`, and
|
||
theme generator `--check`. The final seven-module offline browser suite passed:
|
||
**47 passed in 97.40s** (parent receipt `bash-1727`). Coverage includes cancellation
|
||
while connecting, token failure, microphone denial, disconnect, logout, scroll
|
||
position, selected durable-history isolation, and the HTTP-only compatibility
|
||
composer. Fake transport and simulated microphone failures are browser lifecycle
|
||
evidence, not physical audio qualification.
|
||
|
||
The local qualification above accessed no credentials, live APIs or NAS and did
|
||
not itself deploy anything. Existing dirty iOS, AGENTS and design changes were
|
||
left untouched. Physical microphone, real TTS and native qualification remain
|
||
outside this refinement.
|
||
|
||
### Authorized production follow-up
|
||
|
||
After user deployment authorization, release `8905dc2` was pushed to `master` and
|
||
[Gitea run248](https://git.srmr.xyz/snowspeeder/hope-voice-api/actions/runs/248)
|
||
succeeded, completing at `2026-10-04T14:38:47Z`. Python 3.12 backend CI passed:
|
||
**148 passed in 68.09s**, with one Starlette deprecation warning; theme checks
|
||
also passed. The 47-test offline browser receipt above remains separate evidence.
|
||
|
||
Parent SSH verification found the production voice container running and healthy,
|
||
started at `2026-10-04T14:38:23Z`, with image
|
||
`sha256:e4fb1bdbe73e44c27a8d1b68d75830f15af6b8fdaa5d5742403c4a6beb8a4175`.
|
||
The control image was unchanged (reported prefix `db756e976829`); its container
|
||
restarted at `2026-10-04T14:38:20Z`, remained running, and `/health` returned OK.
|
||
Rollback containers with suffix `20261004T143816Z` were retained stopped.
|
||
Final live verification passed (parent receipt `bash-1839`, exit 0): all seven
|
||
web entrypoint assets returned HTTP 200 and matched release `8905dc2` byte-for-byte
|
||
and by SHA256. Anonymous `GET /api/auth/me` and `POST /token` both returned 401.
|
||
Fresh mobile Chromium at 390×844 showed the login ready and visible with the
|
||
authenticated app hidden, zero JavaScript or console errors, zero unexpected
|
||
requests, and no horizontal overflow. Screenshot receipt:
|
||
[/tmp/hope-voice-live-login.png](/tmp/hope-voice-live-login.png).
|
||
The inspection client alone accepted the self-signed TLS certificate; production
|
||
TLS policy was not changed. No authenticated account-data or CRUD, live voice,
|
||
physical microphone, real TTS or native verification is claimed by this
|
||
deployment receipt.
|
||
|
||
## Web UX refinement — 2026-10-04 (production deployed)
|
||
|
||
Release `ea4e191` deployed successfully via [Gitea run244](https://git.srmr.xyz/snowspeeder/hope-voice-api/actions/runs/244)
|
||
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](docs/WEB_UX_REFINEMENT.md). 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 and `Blue, red` for a blue-left/red-right PNG.
|
||
- `hope-hermes-shane-fc44d140`: ready; real authenticated gateway image request
|
||
at port20001 returned HTTP200 and `Green, 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:20000` and `: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, API `127.0.0.1:20000`.
|
||
- `hope-hermes-shane-fc44d140`: host networking, ready, API `127.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; 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.
|