Use the shared coordination frame for audit heartbeat defaults

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
tegwick 2026-10-15 20:42:29 +02:00
parent bf8a9ede67
commit 5eb7e86f7a
5 changed files with 50 additions and 9 deletions

View file

@ -268,3 +268,19 @@ next. Ordinary denies (`authz_denied`, `tenant_scope`, `unknown_capability`)
never count toward this — only the two reason classes that indicate
probing/exfiltration rather than a client mistake. See
`docs/SecurityPractice.md` §9.3 and `src/qonto_assistant/security_watch.py`.
## Ecosystem coordination-frame adoption — 2026-10-15
The source heartbeat default now imports FRAME_SECONDS from the generated
qonto_assistant/coordination.py. Initial frame is f=3600s, configured once in
coordination-engine/config/coordination-frame.json. The source-owned cadence
and rapp-qonto manifest derive the same interval and preserve config SHA-256
provenance. Regenerate them with net-kingdom/tools/coordination-frame/render.py;
--check detects drift. An explicitly configured instance interval still needs
matching declaration and receiver mapping.
This source update does not deploy rapp-qonto or create its missing collector.
QONTO-WP-0006-T02/T03 and AUDIT-WP-0013-T05 still hold sender/delivery and
independent observation acceptance. Existing bank credential TTL, policy,
identity, rate limits and process-local counter limitations are unchanged.

View file

@ -1,5 +1,4 @@
# Source-owned InfoTechCanon declaration; intended cadence, not delivery proof.
# Generic document 0.2.0 / schema 0.1; owner bundle b08b4d95fc4b0bd3.
# Source-owned cadence; coordination timings generated from coordination-engine/config/coordination-frame.json
schema_version: '0.1'
declaration_id: qonto-assistant.audit.v1
source: qonto-assistant
@ -11,7 +10,7 @@ sources:
form: heartbeat-or-reconciliation
heartbeat:
event_class: audit.heartbeat
interval: PT24H
interval: PT3600S
assertion: nothing-to-report
missing: finding
extensions: &id001
@ -33,7 +32,7 @@ sources:
form: heartbeat-or-reconciliation
heartbeat:
event_class: audit.heartbeat
interval: PT24H
interval: PT3600S
assertion: nothing-to-report
missing: finding
extensions: *id001
@ -53,7 +52,7 @@ sources:
form: heartbeat-or-reconciliation
heartbeat:
event_class: audit.heartbeat
interval: PT24H
interval: PT3600S
assertion: nothing-to-report
missing: finding
extensions: *id001
@ -65,6 +64,13 @@ extensions:
netkingdom:
contract_digest: b08b4d95fc4b0bd3
contract_document_version: 0.2.0
coordination_frame:
ref: coordination-engine/config/coordination-frame.json
sha256: d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67
frame_seconds: 3600
heartbeat_frames: 1
registered_wait_frames: 2
effective_deadline_frames: 3
qonto-assistant:
transport: structured-stdout
instance_boundary: process
@ -76,9 +82,9 @@ extensions:
reset: permitted-only-when-stream_instance_id-changes
heartbeat:
event_class: audit.heartbeat
interval: PT24H
interval: PT3600S
interval_config: QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS
default_interval_seconds: 86400
default_interval_seconds: 3600
lifecycle:
startup: required
periodic_while_process_active: required

View file

@ -4,6 +4,8 @@ import os
from dataclasses import dataclass
from pathlib import Path
from .coordination import FRAME_SECONDS
def _repo_root() -> Path:
return Path(__file__).resolve().parents[2]
@ -92,7 +94,7 @@ class Settings:
os.getenv("QONTO_DENY_ESCALATION_LOCKOUT_SECONDS", "300")
),
audit_heartbeat_interval_seconds=max(
1, int(os.getenv("QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS", "86400"))
1, int(os.getenv("QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS", str(FRAME_SECONDS)))
),
key_cape_jwks_url=os.getenv("QONTO_KEY_CAPE_JWKS_URL") or None,
key_cape_issuer=os.getenv("QONTO_KEY_CAPE_ISSUER", "https://key-cape.netkingdom"),

View file

@ -0,0 +1,6 @@
"""Generated cadence timings; regenerate with NetKingdom coordination-frame tool."""
# Source: coordination-engine/config/coordination-frame.json
FRAME_CONFIG_SHA256 = "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67"
FRAME_SECONDS = 3600
RECEIVER_WAIT_SECONDS = 7200
EFFECTIVE_DEADLINE_SECONDS = 10800

View file

@ -8,6 +8,7 @@ import pytest
import yaml
from qonto_assistant.audit import AUDIT_STREAM_ID, AuditLogger
from qonto_assistant.coordination import FRAME_SECONDS
ROOT = Path(__file__).resolve().parents[1]
@ -17,7 +18,7 @@ def test_declaration_preserves_emitter_boundary_and_counters():
source = declaration["extensions"]["qonto-assistant"]
assert declaration["stream_id"] == AUDIT_STREAM_ID
assert source["instance_boundary"] == "process"
assert source["heartbeat"]["default_interval_seconds"] == 86400
assert source["heartbeat"]["default_interval_seconds"] == FRAME_SECONDS
assert source["reconciliation"]["persistence"] == "process-local"
events = []
audit = AuditLogger(sink=events.append)
@ -58,3 +59,13 @@ def test_declaration_passes_supplied_owner_inventory_against_canonical_schema():
assert report["profile_assessed"] is True
assert report["conformant"] is True
assert report["assessment_scope"] == "supplied-inventory"
def test_default_interval_uses_generated_frame_and_explicit_override_survives(monkeypatch):
from qonto_assistant.config import Settings
monkeypatch.delenv("QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS", raising=False)
assert Settings.from_env().audit_heartbeat_interval_seconds == FRAME_SECONDS
monkeypatch.setenv("QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS", "600")
assert Settings.from_env().audit_heartbeat_interval_seconds == 600