Adds receipt evidence kind, 422 unprocessable, optional revision on research records, audit_stale on the privacy policy, per-route body caps (25 MiB artifact uploads), promotion checks the project exists, memory rules skip content-free statuses. Handoff ledger covers every Wave A card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
10 KiB
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_<hash>: 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.requestedis a friendly mode or a route id; a vendor model name never appears in a contract.buildpins 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. sensitivityandredactionare mandatory on events and memory. Records leaving a tenant are redacted according toredaction.levelfirst.
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 |
| 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 |
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 whosesourcepoints at the old one. Sensitive entries needapproval_mode: askand 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,networkis denied insafe, anddeployalways asks whatever the level or grant says. Explicit grants override the matrix; an unexpireddenybeats everything. Seedefault_capability_matrix()andeffective_decision(). - Approvals move once from
pendingand are then terminal. Choices are the gateway's existingonce|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 inswitchyard-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 onnever_again, dismissal or action. - Release is linear:
reviewed -> merged -> built -> verified -> deployed -> converged -> live_verified, withrolled_backreachable 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.