Prepare guarded cadence registry updates and Qonto receiver admission
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
parent
e32f5c5079
commit
5227fb14e4
6 changed files with 428 additions and 1 deletions
137
audit_core/cadence_registration.py
Normal file
137
audit_core/cadence_registration.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Credential-preserving Approval Engine registry metadata update.
|
||||
|
||||
No credential is created, returned, logged or written to a local file. The
|
||||
caller owns the attended custody envelope and persists only ``receipt``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from audit_core.emission_cadence import parse_declaration
|
||||
from audit_core.senders import SenderRegistry
|
||||
|
||||
CLASSES = {"issuance", "use", "supersession", "revocation"}
|
||||
|
||||
|
||||
def strict_json(raw: bytes | str) -> Any:
|
||||
def unique(pairs):
|
||||
result = {}
|
||||
for key, value in pairs:
|
||||
if key in result:
|
||||
raise ValueError("duplicate_json_key")
|
||||
result[key] = value
|
||||
return result
|
||||
return json.loads(raw, object_pairs_hook=unique)
|
||||
|
||||
|
||||
def prepare(body: dict, declaration_raw: bytes, proposal: dict) -> tuple[dict, dict]:
|
||||
"""Return an in-memory KV data body and a nonsecret receipt.
|
||||
|
||||
Refuse absent/ambiguous identity, wrong scope, credential absence, stale
|
||||
source pins, widened class inventory, or proposal/declaration disagreement.
|
||||
Preserve every sibling sender, credential, expiry, policy and KV property.
|
||||
"""
|
||||
digest = hashlib.sha256(declaration_raw).hexdigest()
|
||||
if proposal.get("emission_cadence_sha256") != digest:
|
||||
raise ValueError("declaration_digest_mismatch")
|
||||
if (proposal.get("sender"), proposal.get("source"), proposal.get("tenant"),
|
||||
proposal.get("may_read")) != ("approval-engine", "approval-engine", "tenant:platform", False):
|
||||
raise ValueError("unexpected_proposal_scope")
|
||||
if proposal.get("preserve_existing_credentials_and_other_registry_fields") is not True:
|
||||
raise ValueError("preservation_not_declared")
|
||||
frame = proposal["coordination_frame"]
|
||||
f = frame["frame_seconds"]
|
||||
if type(f) is not int or f <= 0:
|
||||
raise ValueError("invalid_frame")
|
||||
waits = proposal["heartbeat_classes"]
|
||||
if waits != dict.fromkeys(CLASSES, 2 * f):
|
||||
raise ValueError("unexpected_class_waits")
|
||||
if (proposal.get("receiver_grace_factor"), proposal.get("effective_deadline_seconds")) != (1.5, 3 * f):
|
||||
raise ValueError("unexpected_deadline")
|
||||
declaration = strict_json(declaration_raw)
|
||||
parsed = parse_declaration(declaration)
|
||||
if parsed.source != "approval-engine" or parsed.rates:
|
||||
raise ValueError("unexpected_declaration_source_or_rate")
|
||||
declared = {e["event_class"]: e for e in parsed.other_entries}
|
||||
if len(declared) != len(parsed.other_entries) or set(declared) != CLASSES | {"heartbeat"}:
|
||||
raise ValueError("unexpected_declaration_inventory")
|
||||
if declaration.get("extensions", {}).get("netkingdom", {}).get("coordination_frame") != frame:
|
||||
raise ValueError("frame_provenance_mismatch")
|
||||
for name in CLASSES:
|
||||
beat = declared[name].get("heartbeat", {})
|
||||
ext = beat.get("extensions", {}).get("approval-engine", {})
|
||||
if (beat.get("event_class"), beat.get("interval_seconds"), ext.get("asserted_class"),
|
||||
ext.get("requested_observer_max_gap_seconds"), ext.get("effective_deadline_seconds")) != (
|
||||
"audit-core.heartbeat", f, name, 2 * f, 3 * f):
|
||||
raise ValueError("declaration_wait_mismatch")
|
||||
rows = strict_json(body["senders.json"])
|
||||
if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
|
||||
raise ValueError("invalid_registry")
|
||||
indices = [i for i, row in enumerate(rows) if row.get("name") == "approval-engine"]
|
||||
if len(indices) != 1:
|
||||
raise ValueError("sender_absent_or_ambiguous")
|
||||
i = indices[0]
|
||||
before = rows[i]
|
||||
if (before.get("sources"), before.get("tenants"), before.get("may_write", True),
|
||||
before.get("may_read", False)) != (["approval-engine"], ["tenant:platform"], True, False):
|
||||
raise ValueError("existing_sender_scope_mismatch")
|
||||
if not (before.get("tokens") or before.get("token")):
|
||||
raise ValueError("existing_credentials_required")
|
||||
existing_classes = set(before.get("heartbeat_classes", {}))
|
||||
if existing_classes - CLASSES:
|
||||
raise ValueError("would_drop_existing_heartbeat_class")
|
||||
updated_rows = copy.deepcopy(rows)
|
||||
updated_rows[i]["heartbeat_classes"] = copy.deepcopy(waits)
|
||||
updated_rows[i]["emission_cadence"] = declaration
|
||||
# Validate the entire candidate against the actual receiver parser. No
|
||||
# private data appears in exceptions emitted by the attended wrapper.
|
||||
SenderRegistry.from_env({"AUDIT_CORE_SENDERS": json.dumps(updated_rows)})
|
||||
updated = copy.deepcopy(body)
|
||||
changed = updated_rows != rows
|
||||
if changed:
|
||||
updated["senders.json"] = json.dumps(updated_rows, separators=(",", ":"))
|
||||
receipt = {"sender": "approval-engine", "source": "approval-engine",
|
||||
"tenant": "tenant:platform", "may_read": False,
|
||||
"emission_cadence_sha256": digest, "heartbeat_classes": waits,
|
||||
"effective_deadline_seconds": 3 * f, "changed": changed,
|
||||
"credentials_preserved": True, "other_registry_fields_preserved": True}
|
||||
return updated, receipt
|
||||
|
||||
|
||||
def qonto_metadata(declaration: dict, declaration_sha256: str) -> dict:
|
||||
"""Build a nonsecret sender proposal; custody owner adds tokens in memory.
|
||||
|
||||
The digest is of the owner's YAML bytes, which the custody caller verifies
|
||||
before parsing; JSON serialization cannot stand in for that source digest.
|
||||
"""
|
||||
parsed = parse_declaration(declaration)
|
||||
if parsed.source != "qonto-assistant" or parsed.stream_id != "qonto-assistant.audit" or parsed.rates:
|
||||
raise ValueError("unexpected_qonto_declaration")
|
||||
entries = {e["event_class"]: e for e in parsed.other_entries}
|
||||
kinds = {"audit.allow": "attributive", "audit.deny": "load-bearing", "audit.heartbeat": "load-bearing"}
|
||||
if set(entries) != set(kinds) or len(entries) != len(parsed.other_entries):
|
||||
raise ValueError("unexpected_qonto_inventory")
|
||||
frame = declaration.get("extensions", {}).get("netkingdom", {}).get("coordination_frame", {})
|
||||
f = frame.get("frame_seconds")
|
||||
if type(f) is not int or f <= 0:
|
||||
raise ValueError("invalid_qonto_frame")
|
||||
if len(declaration_sha256) != 64 or any(c not in "0123456789abcdef" for c in declaration_sha256):
|
||||
raise ValueError("invalid_qonto_source_digest")
|
||||
from audit_core.emission_cadence import duration_seconds
|
||||
for name, kind in kinds.items():
|
||||
entry = entries[name]
|
||||
if entry.get("extensions", {}).get("netkingdom", {}).get("evidence_class") != kind:
|
||||
raise ValueError("qonto_classification_mismatch")
|
||||
beat = entry.get("heartbeat", {})
|
||||
if beat.get("event_class") != "audit.heartbeat" or duration_seconds(beat.get("interval")) != f:
|
||||
raise ValueError("qonto_cadence_mismatch")
|
||||
if beat.get("extensions", {}).get("qonto-assistant", {}).get("assertion_scope") != "no-deny-transitions-in-window":
|
||||
raise ValueError("qonto_assertion_scope_mismatch")
|
||||
return {"name": "rapp-qonto-collector", "sources": ["qonto-assistant"],
|
||||
"tenants": ["tenant:friendly:binky"], "may_write": True, "may_read": False,
|
||||
"secret_policy": "redact", "evidence_kind": "load-bearing",
|
||||
"heartbeat_classes": {"audit.deny": 2 * f},
|
||||
"emission_cadence": copy.deepcopy(declaration)}
|
||||
25
deploy/qonto-collector-ingress.yaml
Normal file
25
deploy/qonto-collector-ingress.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# NK-WP-0035 / AUDIT-WP-0013-T05. Prepared, not applied.
|
||||
# Namespace and pod selectors are ANDed within one peer. Collector egress
|
||||
# and credential binding remain independently required.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: audit-core-qonto-collector-ingress
|
||||
namespace: audit-core
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: audit-core
|
||||
app.kubernetes.io/component: receiver
|
||||
policyTypes: [Ingress]
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: rapp-qonto
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: rapp-qonto-audit-collector
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
75
docs/cadence-registration.md
Normal file
75
docs/cadence-registration.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Cadence registry admission — NK-WP-0035
|
||||
|
||||
## Prepared source, not live acceptance
|
||||
|
||||
The 2026-10-15 source frame is f=3600 seconds. Approval Engine declares
|
||||
issuance, use, supersession and revocation heartbeats every f. Register each
|
||||
class wait at 2f=7200; audit-core's existing 1.5 grace makes the effective
|
||||
missing-heartbeat deadline 3f=10800. Class keys are the asserted class names,
|
||||
not the action names approval.issuance etc. Retain write true/read false,
|
||||
source approval-engine and tenant tenant:platform.
|
||||
|
||||
`audit_core.cadence_registration.prepare` takes the protected KV data object,
|
||||
raw source declaration bytes and the reviewed nonsecret proposal. It validates
|
||||
the exact declaration SHA256, source/class/interval/provenance agreement and
|
||||
incumbent scope. It returns the proposed in-memory KV object and metadata-only
|
||||
receipt. Only heartbeat_classes and emission_cadence may change. Every token,
|
||||
rotation overlap, expiry, policy, sibling sender and other KV property remains
|
||||
unchanged. Existing unexpected heartbeat classes refuse a destructive drop.
|
||||
The entire candidate is parsed by the real receiver sender registry parser.
|
||||
|
||||
No registry values belong in operator terminals, agent outputs, local files,
|
||||
Git or State Hub. Root's sanctioned contained platform owner process may call
|
||||
this pure API. `scripts/register-approval-engine-cadence.py` is a silent
|
||||
attended wrapper requiring the warden isolated HOME and non-root platform-admin
|
||||
identity. It defaults to preflight and uses KV-v2 CAS plus exact readback with
|
||||
--apply. It does not mint credentials, synchronize ESO or restart workloads.
|
||||
Consult the current custody route before invoking any credential operation;
|
||||
this helper does not create custody authority.
|
||||
|
||||
## Receiver artifact and database prerequisite
|
||||
|
||||
Read-only native metadata on 2026-10-15 found receiver Deployment UID
|
||||
b85fe3d0-75c9-4e0d-8c34-c6f1df0881bb, ready replicas 1 and image
|
||||
forgejo.coulomb.social/coulomb/audit-core@sha256:ec15f63d49226bfe507af2bc38ffbd5e83f549ba2c6d1ae7a338e7e053f34615.
|
||||
That artifact has heartbeat_classes and stream findings, but does not contain
|
||||
audit_core.emission_cadence or stream_instance_id filtering in either ingest
|
||||
or PostgreSQL. Its database ledger reports migrations 0001-events through
|
||||
0006-chain. Source adds additive 0007-import-receipts. Applying registry cadence
|
||||
alone to that old image is not receiver cadence admission.
|
||||
|
||||
Release gates: verify backup and rollback position; build/test current committed
|
||||
source and pin its immutable image; run the migration Job with the existing
|
||||
migration lease (runtime cannot migrate); verify ledger 0007; update the receiver
|
||||
image without widening selectors/credentials; wait readyz and native feature
|
||||
proof. Then the protected metadata CAS, exact ESO readback and receiver reload
|
||||
must agree on the admitted declaration and class waits. Own-source counts,
|
||||
wrong-scope refusals and actual heartbeat findings need native receipts.
|
||||
No absence-of-finding claim proves completeness or adversarial omission.
|
||||
|
||||
## Qonto receiver mapping
|
||||
|
||||
rapp-qonto owns the collector, qonto-assistant owns declaration/classification,
|
||||
and King's Guard owns independent observation. Proposed sender
|
||||
rapp-qonto-collector permits source qonto-assistant, tenant tenant:friendly:binky,
|
||||
subject qonto-assistant.audit, write true/read false, explicit redact policy.
|
||||
Qonto source ownership must confirm that exact tenant binding. The mixed
|
||||
sender is load-bearing; allow remains attributive in the per-class source
|
||||
inventory. Stable original instance/sequence drives one retry-safe event ID.
|
||||
Allow and deny retain their class; audit.heartbeat maps one-to-one to
|
||||
audit-core.heartbeat, retains event_class audit.heartbeat, and asserts
|
||||
`data.class=audit.deny`. It does not assert quiet allow activity.
|
||||
|
||||
`deploy/qonto-collector-ingress.yaml` is prepared separately from the general
|
||||
network policies. It admits only namespace rapp-qonto AND pod label
|
||||
app.kubernetes.io/name=rapp-qonto-audit-collector to receiver port8080.
|
||||
The endpoint is http://audit-core.audit-core.svc.cluster.local:8080.
|
||||
Sender admission and collector egress remain independently required. No bank
|
||||
credential or bank API operation is required. Retain sequence, instance,
|
||||
source counters/window boundaries and receiver acceptance-time boundaries;
|
||||
compare only after the explicitly bounded drain lag. Per-instance filtering
|
||||
isolates instances but does not detect gaps on its own.
|
||||
|
||||
AUDIT-WP-0013-T05 and NK-WP-0035 remain open until actual accepted records,
|
||||
duplicate/refusal/reconciliation receipts and independent observer evidence
|
||||
are available. Local tests and this prepared ingress are not those receipts.
|
||||
78
scripts/register-approval-engine-cadence.py
Normal file
78
scripts/register-approval-engine-cadence.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent attended registry CAS; no rollout, no credential mint, no local secrets.
|
||||
|
||||
Run through the existing warden attended --exec envelope. Default is preflight
|
||||
only. --apply authorizes exactly the proposal's two metadata fields. Receipt
|
||||
contains metadata only. Receiver ESO synchronization/restart is a separate gate.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from audit_core.cadence_registration import prepare, strict_json
|
||||
|
||||
REGISTRY = "platform/data/workloads/audit-core/senders"
|
||||
|
||||
|
||||
def bao(args, payload=None):
|
||||
proc = subprocess.run(["bao", *args], input=None if payload is None else json.dumps(payload).encode(),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
|
||||
if proc.returncode:
|
||||
raise RuntimeError("bao_operation_failed")
|
||||
return strict_json(proc.stdout)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--proposal", required=True, type=Path)
|
||||
parser.add_argument("--declaration", required=True, type=Path)
|
||||
parser.add_argument("--receipt", required=True, type=Path)
|
||||
parser.add_argument("--apply", action="store_true")
|
||||
args = parser.parse_args()
|
||||
receipt = {"observed_at": datetime.now(timezone.utc).isoformat(),
|
||||
"registry": "platform/workloads/audit-core/senders",
|
||||
"credential_values_emitted": False, "applied": False}
|
||||
try:
|
||||
if Path.home().parent.name != ".warden-attended-login":
|
||||
raise ValueError("attended_warden_envelope_required")
|
||||
if any(os.environ.get(k) for k in ("BAO_TOKEN", "VAULT_TOKEN", "OPENBAO_TOKEN")):
|
||||
raise ValueError("token_environment_forbidden")
|
||||
identity = bao(["token", "lookup", "-format=json"])["data"]
|
||||
policies = identity.get("policies", [])
|
||||
if "platform-admin" not in policies or "root" in policies:
|
||||
raise ValueError("attended_platform_admin_required")
|
||||
raw = bao(["read", "-format=json", REGISTRY])["data"]
|
||||
version = raw["metadata"]["version"]
|
||||
body = raw["data"]
|
||||
updated, detail = prepare(body, args.declaration.read_bytes(), strict_json(args.proposal.read_bytes()))
|
||||
receipt.update(detail, registry_version_before=version)
|
||||
if args.apply and detail["changed"]:
|
||||
result = bao(["write", "-format=json", REGISTRY, "-"],
|
||||
{"options": {"cas": version}, "data": updated})
|
||||
receipt["applied"] = True
|
||||
receipt["request_id"] = result.get("request_id")
|
||||
after = bao(["read", "-format=json", REGISTRY])["data"]
|
||||
if after["metadata"]["version"] != version + 1 or after["data"] != updated:
|
||||
raise RuntimeError("readback_mismatch")
|
||||
receipt["registry_version_after"] = after["metadata"]["version"]
|
||||
receipt["status"] = "registered" if args.apply else "preflight_only"
|
||||
code = 0
|
||||
except Exception:
|
||||
# Never propagate exceptions containing parser input or subprocess
|
||||
# output: either may contain an incumbent registry credential.
|
||||
receipt["status"] = "failed_closed"
|
||||
code = 1
|
||||
args.receipt.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.receipt.write_text(json.dumps(receipt, indent=2) + "\n")
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
98
tests/test_cadence_registration.py
Normal file
98
tests/test_cadence_registration.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
"""Guarded registration never changes custody or unrelated registry state."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from audit_core.cadence_registration import prepare, strict_json
|
||||
|
||||
|
||||
def fixture():
|
||||
frame = {"frame_seconds": 3600}
|
||||
entries = [{"source_id": n, "event_class": n, "form": "heartbeat-or-reconciliation",
|
||||
"heartbeat": {"event_class": "audit-core.heartbeat", "interval_seconds": 3600,
|
||||
"assertion": "nothing-to-report", "missing": "finding",
|
||||
"extensions": {"approval-engine": {"asserted_class": n,
|
||||
"requested_observer_max_gap_seconds": 7200,
|
||||
"effective_deadline_seconds": 10800}}}}
|
||||
for n in ("issuance", "use", "supersession", "revocation")]
|
||||
entries.append({"source_id": "heartbeat", "event_class": "heartbeat",
|
||||
"form": "heartbeat-or-reconciliation", "reconciliation": {
|
||||
"compare_local": "local", "compare_observed": "receiver", "divergence": "finding"}})
|
||||
doc = {"schema_version": "0.1", "declaration_id": "test", "source": "approval-engine",
|
||||
"sources": entries, "extensions": {"netkingdom": {"coordination_frame": frame}}}
|
||||
raw = json.dumps(doc).encode()
|
||||
proposal = {"sender": "approval-engine", "source": "approval-engine", "tenant": "tenant:platform",
|
||||
"may_read": False, "preserve_existing_credentials_and_other_registry_fields": True,
|
||||
"coordination_frame": frame, "emission_cadence_sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"heartbeat_classes": dict.fromkeys(("issuance", "use", "supersession", "revocation"),7200),
|
||||
"receiver_grace_factor": 1.5, "effective_deadline_seconds": 10800}
|
||||
rows = [{"name": "sibling", "sources": ["sibling"], "tokens": ["sibling-fixture"]},
|
||||
{"name": "approval-engine", "sources": ["approval-engine"], "tenants": ["tenant:platform"],
|
||||
"tokens": ["current-fixture", "overlap-fixture"], "may_read": False,
|
||||
"evidence_kind": "load-bearing", "secret_policy": "redact", "owner_property": {"keep": True}}]
|
||||
return {"senders.json": json.dumps(rows), "unrelated": "preserve"}, raw, proposal
|
||||
|
||||
|
||||
def test_preserves_every_non_metadata_field_and_inputs_and_is_idempotent():
|
||||
body, raw, proposal = fixture()
|
||||
original = copy.deepcopy(body)
|
||||
updated, receipt = prepare(body, raw, proposal)
|
||||
before, after = json.loads(body["senders.json"]), json.loads(updated["senders.json"])
|
||||
assert body == original and updated["unrelated"] == "preserve" and before[0] == after[0]
|
||||
assert {k:v for k,v in after[1].items() if k not in ("heartbeat_classes", "emission_cadence")} == before[1]
|
||||
assert "fixture" not in json.dumps(receipt)
|
||||
assert prepare(updated, raw, proposal)[0] == updated
|
||||
assert prepare(updated, raw, proposal)[1]["changed"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["digest", "read", "tenant", "class", "credentials", "duplicate", "absent"])
|
||||
def test_fail_closed(mutation):
|
||||
body, raw, proposal = fixture()
|
||||
rows = json.loads(body["senders.json"])
|
||||
if mutation == "digest": proposal["emission_cadence_sha256"] = "bad"
|
||||
if mutation == "read": rows[1]["may_read"] = True
|
||||
if mutation == "tenant": rows[1]["tenants"] = ["*"]
|
||||
if mutation == "class": rows[1]["heartbeat_classes"] = {"must-not-drop": 3600}
|
||||
if mutation == "credentials": rows[1]["tokens"] = []
|
||||
if mutation == "duplicate": rows.append(copy.deepcopy(rows[1]))
|
||||
if mutation == "absent": rows.pop()
|
||||
body["senders.json"] = json.dumps(rows)
|
||||
with pytest.raises(ValueError): prepare(body, raw, proposal)
|
||||
|
||||
|
||||
def test_duplicate_json_refused():
|
||||
with pytest.raises(ValueError): strict_json('{"name":1,"name":2}')
|
||||
|
||||
|
||||
def test_real_owner_proposal_when_checkout_available():
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
proposal = root / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json"
|
||||
declaration = root / "approval-engine/cadence.yaml"
|
||||
if not proposal.exists() or not declaration.exists(): pytest.skip("owner checkouts unavailable")
|
||||
body, _, _ = fixture()
|
||||
_, receipt = prepare(body, declaration.read_bytes(), strict_json(proposal.read_bytes()))
|
||||
assert receipt["effective_deadline_seconds"] == 10800
|
||||
|
||||
|
||||
def test_qonto_proposal_retains_mixed_classification():
|
||||
from audit_core.cadence_registration import qonto_metadata
|
||||
body, raw, proposal = fixture()
|
||||
doc = strict_json(raw)
|
||||
doc["source"] = "qonto-assistant"
|
||||
doc["stream_id"] = "qonto-assistant.audit"
|
||||
kinds = {"audit.allow": "attributive", "audit.deny": "load-bearing", "audit.heartbeat": "load-bearing"}
|
||||
doc["sources"] = [{"source_id": name, "event_class": name, "form": "heartbeat-or-reconciliation",
|
||||
"extensions": {"netkingdom": {"evidence_class": kind}},
|
||||
"heartbeat": {"event_class": "audit.heartbeat", "interval": "PT3600S",
|
||||
"assertion": "nothing-to-report", "missing": "finding",
|
||||
"extensions": {"qonto-assistant": {"assertion_scope": "no-deny-transitions-in-window"}}}}
|
||||
for name, kind in kinds.items()]
|
||||
entry = qonto_metadata(doc, "a" * 64)
|
||||
assert "tokens" not in entry and entry["may_read"] is False
|
||||
assert entry["heartbeat_classes"] == {"audit.deny": 7200}
|
||||
assert entry["emission_cadence"] == doc
|
||||
doc["sources"][0]["extensions"]["netkingdom"]["evidence_class"] = "load-bearing"
|
||||
with pytest.raises(ValueError, match="classification"): qonto_metadata(doc, "a" * 64)
|
||||
|
|
@ -8,7 +8,7 @@ status: blocked
|
|||
owner: codex
|
||||
topic_slug: audit-evidence-dependencies
|
||||
created: "2026-10-04"
|
||||
updated: "2026-10-04"
|
||||
updated: "2026-10-15"
|
||||
state_hub_workstream_id: "9e400de2-6ba1-520e-9b25-94aa2470e528"
|
||||
---
|
||||
|
||||
|
|
@ -86,3 +86,17 @@ state_hub_task_id: "76b59808-abc3-5b8d-b31a-e3fb611f2cbe"
|
|||
```
|
||||
|
||||
QONTO-WP-0006-T02. Audit-core owns admission and review of accepted mapping; rapp-qonto owns runtime delivery. Required inputs and proposed audit.allow/audit.deny/audit.heartbeat mapping are in docs/cross-repo-evidence-admission.md. Retain original stream instance, sequence and source counts. Approve deterministic retry IDs, heartbeat vouched-for class, authoritative tenant and acceptance-time/sequence-window comparison. Supply pinned collector/runtime artifacts, sanitized success/failure receipts and actual accepted records. Neither source stdout nor finished RAPP-QONTO-WP-0003 closes this gate. KG-WP-0005-T03 observer acceptance follows sender proof, never precedes it.
|
||||
|
||||
### NK-WP-0035 receiver readiness, 2026-10-15
|
||||
|
||||
Prepared credential-preserving Approval Engine cadence transformation and silent
|
||||
attended CAS wrapper; default f=3600, class waits7200, effective10800. Ten focused
|
||||
preservation/refusal/idempotence tests pass. Prepared exact Qonto collector ingress
|
||||
(namespace AND pod selector), contingent on source-owner tenant/mapping acceptance.
|
||||
Read-only native metadata proves deployed ec15f63d artifact lacks cadence module
|
||||
and process-instance filtering; database migration ledger is0006, source adds
|
||||
additive0007-import-receipts. Current-source receiver release/migration is therefore
|
||||
required before registration acceptance. See docs/cadence-registration.md for
|
||||
execution gates. No protected registry was fetched or changed, no sender token
|
||||
was minted and no live resource was mutated by this readiness work. T05 stays
|
||||
wait pending source delivery and actual native custody/mapping receipts.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue