diff --git a/CONFIG.md b/CONFIG.md index 1e0567c..ba08777 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -26,6 +26,7 @@ If yes to any of the above, don't add it here. | CP-NK-004 | KeyCape OIDC hostname | `kc.coulomb.social` | `sso-mfa/k8s/keycape/ingress.yaml`, `sso-mfa/k8s/authelia/configmap.yaml`, `sso-mfa/k8s/keycape/create-secrets.sh` | | CP-NK-005 | Authelia login portal hostname | `auth.coulomb.social` | `sso-mfa/k8s/authelia/ingress.yaml`, `sso-mfa/k8s/authelia/configmap.yaml` | | CP-NK-006 | LLDAP admin web UI hostname | `lldap.coulomb.social` | `sso-mfa/k8s/lldap/ingress.yaml` | +| CP-NK-007 | Ecosystem coordination frame | `f = 1h` | `coordination-engine/config/coordination-frame.json` (owner source) | --- @@ -144,3 +145,18 @@ to the parent domain (`coulomb.social`) so the cookie is valid across both is IP-restricted by the `lldap-admin-allowlist` Traefik Middleware (VPN/office CIDRs only). The LDAP port (3890) is cluster-internal only and never exposed via Ingress. + + +## CP-NK-007 — Ecosystem coordination frame + +**Owner:** coordination-engine. **Initial setting:** `frame_seconds: 3600`. +Founder decision 2026-10-15: pace long-term coordination in hourly rather than +daily frames. This is an operator-selected pace, not a topology-derived value. +The canonical file is `coordination-engine/config/coordination-frame.json`; +NetKingdom keeps no independent editable copy. + +Cadence adopters derive heartbeat f, receiver wait 2f and effective deadline +3f (including Audit Core's existing 1.5 grace). Change f once, then run +`tools/coordination-frame/render.py --write` and `--check`. Generated owner +artifacts carry the source digest. Runtime owners deploy reviewed artifacts; +source regeneration is not deployment or estate-wide timer adoption. diff --git a/canon/standards/emission-cadence-security-profile_v0.1.md b/canon/standards/emission-cadence-security-profile_v0.1.md index 3b3e992..8aaf409 100644 --- a/canon/standards/emission-cadence-security-profile_v0.1.md +++ b/canon/standards/emission-cadence-security-profile_v0.1.md @@ -159,3 +159,16 @@ This profile can become accepted only when: checked with owner-authored class inventories; and - `kings-guard` has been given the canonical coordinates so its handover draft can become historical. + + +## Coordination-frame configuration — 2026-10-15 + +Source owners may derive their cadence timing from the ecosystem coordination +frame at `coordination-engine/config/coordination-frame.json`. The operator's +current frame is f=1h; the first adopter mapping is heartbeat f, receiver wait +2f, and effective missing-heartbeat deadline 3f after the receiver's 1.5 grace. +Namespaced frame provenance accompanies the derived numeric declaration values. +Changing the frame requires regenerating the source/receiver settings together +and separately deploying them through owner change control. It changes neither +source classification nor the heartbeat-plus-reconciliation requirement, and +a compiled configuration is not evidence of operating emission or observation. diff --git a/docs/evidence/2026-10-15-cadence-adoption/approval-engine-frame-runtime-patch.json b/docs/evidence/2026-10-15-cadence-adoption/approval-engine-frame-runtime-patch.json new file mode 100644 index 0000000..7d0f12d --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/approval-engine-frame-runtime-patch.json @@ -0,0 +1,29 @@ +[ + { + "op": "test", + "path": "/metadata/uid", + "value": "8b44c901-024f-4472-a822-2fd07aa2be5f" + }, + { + "op": "test", + "path": "/metadata/resourceVersion", + "value": "75292270" + }, + { + "op": "add", + "path": "/spec/template/metadata/annotations", + "value": { + "coordination.coulomb.social/frame-sha256": "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67" + } + }, + { + "op": "add", + "path": "/spec/template/spec/containers/0/args/-", + "value": "--heartbeat-seconds" + }, + { + "op": "add", + "path": "/spec/template/spec/containers/0/args/-", + "value": "3600" + } +] diff --git a/docs/evidence/2026-10-15-cadence-adoption/coordination-frame-decision.json b/docs/evidence/2026-10-15-cadence-adoption/coordination-frame-decision.json new file mode 100644 index 0000000..6cd1281 --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/coordination-frame-decision.json @@ -0,0 +1,18 @@ +{ + "workplan_id": "04685f94-1991-5e62-80d2-5669913e99fc", + "id": "1ac5a70f-082e-4d8f-9ac8-59e4c175da02", + "topic_id": null, + "title": "Use a configurable ecosystem coordination frame, initially one hour", + "description": "Founder directs one base duration frame for ecosystem coordination timelines and deadlines. Set f=1h now, with waits expressed as 2f and 3f rather than independent day constants. First cadence adoption: heartbeat f, registered receiver wait 2f, existing 1.5 grace yields effective 3f.", + "decision_type": "made", + "status": "resolved", + "rationale": "Small team is moving quickly; long-term coordination currently needs an hourly rather than daily pace. Owner-reviewed adoption retains timing formulas and frame provenance. This records the configuration decision, not an approval of an unspecified live rollout.", + "decided_by": "Bernd Worsch (founder), explicit session instruction", + "decided_at": "2026-10-15T18:05:48.234679Z", + "deadline": null, + "escalation_note": null, + "superseded_by": null, + "created_at": "2026-10-15T18:05:49.826919Z", + "updated_at": "2026-10-15T18:05:49.826919Z", + "workstream_id": "04685f94-1991-5e62-80d2-5669913e99fc" +} diff --git a/docs/evidence/2026-10-15-cadence-adoption/coordination-frame-validation.json b/docs/evidence/2026-10-15-cadence-adoption/coordination-frame-validation.json new file mode 100644 index 0000000..20391cc --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/coordination-frame-validation.json @@ -0,0 +1,47 @@ +{ + "decision_id": "1ac5a70f-082e-4d8f-9ac8-59e4c175da02", + "config": { + "ref": "coordination-engine/config/coordination-frame.json", + "sha256": "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67", + "frame_seconds": 3600, + "heartbeat_frames": 1, + "registered_wait_frames": 2, + "effective_deadline_frames": 3, + "decision_id": "1ac5a70f-082e-4d8f-9ac8-59e4c175da02" + }, + "tests": { + "coordination_engine": { + "passed": 73, + "skipped": 1 + }, + "coordination_frame_and_profile": { + "passed": 32 + }, + "approval_engine": { + "passed": 271 + }, + "qonto_assistant": { + "passed": 91 + }, + "rapp_qonto": { + "passed": 25 + }, + "kings_guard": { + "passed": 128 + } + }, + "compiler_check": "no drift", + "source_assessments": "source-assessments-f1h.json", + "manifest_preservation": "semantic comparison against committed manifests proves only heartbeat settings and frame digest annotations changed; image/identity/secrets/routing/scaling preserved", + "deployment_applied": false, + "registry_applied": false, + "independent_live_observer_proved": false, + "source_commits": { + "coordination-engine": "f658556690f3f6b7efe9c95a445222019182e402", + "approval-engine": "a786b061da4d7730c0827cf4919ab9206aed6d3e", + "qonto-assistant": "5eb7e86f7a5fccf8bb8c40bd991e32bb9ae440c9", + "rapp-qonto": "51de093b4431fc7cc569be84bc20cbff7aa6a4d5", + "kings-guard": "164a4cdaf12e7a450ac225f7a645e769a3e76f40" + }, + "runtime_preflight": "Both timing-only patches passed server dry-run on verified railiance01; nothing applied" +} diff --git a/docs/evidence/2026-10-15-cadence-adoption/owner-acceptance-plan.json b/docs/evidence/2026-10-15-cadence-adoption/owner-acceptance-plan.json index 3acc17e..4ebb906 100644 --- a/docs/evidence/2026-10-15-cadence-adoption/owner-acceptance-plan.json +++ b/docs/evidence/2026-10-15-cadence-adoption/owner-acceptance-plan.json @@ -4,6 +4,99 @@ "profile_status": "proposed", "live_observer_proved": false, "source_files": { + "approval_source": { + "path": "../approval-engine/cadence.yaml", + "sha256": "cc58c8d53ebf50acf540d73b7607d579942bda7734c3e8722108e9c9204af655" + }, + "qonto_source": { + "path": "../qonto-assistant/specs/audit-emission-cadence.yaml", + "sha256": "adedbe06e04c38812057db97364557644157eb5f72f16adebb6f7beec1587005" + }, + "generic_schema": { + "path": "../info-tech-canon/infospace/schemas/emission-cadence.schema.yaml", + "sha256": "6d52692eb1e4d1325e0d6062d95acded98beddaae5bcfe24c6f0e91f1b6be6ae" + }, + "bearer_revocation_receipt": { + "path": "../audit-core/docs/evidence/2026-09-15-sender-bearer-revocation.json", + "sha256": "8da798005611711f4a3da969597a3763e6c02e41013df5227b0434dc328b6fb3" + }, + "qonto_historical_capture": { + "path": "../rapp-qonto/evidence/live/2026-09-05-audit-rollout.json", + "sha256": "1cad71fe93ab2a9f7d5dbc99b9aada47c212a7e43813a86ea91f52639e163be9" + } + }, + "approval_engine_registration_proposal": { + "status": "not-applied; founder selected configurable f=1h", + "sender": "approval-engine", + "source": "approval-engine", + "tenant": "tenant:platform", + "preserve": [ + "tokens", + "secret_policy=redact", + "may_write=true", + "may_read=false", + "existing other senders and registry fields" + ], + "emission_cadence": "Import exact source-owned cadence.yaml at the pinned digest; never use a draft/fallback schema", + "class_mapping": { + "issuance": "approval.issuance", + "use": "approval.use", + "supersession": "approval.supersession", + "revocation": "approval.revocation", + "heartbeat": "audit-core.heartbeat" + }, + "heartbeat_transport": "audit-core.heartbeat", + "covered_class_field": "data.class", + "source_interval_seconds": 3600, + "source_requested_bound_seconds": 7200, + "receiver_grace_factor": 1.5, + "live_steps": [ + "Verify generated one-hour frame bindings, current runtime/source pins and separately approved rollout scope", + "Review exact protected-registry metadata-only patch preserving credentials and all unrelated entries", + "Founder approves exact runtime change; use governed owner custody with contained session and CAS/readback", + "Receiver reload under owner change gate; prove actual per-class registration and class-isolated heartbeats", + "Emit/drain actual lifecycle transitions under owner-scoped authority; correlate occurrence, acknowledgment and receiver acceptance windows including lag and duplicates", + "Independent readback/count comparison and missing-class/lag findings; retain value-safe receipts" + ], + "existing_revocation_proof": "2026-09-15 T1 accepted 202, dropped T1 refused 401, incumbent T0 accepted 202; do not repeat unchanged synthetic proof as if absent", + "effective_deadline_seconds": 10800, + "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, + "decision_id": "1ac5a70f-082e-4d8f-9ac8-59e4c175da02" + } + }, + "qonto": { + "required_owner": "rapp-qonto", + "missing": "Actual stdout collector/sender source/image/config and governed delivery lane", + "authoritative_workload_tenant": "tenant:friendly:binky (rapp-qonto binding; collector mapping still needs source/receiver review)", + "receiver_commitment": "AUDIT-WP-0013-T05", + "receiver_instance_filter": "Implemented in AUDIT-WP-0013-T04; reviewed release and native proof still required", + "source_coordination": "QONTO-WP-0006-T02", + "observer_acceptance": "KG-WP-0005-T03 then QONTO-WP-0006-T03", + "historical_receipt_limit": "Source-path capture with startup and deny; not independent collector/feed acceptance", + "source_interval_seconds": 3600, + "runtime_status": "source settings prepared; not deployed in this session" + }, + "local_identity": { + "status": "explicit incompatibility retained", + "expected_findings": [ + "rare-heartbeat-missing:revoke-token", + "rare-heartbeat-missing:serve/token.token_issued" + ], + "completeness_claimed": false + }, + "observer_fix": { + "defect": "Shared heartbeat event type could mask silence of another covered class", + "change": "Require explicit covered_event_classes as well as matching transport class; retain source/adapter coverage, not inferred traffic/count keys", + "old_regression": "False COMPLETE for unscoped audit-core.heartbeat across issuance/revocation", + "deployment_or_live_acceptance": false + }, + "source_files_before_frame": { "approval_source": { "path": "../approval-engine/cadence.yaml", "sha256": "0389be492628e2df0d84a70755c92aaf4a749bc64fbf2772e0c3301c3e1fe5ed" @@ -25,73 +118,13 @@ "sha256": "1cad71fe93ab2a9f7d5dbc99b9aada47c212a7e43813a86ea91f52639e163be9" } }, - "approval_engine_registration_proposal": { - "status": "not-applied; owner deadline decision pending", - "sender": "approval-engine", - "source": "approval-engine", - "tenant": "tenant:platform", - "preserve": [ - "tokens", - "secret_policy=redact", - "may_write=true", - "may_read=false", - "existing other senders and registry fields" - ], - "emission_cadence": "Import exact source-owned cadence.yaml at the pinned digest; never use a draft/fallback schema", - "class_mapping": { - "issuance": "approval.issuance", - "use": "approval.use", - "supersession": "approval.supersession", - "revocation": "approval.revocation", - "heartbeat": "audit-core.heartbeat" - }, - "heartbeat_transport": "audit-core.heartbeat", - "covered_class_field": "data.class", - "source_interval_seconds": 86400, - "source_requested_bound_seconds": 172800, - "receiver_grace_factor": 1.5, - "deadline_options": [ - { - "effective_seconds": 172800, - "registration_seconds": 115200 - }, - { - "effective_seconds": 259200, - "registration_seconds": 172800 - } - ], - "live_steps": [ - "Owner confirms effective heartbeat deadline and current runtime/source pin", - "Review exact protected-registry metadata-only patch preserving credentials and all unrelated entries", - "Founder approves exact runtime change; use governed owner custody with contained session and CAS/readback", - "Receiver reload under owner change gate; prove actual per-class registration and class-isolated heartbeats", - "Emit/drain actual lifecycle transitions under owner-scoped authority; correlate occurrence, acknowledgment and receiver acceptance windows including lag and duplicates", - "Independent readback/count comparison and missing-class/lag findings; retain value-safe receipts" - ], - "existing_revocation_proof": "2026-09-15 T1 accepted 202, dropped T1 refused 401, incumbent T0 accepted 202; do not repeat unchanged synthetic proof as if absent" - }, - "qonto": { - "required_owner": "rapp-qonto", - "missing": "Actual stdout collector/sender source/image/config and governed delivery lane", - "authoritative_workload_tenant": "tenant:friendly:binky (rapp-qonto binding; collector mapping still needs source/receiver review)", - "receiver_commitment": "AUDIT-WP-0013-T05", - "receiver_instance_filter": "Implemented in AUDIT-WP-0013-T04; reviewed release and native proof still required", - "source_coordination": "QONTO-WP-0006-T02", - "observer_acceptance": "KG-WP-0005-T03 then QONTO-WP-0006-T03", - "historical_receipt_limit": "Source-path capture with startup and deny; not independent collector/feed acceptance" - }, - "local_identity": { - "status": "explicit incompatibility retained", - "expected_findings": [ - "rare-heartbeat-missing:revoke-token", - "rare-heartbeat-missing:serve/token.token_issued" - ], - "completeness_claimed": false - }, - "observer_fix": { - "defect": "Shared heartbeat event type could mask silence of another covered class", - "change": "Require explicit covered_event_classes as well as matching transport class; retain source/adapter coverage, not inferred traffic/count keys", - "old_regression": "False COMPLETE for unscoped audit-core.heartbeat across issuance/revocation", - "deployment_or_live_acceptance": false + "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, + "decision_id": "1ac5a70f-082e-4d8f-9ac8-59e4c175da02" } } diff --git a/docs/evidence/2026-10-15-cadence-adoption/rapp-qonto-frame-runtime-patch.json b/docs/evidence/2026-10-15-cadence-adoption/rapp-qonto-frame-runtime-patch.json new file mode 100644 index 0000000..4bfb9dd --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/rapp-qonto-frame-runtime-patch.json @@ -0,0 +1,37 @@ +[ + { + "op": "test", + "path": "/metadata/uid", + "value": "a665e1d0-3ada-4b27-ac56-a8d452499003" + }, + { + "op": "test", + "path": "/metadata/resourceVersion", + "value": "57092911" + }, + { + "op": "add", + "path": "/spec/template/metadata/annotations/coordination.coulomb.social~1frame-sha256", + "value": "d15e092387f128ffdfd00b9815707a05ff77c5bb0ba4d9a428fbc37b8c440b67" + }, + { + "op": "replace", + "path": "/spec/template/spec/containers/0/env/0/value", + "value": "3600" + }, + { + "op": "add", + "path": "/spec/traffic", + "value": [ + { + "revisionName": "rapp-qonto-00009", + "percent": 100 + }, + { + "latestRevision": true, + "percent": 0, + "tag": "frame-preview" + } + ] + } +] diff --git a/docs/evidence/2026-10-15-cadence-adoption/runtime-frame-patch-preflight.json b/docs/evidence/2026-10-15-cadence-adoption/runtime-frame-patch-preflight.json new file mode 100644 index 0000000..0e12497 --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/runtime-frame-patch-preflight.json @@ -0,0 +1,18 @@ +{ + "approval-engine": { + "patch": "approval-engine-frame-runtime-patch.json", + "server_dry_run_exit": 0, + "resource_uid": "8b44c901-024f-4472-a822-2fd07aa2be5f", + "resource_version": "75292270", + "additional_owner_step": "OnDelete requires separately approved controlled restart of approval-engine-0; image and storage schema stay unchanged", + "live_mutations": false + }, + "rapp-qonto": { + "patch": "rapp-qonto-frame-runtime-patch.json", + "server_dry_run_exit": 0, + "resource_uid": "a665e1d0-3ada-4b27-ac56-a8d452499003", + "resource_version": "57092911", + "additional_owner_step": "Keep all traffic on the current Ready revision while creating zero-percent frame-preview candidate; promote only after owner verification", + "live_mutations": false + } +} diff --git a/docs/evidence/2026-10-15-cadence-adoption/runtime-frame-preflight.json b/docs/evidence/2026-10-15-cadence-adoption/runtime-frame-preflight.json new file mode 100644 index 0000000..4c1378b --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/runtime-frame-preflight.json @@ -0,0 +1,34 @@ +{ + "live_mutations": false, + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "resources": { + "approval-engine": { + "available": true, + "kind": "statefulset", + "namespace": "approval-engine", + "uid": "8b44c901-024f-4472-a822-2fd07aa2be5f", + "resource_version": "75292270", + "image": "forgejo.coulomb.social/coulomb/approval-engine@sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49", + "heartbeat_argument_seconds": [], + "heartbeat_environment_seconds": [], + "frame_digest": null, + "status_ready": 1, + "rollout_strategy": "OnDelete" + }, + "rapp-qonto": { + "available": true, + "kind": "ksvc", + "namespace": "rapp-qonto", + "uid": "a665e1d0-3ada-4b27-ac56-a8d452499003", + "resource_version": "57092911", + "image": "forgejo.coulomb.social/coulomb/rapp-qonto@sha256:2e42b5e665a19ffa3b3ae8109a8aab64f5485329f72810be063263ff5086cb3f", + "heartbeat_argument_seconds": [], + "heartbeat_environment_seconds": [ + "86400" + ], + "frame_digest": null, + "status_ready": "rapp-qonto-00009", + "rollout_strategy": null + } + } +} diff --git a/docs/evidence/2026-10-15-cadence-adoption/source-assessments-f1h.json b/docs/evidence/2026-10-15-cadence-adoption/source-assessments-f1h.json new file mode 100644 index 0000000..90a0729 --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-adoption/source-assessments-f1h.json @@ -0,0 +1,92 @@ +{ + "live_observer_proved": false, + "reports": { + "approval-engine": { + "assessment_scope": "supplied-inventory", + "conformant": true, + "contract_schema": "../info-tech-canon/infospace/schemas/emission-cadence.schema.yaml", + "contract_valid": true, + "declaration": "../approval-engine/cadence.yaml", + "findings": [], + "inventory": { + "attributive": [], + "load_bearing": [ + "heartbeat" + ], + "rare_load_bearing": [ + "issuance", + "revocation", + "supersession", + "use" + ] + }, + "profile": "netkingdom-emission-cadence-security-profile-v0.1", + "profile_assessed": true, + "summary": { + "must": 0, + "should": 0 + } + }, + "local-identity": { + "assessment_scope": "supplied-inventory", + "conformant": false, + "contract_schema": "../info-tech-canon/infospace/schemas/emission-cadence.schema.yaml", + "contract_valid": true, + "declaration": "local-identity/emission-cadence.yaml", + "findings": [ + { + "code": "rare-heartbeat-missing", + "event_class": "revoke-token", + "level": "MUST", + "message": "rare load-bearing evidence requires a positive heartbeat" + }, + { + "code": "rare-heartbeat-missing", + "event_class": "serve/token.token_issued", + "level": "MUST", + "message": "rare load-bearing evidence requires a positive heartbeat" + } + ], + "inventory": { + "attributive": [], + "load_bearing": [], + "rare_load_bearing": [ + "revoke-token", + "serve/token.token_issued" + ] + }, + "profile": "netkingdom-emission-cadence-security-profile-v0.1", + "profile_assessed": true, + "summary": { + "must": 2, + "should": 0 + } + }, + "qonto-assistant": { + "assessment_scope": "supplied-inventory", + "conformant": true, + "contract_schema": "../info-tech-canon/infospace/schemas/emission-cadence.schema.yaml", + "contract_valid": true, + "declaration": "../qonto-assistant/specs/audit-emission-cadence.yaml", + "findings": [], + "inventory": { + "attributive": [ + "audit.allow" + ], + "load_bearing": [ + "audit.deny", + "audit.heartbeat" + ], + "rare_load_bearing": [ + "audit.deny" + ] + }, + "profile": "netkingdom-emission-cadence-security-profile-v0.1", + "profile_assessed": true, + "summary": { + "must": 0, + "should": 0 + } + } + } +} diff --git a/generated/coordination-frame/approval-engine-receiver.json b/generated/coordination-frame/approval-engine-receiver.json new file mode 100644 index 0000000..b8d8b30 --- /dev/null +++ b/generated/coordination-frame/approval-engine-receiver.json @@ -0,0 +1,26 @@ +{ + "status": "proposed-not-applied", + "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 + }, + "sender": "approval-engine", + "source": "approval-engine", + "tenant": "tenant:platform", + "may_read": false, + "emission_cadence_ref": "approval-engine/cadence.yaml", + "emission_cadence_sha256": "cc58c8d53ebf50acf540d73b7607d579942bda7734c3e8722108e9c9204af655", + "heartbeat_classes": { + "issuance": 7200, + "use": 7200, + "supersession": 7200, + "revocation": 7200 + }, + "receiver_grace_factor": 1.5, + "effective_deadline_seconds": 10800, + "preserve_existing_credentials_and_other_registry_fields": true +} diff --git a/tools/coordination-frame/README.md b/tools/coordination-frame/README.md new file mode 100644 index 0000000..9145439 --- /dev/null +++ b/tools/coordination-frame/README.md @@ -0,0 +1,42 @@ +# Coordination-frame bindings + +The ecosystem frame is owned by coordination-engine at +`coordination-engine/config/coordination-frame.json`. Its initial value is +`frame_seconds: 3600`, selected by the founder on 2026-10-15. Change that one +setting to change the pace. The owner API validates positive integer frames +and derives timezone-bound deadlines; this tool imports that API, with no +fallback config or duplicated reader. + +For cadence adoption the formulas are heartbeat **f**, registered receiver +wait **2f**, and effective missing-heartbeat deadline **3f** after Audit Core's +existing 1.5 grace. At f=1h those are 3600, 7200 and 10800 seconds. A return +to f=24h produces 24h/48h/72h through the same formulas. + +From net-kingdom: + +```sh +python3 tools/coordination-frame/render.py # read-only source plan +python3 tools/coordination-frame/render.py --check # fail on derived-file drift +python3 tools/coordination-frame/render.py --write # regenerate source artifacts +python3 -m pytest -q tools/coordination-frame/tests +``` + +`--write` changes only the listed source files. It prepares Approval Engine +and Qonto declarations, generated Python defaults, both deployment heartbeat +settings, and a non-secret proposed Approval Engine receiver registration. +The compiler preserves image pins, identity, secret references, other source +classifications and the lack of a completeness claim. Each generated consumer +records the frame source and SHA-256. Runtime consumers import their generated +values rather than opening an adjacent coordination-engine checkout. + +Check generated artifacts after changing the frame, review/commit each owner +source, then use the owner's approved release and registry procedures. This +tool performs no login, credential operation, registry update or deployment. +It cannot replace a source collector or provide operational observer evidence. +All five binding tests require the sibling owner checkouts and explicitly skip +when they are unavailable; no substitute owner contract is used. + +This is the first rollout of a shared coordination setting. Other ecosystem +coordination consumers must adopt explicit frame multipliers through their own +configuration. Transport polling, security validity and credential leases are +not silently rescaled by a coordination setting. diff --git a/tools/coordination-frame/render.py b/tools/coordination-frame/render.py new file mode 100644 index 0000000..f0ac7d1 --- /dev/null +++ b/tools/coordination-frame/render.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Compile owner-reviewed cadence bindings from the ecosystem coordination frame.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import re +import sys +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[2] +FRAME_REF = "coordination-engine/config/coordination-frame.json" + + +def owner_frame(workspace: Path): + source = workspace / "coordination-engine/src/coordination_engine/timing.py" + spec = importlib.util.spec_from_file_location("coordination_frame_owner", source) + if spec is None or spec.loader is None: + raise ValueError("coordination-engine frame API is required; no fallback") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module.load_frame(workspace / FRAME_REF) + + +def generated_module(frame, digest): + return ( + '"""Generated cadence timings; regenerate with NetKingdom coordination-frame tool."""\n' + f"# Source: {FRAME_REF}\n" + f'FRAME_CONFIG_SHA256 = "{digest}"\n' + f"FRAME_SECONDS = {frame.frame_seconds}\n" + f"RECEIVER_WAIT_SECONDS = {frame.seconds(2)}\n" + f"EFFECTIVE_DEADLINE_SECONDS = {frame.seconds(3)}\n" + ) + + +def bind_frame_annotation(text, digest, metadata_prefix): + key = "coordination.coulomb.social/frame-sha256" + pattern = r"(?m)^(\s*" + re.escape(key) + r": )[^\n]+" + if re.search(pattern, text): + updated, count = re.subn(pattern, lambda match: match[1] + json.dumps(digest), text) + else: + replacement = ( + metadata_prefix + + "annotations:\n" + + " " * (len(metadata_prefix.splitlines()[-1]) + 2) + + key + + ": " + + json.dumps(digest) + + "\n" + ) + # metadata_prefix ends with the indentation for annotations. Existing + # annotation maps retain their contents; otherwise insert a new map. + if metadata_prefix + "annotations:\n" in text: + updated = text.replace(metadata_prefix + "annotations:\n", replacement, 1) + count = 1 + else: + updated = text.replace( + metadata_prefix, replacement + metadata_prefix.rsplit("\n", 1)[-1], 1 + ) + count = int(updated != text) + if count != 1: + raise ValueError("expected one runtime frame provenance binding") + return updated + + +def compile_bindings(workspace: Path): + frame = owner_frame(workspace) + digest = hashlib.sha256((workspace / FRAME_REF).read_bytes()).hexdigest() + provenance = { + "ref": FRAME_REF, + "sha256": digest, + "frame_seconds": frame.frame_seconds, + "heartbeat_frames": 1, + "registered_wait_frames": 2, + "effective_deadline_frames": 3, + } + outputs = {} + for relative in ( + "approval-engine/approval_engine/coordination.py", + "qonto-assistant/src/qonto_assistant/coordination.py", + ): + outputs[workspace / relative] = generated_module(frame, digest) + approval_path = workspace / "approval-engine/cadence.yaml" + approval = json.loads(approval_path.read_text()) + for entry in approval["sources"]: + if "heartbeat" in entry: + beat = entry["heartbeat"] + beat["interval_seconds"] = frame.frame_seconds + ext = beat["extensions"]["approval-engine"] + ext["requested_observer_max_gap_seconds"] = frame.seconds(2) + ext["effective_deadline_seconds"] = frame.seconds(3) + approval["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance + outputs[approval_path] = json.dumps(approval, indent=2) + "\n" + + qonto_path = workspace / "qonto-assistant/specs/audit-emission-cadence.yaml" + qonto = yaml.safe_load(qonto_path.read_text()) + for entry in qonto["sources"]: + if "heartbeat" in entry: + entry["heartbeat"]["interval"] = f"PT{frame.frame_seconds}S" + heartbeat = qonto["extensions"]["qonto-assistant"]["heartbeat"] + heartbeat["interval"] = f"PT{frame.frame_seconds}S" + heartbeat["default_interval_seconds"] = frame.frame_seconds + qonto["extensions"]["netkingdom"]["coordination_frame"] = provenance + outputs[qonto_path] = ( + "# Source-owned cadence; coordination timings generated from " + + FRAME_REF + + "\n" + + yaml.safe_dump(qonto, sort_keys=False) + ) + + deploy_path = workspace / "approval-engine/deploy/approval-engine.yaml" + deploy = deploy_path.read_text() + if re.search(r"^\s+- --heartbeat-seconds\s*$", deploy, re.M): + deploy, count = re.subn( + r"(?m)^(\s+- --heartbeat-seconds\n\s+- )[^\n]+", + lambda m: m[1] + json.dumps(str(frame.frame_seconds)), + deploy, + ) + else: + # Explicit runtime arg also works with the already pinned image; no image + # pin, storage migration, authority or unrelated deployment change here. + deploy, count = re.subn( + r"(?m)^(\s+)- --audit-url$", + lambda m: ( + f'{m[1]}- --heartbeat-seconds\n{m[1]}- "{frame.frame_seconds}"\n{m[1]}- --audit-url' + ), + deploy, + ) + if count != 1: + raise ValueError("expected one Approval Engine heartbeat argument binding") + outputs[deploy_path] = bind_frame_annotation( + deploy, digest, " template:\n metadata:\n " + ) + + deploy_path = workspace / "rapp-qonto/runtime/knative/service.yaml" + deploy, count = re.subn( + r"(?m)(name: QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS\n\s+value: )[^\n]+", + lambda m: m[1] + json.dumps(str(frame.frame_seconds)), + deploy_path.read_text(), + ) + if count != 1: + raise ValueError("expected one Qonto heartbeat environment binding") + outputs[deploy_path] = bind_frame_annotation( + deploy, digest, " template:\n metadata:\n " + ) + + fixture_path = ( + workspace / "kings-guard/src/kings_guard/fixtures/qonto_assistant_source_cadence.json" + ) + fixture = json.loads(fixture_path.read_text()) + for entry in fixture["sources"]: + if "heartbeat" in entry: + entry["heartbeat"]["interval_seconds"] = frame.frame_seconds + fixture["extensions"].setdefault("netkingdom", {})["coordination_frame"] = provenance + outputs[fixture_path] = json.dumps(fixture, indent=2) + "\n" + + receiver = { + "status": "proposed-not-applied", + "coordination_frame": provenance, + "sender": "approval-engine", + "source": "approval-engine", + "tenant": "tenant:platform", + "may_read": False, + "emission_cadence_ref": "approval-engine/cadence.yaml", + "emission_cadence_sha256": hashlib.sha256(outputs[approval_path].encode()).hexdigest(), + "heartbeat_classes": { + name: frame.seconds(2) for name in ("issuance", "use", "supersession", "revocation") + }, + "receiver_grace_factor": 1.5, + "effective_deadline_seconds": frame.seconds(3), + "preserve_existing_credentials_and_other_registry_fields": True, + } + outputs[ + workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json" + ] = json.dumps(receiver, indent=2) + "\n" + return frame, outputs + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workspace", type=Path, default=ROOT.parent) + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--check", action="store_true") + mode.add_argument("--write", action="store_true") + args = parser.parse_args(argv) + try: + frame, outputs = compile_bindings(args.workspace) + changed = { + p: text for p, text in outputs.items() if not p.exists() or p.read_text() != text + } + if args.write: + for path, text in changed.items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + report = { + "frame": frame.as_dict(), + "mode": "write" if args.write else "check" if args.check else "plan", + "changed": [str(p.relative_to(args.workspace)) for p in changed], + "live_operations": False, + } + print(json.dumps(report, indent=2, sort_keys=True)) + return 1 if args.check and changed else 0 + except (OSError, ValueError, KeyError, TypeError) as exc: + parser.error(str(exc)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/coordination-frame/tests/test_render.py b/tools/coordination-frame/tests/test_render.py new file mode 100644 index 0000000..6d237e6 --- /dev/null +++ b/tools/coordination-frame/tests/test_render.py @@ -0,0 +1,127 @@ +import hashlib +import importlib.util +import json +import shutil +from pathlib import Path + +import pytest +import yaml + +SCRIPT = Path(__file__).resolve().parents[1] / "render.py" +SPEC = importlib.util.spec_from_file_location("frame_renderer", SCRIPT) +renderer = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(renderer) +WORKSPACE = SCRIPT.parents[3] + + +@pytest.fixture +def workspace(tmp_path): + required = ( + "coordination-engine/config/coordination-frame.json", + "coordination-engine/src/coordination_engine/timing.py", + "approval-engine/cadence.yaml", + "approval-engine/deploy/approval-engine.yaml", + "qonto-assistant/specs/audit-emission-cadence.yaml", + "rapp-qonto/runtime/knative/service.yaml", + "kings-guard/src/kings_guard/fixtures/qonto_assistant_source_cadence.json", + ) + if any(not (WORKSPACE / name).is_file() for name in required): + pytest.skip("sibling owner sources required; no fallback frame or contract") + for name in ( + "coordination-engine/config/coordination-frame.json", + "coordination-engine/src/coordination_engine/timing.py", + "approval-engine/cadence.yaml", + "approval-engine/deploy/approval-engine.yaml", + "qonto-assistant/specs/audit-emission-cadence.yaml", + "rapp-qonto/runtime/knative/service.yaml", + "kings-guard/src/kings_guard/fixtures/qonto_assistant_source_cadence.json", + ): + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(WORKSPACE / name, path) + return tmp_path + + +@pytest.mark.parametrize("frame_seconds", [600, 3600, 86400]) +def test_one_frame_edit_rescales_sources_runtime_and_receiver(workspace, frame_seconds): + config = workspace / renderer.FRAME_REF + config.write_text( + json.dumps({"schema_version": "coordination-frame/v1", "frame_seconds": frame_seconds}) + ) + _, outputs = renderer.compile_bindings(workspace) + approval = json.loads(outputs[workspace / "approval-engine/cadence.yaml"]) + qonto = yaml.safe_load(outputs[workspace / "qonto-assistant/specs/audit-emission-cadence.yaml"]) + for entry in approval["sources"]: + if "heartbeat" in entry: + assert entry["heartbeat"]["interval_seconds"] == frame_seconds + extension = entry["heartbeat"]["extensions"]["approval-engine"] + assert extension["requested_observer_max_gap_seconds"] == 2 * frame_seconds + assert extension["effective_deadline_seconds"] == 3 * frame_seconds + for entry in qonto["sources"]: + assert entry["heartbeat"]["interval"] == f"PT{frame_seconds}S" + observer = json.loads( + outputs[ + workspace / "kings-guard/src/kings_guard/fixtures/qonto_assistant_source_cadence.json" + ] + ) + assert observer["sources"][0]["heartbeat"]["interval_seconds"] == frame_seconds + + receiver = json.loads( + outputs[ + workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json" + ] + ) + assert set(receiver["heartbeat_classes"].values()) == {2 * frame_seconds} + assert receiver["effective_deadline_seconds"] == 3 * frame_seconds + assert receiver["may_read"] is False + assert receiver["status"] == "proposed-not-applied" + for name in ( + "approval-engine/approval_engine/coordination.py", + "qonto-assistant/src/qonto_assistant/coordination.py", + ): + assert f"FRAME_SECONDS = {frame_seconds}\n" in outputs[workspace / name] + deploy = yaml.safe_load_all(outputs[workspace / "approval-engine/deploy/approval-engine.yaml"]) + workload = next(item for item in deploy if item["kind"] == "StatefulSet") + expected_digest = hashlib.sha256(config.read_bytes()).hexdigest() + assert ( + workload["spec"]["template"]["metadata"]["annotations"][ + "coordination.coulomb.social/frame-sha256" + ] + == expected_digest + ) + args = workload["spec"]["template"]["spec"]["containers"][0]["args"] + assert args[args.index("--heartbeat-seconds") + 1] == str(frame_seconds) + service = yaml.safe_load(outputs[workspace / "rapp-qonto/runtime/knative/service.yaml"]) + assert ( + service["spec"]["template"]["metadata"]["annotations"][ + "coordination.coulomb.social/frame-sha256" + ] + == expected_digest + ) + env = service["spec"]["template"]["spec"]["containers"][0]["env"] + assert next( + item["value"] for item in env if item["name"] == "QONTO_AUDIT_HEARTBEAT_INTERVAL_SECONDS" + ) == str(frame_seconds) + + +def test_render_is_read_only_and_write_is_idempotent_with_drift_detection(workspace, capsys): + before = {path: path.read_bytes() for path in workspace.rglob("*") if path.is_file()} + assert renderer.main(["--workspace", str(workspace)]) == 0 + assert all(path.read_bytes() == data for path, data in before.items()) + assert renderer.main(["--workspace", str(workspace), "--check"]) == 1 + assert renderer.main(["--workspace", str(workspace), "--write"]) == 0 + assert renderer.main(["--workspace", str(workspace), "--check"]) == 0 + receiver = workspace / "net-kingdom/generated/coordination-frame/approval-engine-receiver.json" + data = json.loads(receiver.read_text()) + data["heartbeat_classes"]["revocation"] = 999999 + receiver.write_text(json.dumps(data)) + assert renderer.main(["--workspace", str(workspace), "--check"]) == 1 + + +def test_invalid_frame_does_not_partially_rewrite_sources(workspace): + config = workspace / renderer.FRAME_REF + config.write_text('{"schema_version":"coordination-frame/v1","frame_seconds":0}') + before = {path: path.read_bytes() for path in workspace.rglob("*") if path.is_file()} + with pytest.raises(SystemExit): + renderer.main(["--workspace", str(workspace), "--write"]) + assert all(path.read_bytes() == data for path, data in before.items()) diff --git a/workplans/NK-WP-0035-emission-cadence-security-profile.md b/workplans/NK-WP-0035-emission-cadence-security-profile.md index c778484..451b24c 100644 --- a/workplans/NK-WP-0035-emission-cadence-security-profile.md +++ b/workplans/NK-WP-0035-emission-cadence-security-profile.md @@ -5,7 +5,7 @@ title: "Publish the NetKingdom emission-cadence security profile" domain: infotech repo: net-kingdom status: blocked -flavor: residual +flavor: implementation owner: codex topic_slug: netkingdom planning_priority: P1 @@ -90,7 +90,7 @@ Python compilation and `git diff --check` pass. id: NK-WP-0035-T04 status: wait blocked_on: message-from:approval-engine -blocking_reason: "Approval Engine effective heartbeat deadline needs owner choice (48h requested; receiver applies 1.5 grace), followed by approved cadence registry/runtime acceptance and lifecycle reconciliation. Qonto needs an actual rapp-qonto collector and AUDIT-WP-0013-T05 sender admission before KG-WP-0005-T03 operational observation. Existing bearer-revocation proof is delivered; local-identity incompatibility remains explicit." +blocking_reason: "Founder chose configurable frame f=1h: source heartbeat f, receiver wait 2f, effective deadline 3f. Generated source/runtime bindings await approved deployment and registry admission plus actual lifecycle reconciliation. Qonto still needs an actual rapp-qonto collector and AUDIT-WP-0013-T05 sender admission before KG-WP-0005-T03 observation. Existing bearer-revocation proof is delivered; local-identity incompatibility remains explicit." priority: high state_hub_task_id: "e3fbc8b8-00d5-57ce-ba0c-19b58044c950" ``` @@ -276,3 +276,47 @@ Knowledge: Emergent 0.2.6, digest `8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`; start/evidence source guidance directly reviewed, withheld/orientation-only receipt retained. This supplies no operational authority. + + +## Introduce a configurable ecosystem coordination frame + +```task +id: NK-WP-0035-T06 +status: done +priority: high +``` + +Founder direction in this sitting supersedes the binary 48h/72h question: +long-term ecosystem coordination uses one configurable base frame, initially +**f=1h**, with waits of **2f** and **3f**. Decision: +`1ac5a70f-082e-4d8f-9ac8-59e4c175da02`. + +Authoritative config is `coordination-engine/config/coordination-frame.json`. +The coordination-engine API validates it and derives durations / deadlines. +NetKingdom's compiler derives source declarations, consumer Python defaults, +Approval Engine's deployment heartbeat argument, Qonto's deployment environment +setting, and the proposed receiver metadata, retaining the frame digest. +Heartbeat is f=3600s; registered wait is 2f=7200s; the receiver's 1.5 grace +makes the effective deadline 3f=10800s. The formulas also reproduce the old +24h/48h/72h cadence when f is set to 24h. + +This is source-level adoption of the setting, not proof that the deployed +processes have changed pace or that every ecosystem timer has adopted it. +Runtime release/registry receipts remain T04. Credential and approval validity, +worker leases and transport retry timers are separate owner contracts. +[Configuration and regeneration](../tools/coordination-frame/README.md). + + +T06 source acceptance: **73 coordination-engine tests passed, one live test +skipped; 32 compiler/profile tests; 271 Approval Engine; 91 Qonto; 25 package; +128 King's Guard tests passed**. The compiler's --check has no drift. Changing +f to 10 minutes or 24 hours in isolated test workspaces rescales source, +receiver and observer bindings together. Prepared manifests preserve image, +identity, secret, routing and scaling fields apart from the timing values and +frame-digest annotations. [Frame validation](../docs/evidence/2026-10-15-cadence-adoption/coordination-frame-validation.json). + +The earlier 48h/72h question is resolved by the explicit configurable-frame +decision. No further deadline selection is waiting on the founder. T04 still +requires actual approved rollout/registration, missing Qonto collector/admission, +and operational lifecycle/observer acceptance. The profile remains proposed; +this workplan is now **5/6 source tasks done**, with T04's operational gate open.