Implement hosted Breach/Compliance Record publication (WP-0006-T09)

migrations/0004_breach_records.sql models a case's lifecycle as
append-only events (alleged/cured/determined/terminated) grouped by
case_id rather than one mutable row - resolution is always a new,
later event, never an edit. A CHECK constraint makes the
anonymized-default rule (License V1C1 §7.4) a database fact:
named_entitlement_holder can be set if and only if anonymized = false.

src/target_revenue/breach_record.py's publish_breach_event() enforces
per-Licensor phase ownership and rejects named-disclosure requests
that don't also set named_disclosure_authorized_under_cua: true - the
Trust Service records the Licensor's assertion that the CUA's naming
clause authorizes it, it never verifies the underlying CUA text
itself. Signs every event with the same instance Ed25519 key already
used for Ledger entries and Attestations.

Adds POST/GET /phases/{id}/breach-records. Guarded the .registry
import behind a lazy in-function import (matching attestation.py's
TYPE_CHECKING pattern) so tests/test_breach_record.py (7 tests) runs
under plain system Python with no psycopg dependency. 5 new
Docker-gated tests cover the default-anonymized lifecycle, the
named-disclosure authorization gate, cross-Licensor rejection,
signature verification, and DB-level UPDATE/DELETE rejection.

This closes WP-0006 again - all 9 tasks done.
This commit is contained in:
tegwick 2026-07-29 22:23:41 +02:00
parent a2f1dbfa2e
commit b7b985d96d
9 changed files with 471 additions and 4 deletions

View file

@ -4,7 +4,7 @@ type: workplan
title: "Trust Service reference implementation (PRD Phase 4b)"
domain: infotech
repo: target-revenue
status: active
status: finished
owner: claude
topic_slug: infotech
created: "2026-07-29"
@ -351,7 +351,7 @@ left running.
```task
id: TREV-WP-0006-T09
status: todo
status: done
priority: medium
state_hub_task_id: "1a25114c-ccbb-417a-aa73-68f33120c5e4"
```
@ -384,3 +384,32 @@ workplan's ninth task, per TrustServicePRD TS-FR-7:
similar), `src/target_revenue/breach_record.py`, corresponding
`service/app.py` endpoints, and Docker-gated tests following the existing
pattern in `tests/test_hosted_conformance.py`/`test_ledger_hosting.py`.
**Result:** `migrations/0004_breach_records.sql` models a case's lifecycle
as append-only events (`alleged`/`cured`/`determined`/`terminated`)
grouped by `case_id`, rather than one mutable row per case — resolution is
always a new, later event, never an edit of the `alleged` one (`trf_app`
again has no UPDATE/DELETE grant). A `CHECK` constraint makes the
anonymized-default rule a database fact, not just an API convention:
`named_entitlement_holder` can be set if and only if `anonymized = false`.
`src/target_revenue/breach_record.py`'s `publish_breach_event()` enforces
per-Licensor phase ownership (same pattern as `ledger.py`), and — the
naming-authorization requirement License V1C1 §7.4/CUA §9 impose — rejects
any named-disclosure request that doesn't also set
`named_disclosure_authorized_under_cua: true`; the Trust Service never
verifies the underlying CUA text, it only records that the Licensor
asserted the clause authorizes it. Signs every published event with the
same instance Ed25519 key already used for Ledger entries and
Attestations. Added `POST/GET /phases/{id}/breach-records` to
`service/app.py` (write requires the Phase's own Licensor token; read is
public). Guarded the `.registry` import behind a lazy, in-function import
(matching `attestation.py`'s `TYPE_CHECKING` pattern) so
`tests/test_breach_record.py` (7 tests) runs its pure validation-logic
checks under plain system Python with no psycopg dependency. 5 new
Docker-gated tests in `tests/test_ledger_hosting.py` cover the default-
anonymized lifecycle (alleged → determined, both events preserved,
neither edited), the named-disclosure authorization gate (rejected without
it, accepted with it), cross-Licensor rejection, signature verification,
and DB-level UPDATE/DELETE rejection. Full suite: 56 passing offline
(plain system Python), 89 passing with Docker; no stray containers left
running. WP-0006 is finished again — all 9 tasks done.