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
140 lines
11 KiB
Markdown
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.
|