atlas-iac/docs/hux/ADR-0001-hux-v1-contract-freeze.md

92 lines
5.8 KiB
Markdown
Raw Normal View History

# ADR-0001: freeze the hux.v1 contract at 1.0.0
Status: accepted, 2026-08-24. Owners: Claude (backend), Codex (integration).
## Decision
`services/hermes/contracts/hux/` is frozen as contract version `1.0.0`
(`VERSION`, `flags.json.contract_version`). It is the only shape any HUX
surface (Chat, Worker, Telegram, voice) codes against. The Python reference
implementation lives in `dockerfiles/hermes-hux-foundation/hux/`:
`contracts.py` validates records, `rules.py` holds the governance rules the
schemas cannot express.
Records carry their own version in `schema` (`hux.event.v1`). Wire fixtures
under `services/hermes/contracts/hux/examples/` are validated by
`testing/tests/test_hermes_hux_contract_schemas.py` and are the exchange
format with Codex: a change to a fixture is a change to the contract.
## What 1.0.0 covers
| Requirement | Where |
|---|---|
| Tenant and surface identity | `common.identity` (tenant_slot, hashed subject, surface, trust: router/relay/worker) — carried on every stored record that a caller can create |
| Capability negotiation, default-off flags | `identity.capabilities` (`GET /hux/v1/capabilities`), `flags.json`; a card is enabled only when its dependency chain is enabled (`rules.flag_enabled`) |
| Redacted activity events | `event`: `seq` (monotonic per conversation), `turn`, `correlation_id`, `idempotency_key`, replay by `after_seq`, kinds for decisions, delegation, failure/completion, cancellation, budget and side-effect gates; `detail` is allowlisted per kind and scrubbed |
| Memory | `memory`: provenance, `source`, `reason`, `sensitivity`, `topic`, `ttl`, `approval_mode` incl. `no_store`, `status` incl. `no_store`, edit via `supersedes`, delete via `forgotten`, retrieval removal via `retrievable=false`, revisioned |
| Projects, lineage, artifacts, versions, diffs, authorization | `project`, `conversation.branch`, `artifact.versions[]` immutable with `content_ref` hash, `diff_from`, `lineage`, `promotion`, `access`, `revision` |
| Autonomy | `permission.policy` (levels, grants, budgets: tokens/tool calls/wall clock/spend/subagents/delegations/scope), `approval` with `request.external`, `cancellation_receipt`, `budget_state` |
| Research | `citation.source/passage/citation/notebook` with classification, support verdict, notes, assumptions, unresolved questions, `dedupe_key` |
| Additive migrations, rollback readers | `identity.manifest` (`data_layout_version`, `min_reader_contract_version`); rules in "Compatibility" below |
## Wire conventions
- Identity is asserted by the trusted hop, never by the client body: the
chat router sets `X-Hermes-Tenant-Identity: slot-N`, `X-Hux-Subject:
usr_<hash>`, `X-Hux-Surface: chat`; the Telegram relay adds the relay key
and `trust: relay`; the Worker uses `trust: worker`. Requests without a
complete, well-formed identity are `401 unauthorized`. The service must
only be reachable from those hops (NetworkPolicy is Codex's to enforce).
- Mutations of revisioned records send `If-Match: <revision>`; a mismatch is
`409 conflict` with `hux.error.v1`. Creates accept `Idempotency-Key`; a
repeat returns the original record with `200` instead of a duplicate.
- Lists return `{"items": [...], "next": <cursor|null>}`. Event pages use
`after_seq`; the SSE stream sets `id:` to `seq` so `Last-Event-ID`
resumes exactly.
- Errors are always `hux.error.v1`. A disabled card answers `404 flag_off`
so an old client and a flag-off server look identical.
- Every read and mutation writes a `common.audit_outcome`.
## Compatibility rules
1. Within `1.x`: only add optional fields, enum values or new record kinds.
Never rename, remove, change a type, or make an optional field required.
2. Readers ignore unknown fields and unknown enum values they do not need;
a reader that needs a new enum value must check `contract_version`.
3. On-disk data is append-only per family; `data_layout_version` bumps only
for additive layout changes, and `min_reader_contract_version` says the
oldest service that can still read it. Rolling the service back to any
version ≥ `min_reader_contract_version` leaves data readable.
4. Anything outside 1–3 is `hux.v2`, served beside v1 until every surface
has moved.
## Revision 1.1.0 (2026-08-24, additive)
Raised after the three Wave A lanes reported. Adds `receipt` to
`evidence_ref.kind`, `unprocessable` (422) to error codes, optional
`revision` on source/passage/citation, optional `audit_stale` on the privacy
policy, and per-route body caps (artifact uploads up to 25 MiB content). The
argument hash a `once` approval is released against travels as
`request.evidence[] = {kind: "tool_call", id, hash}`; the agent hook must
send it or the gate can never release. Memory rules skip content-free
statuses (`no_store`, `forgotten`, `rejected`).
Review follow-up (same revision, still additive): `cancellation_receipt`
gains optional `revision`. Stored documents in every revisioned family carry
the store's `revision`, and a `failed_to_cancel` receipt is the one
non-terminal outcome: a later stop that the gateway (worker trust) confirms
supersedes it with a revision bump; an identical repeat replays. Only a
human actor (router/relay trust on chat, telegram or voice) may write a
policy or hold an `allow` grant; `trust: worker` reaches only the routes in
`hux.flags.WORKER_ROUTES` (SO-08). External side effects release only
against an approval for the same run and argument hash. Cards HUX-06,
HUX-09 and HUX-12 declare no routes until they ship.
## Consequences
- Codex codes UI against the fixtures, not against the running service.
- Claude's implementation cards (HUX-01/02/03/04/05/08/10/11) may add
optional fields but must ship the fixture change in the same commit.
- If Codex needs a shared-interface change, it lands as a `1.(x+1).0`
revision of the schemas and fixtures here, never as an informal edit.