diff --git a/credential-change-requests/CCR-2026-0047-rapp-qonto-audit-sender.yaml b/credential-change-requests/CCR-2026-0047-rapp-qonto-audit-sender.yaml index 9f5c8be..3b6c749 100644 --- a/credential-change-requests/CCR-2026-0047-rapp-qonto-audit-sender.yaml +++ b/credential-change-requests/CCR-2026-0047-rapp-qonto-audit-sender.yaml @@ -3,7 +3,7 @@ kind: credential-change-request schema_version: 1 request_type: workload-kv-read title: rapp-qonto collector exact audit sender custody and delivery -status: approved +status: applied created: '2026-10-15' updated: '2026-10-15' requester: @@ -104,7 +104,24 @@ verification: - Apply only reviewed exact policy/role/projection; retain custody on failure and resume explicitly; no destructive rollback or blind rotation. - No claim of operational completion until native positive/negative and independent observer receipts. - evidence: [] + evidence: + - at: '2026-10-15T19:40:23.546597+00:00' + actor: codex via attended user platform-admin (root execution) + kind: cadence_receiver_custody_and_native_sender + result: passed + details: + - "Native custody receipt receiver-custody-live.json: protected registry CAS15\u219216, version1\ + \ new sender custody, Approval credentials and all other senders preserved; exact readers/negative\ + \ checks/revocation, ESO values and loaded receiver registry matched." + - Independent attended wrapper exit0/session self-revocation confirmed by root; initial rejected + OPENBAO_TOKEN-ambient attempt made no credential mutation and session self-revocation was confirmed. + Retry launch removed all three token aliases before OIDC. + - 'Native collector source/tenant admission: heartbeat1 and deny1 reconciled for one exact instance; + duplicate200, wrong-source400, wrong-tenant400, archive-read403, invalid-bearer401; pending0. + Original202 receipts persisted across actual collector pod restart.' + - Source receipts copied unchanged to docs/evidence/2026-10-15-cadence-receiver/. Status applied + only; elapsed-hour periodic observation, independent protected observer readback and complete + native matrix remain pending. lifecycle: deactivate: Stop only collector; remove only its bearer from receiver via CAS and prove refusal; detach exact reader role and ESO projection; retain custody versions and evidence. diff --git a/docs/cadence-receiver-custody.md b/docs/cadence-receiver-custody.md index 679925e..9022f6a 100644 --- a/docs/cadence-receiver-custody.md +++ b/docs/cadence-receiver-custody.md @@ -42,6 +42,21 @@ that deployed gate. Run `apply` only inside the existing attended Warden platform-admin `--exec` envelope. Use credential routing first; do not export a token into a shell. +Before starting OIDC, remove all three ambient token aliases from the launcher +environment without reading or printing their values: + +```bash +env -u BAO_TOKEN -u VAULT_TOKEN -u OPENBAO_TOKEN \ + warden access openbao-platform-admin-login --exec -- \ + python3 /home/worsch/railiance-platform/scripts/cadence_receiver_custody.py apply +``` + +This is required for the custody and fresh observer-readback launches. The +initial native attempt demonstrated that OPENBAO_TOKEN is not scrubbed by the +existing Warden wrapper; the contained helper correctly refused it. Session +self-revocation was confirmed and no custody mutation occurred in that attempt. +Do not inspect, copy or relax the guard for an ambient credential. + The owner wrapper must preserve its independent exit/self-revocation receipt. The contained command is: @@ -126,9 +141,12 @@ The sanitized event projection includes instance/sequence, action, observed and accepted timestamps, and exact heartbeat coverage. Actual custody accepted_at can distinguish a duplicate delivery receipt from original custody time; a collector duplicate200 is not itself first-acceptance timing proof. -Approval Engine source/tenant is also allowed, but this helper requires an -instance-bearing source record and therefore cannot substitute for its -historical instance-free lifecycle acceptance path. +Approval Engine source/tenant is also allowed; omit --instance for its +instance-free lifecycle records. The helper does not invent process identity +or sequence for Approval and labels its conclusion as a bounded source +lifecycle acceptance window. Qonto still requires an explicit exact instance. +Receiver-normalized timezone spelling is compared by timestamp semantics; +source, tenant and actual instance bindings remain exact. Exit0 reports `independent_custody_readback_complete`, which proves only the bounded supplied custody reads. The source snapshot, contiguous startup/ diff --git a/docs/evidence/2026-10-15-cadence-receiver/qonto-collector-native-admission.json b/docs/evidence/2026-10-15-cadence-receiver/qonto-collector-native-admission.json new file mode 100644 index 0000000..7ba89c4 --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-receiver/qonto-collector-native-admission.json @@ -0,0 +1,59 @@ +{ + "own_reconciliation_status": 200, + "own_reconciliation": { + "source": "qonto-assistant", + "tenant": "tenant:friendly:binky", + "since": "2026-10-15T19:36:00+00:00", + "until": "2026-10-15T19:37:05.369346+00:00", + "counts": [ + { + "class": "audit-core.heartbeat", + "count": 1 + }, + { + "class": "audit.deny", + "count": 1 + } + ], + "stream_instance_id": "b351fa6b-e0d6-4f88-b7c9-6bacb1a112b7", + "means": "the count of events audit-core accepted and stored in this window. Divergence from the source's own count is a finding for the source; agreement proves neither completeness nor that any event occurred." + }, + "duplicate_status": 200, + "wrong_source_status": 400, + "wrong_tenant_status": 400, + "read_refusal_status": 403, + "invalid_bearer_status": 401, + "global_pending_count": 0, + "instance_receipts": [ + { + "accepted_at": "2026-10-15T19:36:30.769604+00:00", + "covered_event_classes": [ + "audit.deny" + ], + "event_id": "28dad6de11fb25fe6e0e145351f047360d6898afb53afd4cd677e560ec69132a", + "source": "qonto-assistant", + "status_code": 202, + "stream_instance_id": "b351fa6b-e0d6-4f88-b7c9-6bacb1a112b7", + "stream_sequence": 1, + "tenant": "tenant:friendly:binky", + "type": "audit-core.heartbeat" + }, + { + "accepted_at": "2026-10-15T19:36:30.861014+00:00", + "covered_event_classes": [], + "event_id": "6a9f34445e13062e38ab309a6fc800dc9a2fa24cb8a46ecd919ae18fe9409874", + "source": "qonto-assistant", + "status_code": 202, + "stream_instance_id": "b351fa6b-e0d6-4f88-b7c9-6bacb1a112b7", + "stream_sequence": 2, + "tenant": "tenant:friendly:binky", + "type": "audit.deny" + } + ], + "observed_at": "2026-10-15T19:37:03.422744+00:00", + "credential_values_emitted": false, + "source": "qonto-assistant", + "tenant": "tenant:friendly:binky", + "stream_instance_id": "b351fa6b-e0d6-4f88-b7c9-6bacb1a112b7", + "independent_readback": false +} diff --git a/docs/evidence/2026-10-15-cadence-receiver/qonto-collector-restart-persistence.json b/docs/evidence/2026-10-15-cadence-receiver/qonto-collector-restart-persistence.json new file mode 100644 index 0000000..2196930 --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-receiver/qonto-collector-restart-persistence.json @@ -0,0 +1,37 @@ +{ + "receipts": [ + { + "accepted_at": "2026-10-15T19:36:30.769604+00:00", + "covered_event_classes": [ + "audit.deny" + ], + "event_id": "28dad6de11fb25fe6e0e145351f047360d6898afb53afd4cd677e560ec69132a", + "source": "qonto-assistant", + "status_code": 202, + "stream_instance_id": "b351fa6b-e0d6-4f88-b7c9-6bacb1a112b7", + "stream_sequence": 1, + "tenant": "tenant:friendly:binky", + "type": "audit-core.heartbeat" + }, + { + "accepted_at": "2026-10-15T19:36:30.861014+00:00", + "covered_event_classes": [], + "event_id": "6a9f34445e13062e38ab309a6fc800dc9a2fa24cb8a46ecd919ae18fe9409874", + "source": "qonto-assistant", + "status_code": 202, + "stream_instance_id": "b351fa6b-e0d6-4f88-b7c9-6bacb1a112b7", + "stream_sequence": 2, + "tenant": "tenant:friendly:binky", + "type": "audit.deny" + } + ], + "pending_count": 0, + "started_at": "2026-10-15T19:37:41.503622+00:00", + "pod_uid_before": "1a39bd4b-19dc-4c1f-adaa-56a11423d945", + "native_admission_receipt": "qonto-collector-native-admission.json", + "pod_uid_after": "92ef9104-469c-4b28-a0cc-f781f53bb26e", + "observed_at": "2026-10-15T19:38:35.874453+00:00", + "first_202_receipts_preserved": true, + "pvc": "rapp-qonto-audit-outbox", + "image": "forgejo.coulomb.social/coulomb/rapp-qonto-audit-collector@sha256:82bfd4100e14ddb2cefc4acc02b6b74609846b4a26ad40d9089ed564693f250a" +} diff --git a/docs/evidence/2026-10-15-cadence-receiver/receiver-custody-live.json b/docs/evidence/2026-10-15-cadence-receiver/receiver-custody-live.json new file mode 100644 index 0000000..14014d2 --- /dev/null +++ b/docs/evidence/2026-10-15-cadence-receiver/receiver-custody-live.json @@ -0,0 +1,87 @@ +{ + "schema_version": "platform.cadence-custody/v1", + "status": "delivered_pending_native_producer_and_independent_acceptance", + "credential_values_emitted": false, + "started_at": "2026-10-15T19:35:14.571483+00:00", + "packet_sha256": "81478f43abd2dbcb924966e9a3404e26bc226c91f2921b493ef0d5e2c23d4729", + "receiver_before": { + "image": "forgejo.coulomb.social/coulomb/audit-core@sha256:b479801b0bc76df66a36a85376c45aaf399c6d82feade9ba81430b774c4a8271", + "deployment_uid": "b85fe3d0-75c9-4e0d-8c34-c6f1df0881bb", + "deployment_resource_version": "75784583", + "pod_uid": "22e4df05-dfb0-4e70-8d5a-5a5950c860bd", + "capabilities": { + "load_bearing": true, + "redact": true, + "write_only": true, + "source_exact": true, + "tenant_exact": true + }, + "synthetic_probe_only": true, + "credential_reads": 0, + "cadence_instance_capabilities": { + "cadence_registration": true, + "sqlite_instance_filter": true, + "postgres_instance_filter": true, + "synthetic_cadence_parse": true, + "synthetic_heartbeat_parse": true, + "synthetic_exact_scope": true + } + }, + "phase": "delivery", + "registry_version_before": 15, + "custody_version": 1, + "custody_request_id": "77a526ef-dbcc-147d-b98b-a339f4efcd62", + "approval": { + "sender": "approval-engine", + "source": "approval-engine", + "tenant": "tenant:platform", + "may_read": false, + "emission_cadence_sha256": "cc58c8d53ebf50acf540d73b7607d579942bda7734c3e8722108e9c9204af655", + "heartbeat_classes": { + "issuance": 7200, + "use": 7200, + "supersession": 7200, + "revocation": 7200 + }, + "effective_deadline_seconds": 10800, + "changed": true, + "credentials_preserved": true, + "other_registry_fields_preserved": true + }, + "registry_request_id": "47ca933d-4a5d-699b-c474-094947f78fb9", + "registry_version_after": 16, + "registry_readback_matches": true, + "other_senders_preserved": true, + "approval_credentials_preserved": true, + "reader_exact_path_negatives_and_revocation": true, + "wrong_identity_denied": true, + "coding_agent_deny_wins": true, + "projection_matches": true, + "receiver_registry_projection_matches": true, + "receiver_after": { + "image": "forgejo.coulomb.social/coulomb/audit-core@sha256:b479801b0bc76df66a36a85376c45aaf399c6d82feade9ba81430b774c4a8271", + "deployment_uid": "b85fe3d0-75c9-4e0d-8c34-c6f1df0881bb", + "deployment_resource_version": "75785764", + "pod_uid": "8333754a-eca0-4dd0-9230-72e41bb1e632", + "capabilities": { + "load_bearing": true, + "redact": true, + "write_only": true, + "source_exact": true, + "tenant_exact": true + }, + "synthetic_probe_only": true, + "credential_reads": 0, + "cadence_instance_capabilities": { + "cadence_registration": true, + "sqlite_instance_filter": true, + "postgres_instance_filter": true, + "synthetic_cadence_parse": true, + "synthetic_heartbeat_parse": true, + "synthetic_exact_scope": true + } + }, + "receiver_reloaded": true, + "receiver_runtime_registry_matches": true, + "completed_at": "2026-10-15T19:35:47.205117+00:00" +} diff --git a/scripts/cadence_observer_readback.py b/scripts/cadence_observer_readback.py index f2be88d..1f72060 100644 --- a/scripts/cadence_observer_readback.py +++ b/scripts/cadence_observer_readback.py @@ -36,12 +36,15 @@ def stamp(value): def paths(source, tenant, instance, since, until, event_ids): require(SCOPES.get(source) == tenant, "exact_source_tenant_required") - require( - isinstance(instance, str) - and 1 <= len(instance) <= 256 - and instance.strip() == instance, - "exact_instance_required", - ) + if source == "qonto-assistant": + require( + isinstance(instance, str) + and 1 <= len(instance) <= 256 + and instance.strip() == instance, + "exact_instance_required", + ) + else: + require(instance is None, "approval_has_no_declared_instance_filter") require( stamp(since) <= stamp(until) <= datetime.now(timezone.utc), "invalid_acceptance_window", @@ -57,15 +60,10 @@ def paths(source, tenant, instance, since, until, event_ids): ), "invalid_event_id", ) - query = urlencode( - dict( - source=source, - tenant=tenant, - stream_instance_id=instance, - since=since, - until=until, - ) - ) + query_values = dict(source=source, tenant=tenant, since=since, until=until) + if instance is not None: + query_values["stream_instance_id"] = instance + query = urlencode(query_values) return ["/v1/reconciliation?" + query, "/v1/stream-findings"] + [ "/v1/events/" + x for x in event_ids ] @@ -95,11 +93,15 @@ def sanitize_record(record, source, tenant, instance, event_id, since, until): "record_source_tenant_id_mismatch", ) detail = record["details"]["data"] - require(detail.get("stream_instance_id") == instance, "record_instance_mismatch") - require( - type(detail.get("stream_sequence")) is int and detail["stream_sequence"] > 0, - "record_sequence_missing", - ) + if source == "qonto-assistant": + require( + detail.get("stream_instance_id") == instance, "record_instance_mismatch" + ) + require( + type(detail.get("stream_sequence")) is int + and detail["stream_sequence"] > 0, + "record_sequence_missing", + ) accepted = record["accepted_at"] require( stamp(since) <= stamp(accepted) < stamp(until), @@ -118,12 +120,25 @@ def sanitize_record(record, source, tenant, instance, event_id, since, until): ) if action == "audit-core.heartbeat": require(coverage == ["audit.deny"], "record_heartbeat_scope_mismatch") + if source == "approval-engine": + names = {"issuance", "use", "supersession", "revocation"} + require( + action in {"approval." + name for name in names} | {"audit-core.heartbeat"}, + "record_undeclared_class", + ) + if action == "audit-core.heartbeat": + require( + len(coverage) == 1 and coverage[0] in names, + "record_heartbeat_scope_mismatch", + ) return { "event_id": event_id, "source": source, "tenant": tenant, "stream_instance_id": instance, - "stream_sequence": detail["stream_sequence"], + "stream_sequence": detail.get("stream_sequence") + if instance is not None + else None, "type": action, "accepted_at": accepted, "observed_at": record["observed_at"], @@ -136,16 +151,11 @@ def readback(origin, token, source, tenant, instance, since, until, event_ids): allowed = paths(source, tenant, instance, since, until, event_ids) reconciliation = read_http(origin, token, allowed[0], allowed) require( - all( - reconciliation.get(k) == v - for k, v in ( - ("source", source), - ("tenant", tenant), - ("stream_instance_id", instance), - ("since", since), - ("until", until), - ) - ), + reconciliation.get("source") == source + and reconciliation.get("tenant") == tenant + and reconciliation.get("stream_instance_id") == instance + and stamp(reconciliation["since"]) == stamp(since) + and stamp(reconciliation["until"]) == stamp(until), "reconciliation_scope_window_mismatch", ) raw_findings = read_http(origin, token, allowed[1], allowed) @@ -183,7 +193,11 @@ def readback(origin, token, source, tenant, instance, since, until, event_ids): "reconciliation": reconciliation, "stream_findings": safe_findings, "events": records, - "scope": "bounded same-instance accepted custody; source-global findings require active-instance interpretation", + "scope": ( + "bounded same-instance accepted custody; source-global findings require active-instance interpretation" + if instance is not None + else "bounded source lifecycle acceptance window; no process-instance completeness claim" + ), } @@ -192,7 +206,6 @@ def main(): p.add_argument("--source", choices=SCOPES, required=True) for key in ( "tenant", - "instance", "since", "until", "kubeconfig", @@ -200,6 +213,10 @@ def main(): "expected-receiver-image", ): p.add_argument("--" + key, required=True) + p.add_argument( + "--instance", + help="Required only for Qonto; Approval has no declared instance field", + ) p.add_argument("--event-id", action="append", required=True) p.add_argument("--receipt", type=Path, required=True) a = p.parse_args() diff --git a/tests/test_cadence_observer_readback.py b/tests/test_cadence_observer_readback.py index 4a08987..6212b8d 100644 --- a/tests/test_cadence_observer_readback.py +++ b/tests/test_cadence_observer_readback.py @@ -156,5 +156,82 @@ class ReadbackBounds(unittest.TestCase): ) +class ApprovalReadback(unittest.TestCase): + def test_approval_omits_instance_filter_without_inventing_one(self): + paths = lane.paths( + "approval-engine", "tenant:platform", None, SINCE, UNTIL, ["approval-1"] + ) + self.assertNotIn("stream_instance_id", paths[0]) + with self.assertRaisesRegex(lane.LaneError, "no_declared_instance"): + lane.paths( + "approval-engine", + "tenant:platform", + "invented", + SINCE, + UNTIL, + ["approval-1"], + ) + with self.assertRaisesRegex(lane.LaneError, "exact_instance_required"): + lane.paths( + "qonto-assistant", "tenant:friendly:binky", None, SINCE, UNTIL, ["q-1"] + ) + + def test_actual_approval_record_needs_no_instance_sequence(self): + record = dict( + event_id="approval-1", + source="approval-engine", + tenant="tenant:platform", + action="approval.issuance", + accepted_at="2026-10-15T10:30:00Z", + observed_at="2026-10-15T10:29:59Z", + details={"data": {"transition": "issuance"}}, + ) + result = lane.sanitize_record( + record, + "approval-engine", + "tenant:platform", + None, + "approval-1", + SINCE, + UNTIL, + ) + self.assertIsNone(result["stream_instance_id"]) + self.assertIsNone(result["stream_sequence"]) + self.assertEqual(result["type"], "approval.issuance") + + def test_reconciliation_compares_semantic_window_times(self): + response = dict( + source="approval-engine", + tenant="tenant:platform", + since="2026-10-15T10:00:00+00:00", + until="2026-10-15T11:00:00+00:00", + counts=[], + ) + record = dict( + event_id="approval-1", + source="approval-engine", + tenant="tenant:platform", + action="audit-core.heartbeat", + accepted_at="2026-10-15T10:30:00Z", + observed_at="2026-10-15T10:29:59Z", + details={"data": {"class": "revocation"}}, + ) + with patch.object( + lane, "read_http", side_effect=[response, {"stream_findings": []}, record] + ): + result = lane.readback( + "http://127.0.0.1:1", + "fixture-only", + "approval-engine", + "tenant:platform", + None, + SINCE, + UNTIL, + ["approval-1"], + ) + self.assertEqual(result["events"][0]["covered_event_classes"], ["revocation"]) + self.assertIn("no process-instance completeness claim", result["scope"]) + + if __name__ == "__main__": unittest.main() diff --git a/workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md b/workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md index 9fda8a6..71c56ea 100644 --- a/workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md +++ b/workplans/RPF-WP-0059-hourly-cadence-receiver-custody.md @@ -50,7 +50,7 @@ immutable compatible receiver image before attended execution. ```task id: RPF-WP-0059-T03 -status: progress +status: done priority: high state_hub_task_id: "c4e69d60-030e-592b-ac56-9f77468f7da9" ``` @@ -64,7 +64,7 @@ resume on interruption. No live effect or secret read occurred in preparation. ```task id: RPF-WP-0059-T04 -status: wait +status: progress priority: high state_hub_task_id: "7d8e5315-9470-5596-a865-d451284f6f50" ``` @@ -74,3 +74,23 @@ source/tenant/instance scope and real elapsed-hour periodic evidence, isolated wrong-namespace denial, actual formerly-valid bearer revocation, independent protected readback, missing-class/lag/divergence, restart/scale-zero evidence. Never infer profile acceptance from a model test or registry projection. + + +### Native admission progress — 2026-10-15 + +Root executed the explicit approved packet in an attended platform-admin +session. Wrapper exit0 and self-revocation confirmed; receiver-custody-live.json +records registry15→16 CAS, durable version1 custody, preserved Approval +credentials/other senders, reader scope/negative/revoke checks, ESO projection +and actual loaded registry equality, and Ready receiver reload. CCR0047 is +applied, not verified/active. An initial ambient OPENBAO_TOKEN attempt was +correctly refused before custody mutation; that session also self-revoked. +Retry removed token aliases before launching OIDC, without relaxing guards. + +Collector native receipt proves heartbeat1/deny1 own-instance reconciliation, +duplicate200, wrong-source400/wrong-tenant400/archive-read403/invalid-bearer401, +pending0; actual collector pod restart preserved first202 receipts and pending0. +Exact root receipts copied to docs/evidence/2026-10-15-cadence-receiver/. +The real hourly observation started19:36 UTC with authorized health polling; +it is still running. No elapsed-hour or independent observer acceptance is +claimed. T04 remains progress for actual timed capture and protected readback.