Evaluate declared emission cadence and close AUDIT-WP-0009 (T05)
The contract T05 waited on is published: info-tech-canon emission-cadence wire schema 0.1, contract digest b08b4d95fc4b0bd3. A source-owned declaration now travels as emission_cadence on the sender registration; expected-rate entries raise below_declared_cadence on /v1/stream-findings from the same counts /v1/reconciliation returns. heartbeat-or-reconciliation entries are validated and left to T04/T06. Records the observer evaluation of net-kingdom's local-identity declaration: structurally clean, not operationally evaluated, one heartbeat event_class mapping incompatibility. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 151986@bnt-lap001 Assistant-Session: ccd02b6b-80ae-48e5-8cad-9c8f74d21a67
This commit is contained in:
parent
3b7bedc8e0
commit
01468ffc41
8 changed files with 645 additions and 22 deletions
|
|
@ -22,6 +22,7 @@ from pathlib import Path
|
|||
from typing import Any, Iterable
|
||||
|
||||
from audit_core.redaction import POLICIES, POLICY_REDACT
|
||||
from audit_core.emission_cadence import parse_declaration
|
||||
|
||||
WILDCARD = "*"
|
||||
|
||||
|
|
@ -71,9 +72,9 @@ class SenderIdentity:
|
|||
# §9.6 heartbeat declaration (AUDIT-WP-0009-T04): event class -> the
|
||||
# longest gap, in seconds, that is not yet a finding.
|
||||
#
|
||||
# This is deliberately NOT the §17 emission-cadence schema that T05 waits
|
||||
# on. Cadence describes a stream's expected *rate* and belongs to Taxonomy;
|
||||
# this is a registration property saying how often a source promises to say
|
||||
# This is deliberately NOT the emission-cadence declaration (T05, below).
|
||||
# Cadence describes a stream's expected *rate* and its shape is
|
||||
# info-tech-canon's; this is a registration property saying how often a source promises to say
|
||||
# "nothing to report" for a class that may legitimately be silent. The two
|
||||
# are complementary and audit-core is not inventing a competing rate shape.
|
||||
#
|
||||
|
|
@ -82,6 +83,10 @@ class SenderIdentity:
|
|||
# nothing about the quiet, security-relevant one — which is the only case
|
||||
# heartbeats exist for.
|
||||
heartbeat_classes: tuple[tuple[str, int], ...] = ()
|
||||
# The source-owned emission cadence declaration (AUDIT-WP-0009-T05),
|
||||
# parsed from info-tech-canon wire schema 0.1 — the §17 shape the heartbeat
|
||||
# field above deliberately did not invent. ``None`` means undeclared.
|
||||
emission_cadence: Any = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.name:
|
||||
|
|
@ -144,6 +149,9 @@ class SenderIdentity:
|
|||
"completeness_claimed": False,
|
||||
"completeness_trade": self.completeness_trade,
|
||||
"heartbeat_classes": dict(self.heartbeat_classes),
|
||||
"emission_cadence": (
|
||||
self.emission_cadence.summary() if self.emission_cadence else None
|
||||
),
|
||||
"detection_surface": None,
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +323,7 @@ def _apply_scope_overlay(
|
|||
else identity.completeness_trade
|
||||
),
|
||||
heartbeat_classes=_overlay_heartbeats(identity, extra),
|
||||
emission_cadence=_overlay_cadence(identity, extra),
|
||||
)
|
||||
)
|
||||
return merged
|
||||
|
|
@ -370,6 +379,22 @@ def _overlay_heartbeats(
|
|||
return tuple(sorted(proposed.items()))
|
||||
|
||||
|
||||
def _overlay_cadence(identity: SenderIdentity, extra: dict[str, Any]) -> Any:
|
||||
"""The overlay may not touch an emission cadence declaration.
|
||||
|
||||
A declaration is the source's, pinned to a contract digest; a partial
|
||||
overlay has no sound merge with it, and replacing it wholesale could widen
|
||||
a window or drop a class without anyone deciding to. Changing it is a
|
||||
change to the Secret-backed registration.
|
||||
"""
|
||||
if "emission_cadence" in extra:
|
||||
raise ValueError(
|
||||
f"sender {identity.name!r}: the scope overlay may not set or change "
|
||||
"emission_cadence"
|
||||
)
|
||||
return identity.emission_cadence
|
||||
|
||||
|
||||
def _parse_identities(raw: str) -> list[SenderIdentity]:
|
||||
try:
|
||||
entries = json.loads(raw)
|
||||
|
|
@ -396,6 +421,11 @@ def _identity_from(entry: Any) -> SenderIdentity:
|
|||
evidence_kind=str(entry.get("evidence_kind", EVIDENCE_ATTRIBUTIVE)),
|
||||
completeness_trade=_clean_trade(entry.get("completeness_trade")),
|
||||
heartbeat_classes=_heartbeat_classes(entry.get("heartbeat_classes")),
|
||||
emission_cadence=(
|
||||
parse_declaration(entry["emission_cadence"])
|
||||
if entry.get("emission_cadence") is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue