AUDIT-WP-0018-T03: report bounded-executor runs that started without a receipt
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s

A sender may declare closing_classes; nk-bounded-executor declares
execution.run.started closed by execution.attribution.receipt within
7200s. GET /v1/stream-findings reports opening_without_closing past the
bound, counted from the receiver's accepted_at plus the heartbeat grace
allowance; a per-run data.window.not_after tightens but never extends.
Identifiers and timing only. The overlay may add or shorten, never
remove, rebind or lengthen. Both backends: 403 passed (1 unrelated
live-catalog env failure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 482098@bnt-lap001
Assistant-Session: 9643c95a-a398-4f4a-b56a-6119be4e845e
This commit is contained in:
tegwick 2026-11-03 22:04:14 +01:00
parent 9fb167871b
commit 2b8c9097ad
13 changed files with 654 additions and 6 deletions

View file

@ -482,6 +482,29 @@ class PostgresAuditBackend:
sql += " GROUP BY action ORDER BY action"
return [{"class": action, "count": count} for action, count in self._query(sql, params)]
def unclosed_openings(
self, source: str, opening: str, closing: str, since: str, limit: int = 500
) -> list[dict]:
"""Opening-class events with no closing-class event on the same
``correlation_id`` (AUDIT-WP-0018-T03). Identifiers and timing only."""
rows = self._query(
"SELECT o.event_id, o.correlation_id, o.accepted_at, "
"CASE WHEN jsonb_typeof(o.record->'details'->'data'->'window'->'not_after') = 'string' "
"THEN o.record->'details'->'data'->'window'->>'not_after' END "
f"FROM {self._events} o "
"WHERE o.source = %s AND o.action = %s AND o.accepted_at >= %s "
f"AND NOT EXISTS (SELECT 1 FROM {self._events} c "
"WHERE c.correlation_id = o.correlation_id "
"AND c.source = %s AND c.action = %s) "
"ORDER BY o.accepted_at, o.event_id LIMIT %s",
(source, opening, since, source, closing, int(limit)),
)
return [
{"event_id": eid, "correlation_id": cid, "accepted_at": _iso(at),
"window_not_after": na}
for eid, cid, at, na in rows
]
def last_heartbeats(self, source: str) -> dict[str, str]:
"""Most recent heartbeat per class for one source."""
rows = self._query(