atlas-iac/docs/hermes_hux_contracts.md
jenkins 18b980d6fa hermes(hux): expose conversation privacy state for the agent hook
GET /hux/v1/conversations/{id}/privacy reports forgotten, memory_disabled,
topics, mode and memory_writes_allowed; the worker hook's memory gate reads it
and fails closed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
2026-08-24 00:54:34 -03:00

140 lines
11 KiB
Markdown

# 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.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.