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
107 lines
11 KiB
Markdown
107 lines
11 KiB
Markdown
# hermes-next: Claude backend handoff ledger
|
||
|
||
Branch `feature/hermes-next-hux` (origin, pushed after every card); draft PR #55 https://scm.bstein.dev/atlas/titan-iac/pulls/55 (opened through the SCM broker, never merged by Claude). Board
|
||
`hermes-next` on the operator Hermes Kanban; cards HUX-01..HUX-12 are parked
|
||
(`blocked`) on purpose so the CLI lane runner never auto-claims them — the
|
||
Claude coordinator and the Codex root session are the only implementers.
|
||
Contract: `hux.v1` 1.0.0 (`services/hermes/contracts/hux/VERSION`,
|
||
`docs/hux/ADR-0001-hux-v1-contract-freeze.md`). Threat model:
|
||
`docs/hux/THREAT-MODEL.md` (SO-01..SO-54). Data model: `docs/hux/DATA-MODEL.md`.
|
||
|
||
Run everything with the CI interpreter:
|
||
`python -m pytest testing/tests/test_hermes_hux_*.py -q` and
|
||
`python -m coverage run --branch --source=dockerfiles/hermes-hux-foundation -m pytest testing/tests/test_hermes_hux_*.py`.
|
||
|
||
## Codex integration requirements common to every card
|
||
|
||
- Package `dockerfiles/hermes-hux-foundation/hux/` runs as `python -m hux.server`
|
||
inside each `hermes-chat-tenant` pod (and the Worker) on `127.0.0.1:8790`.
|
||
Image, manifest, NetworkPolicy and Flux wiring are Codex's (SO-01, SO-02,
|
||
SO-45: separate uid, `/opt/data/hux` mode 0700).
|
||
- Env: `HUX_DATA_ROOT` (tenant PVC, default `/opt/data`), `HUX_TENANT_SLOT`
|
||
(pin, SO-03), `HUX_FLAGS` (comma list, default empty = everything off),
|
||
`HUX_RELAY_KEY` (Telegram relay), `HUX_WORKER_KEY` (Worker),
|
||
`HUX_BUILD_COMMIT`, `HUX_IMAGE_DIGEST`, `HUX_CONTRACT_DIR` (schemas path in
|
||
the image; default resolves relative to the repo layout).
|
||
- Router → service headers (SO-04): `X-Hermes-Tenant-Identity: slot-N`,
|
||
`X-Hux-Subject: usr_<hash>`, `X-Hux-Surface: chat|telegram|voice|api|worker`,
|
||
optional `X-Hux-Trust: relay|worker` + `X-Hux-Relay-Key`. The router must
|
||
strip any inbound `X-Hux-*` before setting its own.
|
||
- First client call: `GET /hux/v1/capabilities` → which cards/routes are on.
|
||
Disabled or unknown → `404` (`flag_off` vs `not_found` only in the body).
|
||
- Mutations: `If-Match: <revision>` on revisioned records (409 on mismatch);
|
||
`Idempotency-Key` on creates. Errors are `hux.error.v1`.
|
||
- Fixtures Codex codes against: `services/hermes/contracts/hux/examples/*.json`
|
||
(validated in CI by `test_hermes_hux_contract_schemas.py`).
|
||
|
||
## HUX-11 shared foundation — DONE (backend)
|
||
|
||
| | |
|
||
|---|---|
|
||
| Commits | `47a9fd83` (contract freeze 1.0.0), `7cf9a714` (service core) |
|
||
| Files | `services/hermes/contracts/hux/{common,identity}.schema.json` + examples, `VERSION`; `dockerfiles/hermes-hux-foundation/hux/{__init__,errors,identity,flags,store,audit,http,foundation,server,contracts,rules}.py`; `docs/hux/ADR-0001-*.md`, `THREAT-MODEL.md`, `DATA-MODEL.md` |
|
||
| Tests | `testing/tests/test_hermes_hux_contract_schemas.py` (61), `test_hermes_hux_contract_foundation.py` (30): identity header rejection matrix, relay/worker keys, slot pinning, fail-closed flag chains, capabilities record validity, tenant-scoped paths + traversal rejection, revisions/conflicts, torn-ledger recovery, blobs, manifest, concurrent writers, audit rows, flag-off == not-found, 401 never touches storage, real HTTP server JSON + SSE, loopback-only bind, all errors validate, ≤500 LOC guard |
|
||
| Coverage | 99% line / 98% branch over the package |
|
||
| Contract | 1.0.0 |
|
||
| Flag | `hux.foundation` (root of every dependency chain) |
|
||
| Risks | Rate limiting (SO-53) and hash-chained audit (SO-46) not yet implemented — tracked for the Wave A review; relay callers still carry `X-Hux-Subject` (deviates from SO-06; the slot→owner mapping lives in the router, so the header is redundant but harmless and lets the service pin the subject) |
|
||
| Codex needs | Wire the service into the tenant pod + Worker, NetworkPolicy, router header contract above, `HUX_FLAGS=hux.foundation` for the first canary; `testing/quality_contract.json` may add `dockerfiles/hermes-hux-foundation/**/*.py` to `line_limit_globs` and the two moved modules to `managed_modules` (Claude does not edit that file) |
|
||
|
||
## Wave A backends — DONE, awaiting adversarial review and Codex integration
|
||
|
||
Full suite: `testing/tests/test_hermes_hux_*.py` = 325 tests, 99% line / 99% branch over `dockerfiles/hermes-hux-foundation/hux/` (every family module 99–100%). Every module ≤ 500 LOC (guarded by tests). Contract revised additively to 1.1.0 (ADR-0001).
|
||
|
||
| Card | Commit | Files | Tests | Flag | Codex needs |
|
||
|---|---|---|---|---|---|
|
||
| HUX-01 activity timeline | `1cb6f07c` | `hux/events.py`, `hux/redaction.py` | `test_hermes_hux_contract_events.py` (34): ordering, idempotency, replay/reconnect, redaction by surface, cancellation receipts, cross-tenant | `hux.activity_timeline` | Router forwards `Last-Event-ID`; telegram/voice surfaces get partial redaction; agent hook posts events with `Idempotency-Key`; `HUX_CANARY_FILE=/opt/data/.env` so secrets are scrubbed |
|
||
| HUX-02 memory center | `1cb6f07c` | `hux/memory.py` | `test_hermes_hux_memory_ledger.py` (20), `test_hermes_hux_memory_retrieval.py` (7): no-store, suggest-only, edit/supersede, forget, retrieval removal, export, If-Match | `hux.memory_control` (needs `hux.privacy`) | Agent memory tool must call `hux.memory.retrieve` semantics (tombstones first) instead of the upstream memory file; UI per-message controls call `/memory/{id}/{action}` with `If-Match` |
|
||
| HUX-10 privacy | `1cb6f07c` | `hux/privacy.py` | `test_hermes_hux_privacy_topics.py` (19), `test_hermes_hux_privacy_retention.py` (6) | `hux.privacy` | Schedule `run_retention` daily (no thread inside the service by design); surface `HUX-Audit-Stale`; show notices and post `chosen` |
|
||
| HUX-04 artifacts | `ada74ce0` (+ upload caps, project check in the 1.1.0 batch) | `hux/artifacts.py`, `hux/diffs.py` | `test_hermes_hux_artifact_versions.py`, `test_hermes_hux_artifact_auth.py` (immutable versions, concurrency, lineage forgery 404, caps 413, diffs, promotion) | `hux.artifacts` (needs `hux.projects`) | Version content served with `nosniff` + attachment; uploads ≤ 25 MiB content via base64; sharing intentionally not implemented (`access.mode` always `owner`) |
|
||
| HUX-08 research | `ada74ce0` | `hux/research.py` | `test_hermes_hux_research_citations.py`, `test_hermes_hux_research_notebook.py` (dedupe, integrity, notebook state machine) | `hux.research` | Service never fetches URIs (SO-19): the web tool records sources/passages after its own fetch; citation strip = `GET /messages/{id}/citations` (passages + sources embedded) |
|
||
| HUX-05 autonomy | `b3de70ba` | `hux/policy.py`, `hux/budgets.py` | `test_hermes_hux_policy_matrix.py` (22), `_approvals.py` (22), `_receipts.py` (13) | `hux.autonomy` (needs `hux.activity_timeline`) | Agent hook: `POST /approvals` before any external side effect, `POST /runs/{id}/gate` immediately before executing with the canonical argument hash, `POST /runs/{id}/budget` per turn, `POST /runs/{id}/stop` with `process_registry_empty` from the real process registry; decisions only from human surfaces (worker trust → 403) |
|
||
| HUX-03 organization API | `b3de70ba` | `hux/organization.py` | `test_hermes_hux_contract_organization.py` (14) | `hux.projects` | Codex owns the UI + migration of upstream WebUI projects; search over `message_text` is a later increment |
|
||
|
||
Known gaps carried to the Wave A review: rate limiting (SO-53), hash-chained audit (SO-46), retention scheduler ownership (Codex cron vs service thread), approval expiry applied lazily on read, linear scans for idempotency/dedupe (fine at documented caps).
|
||
|
||
## Wave A adversarial review — CLOSED
|
||
|
||
A fresh reviewer attacked HEAD `124206b7` (tenant isolation, privacy, autonomy, rollback) and reported 13 findings; the repro harness lives outside the repo. Every finding is closed by a regression test that cites it:
|
||
|
||
| # | Severity | Finding | Fix commit |
|
||
|---|---|---|---|
|
||
| F1 | critical | worker/api trust could `PUT /policy` and self-approve | `dd80e4bd` — policy writes and allow grants are human-surface only |
|
||
| F2 | high | worker trust could read every tenant record (SO-08) | `dd80e4bd` — `flags.WORKER_ROUTES` allowlist enforced in `Router.dispatch`; unexpected exceptions become audited 500 records |
|
||
| F3 | high | memory `edit` skipped topic/sensitivity/private-mode gates | `4417475d` — one `_classify` path for proposals and edits |
|
||
| F4 | high | a `session` approval for an external effect released unrelated later effects | `dd80e4bd` — external effects match only the same run + argument hash; gate conversation from the run, not the body |
|
||
| F5 | high | seq duplicated after a crash between append and checkpoint | `4417475d` — seq = max(checkpoint, ledger tail + 1) under the lock |
|
||
| F6 | medium | gate ignored budget exhaustion | `dd80e4bd` |
|
||
| F7 | medium | stale unconditional write could resurrect a forgotten memory | `4417475d` — re-read under lock, always write with the loaded revision |
|
||
| F8 | medium | any caller could assert `process_registry_empty`; failed receipts were sticky | `dd80e4bd` — gateway (worker trust) only; failed receipts supersedable |
|
||
| F9 | medium | secrets stored verbatim in titles/passages/claims/notebooks; forget left the title | `4417475d` — scrub applied; forget blanks the document. Artifact bodies stay verbatim (user-owned) but are forced `restricted` and audited (`this commit`) |
|
||
| F10 | medium | SO-46/48/53 claimed but absent | threat model amended (`5acf640d`): tracked as open, not claimed |
|
||
| F11 | low | stored receipts carry `revision` the schema forbade | `dd80e4bd` — optional `revision` on the receipt |
|
||
| F12 | low | capabilities advertised routes without handlers | `dd80e4bd` — HUX-06/09/12 routes empty until shipped |
|
||
| F13 | low | misc (healthz version, ghost conversations from notices, idempotency outside the lock, artifact titles not searchable, unnormalised paths, multi-hash once approvals) | `dd80e4bd` + `4417475d` |
|
||
|
||
Suite after repairs: 377 tests, 99% line / 99% branch over `hermes-hux-foundation` and `hermes-worker-hux`. Full repo gate: same 13 pre-existing, unrelated failures as `main`.
|
||
|
||
## Worker hook library — DONE
|
||
|
||
| | |
|
||
|---|---|
|
||
| Commit | `532add3a` |
|
||
| Files | `dockerfiles/hermes-worker-hux/hux_hook/{__init__,client,hooks}.py`, `NOTES.md` (wiring guide for the runtime patch, which is Codex's file) |
|
||
| Tests | `test_hermes_hux_policy_hook.py` (11), `test_hermes_hux_contract_hook.py` (10) — real service in-process, end-to-end approval → human decision → gate released once, canary never persisted, unreachable service fails closed for side effects |
|
||
| Codex needs | Agent container env `HUX_BASE_URL`, `HUX_TENANT_SLOT`, `HUX_SUBJECT`, `HUX_WORKER_KEY`; call order per `NOTES.md`: `before_tool` → execute only on `proceed` → `after_tool`; `record_spend` per turn; `on_stop` returning `None` means the stop is not done |
|
||
|
||
## Conversation privacy state — DONE
|
||
|
||
`GET /hux/v1/conversations/{id}/privacy` (HUX-10, worker-callable) returns forgotten / memory_disabled / topics / mode / `memory_writes_allowed`; `hux_hook.memory_gate` now reads it and fails closed. Tests: `test_hermes_hux_privacy_topics.py::test_conversation_privacy_state_route`, hook memory-gate test extended.
|
||
|
||
## Open items (not blockers for Codex integration)
|
||
|
||
- SO-46 hash-chained audit, SO-48 single-writer lock, SO-53 rate limits.
|
||
- Retention scheduler: `privacy.run_retention` is on-demand; Codex decides cron vs sidecar.
|
||
- Search over `message_text`; artifact sharing (`shared_readonly`).
|
||
- `testing/quality_contract.json` line-limit globs / managed modules for the two new package paths (Codex's file).
|