# Hermes chat UX program (HUX): shared contracts This is the contract-only foundation for the twelve-card program that makes `chat.bstein.dev` a first-class assistant surface. Nothing here changes runtime behaviour. It fixes the shapes, state machines and ownership that every later slice codes against, so the UI side and the backend side can work in parallel without inventing informal shapes. Schemas live in `services/hermes/contracts/hux/` as JSON Schema 2020-12 with one worked example per record under `examples/`. The rules a schema cannot express (state machines, capability matrix, mode catalog, privacy defaults, suggestion gating, flag dependencies) live in `dockerfiles/hermes-hux-foundation/hux/rules.py`; `hux/contracts.py` validates records without external packages. `testing/tests/test_hermes_hux_contract_schemas.py` keeps schemas, examples, rules and the live Switchyard catalog in agreement. The contract is frozen at `1.1.0` (see `docs/hux/ADR-0001-hux-v1-contract-freeze.md` for identity headers, `If-Match`, idempotency and compatibility rules). ## Where the backend lives The chat product has exactly one repo-owned HTTP surface on the request path: the Go tenant router (`services/hermes/router/`). It already owns the Keycloak identity to slot mapping, the admin-surface deny list, durable state on its PVC, and the JS/CSS bridge it injects into every WebUI page. New HUX APIs are therefore router routes under `/hux/v1/`, storing per-tenant records on the tenant PVC beside the existing Telegram media store. The upstream WebUI is a pinned image patched at build time; UI work goes through the injected bridge and, where unavoidable, the fail-closed patch scripts in `dockerfiles/`. `/hux/v1` returns 404 until `hux.foundation` is enabled. Every other flag depends on it (see `flags.json`), and the router refuses to serve a flag whose dependencies are off. ## Identity and provenance rules - Users are `usr_`: the hashed Keycloak subject the router already uses. Raw subjects, emails and Telegram ids never appear in a HUX record. - Every record carries `provenance` (surface, actor, time, session/run, route, build). `route.requested` is a friendly mode or a route id; a vendor model name never appears in a contract. `build` pins the commit and image digest so the timeline can say which release produced a decision. - Evidence is referenced, never inlined (`evidence_ref`). The UI expands a reference through the API that owns it, which is how redaction stays in one place. - `sensitivity` and `redaction` are mandatory on events and memory. Records leaving a tenant are redacted according to `redaction.level` first. ## API surface (contract only) All routes are tenant-scoped through the router; the caller never names the tenant. Responses are the records below, wrapped as `{"items": [...], "next": cursor}` for lists. | Area | Routes | Record | |---|---|---| | Foundation (HUX-11) | `GET /hux/v1/capabilities`, `GET /hux/v1/manifest` | `hux.capabilities.v1`, `hux.manifest.v1`, errors as `hux.error.v1` | | Events (HUX-01) | `GET/POST /hux/v1/conversations/{id}/events?after_seq=N&limit=` (JSON, ≤200), `GET .../events/stream` (SSE, `id:` = seq, resumes from `Last-Event-ID`) | `hux.event.v1` | | Memory (HUX-02) | `GET/POST /hux/v1/memory`, `GET /hux/v1/memory/{id}`, `POST /hux/v1/memory/{id}/{approve,reject,forget,edit,remove_retrieval,restore_retrieval}`, `GET /hux/v1/memory/export` | `hux.memory.v1` | | Projects (HUX-03) | `GET/POST /hux/v1/projects`, `GET/PATCH /hux/v1/projects/{id}`, `GET/POST /hux/v1/conversations`, `GET/PATCH /hux/v1/conversations/{id}`, `POST /hux/v1/conversations/{id}/branch`, `GET /hux/v1/conversations/{id}/lineage`, `GET /hux/v1/search?q=&project_id=` (message_text not indexed yet; response says `indexed`/`not_indexed`) | `hux.project.v1`, `hux.conversation.v1` | | Artifacts (HUX-04) | `GET/POST /hux/v1/artifacts`, `POST /hux/v1/artifacts/{id}/versions`, `GET .../versions/{n}/diff?from=`, `POST .../promote` | `hux.artifact.v1` | | Autonomy (HUX-05) | `GET/PUT /hux/v1/policy?scope=`, `GET/POST /hux/v1/approvals` (agent hook creates with `request.evidence[{kind: tool_call, id, hash}]`), `GET/POST /hux/v1/approvals/{id}` (human decision `once|session|always|deny`; worker/api trust gets 403), `POST /hux/v1/runs/{id}/gate` (`{capability, argument_hash, external}` → `{proceed, approval_id?, reason}`), `GET/POST /hux/v1/runs/{id}/budget`, `POST /hux/v1/runs/{id}/stop` returns the receipt (repeat returns the same one) | `hux.policy.v1`, `hux.approval.v1`, `hux.cancel_receipt.v1`, `hux.budget_state.v1` | | Modes (HUX-06) | `GET /hux/v1/modes`, `PUT /hux/v1/conversations/{id}/mode` | `hux.mode.v1` | | Research (HUX-08) | `POST /hux/v1/sources`, `GET /hux/v1/sources/{id}`, `POST /hux/v1/passages`, `GET/POST /hux/v1/messages/{id}/citations`, `POST /hux/v1/notebooks`, `GET/PATCH /hux/v1/notebooks/{id}` | `hux.source.v1`, `hux.passage.v1`, `hux.citation.v1`, `hux.research_notebook.v1` | | Onboarding (HUX-09) | `GET /hux/v1/suggestions?context=`, `POST /hux/v1/suggestions/{id}/{dismiss,never,acted}` | `hux.suggestion.v1`, `hux.suggestion_state.v1` | | Privacy (HUX-10) | `GET /hux/v1/privacy/policy` (+ `HUX-Audit-Stale` header), `POST /hux/v1/privacy/notices` (optional `chosen` control), `POST /hux/v1/conversations/{id}/forget`, `GET /hux/v1/privacy/audit`, `GET /hux/v1/conversations/{id}/privacy` (forgotten / memory_disabled / topics / mode / memory_writes_allowed — the hook reads this before proposing memory) | `hux.privacy_policy.v1`, `hux.privacy_notice.v1`, `hux.retention_audit.v1` | | Release (HUX-12) | `GET /hux/v1/releases` (operator surface, worker only) | `hux.release.v1` | Multimodal (HUX-07) reuses artifacts for images/audio (`type` image/audio, lineage for variants) and events for transcript corrections (`kind: artifact.version` on the transcript artifact). It has no schema of its own by design. ## State machines and rules that are fixed here - **Memory** is a ledger: `proposed -> active|rejected`, `active -> expired|forgotten`, `expired -> forgotten`. Nothing edits content in place; a correction is a new entry whose `source` points at the old one. Sensitive entries need `approval_mode: ask` and must expire or decay; restricted content and the credentials/minors/location/biometric topics are never written. Forgotten entries keep their audit trail and drop their content. - **Autonomy** has three levels. Read-only capabilities are always allowed, mutating ones ask unless the level is `autonomous`, `network` is denied in `safe`, and `deploy` always asks whatever the level or grant says. Explicit grants override the matrix; an unexpired `deny` beats everything. See `default_capability_matrix()` and `effective_decision()`. - **Approvals** move once from `pending` and are then terminal. Choices are the gateway's existing `once|session|always|deny`. - **Cancellation** produces a receipt with an outcome and the side effects observed, each marked reverted or not. "Stop" is not done until the receipt exists. - **Modes** are intents mapped to Switchyard constraints, never to a vendor. Fast, Thoughtful, Research and Create all allow both hosted providers; Private is local-only, memory off, web off, ephemeral, and cannot be overridden to a hosted route. The advanced drawer may pin an exact `atlas/manual/...` route id; the test suite checks each default route exists in `switchyard-configmap.yaml`. - **Privacy** scopes sensitive topics to the conversation, never shares them across surfaces, decays them within 30 days, and shows a just-in-time notice with real controls (`forget_this_conversation`, `switch_to_private`, `disable_memory_here`, `dismiss`). A daily retention audit record proves the rules ran. - **Suggestions** need a trigger context, are always dismissable, honour `max_shows` (at most 5) and a cooldown of at least an hour, and stop for good on `never_again`, dismissal or action. - **Release** is linear: `reviewed -> merged -> built -> verified -> deployed -> converged -> live_verified`, with `rolled_back` reachable from any state that has an image. Each step names its evidence: merge commit, CI build plus image digest, Harbor digest equal to the image digest, Flux revision, running pod digest equal to the image digest, passing health probe. `release_transition_problems()` is the single source of truth the Worker's task state must call before it claims anything is deployed. ## Cards, owners, waves, flags `flags.json` is the registry. All twelve flags default off and are read from the `HUX_FLAGS` comma list; a flag counts only when its dependency chain is on. | Card | Flag | Wave | Backend (Claude) | Frontend (Codex) | |---|---|---|---|---| | HUX-11 Shared foundation | `hux.foundation` | A | `/hux/v1` router scaffold, provenance, redaction | Bridge integration in Chat, Worker, Telegram, voice | | HUX-01 Activity timeline | `hux.activity_timeline` | A | Event store and stream | Timeline UI, decision cards, evidence expansion | | HUX-02 Memory control | `hux.memory_control` | A | Ledger, provenance, TTL, approval, migration | Memory page, per-message controls, export | | HUX-05 Autonomy controls | `hux.autonomy` | A | Policy engine, grants, budgets, approvals, receipts | Ask First/Safe/Autonomous, permission matrix, stop UX | | HUX-10 Privacy behaviour | `hux.privacy` | A | Threat model, topic policy, scoping, audit | Notices, controls, regression tests | | HUX-12 Deployment follow-through | `hux.release_followthrough` | A | Release state machine in the Worker | Exact-SHA CI, Flux convergence, live evidence, rollback UI | | HUX-03 Projects/chats | `hux.projects` | B | Data model, indexes, branch lineage | Navigation, search, branch graph, migration UI | | HUX-04 Artifact workspace | `hux.artifacts` | B | Typed versions, diffs, lineage, promotion | Preview/edit/diff/share workspace | | HUX-06 Friendly modes | `hux.friendly_modes` | B | Mode catalog and Switchyard mapping | Chooser and advanced route drawer | | HUX-08 Research/citations | `hux.research` | B | Sources, passages, citations, notebook | Citation strip, source drawer, integrity E2E | | HUX-07 Multimodal | `hux.multimodal` | C | Attachment lineage, transcript correction, permissions | Drag/drop, previews, annotation, consent | | HUX-09 Onboarding | `hux.onboarding` | C | Suggestion policy and suppression | Dismissible UI, restraint tests | ## Versioning Record schemas carry their version in the `schema` field (`hux.event.v1`). Adding optional fields is a compatible change; anything else is a `v2` record served beside `v1` until every surface has moved. Consumers must ignore unknown optional fields and must not reuse record ids across versions.