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:
tegwick 2026-10-15 21:19:05 +02:00
parent e32f5c5079
commit 5227fb14e4
6 changed files with 428 additions and 1 deletions

View 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)}

View 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

View 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.

View 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())

View 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)

View file

@ -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.