diff --git a/specs/audit-emission-cadence.yaml b/specs/audit-emission-cadence.yaml index e358dd3..095b133 100644 --- a/specs/audit-emission-cadence.yaml +++ b/specs/audit-emission-cadence.yaml @@ -1,66 +1,112 @@ -# Source-owned declaration for the qonto-assistant structured audit stream. -# The semantic shape follows the Taxonomy draft in -# kings-guard/specs/EmissionCadenceDeclaration.md. This repository owns the -# concrete emission claim even while the fleet-wide schema remains a draft. - -audit_emission_cadence: - schema_version: "0.1" - source: qonto-assistant - stream_id: qonto-assistant.audit - transport: structured-stdout - instance_boundary: process - - ordering: - instance_field: stream_instance_id - sequence_field: stream_sequence - sequence_starts_at: 1 - gap: finding - reset: permitted-only-when-stream_instance_id-changes - - event_classes: - audit.allow: - evidence_class: attributive - completeness_claimed: false - audit.deny: - evidence_class: load-bearing - form: heartbeat-or-reconciliation - rate_monitoring: forbidden - completeness_claimed: true - audit.heartbeat: - evidence_class: load-bearing - role: positive-liveness-and-reconciliation-claim - +# Source-owned InfoTechCanon declaration; intended cadence, not delivery proof. +# Generic document 0.2.0 / schema 0.1; owner bundle b08b4d95fc4b0bd3. +schema_version: '0.1' +declaration_id: qonto-assistant.audit.v1 +source: qonto-assistant +stream_id: qonto-assistant.audit +sources: +- source_id: qonto-assistant.audit.allow + source_system: qonto-assistant + event_class: audit.allow + form: heartbeat-or-reconciliation heartbeat: event_class: audit.heartbeat interval: PT24H - interval_config: QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS - default_interval_seconds: 86400 - lifecycle: - startup: required - periodic_while_process_active: required - shutdown: best-effort - quiet_assertion: nothing-to-report - active_instance_missing: finding - scale_to_zero_semantics: > - No periodic heartbeat is promised while no process instance exists. - Observers track each stream_instance_id from its startup heartbeat until - a shutdown heartbeat or expiry after a missing active-instance cadence. - + assertion: nothing-to-report + missing: finding + extensions: &id001 + qonto-assistant: + assertion_scope: no-deny-transitions-in-window + active_instance_only: true + extensions: + netkingdom: + evidence_class: attributive + completeness_claimed: false reconciliation: - source_counts_field: source_transition_counts - window_counts_field: window_transition_counts - counted_classes: - - audit.allow - - audit.deny - snapshot_endpoint: /v1/audit/reconciliation - compare_observed: > - Count received request events by event_class and stream_instance_id, then - compare them with the heartbeat source counts and stream sequence. + compare_local: source_transition_counts.audit.allow + compare_observed: evidence_counts.audit.allow divergence: finding undrained_local: lag-not-divergence - persistence: process-local +- source_id: qonto-assistant.audit.deny + source_system: qonto-assistant + event_class: audit.deny + form: heartbeat-or-reconciliation + heartbeat: + event_class: audit.heartbeat + interval: PT24H + assertion: nothing-to-report + missing: finding + extensions: *id001 + extensions: + netkingdom: + evidence_class: load-bearing + completeness_claimed: false + rate_monitoring: forbidden + reconciliation: + compare_local: source_transition_counts.audit.deny + compare_observed: evidence_counts.audit.deny + divergence: finding + undrained_local: lag-not-divergence +- source_id: qonto-assistant.audit.heartbeat + source_system: qonto-assistant + event_class: audit.heartbeat + form: heartbeat-or-reconciliation + heartbeat: + event_class: audit.heartbeat + interval: PT24H + assertion: nothing-to-report + missing: finding + extensions: *id001 + extensions: + netkingdom: + evidence_class: load-bearing + completeness_claimed: false +extensions: + netkingdom: + contract_digest: b08b4d95fc4b0bd3 + contract_document_version: 0.2.0 + qonto-assistant: + transport: structured-stdout + instance_boundary: process + ordering: + instance_field: stream_instance_id + sequence_field: stream_sequence + sequence_starts_at: 1 + gap: finding + reset: permitted-only-when-stream_instance_id-changes + heartbeat: + event_class: audit.heartbeat + interval: PT24H + interval_config: QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS + default_interval_seconds: 86400 + lifecycle: + startup: required + periodic_while_process_active: required + shutdown: best-effort + quiet_assertion: nothing-to-report + active_instance_missing: finding + scale_to_zero_semantics: 'No periodic heartbeat is promised while no process + instance exists. Observers track each stream_instance_id from its startup + heartbeat until a shutdown heartbeat or expiry after a missing active-instance + cadence. - event_context: - identity_binding_field: identity_binding - egress_destination_field: egress_destination - qonto_egress_destination: qonto-thirdparty-api + ' + reconciliation: + source_counts_field: source_transition_counts + window_counts_field: window_transition_counts + counted_classes: + - audit.allow + - audit.deny + snapshot_endpoint: /v1/audit/reconciliation + compare_observed: 'Count received request events by event_class and stream_instance_id, + then compare them with the heartbeat source counts and stream sequence. + + ' + divergence: finding + undrained_local: lag-not-divergence + persistence: process-local + event_context: + identity_binding_field: identity_binding + egress_destination_field: egress_destination + qonto_egress_destination: qonto-thirdparty-api + operational_acceptance: pending-audit-core-sender-mapping-and-independent-observer diff --git a/specs/audit-event-class-inventory.yaml b/specs/audit-event-class-inventory.yaml new file mode 100644 index 0000000..d74caad --- /dev/null +++ b/specs/audit-event-class-inventory.yaml @@ -0,0 +1,14 @@ +# Source classifications for the supplied-inventory NetKingdom assessment. +source: qonto-assistant +classes: + audit.allow: + evidence_class: attributive + rare: false + audit.deny: + evidence_class: load-bearing + rare: true + rate_monitoring: forbidden + audit.heartbeat: + evidence_class: load-bearing + rare: false + role: positive-liveness-and-reconciliation-claim diff --git a/tests/test_cadence_declaration.py b/tests/test_cadence_declaration.py new file mode 100644 index 0000000..4d33e12 --- /dev/null +++ b/tests/test_cadence_declaration.py @@ -0,0 +1,60 @@ +"""Validate source claims against existing emitter and owner-supplied contracts.""" +import json +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +from qonto_assistant.audit import AUDIT_STREAM_ID, AuditLogger + +ROOT = Path(__file__).resolve().parents[1] + + +def test_declaration_preserves_emitter_boundary_and_counters(): + declaration = yaml.safe_load((ROOT / "specs/audit-emission-cadence.yaml").read_text()) + 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["reconciliation"]["persistence"] == "process-local" + events = [] + audit = AuditLogger(sink=events.append) + audit.emit({"decision": "deny"}) + heartbeat = audit.emit_heartbeat() + for entry in declaration["sources"]: + assert entry["heartbeat"]["event_class"] == heartbeat["event_class"] + assert entry["extensions"]["netkingdom"]["completeness_claimed"] is False + if "reconciliation" in entry: + field, event_class = entry["reconciliation"]["compare_local"].split(".", 1) + assert event_class in heartbeat[field] + assert heartbeat["source_transition_counts"]["audit.deny"] == 1 + assert heartbeat["assertion"] == "transitions-reported" + + +def test_declaration_passes_supplied_owner_inventory_against_canonical_schema(): + checker = ROOT.parent / "net-kingdom/tools/emission-cadence-profile/emission_cadence_profile.py" + schema = ROOT.parent / "info-tech-canon/infospace/schemas/emission-cadence.schema.yaml" + if not checker.is_file() or not schema.is_file(): + pytest.skip("Owner schema and profile checkout required; no fallback contract") + pytest.importorskip("jsonschema") + inventory = yaml.safe_load((ROOT / "specs/audit-event-class-inventory.yaml").read_text()) + command = [sys.executable, str(checker), "--contract-schema", str(schema)] + for event_class, classification in inventory["classes"].items(): + flag = ( + "--attributive" + if classification["evidence_class"] == "attributive" + else "--load-bearing" + ) + command.extend([flag, event_class]) + if classification.get("rare") and classification["evidence_class"] == "load-bearing": + command.extend(["--rare-load-bearing", event_class]) + command.append(str(ROOT / "specs/audit-emission-cadence.yaml")) + completed = subprocess.run(command, capture_output=True, text=True, check=False) + assert completed.returncode == 0, completed.stdout + completed.stderr + report = json.loads(completed.stdout) + assert report["contract_valid"] is True + assert report["profile_assessed"] is True + assert report["conformant"] is True + assert report["assessment_scope"] == "supplied-inventory" diff --git a/workplans/QONTO-WP-0006-canonical-cadence-adoption.md b/workplans/QONTO-WP-0006-canonical-cadence-adoption.md new file mode 100644 index 0000000..8726d52 --- /dev/null +++ b/workplans/QONTO-WP-0006-canonical-cadence-adoption.md @@ -0,0 +1,109 @@ +--- +id: QONTO-WP-0006 +type: workplan +title: "Adopt canonical cadence and establish observer delivery requirements" +domain: infotech +repo: qonto-assistant +status: blocked +owner: codex +topic_slug: qonto-canonical-cadence-adoption +created: "2026-10-03" +updated: "2026-10-03" +--- + +# Canonical cadence adoption + +Source-only continuation of QONTO-WP-0005 and NetKingdom NK-WP-0035. The +existing sequenced process heartbeat and reconciliation implementation is reused. +No live deployment, credential, policy or finance API change is included. + +## Migrate source declaration and validate its inventory + +```task +id: QONTO-WP-0006-T01 +status: done +priority: high +``` + +Migrated the draft wrapper to the InfoTechCanon 0.2.0 candidate contract, +schema 0.1 (bundle `b08b4d95fc4b0bd3`). Generic fields use the canonical envelope; +process lifecycle, ordering, counters, identity and egress remain namespaced. +Published a separate source-owned inventory. The deny class remains rare and +load-bearing, with rate monitoring forbidden. Heartbeat remains load-bearing; +allow remains attributive. Preserved PT24H/default 86400/configurable interval, +startup and best-effort shutdown, process-local counters and scale-to-zero. +The earlier completeness claim is explicitly false until independent delivery +and observation evidence exists. Tests and the sibling canonical profile checker +validate this declaration without substituting a compatibility schema. + +## Establish actual sender and accepted-count mapping + +```task +id: QONTO-WP-0006-T02 +status: wait +priority: high +blocking_reason: "Runtime owner and audit-core must supply the structured-stdout delivery binding, accepted-class mapping and sender receipts. No actual audit-core sender exists in this source implementation." +``` + +Qonto runtime owner must provide the pinned collector/sender implementation, +Deployment/image/config coordinates, per-process stream identity and restart +semantics, redacted sender success/failure receipts, retry/drop behavior and any +bounded lag signal. Reuse audit-core `docs/event-envelope.md`, `docs/stream-completeness.md`, +registration `emission_cadence` and `heartbeat_classes`, owned-count +`GET /v1/reconciliation` and `GET /v1/stream-findings`; these already exist. +audit-core must provide the bounded Qonto sender registration/admission and +mapping from `audit.allow`, `audit.deny`, `audit.heartbeat`, preserving +`stream_instance_id`, `stream_sequence`, heartbeat timestamps and source counts. +`audit.heartbeat` currently differs from the receiver +`type: audit-core.heartbeat` with `data.class` naming the vouched-for class. +Supply an adapter or receiver change with both the declaration and per-class +registration reviewed together; do not rename the emitted event in documentation. +`evidence_counts.` must mean audit-core accepted records for that same +instance/window, not request attempts or aggregate estate traffic. The existing +reconciliation route scopes by source, tenant and time rather than process +instance; provide an equivalent count derivation or observer feed retaining +instance boundaries and source sequences. Do not compare restart-reset source +counters to lifetime receiver counts. Source counters +are process-local transitions, not durable transactional commits or delivery +acknowledgements. A process crash can lose them; runtime evidence must state +that limitation. No bank key may enter audit payload or observer configuration. +A source fixture or stdout event alone does not close this task. + +## Obtain independent observer acceptance + +```task +id: QONTO-WP-0006-T03 +status: wait +priority: high +blocking_reason: "King's Guard requires actual sender/mapping proof before independent missing-heartbeat, gap and count-divergence acceptance." +``` + +King's Guard must consume the canonical declaration and owner inventory, bind +its observation to the actual accepted audit-core stream, and return independently +correlated positive/negative evidence: quiet active instance, deny transition, +missing active-instance heartbeat, sequence gap, source/accepted count divergence, +lag distinguished from divergence, restart and scale-to-zero. Qonto supplies +non-secret process-local reconciliation snapshots. Evidence must include actual +configured interval (the default is 24h), observer grace/window and source/image +pins; neither declaration conformance nor a fixture proves live completeness. +NetKingdom imports these receipts for NK-WP-0035; its proposed profile status is +unchanged by this source migration. Risk Nexus owns the independent risk finding +closure after observer acceptance. No generic profile or canon approval is requested. + +Orientation consulted: emergent-security start 0.2.5 verified-files digest +`9e186dbd6f2d5eb455bca24db4b03cd866c63c4e94660430dd15f2ca9856215f`; +local-work 0.2.6 verified-files digest +`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`. +These are orientation-only receipts, not operational authority. + +## Verification 2026-10-03 + +Full source suite: `90 passed` (including both new declaration tests). Changed-file +Ruff and `git diff --check` passed. Canonical profile report: `contract_valid: true`, +`profile_assessed: true`, `conformant: true`, `assessment_scope: supplied-inventory`, +zero MUST/SHOULD findings. Exact source-owned assertions: `--attributive audit.allow`, +`--load-bearing audit.deny`, `--rare-load-bearing audit.deny`, +`--load-bearing audit.heartbeat`. Schema SHA-256 +`6d52692eb1e4d1325e0d6062d95acded98beddaae5bcfe24c6f0e91f1b6be6ae`; +reviewed contract/schema revision `4d0851c3fca306538b53838421f4499baf352778`. +This is static source adoption, not live runtime or observer acceptance.