diff --git a/docs/hux/HANDOFF.md b/docs/hux/HANDOFF.md index 3609dcd0..0d09cd6e 100644 --- a/docs/hux/HANDOFF.md +++ b/docs/hux/HANDOFF.md @@ -38,7 +38,7 @@ Run everything with the CI interpreter: | | | |---|---| -| Commits | `47a9fd83` (contract freeze 1.0.0), `7cf9a714` (service core) | +| Commits | `b4145861` (contract freeze 1.0.0), `6a5e0d87` (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 | @@ -53,35 +53,35 @@ Full suite: `testing/tests/test_hermes_hux_*.py` = 325 tests, 99% line / 99% bra | 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 | +| HUX-01 activity timeline | `b8fb72fb` | `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 | `b8fb72fb` | `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 | `b8fb72fb` | `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 | `9f0ffe03` (+ 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 | `9f0ffe03` | `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 | `3ebee2cc` | `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 | `3ebee2cc` | `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: +A fresh reviewer attacked HEAD `1b1a14e9` (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` | +| F1 | critical | worker/api trust could `PUT /policy` and self-approve | `6964a9d8` — policy writes and allow grants are human-surface only | +| F2 | high | worker trust could read every tenant record (SO-08) | `6964a9d8` — `flags.WORKER_ROUTES` allowlist enforced in `Router.dispatch`; unexpected exceptions become audited 500 records | +| F3 | high | memory `edit` skipped topic/sensitivity/private-mode gates | `681b0408` — one `_classify` path for proposals and edits | +| F4 | high | a `session` approval for an external effect released unrelated later effects | `6964a9d8` — 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 | `681b0408` — seq = max(checkpoint, ledger tail + 1) under the lock | +| F6 | medium | gate ignored budget exhaustion | `6964a9d8` | +| F7 | medium | stale unconditional write could resurrect a forgotten memory | `681b0408` — re-read under lock, always write with the loaded revision | +| F8 | medium | any caller could assert `process_registry_empty`; failed receipts were sticky | `6964a9d8` — gateway (worker trust) only; failed receipts supersedable | +| F9 | medium | secrets stored verbatim in titles/passages/claims/notebooks; forget left the title | `681b0408` — scrub applied; forget blanks the document. Artifact bodies stay verbatim (user-owned) but are forced `restricted` and audited (`a75ca299`) | +| F10 | medium | SO-46/48/53 claimed but absent | threat model amended (`74c65499`): tracked as open, not claimed | +| F11 | low | stored receipts carry `revision` the schema forbade | `6964a9d8` — optional `revision` on the receipt | +| F12 | low | capabilities advertised routes without handlers | `6964a9d8` — 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) | `6964a9d8` + `681b0408` | 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`. @@ -89,7 +89,7 @@ Suite after repairs: 377 tests, 99% line / 99% branch over `hermes-hux-foundatio | | | |---|---| -| Commit | `532add3a` | +| Commit | `aeef0f84` | | 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 | diff --git a/docs/hux/THREAT-MODEL.md b/docs/hux/THREAT-MODEL.md index 365bfb07..431696a3 100644 --- a/docs/hux/THREAT-MODEL.md +++ b/docs/hux/THREAT-MODEL.md @@ -249,7 +249,7 @@ Sized for 4-8 tenants, one human each, on a 10 Gi PVC shared with the WebUI. ## Wave A review outcome (2026-08-24) -A fresh adversarial review of HEAD `124206b7` produced thirteen findings +A fresh adversarial review of HEAD `1b1a14e9` produced thirteen findings (see `docs/hux/HANDOFF.md`, "Wave A review"). Corrections to the obligations above as a result: