Align cadence observer with canonical NetKingdom namespace
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29
This commit is contained in:
parent
200fd17ab3
commit
4b8b79dc03
6 changed files with 213 additions and 19 deletions
80
docs/CadenceObserverAdmission.md
Normal file
80
docs/CadenceObserverAdmission.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Canonical cadence observer admission
|
||||
|
||||
NK-WP-0035; KG-WP-0005. The observer now consumes source-authored
|
||||
`extensions.netkingdom.evidence_class`, matching NetKingdom's profile namespace.
|
||||
The historical `extensions.net-kingdom` is refused with an explicit migration
|
||||
diagnostic, including conflicting dual-namespace inputs. Missing/malformed owner
|
||||
classification is an error; no classifier is inferred from traffic or event names.
|
||||
The two local copied fixtures use the canonical namespace. They are local
|
||||
algorithm inputs, not deployment, source approval, or profile-wide adoption.
|
||||
|
||||
The profile remains proposed. Imported contract is InfoTechCanon candidate
|
||||
document 0.2.0 / wire schema 0.1, bundle `b08b4d95fc4b0bd3`, schema SHA256
|
||||
`6d52692eb1e4d1325e0d6062d95acded98beddaae5bcfe24c6f0e91f1b6be6ae`.
|
||||
Schema validation belongs to the owner artifact; no competing generic schema
|
||||
is bundled here. The source class inventory stays source-owned.
|
||||
|
||||
## Reuse the evidence engine's delivered interfaces
|
||||
|
||||
AUDIT-WP-0009-T04/T05/T06/T07 are done. Reuse, without duplicating:
|
||||
|
||||
- `POST /v1/events` authenticated envelope; receiver heartbeat type
|
||||
`audit-core.heartbeat` with asserted covered class at `data.class`.
|
||||
- Protected `AUDIT_CORE_SENDERS` registry object carries source-owned canonical
|
||||
`emission_cadence`, plus exact source/tenant/evidence/credential bindings.
|
||||
`heartbeat_classes` maps covered event class to maximum gap seconds.
|
||||
The non-secret scope overlay may add/shorten heartbeat bounds but cannot
|
||||
insert/change `emission_cadence`; platform must provision the protected registry.
|
||||
- `GET /v1/reconciliation?source=<exact>&tenant=<exact>&since=<bound>&until=<bound>`
|
||||
gives accepted counts, not payloads. Own-source sender read is allowed with
|
||||
may_read=false; cross-source requests return403. Explicit equal source/tenant
|
||||
and window boundaries are required for a meaningful local comparison.
|
||||
- `GET /v1/stream-findings` evaluates missing registered heartbeats and declared
|
||||
rates. It requires a may_read identity with full tenant scope. It is not a
|
||||
tenant-filtered observer feed; request an owner-delivered sanitized observation
|
||||
or separately governed read lane rather than widening a source writer token.
|
||||
|
||||
Approval Engine already publishes receiver-compatible heartbeats and has exact
|
||||
sender/source `approval-engine`, tenant `tenant:platform`. Its local transition
|
||||
classes use bare issuance/use/supersession/revocation while accepted event types
|
||||
are approval.issuance/use/supersession/revocation; owner mapping must preserve
|
||||
both count coordinates. Requested heartbeat_classes bare classes at172800sec
|
||||
must be reconciled with emitted interval86400sec and the receiver's1.5grace.
|
||||
Canonical declaration admission and heartbeat mapping need actual owner receipts;
|
||||
existing ingestion evidence alone does not prove cadence monitoring was enabled.
|
||||
|
||||
Qonto's source is structured stdout, process-instance scoped with event class
|
||||
audit.heartbeat and its own source transition/snapshot counters. Its deployment
|
||||
owner must deliver a bounded capture and snapshot; no audit-core sender/feed is
|
||||
declared in the reviewed receiver source. If routed through Audit Core, owners
|
||||
must explicitly translate the heartbeat envelope, bind exact sender+tenant,
|
||||
retain source instance/sequence/window semantics, and admit its declaration.
|
||||
An unbounded database accepted count cannot replace a process-local snapshot.
|
||||
|
||||
local-identity has no registered sender or evidence feed, declares no heartbeat,
|
||||
and remains the owner's explicit activity-scoped incompatibility. Do not accept
|
||||
reconciliation-only as satisfying NetKingdom's rare-class conjunction or create
|
||||
an observation from a local TSV file merely to close adoption.
|
||||
|
||||
## Owned operational wait and acceptance
|
||||
|
||||
KG-WP-0005-T03 remains wait/blocked on a runtime-owner supplied authorized Qonto
|
||||
capture from startup through a request allow/deny, periodic heartbeat at the
|
||||
declared interval, and same-instance reconciliation. The namespace migration
|
||||
does not produce that feed. `main --live` still drives the adjacent source's
|
||||
AuditLogger with a fixture client; it is local emit-path evidence.
|
||||
|
||||
Existing executable seams are `live._confirm_stream` (instance/sequence/count
|
||||
admission), `cadence.emission_cadence_from_dict` (owner-classified projection),
|
||||
`stream.evaluate_stream` (heartbeats/reconciliation/observations), and
|
||||
`posture.evaluate_with_stream`. Feed integration must retain source_id/tenant,
|
||||
declared heartbeat vs covered-class mapping, time bounds, process instance,
|
||||
contiguous sequence and durable/local count scope; missing or malformed inputs
|
||||
remain UNKNOWN or are rejected. A runtime owner returns bounded non-secret
|
||||
positive/missing-heartbeat/count-divergence plus failed-read receipts. Matching
|
||||
counts prove neither completeness nor event occurrence; findings detect omission
|
||||
after the fact. There is no automatic containment authority in this delivery.
|
||||
|
||||
Knowledge orientation reloaded 2026-10-03: Emergent Security0.2.5 verified-files,
|
||||
package digest `9e186dbd6f2d5eb455bca24db4b03cd866c63c4e94660430dd15f2ca9856215f`,
|
||||
generic start plus evidence source card. Orientation is not a readiness grant.
|
||||
|
|
@ -121,8 +121,21 @@ def emission_cadence_from_dict(data: Mapping[str, Any]) -> EmissionCadence:
|
|||
raise ValueError("expected canonical emission-cadence 0.1 declaration")
|
||||
provenance = data.get("extensions", {}).get("kings-guard", {})
|
||||
for item in data["sources"]:
|
||||
profile = item.get("extensions", {}).get("net-kingdom", {})
|
||||
evidence_class = EvidenceClass(str(profile["evidence_class"]))
|
||||
extensions = item.get("extensions", {})
|
||||
if not isinstance(extensions, Mapping):
|
||||
raise ValueError("cadence entry extensions must be an object")
|
||||
if "net-kingdom" in extensions:
|
||||
raise ValueError("legacy extensions.net-kingdom must migrate to extensions.netkingdom")
|
||||
profile = extensions.get("netkingdom")
|
||||
if not isinstance(profile, Mapping):
|
||||
raise ValueError("cadence entry requires source-owned extensions.netkingdom")
|
||||
try:
|
||||
evidence_class = EvidenceClass(profile["evidence_class"])
|
||||
except (KeyError, ValueError, TypeError) as exc:
|
||||
raise ValueError(
|
||||
"extensions.netkingdom.evidence_class must be source-declared "
|
||||
"load-bearing or attributive"
|
||||
) from exc
|
||||
form = str(item["form"])
|
||||
if form == "expected-rate":
|
||||
rates.append(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"expected_min": 1,
|
||||
"drop_below": "finding",
|
||||
"extensions": {
|
||||
"net-kingdom": {
|
||||
"netkingdom": {
|
||||
"evidence_class": "attributive"
|
||||
},
|
||||
"kings-guard": {
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"divergence": "finding"
|
||||
},
|
||||
"extensions": {
|
||||
"net-kingdom": {
|
||||
"netkingdom": {
|
||||
"evidence_class": "load-bearing",
|
||||
"rate_monitoring": "forbidden"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
"divergence": "finding"
|
||||
},
|
||||
"extensions": {
|
||||
"net-kingdom": {
|
||||
"netkingdom": {
|
||||
"evidence_class": "load-bearing",
|
||||
"rate_monitoring": "forbidden"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,25 +41,41 @@ def test_qonto_source_declaration_is_consumed_separately_from_the_taxonomy_draft
|
|||
)
|
||||
def test_qonto_source_cadence_fixture_matches_the_source_owned_declaration() -> None:
|
||||
yaml = pytest.importorskip("yaml")
|
||||
source = yaml.safe_load(QONTO_SOURCE_DECLARATION.read_text(encoding="utf-8"))[
|
||||
"audit_emission_cadence"
|
||||
]
|
||||
document = yaml.safe_load(QONTO_SOURCE_DECLARATION.read_text(encoding="utf-8"))
|
||||
source = document.get("audit_emission_cadence", document)
|
||||
cadence = load_qonto_assistant_source_cadence()
|
||||
|
||||
assert cadence.source_system == source["source"]
|
||||
assert cadence.heartbeats[0].event_class == source["heartbeat"]["event_class"]
|
||||
assert (
|
||||
cadence.heartbeats[0].interval.total_seconds()
|
||||
== source["heartbeat"]["default_interval_seconds"]
|
||||
)
|
||||
assert source["event_classes"]["audit.deny"]["form"] in cadence.forms()
|
||||
assert source["event_classes"]["audit.allow"]["completeness_claimed"] is False
|
||||
if "sources" in source:
|
||||
deny = next(item for item in source["sources"] if item["event_class"] == "audit.deny")
|
||||
heartbeat = deny["heartbeat"]
|
||||
assert cadence.heartbeats[0].event_class == heartbeat["event_class"]
|
||||
from kings_guard.cadence import parse_interval
|
||||
|
||||
interval = heartbeat.get("interval_seconds", heartbeat.get("interval"))
|
||||
assert cadence.heartbeats[0].interval == parse_interval(interval)
|
||||
assert deny["form"] in cadence.forms()
|
||||
assert deny["extensions"]["netkingdom"]["evidence_class"] == "load-bearing"
|
||||
else:
|
||||
# Historical owner envelope is checked only as provenance during migration.
|
||||
# The runtime reader accepts the canonical namespace exclusively.
|
||||
assert cadence.heartbeats[0].event_class == source["heartbeat"]["event_class"]
|
||||
assert (
|
||||
cadence.heartbeats[0].interval.total_seconds()
|
||||
== source["heartbeat"]["default_interval_seconds"]
|
||||
)
|
||||
assert source["event_classes"]["audit.deny"]["form"] in cadence.forms()
|
||||
assert source["event_classes"]["audit.allow"]["completeness_claimed"] is False
|
||||
assert cadence.rates == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filename", [
|
||||
"qonto_assistant_cadence.json", "qonto_assistant_source_cadence.json",
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
[
|
||||
"qonto_assistant_cadence.json",
|
||||
"qonto_assistant_source_cadence.json",
|
||||
],
|
||||
)
|
||||
def test_cadence_fixtures_conform_to_owner_schema(filename):
|
||||
import json
|
||||
|
||||
|
|
@ -81,3 +97,67 @@ def test_canonical_compound_durations_are_consumed(value, seconds):
|
|||
from kings_guard.cadence import parse_interval
|
||||
|
||||
assert parse_interval(value).total_seconds() == seconds
|
||||
|
||||
|
||||
def _canonical_cadence_document():
|
||||
import json
|
||||
|
||||
return json.loads(
|
||||
(ROOT / "src/kings_guard/fixtures/qonto_assistant_source_cadence.json").read_text()
|
||||
)
|
||||
|
||||
|
||||
def test_canonical_profile_namespace_preserves_source_classification():
|
||||
from kings_guard.cadence import emission_cadence_from_dict
|
||||
from kings_guard.contracts import EvidenceClass
|
||||
|
||||
payload = _canonical_cadence_document()
|
||||
cadence = emission_cadence_from_dict(payload)
|
||||
assert cadence.heartbeats[0].evidence_class is EvidenceClass.LOAD_BEARING
|
||||
payload["sources"][0]["extensions"]["netkingdom"]["evidence_class"] = "attributive"
|
||||
assert (
|
||||
emission_cadence_from_dict(payload).heartbeats[0].evidence_class
|
||||
is EvidenceClass.ATTRIBUTIVE
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("retain_canonical", [False, True])
|
||||
def test_legacy_profile_namespace_requires_explicit_owner_migration(retain_canonical):
|
||||
from kings_guard.cadence import emission_cadence_from_dict
|
||||
|
||||
payload = _canonical_cadence_document()
|
||||
extensions = payload["sources"][0]["extensions"]
|
||||
extensions["net-kingdom"] = dict(extensions["netkingdom"])
|
||||
if not retain_canonical:
|
||||
del extensions["netkingdom"]
|
||||
with pytest.raises(ValueError, match="legacy extensions.net-kingdom must migrate"):
|
||||
emission_cadence_from_dict(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile", [None, [], "load-bearing"])
|
||||
def test_missing_or_malformed_profile_does_not_infer_classification(profile):
|
||||
from kings_guard.cadence import emission_cadence_from_dict
|
||||
|
||||
payload = _canonical_cadence_document()
|
||||
payload["sources"][0]["extensions"]["netkingdom"] = profile
|
||||
with pytest.raises(ValueError, match="requires source-owned extensions.netkingdom"):
|
||||
emission_cadence_from_dict(payload)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("classification", [None, True, "unknown"])
|
||||
def test_invalid_evidence_classification_is_refused(classification):
|
||||
from kings_guard.cadence import emission_cadence_from_dict
|
||||
|
||||
payload = _canonical_cadence_document()
|
||||
payload["sources"][0]["extensions"]["netkingdom"] = {"evidence_class": classification}
|
||||
with pytest.raises(ValueError, match="evidence_class must be source-declared"):
|
||||
emission_cadence_from_dict(payload)
|
||||
|
||||
|
||||
def test_malformed_entry_extensions_are_diagnosed():
|
||||
from kings_guard.cadence import emission_cadence_from_dict
|
||||
|
||||
payload = _canonical_cadence_document()
|
||||
payload["sources"][0]["extensions"] = []
|
||||
with pytest.raises(ValueError, match="extensions must be an object"):
|
||||
emission_cadence_from_dict(payload)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ status: blocked
|
|||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-09-04"
|
||||
updated: "2026-09-27"
|
||||
updated: "2026-10-03"
|
||||
origin: cross-repo
|
||||
origin_ref: QONTO-WP-0005
|
||||
state_hub_workstream_id: "db9c1dfb-cab3-5887-963a-bfcdf01cff69"
|
||||
|
|
@ -84,3 +84,24 @@ has no route to produce itself — AGENTS.md forbids opening a direct client to
|
|||
a Tooling-layer system or deployment to obtain it. Workplan moved from
|
||||
`active` to `blocked` pending that external input; no further action is
|
||||
possible here until it arrives.
|
||||
|
||||
|
||||
## Align the observer with the NetKingdom profile namespace
|
||||
|
||||
```task
|
||||
id: KG-WP-0005-T04
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
2026-10-03: migrated the parser and both copied cadence fixtures to source-owned
|
||||
extensions.netkingdom. Legacy extensions.net-kingdom, conflicting dual namespace,
|
||||
missing/malformed profile or invalid classification receive explicit rejection
|
||||
diagnostics; classifications are never inferred. Tests retain generic owner-schema
|
||||
checks and source fixture-only evidence. See docs/CadenceObserverAdmission.md.
|
||||
|
||||
T03 remains the owned operational wait: require runtime-owner deployed startup,
|
||||
allow/deny, periodic heartbeat and same-instance snapshot plus actual receiver
|
||||
read/mapping receipts. Reuse AUDIT-WP-0009's delivered reconciliation, cadence
|
||||
registration and findings interfaces. No source sender, protected registry,
|
||||
credential, live deployment, containment or profile acceptance was changed.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue