AUDIT-WP-0009 — record T04/T06/T07 as done
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv Assistant: claude-code Assistant-Model: opus Assistant-Process: 2069992@bnt-lap001 Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
This commit is contained in:
parent
b098fb12ca
commit
8a2a09deae
1 changed files with 57 additions and 3 deletions
|
|
@ -164,10 +164,32 @@ occurred. Twelve tests in `tests/test_senders.py`.
|
|||
|
||||
```task
|
||||
id: AUDIT-WP-0009-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f36470af-4019-54b2-96d7-e049d867c7db"
|
||||
```
|
||||
Done 2026-09-10. `audit_core/stream_findings.py`, surfaced at
|
||||
`GET /v1/stream-findings`, documented in `docs/stream-completeness.md`.
|
||||
|
||||
A heartbeat is an **ordinary event** — same envelope, same append-only custody,
|
||||
same chain, no special table. A heartbeat stored outside the chain would be the
|
||||
one record in this store that could be back-dated.
|
||||
|
||||
Declared **per class**, not per source, on the sender registration
|
||||
(`heartbeat_classes`). `informed-decision` raised the shape first and it
|
||||
generalises: a per-source heartbeat from a mixed-volume emitter is satisfied by
|
||||
its chattiest class and says nothing about the quiet, security-relevant one,
|
||||
which is the only reason heartbeats exist.
|
||||
|
||||
`no_heartbeat_since_registration` is a distinct finding kind rather than a skip
|
||||
— a source that declared a heartbeat and never sent one is the case most likely
|
||||
to be a broken integration, and it is exactly what a "compare against last
|
||||
seen" implementation drops silently. A wildcard source is refused rather than
|
||||
guessed at: there is no determinate set of streams to expect a heartbeat from.
|
||||
|
||||
Explicitly not T05's territory. Cadence describes expected *rate* and belongs
|
||||
to the §17 Taxonomy schema; this says how often a source promises to say
|
||||
*nothing to report* for a class that may legitimately be silent.
|
||||
Heartbeat ingestion and missing-heartbeat findings. The required form for
|
||||
low-volume load-bearing classes, and the only control covering adversarial
|
||||
omission for exactly the rare negative events — revocation, denial, containment
|
||||
|
|
@ -191,10 +213,29 @@ alternatives.
|
|||
|
||||
```task
|
||||
id: AUDIT-WP-0009-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "0a8d6eed-75dd-5aaa-bf86-60be9dadca03"
|
||||
```
|
||||
Done 2026-09-10. `GET /v1/reconciliation?source=&tenant=&since=&until=`,
|
||||
per-class counts, `event_counts` on both the SQLite and Postgres backends.
|
||||
Counts only — never payloads.
|
||||
|
||||
**The read-model tension, resolved rather than ignored.** The task said respect
|
||||
`may_read`. But every registered sender holds `may_read: false`, so read
|
||||
literally that makes the §9.6 reconciliation obligation undischargeable by
|
||||
every source actually registered — a strange end for a rule audit-core argued
|
||||
for. The resolution: a source asking how many of *its own* events audit-core
|
||||
holds is not reading the archive, because it learns nothing it did not itself
|
||||
emit. So the surface is scoped to the caller's own permitted sources and
|
||||
tenants and carries no payloads; another source's counts, or an unscoped
|
||||
aggregate, stay behind `may_read` and full tenant scope.
|
||||
|
||||
Two refusals: another source's counts return **403, not an empty count**, since
|
||||
a zero would read as "we hold none of yours" — a false answer to a question
|
||||
about completeness. And there is **no default window**: a count whose bounds the
|
||||
caller did not choose is not comparable to anything the caller computed, and
|
||||
would be quoted as though it were.
|
||||
Reconciliation surface: per-source, per-class event counts over a bounded
|
||||
window, readable by the source itself, so an emitter can compare audit-core's
|
||||
count against its own state transitions and treat divergence as a finding.
|
||||
|
|
@ -204,10 +245,23 @@ scoping, and no payloads in a counts response.
|
|||
|
||||
```task
|
||||
id: AUDIT-WP-0009-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "f572dfeb-0d1b-58d6-be83-405125189028"
|
||||
```
|
||||
Done 2026-09-10. `GET /v1/stream-findings`, following the existing
|
||||
findings-surface conventions rather than adding a new shape: `may_read` plus
|
||||
full tenant scope, because findings span every registered sender and carry no
|
||||
tenant key to filter on — the same rule as dead letters, secret findings and
|
||||
integrity.
|
||||
|
||||
INTENT principle 10 had the principle and no mechanism. It has one now.
|
||||
|
||||
Every finding and every reconciliation response carries a `means` field stating
|
||||
the bound — a missing heartbeat is not proof of suppression, and agreement on
|
||||
counts proves neither completeness nor that any event occurred. On the response
|
||||
rather than in a document nobody opens beside it, because these are the numbers
|
||||
most likely to be quoted out of context in someone else's conformance argument.
|
||||
Give stream-completeness findings a home. `/v1/dead-letters` and
|
||||
`/v1/secret-findings` exist; a cadence miss (T05) and a missing heartbeat (T04)
|
||||
have nowhere to surface. INTENT principle 10 already says a degraded audit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue