From e9b9d1c7088678c0d87043b4b6bb14126c985ec9 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 9 Oct 2026 20:23:43 +0200 Subject: [PATCH] Activate scoped user-engine mail and finish EMAIL-WP-0008 Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a11fd9-caf6-7a63-b578-9daaaa134ff2 --- deploy/backup/runtime-scoped-2026-10-09.json | 34 ++++ deploy/closure/plan.json | 7 +- deploy/closure/prepare.py | 1 + deploy/k8s/railiance/deployment.yaml | 29 ++- deploy/k8s/railiance/kustomization.yaml | 2 + .../k8s/railiance/scoped-caller-delivery.json | 88 +++++++++ .../k8s/railiance/scoped-caller-policy.json | 12 ++ docs/caller-policy-draft-2026-10-08.md | 53 ++++++ docs/ledger-backup-review-2026-10-08.md | 21 ++ ...dger-scoped-image-recovery-2026-10-09.json | 179 ++++++++++++++++++ docs/scoped-caller-live-2026-10-09.json | 85 +++++++++ scripts/admit-scoped-caller.py | 18 +- scripts/capture-ledger.py | 2 +- scripts/rehearse-ledger.py | 2 +- tests/test_closure_custody.py | 53 +++++- .../EMAIL-WP-0007-ledger-backup-recovery.md | 21 ++ ...IL-WP-0008-scoped-transactional-callers.md | 63 +++++- 17 files changed, 651 insertions(+), 19 deletions(-) create mode 100644 deploy/backup/runtime-scoped-2026-10-09.json create mode 100644 deploy/k8s/railiance/scoped-caller-delivery.json create mode 100644 deploy/k8s/railiance/scoped-caller-policy.json create mode 100644 docs/ledger-scoped-image-recovery-2026-10-09.json create mode 100644 docs/scoped-caller-live-2026-10-09.json diff --git a/deploy/backup/runtime-scoped-2026-10-09.json b/deploy/backup/runtime-scoped-2026-10-09.json new file mode 100644 index 0000000..022010d --- /dev/null +++ b/deploy/backup/runtime-scoped-2026-10-09.json @@ -0,0 +1,34 @@ +{ + "schema": "email-connect.ledger-runtime-bundle.v2", + "files": { + "email-connect-ledger-freshness.service": "a9ce8d1e92fadcda8bb125bf952c42f338514545b6a980cfd8be0dd95bc5eb06", + "backup-primary.lock": "7142d3690855a6fcfdd2dac02113e7a76b6731988d52b4b013a50d97570f2aaa", + "email-connect-ledger-backup.service": "e2f41257ac7df0aac6250822a794289946f4282e68ee61d751c2c84a1caeb1e5", + "notify-ledger-failure.py": "d5bc735963896b2e909660e678b8ed1b220ec8f1038014399c5b995a577c90b6", + "primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "rehearse-ledger.py": "5c43b655149d8ae577fab80e76358be35f90cb0357df9b726ac7cb23346946a0", + "backup-ledger.py": "e74c41948ca01dae09578e2abfba14b96fb1f158af0ec91a0fb1aa9ecaf01680", + "capture-ledger.py": "7994fe7b4fbf72e28a4296212005a88d0ef9fcb626b066e6bcf2885500bc69d6", + "email-connect-ledger-alert@.service": "4ee43ffab27478556c75153e89668a3bb36e6c56d6dd3b52d0db8d8603b9263c", + "email-connect-ledger-backup.timer": "ffb64bf39763c5e3d52cea5a13aa3fdffe243c3ff5a81555eb47048746527782", + "ledger_archive.py": "92d9d5e1908c7d433eb3f4ada6460f7d9b639017a4c2311cb7ffaacd262f33c3", + "email-connect-ledger-freshness.timer": "9103655972b13c25dcbb6a8d53f3076fe8b9b7eb04a91e85c1cd1a875bf3310f" + }, + "receiver_image": "5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "timers_enabled": false, + "failure_recipient": "platform-root", + "notification_delivery_verified": true, + "runtime_installed": true, + "sdk_hash_locked": true, + "hardened_preflight_verified": true, + "notification_message_id": "adcebf95-26c4-4adb-99b0-4446c8505a42", + "failure_hook_test_verified": true, + "freshness_verified": true, + "human_acknowledged": false, + "units_installed": true, + "current_offsite_version": "1791569930973785", + "current_image_isolated_recovery_verified": true, + "offline_kit_rehearsal_verified": true, + "independent_scaleway_identity_and_kit_custody_verified": false, + "previous_runtime_evidence": "deploy/backup/runtime-manifest-2026-10-09.json" +} diff --git a/deploy/closure/plan.json b/deploy/closure/plan.json index b87ca55..87ce0a0 100644 --- a/deploy/closure/plan.json +++ b/deploy/closure/plan.json @@ -137,11 +137,12 @@ "deploy/closure/caller-delivery.json": "6fed12e204533ec679e80b3d9d0ba814cbf1d6f91a5a02211f0334617a92b0eb", "deploy/closure/receiver.patch.json": "449ce8456323347eca81190e2ccca334c90397c6699498466308bf8bfe63a9de", "deploy/closure/user-engine.patch.json": "2cae6ea6259ac1031fb7b8b11f011686baad8cb576cd72ee3f74461924daef46", - "scripts/admit-scoped-caller.py": "592bd46740f9eea4ad1ae78aa674f6c381ce05ab6044cedf0af968a1c340adf2", + "scripts/admit-scoped-caller.py": "17bf68f85f0bb08b3ca89eee34e62e794ee4754fbbb21646c28c85a1c1dd0e0e", "scripts/scaleway-recovery-custody.py": "85874f0620dc9353a9aaae1194db054de166296c4385743e4d541bbe42486c15", "scripts/publish-ledger-recovery-kit.py": "cd7c6e8e6a4e8417ba55f8170d763d7c5a9e6dd9b5878dc3459021a0b64ea732", "scripts/prepare-ledger-recovery-kit.py": "db92a29eb68e1b555cf18b17229d1e479b3147da7600ffa0616ac8b4d011ff55", - "scripts/rehearse-ledger.py": "d3da88972e44dac3800afb48d21e525dc1476ca58de3e7702f7751aea33eda53", - "scripts/backup-ledger.py": "e74c41948ca01dae09578e2abfba14b96fb1f158af0ec91a0fb1aa9ecaf01680" + "scripts/rehearse-ledger.py": "5c43b655149d8ae577fab80e76358be35f90cb0357df9b726ac7cb23346946a0", + "scripts/backup-ledger.py": "e74c41948ca01dae09578e2abfba14b96fb1f158af0ec91a0fb1aa9ecaf01680", + "scripts/capture-ledger.py": "7994fe7b4fbf72e28a4296212005a88d0ef9fcb626b066e6bcf2885500bc69d6" } } diff --git a/deploy/closure/prepare.py b/deploy/closure/prepare.py index eaae056..0c1ab19 100644 --- a/deploy/closure/prepare.py +++ b/deploy/closure/prepare.py @@ -131,6 +131,7 @@ artifacts = [ "scripts/scaleway-recovery-custody.py", "scripts/publish-ledger-recovery-kit.py", "scripts/prepare-ledger-recovery-kit.py", "scripts/rehearse-ledger.py", "scripts/backup-ledger.py", + "scripts/capture-ledger.py", ] plan["artifacts"] = {name: hashlib.sha256((ROOT / name).read_bytes()).hexdigest() for name in artifacts} save("plan.json", plan) diff --git a/deploy/k8s/railiance/deployment.yaml b/deploy/k8s/railiance/deployment.yaml index 5070336..8f5268b 100644 --- a/deploy/k8s/railiance/deployment.yaml +++ b/deploy/k8s/railiance/deployment.yaml @@ -14,8 +14,9 @@ metadata: annotations: email-connect.railiance.io/rollback-note: >- SQLite schema is additive. Preserve deliveries and send_attempts. - Stop delivery and reconcile any in_flight reservations before reverting - to a pre-P05 image, which cannot enforce uncertain-outcome reservations. + Stop delivery and reconcile any in_flight reservations before reverting. + Preserve scoped caller admission and both caller projections. The retired + legacy ingest bearer must never be restored or used for rollback. spec: replicas: 1 revisionHistoryLimit: 5 @@ -42,8 +43,8 @@ spec: terminationGracePeriodSeconds: 15 containers: - name: email-connect - # Caller controls and terminal invitation retry fix, source c9756b0; approved image-only rollout. - image: forgejo.coulomb.social/coulomb/email-connect@sha256:eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2 + # Scoped original user-engine caller and compatible legacy evidence adoption. + image: forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d imagePullPolicy: IfNotPresent ports: - name: http @@ -54,6 +55,9 @@ spec: - secretRef: name: email-connect-runtime optional: false + env: + - name: EMAIL_CONNECT_CALLERS_FILE + value: /etc/email-connect-callers/callers.json resources: requests: cpu: 25m @@ -74,6 +78,12 @@ spec: mountPath: /data - name: tmp mountPath: /tmp + - name: caller-policy + mountPath: /etc/email-connect-callers + readOnly: true + - name: caller-credentials + mountPath: /var/run/secrets/email-connect-callers + readOnly: true startupProbe: httpGet: {path: /healthz, port: http} periodSeconds: 3 @@ -94,3 +104,14 @@ spec: claimName: email-connect-data - name: tmp emptyDir: {} + - name: caller-policy + configMap: + name: email-connect-callers-user-engine-276257849bf2 + defaultMode: 0444 + - name: caller-credentials + secret: + secretName: email-connect-caller-user-engine + defaultMode: 0440 + items: + - key: token + path: user-engine diff --git a/deploy/k8s/railiance/kustomization.yaml b/deploy/k8s/railiance/kustomization.yaml index da55daa..5510c78 100644 --- a/deploy/k8s/railiance/kustomization.yaml +++ b/deploy/k8s/railiance/kustomization.yaml @@ -9,4 +9,6 @@ resources: - service.yaml - networkpolicies.yaml - externalsecret.yaml + - scoped-caller-delivery.json + - scoped-caller-policy.json - deployment.yaml diff --git a/deploy/k8s/railiance/scoped-caller-delivery.json b/deploy/k8s/railiance/scoped-caller-delivery.json new file mode 100644 index 0000000..c7d09c9 --- /dev/null +++ b/deploy/k8s/railiance/scoped-caller-delivery.json @@ -0,0 +1,88 @@ +{ + "apiVersion": "v1", + "kind": "List", + "items": [ + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "name": "email-connect-scoped-caller-eso", + "namespace": "email-connect" + }, + "automountServiceAccountToken": false + }, + { + "apiVersion": "external-secrets.io/v1", + "kind": "ClusterSecretStore", + "metadata": { + "name": "openbao-email-connect-scoped-caller" + }, + "spec": { + "conditions": [ + { + "namespaces": [ + "email-connect" + ] + } + ], + "provider": { + "vault": { + "server": "http://openbao.openbao.svc:8200", + "path": "platform", + "version": "v2", + "auth": { + "kubernetes": { + "mountPath": "kubernetes", + "role": "email-connect-scoped-caller-eso", + "serviceAccountRef": { + "name": "email-connect-scoped-caller-eso", + "namespace": "email-connect", + "audiences": [ + "openbao" + ] + } + } + } + } + } + } + }, + { + "apiVersion": "external-secrets.io/v1", + "kind": "ExternalSecret", + "metadata": { + "name": "email-connect-caller-user-engine", + "namespace": "email-connect" + }, + "spec": { + "refreshInterval": "5m", + "secretStoreRef": { + "kind": "ClusterSecretStore", + "name": "openbao-email-connect-scoped-caller" + }, + "target": { + "name": "email-connect-caller-user-engine", + "creationPolicy": "Owner", + "deletionPolicy": "Retain", + "template": { + "metadata": { + "labels": { + "email-connect.io/caller": "user-engine" + }, + "annotations": {} + } + } + }, + "data": [ + { + "secretKey": "token", + "remoteRef": { + "key": "platform/workloads/email-connect/callers/user-engine", + "property": "TOKEN" + } + } + ] + } + } + ] +} diff --git a/deploy/k8s/railiance/scoped-caller-policy.json b/deploy/k8s/railiance/scoped-caller-policy.json new file mode 100644 index 0000000..0a9fc80 --- /dev/null +++ b/deploy/k8s/railiance/scoped-caller-policy.json @@ -0,0 +1,12 @@ +{ + "apiVersion": "v1", + "kind": "ConfigMap", + "immutable": true, + "metadata": { + "name": "email-connect-callers-user-engine-276257849bf2", + "namespace": "email-connect" + }, + "data": { + "callers.json": "{\n \"callers\": [\n {\n \"adopt_legacy_invitations\": true,\n \"adopt_legacy_verifications\": true,\n \"binding_patterns\": [\n [\n \"tenant:*-family\",\n \"coulomb-social\"\n ],\n [\n \"tenant:family-*\",\n \"coulomb-social\"\n ]\n ],\n \"bindings\": [\n [\n \"tenant:coulomb\",\n \"coulomb-social\"\n ]\n ],\n \"burst\": 5,\n \"caller_id\": \"user-engine\",\n \"max_payload_bytes\": 8192,\n \"operations\": [\n \"send\",\n \"delivery-status\",\n \"status\",\n \"verify\",\n \"consume\",\n \"cancel\"\n ],\n \"rate_per_minute\": 10,\n \"source\": \"user-engine\",\n \"templates\": [\n \"family_member.invited\",\n \"family_invitation.resent\"\n ],\n \"token_file\": \"/var/run/secrets/email-connect-callers/user-engine\"\n }\n ],\n \"schema_version\": 1\n}\n" + } +} diff --git a/docs/caller-policy-draft-2026-10-08.md b/docs/caller-policy-draft-2026-10-08.md index 98f8b82..9afa088 100644 --- a/docs/caller-policy-draft-2026-10-08.md +++ b/docs/caller-policy-draft-2026-10-08.md @@ -132,3 +132,56 @@ denials/quotas and legacy handle continuity without sending mail. Repin backup capture/rehearsal together only after actual receiver activation and refresh the recovery kit. No new credential, image rollout or policy activation has occurred. T03 remains progress. Full email regression: 193 passed, zero skips. + +## Scoped activation complete — 2026-10-09 + +EMAIL-WP-0008 now runs the published scoped receiver and compatible user-engine +images, with one Ready replica each. Decision +8edbff8c-f62d-4357-8465-8eb37300f311 admitted continuation of the reviewed package. +The attended child installed both exact policies/roles and created/read back +caller KV version 1, then failed because its writer self-revocation CLI command +was invalid. User confirmed parent login revocation. Corrected to `bao token +revoke -self`; a real CLI test against a synthetic local HTTP endpoint verifies +that call. Failure receipts now retain only allow-listed diagnostic codes. +No second administrator session was used. + +Both approved ESO readers deliver equal credentials. Exact policy/900-second +bounds and own-leaf read pass; sibling data, metadata, parent list, administrative +policy read and a non-mutating CAS0 write to the existing leaf deny. Wrong bound +identity, account and audience logins deny. Both proof tokens self-revoke and +subsequent lookup denies. User-engine's invitation/registration credentials match +its own projection and its binding JSON matches the reviewed exact/glob pairs. + +Quiesced pre-release offsite capture version 1791569299633609 precedes coordinated +activation. A probe initially rejected a newline in the retired legacy bearer; +Python rendered that bearer in its error. Automatic rollback restored baseline, +then dispatch was fenced and scoped mode restored immediately. The corrected +contained probe strips/rejects invalid header characters. The live receiver now +returns 401 for the exposed legacy bearer. New caller/provider credentials were +not exposed. Platform-root notice: b78dfaf0-953e-410c-b9fb-0309af4c42ae. Never restore +the retired bearer or the old single-caller rollback spec. Its custody record was +not deleted; retirement is enforced by the active scoped policy and canonical +receiver manifests. Productive provider traffic remains separately owned. + +Live no-send probes verify forbidden suppression, foreign tenant, 8192-byte cap, +burst five and preservation of both existing accepted deliveries. Non-quota +ledger tables remain unchanged; SMTP authentication/database diagnostics pass. +The synthetic recovery/package tests retain unknown-outcome, consumed/cancelled +handle and legacy adoption coverage; those live state tables were empty. + +Canonical receiver kustomization now retains the scoped image, immutable policy +and exact receiver-side ESO projection and passes server dry-run. The user-engine +activation patch remains deploy/closure/user-engine.patch.json. Capture/rehearsal +pins are changed together in source and installed runtime to the actual scoped +image. Fresh offsite version 1791569930973785 and isolated UID10001/network-none +recovery preserve all six tables, including the populated caller quota bucket. +A refreshed private kit bundles the image/tools/age ciphertext/public receipts +and excludes the private age identity. It remains local, not independent escrow. + +Public proofs: docs/scoped-caller-live-2026-10-09.json and + docs/ledger-scoped-image-recovery-2026-10-09.json. Focused backup/notification/ +custody regressions: 41 passed; focused syntax checks pass. Earlier full email +regression remains 193 passed, zero skips. EMAIL-WP-0008-T03 is done. Independent +Scaleway custody, generation retention and recurring backup activation remain +EMAIL-WP-0007; backup timers stay disabled. Productive/browser sender/reply/bounce +acceptance remains CSOC-WP-0015. diff --git a/docs/ledger-backup-review-2026-10-08.md b/docs/ledger-backup-review-2026-10-08.md index 08a8a49..20b5b09 100644 --- a/docs/ledger-backup-review-2026-10-08.md +++ b/docs/ledger-backup-review-2026-10-08.md @@ -247,3 +247,24 @@ Actual pre-capture capacity check passes: retained local files 148,994 bytes, Receiver image/pins remain eb3e7b68 and both timers remain disabled. Receipt: deploy/backup/runtime-update-2026-10-09.json. Earlier preparation evidence retains its historical statement that node helpers had not yet changed. + +## Scoped receiver recovery point — 2026-10-09 + +Capture/rehearsal source and installed runtime now bind the live scoped receiver +5d79fd1f. Actual new offsite version 1791569930973785 has ciphertext SHA256 +1a7ae09129d8252053ea210cfe37d8a27d25bc4ee615bfe85f65c207b3e5f38e. +Capture/resume took 57.47 seconds. Private retrieval to the workstation, existing +age identity decrypt and UID10001/network-none application rehearsal preserve all +six tables, including one caller quota bucket, two accepted deliveries and one +suppression. Other transactional tables remain empty in this live point. +Proof: docs/ledger-scoped-image-recovery-2026-10-09.json. + +Refreshed private recovery kit: 47,624,219 bytes, SHA256 +3ce8788375640398cd464996ea371a3d3d61a9252b2fb06a2f4192ad70f3811b, +with the current receiver image/tools and ciphertext, excluding the private age +identity. It has not been uploaded into independent durable custody. Existing +publisher readback is not a separate recovery IAM caller. Both deployments resume +Ready. EMAIL-WP-0008 is finished; T03/T04 here remain progress for independent +Scaleway custody/grants, retention/schedule and recurring operation. Timers remain +disabled. The user was asked for an existing Scaleway operator lane or Console +custody setup, since the existing publisher cannot administer escrow resources. diff --git a/docs/ledger-scoped-image-recovery-2026-10-09.json b/docs/ledger-scoped-image-recovery-2026-10-09.json new file mode 100644 index 0000000..879d6fd --- /dev/null +++ b/docs/ledger-scoped-image-recovery-2026-10-09.json @@ -0,0 +1,179 @@ +{ + "schema": "email-connect.scoped-image-backup-recovery.v1", + "publication": { + "schema": "platform.application-primary-archive.v1", + "status": "offsite_fetched_verified", + "application": "email-connect", + "captured_at": "2026-10-09T18:17:51Z", + "object_key": "platform-pg/application-archives/email-connect/2026/10/09/20261009T181751Z-7e1211057348f51569d8b9e4.tar.age", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/email-connect/2026/10/09/20261009T181751Z-7e1211057348f51569d8b9e4.tar.age", + "ciphertext_sha256": "1a7ae09129d8252053ea210cfe37d8a27d25bc4ee615bfe85f65c207b3e5f38e", + "ciphertext_bytes": 71896, + "remote_retrieval_verified": true, + "stage": "transfer_verified", + "multipart_completed": true, + "version_id": "1791569930973785", + "immutable_create": true, + "image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "database_sha256": "6694bddc3b57b5918852fbdd79d569d5bf1393831ed55aca25bb19a59e601feb", + "database_bytes": 57344, + "ledger_fingerprint": { + "schema_sha256": "13148e5a9c6e01cc593bb07278a8649b13e43b26c30b0ed1a6e15995f7b00732", + "user_version": 0, + "tables": { + "caller_buckets": { + "rows": 1, + "sha256": "0ce5a0b678fdcf55de7a4b2314a02d12f168e0f08b2a0d42d82c3e1dc514778c" + }, + "deliveries": { + "rows": 2, + "sha256": "a0387716815b2ec191187cb34bb3252b9cf51f5fb15a2019918a9cf1737c061e" + }, + "send_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "suppressions": { + "rows": 1, + "sha256": "ae26d10dfec16f1b626292b342a5e86afd1858fcaed7c29d4e68e7cf821183d7" + }, + "verification_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "verifications": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } + }, + "pause_and_capture_seconds": 57.47, + "local_archive": "20261009T181751Z-7e1211057348f51569d8b9e4.tar.age", + "application_restore_verified": false, + "completed_at": "2026-10-09T18:18:51.557894+00:00" + }, + "application_recovery": { + "status": "application_restored_verified", + "application": "email-connect", + "object_key": "platform-pg/application-archives/email-connect/2026/10/09/20261009T181751Z-7e1211057348f51569d8b9e4.tar.age", + "version_id": "1791569930973785", + "ciphertext_sha256": "1a7ae09129d8252053ea210cfe37d8a27d25bc4ee615bfe85f65c207b3e5f38e", + "database_sha256": "6694bddc3b57b5918852fbdd79d569d5bf1393831ed55aca25bb19a59e601feb", + "ledger_fingerprint": { + "schema_sha256": "13148e5a9c6e01cc593bb07278a8649b13e43b26c30b0ed1a6e15995f7b00732", + "user_version": 0, + "tables": { + "caller_buckets": { + "rows": 1, + "sha256": "0ce5a0b678fdcf55de7a4b2314a02d12f168e0f08b2a0d42d82c3e1dc514778c" + }, + "deliveries": { + "rows": 2, + "sha256": "a0387716815b2ec191187cb34bb3252b9cf51f5fb15a2019918a9cf1737c061e" + }, + "send_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "suppressions": { + "rows": 1, + "sha256": "ae26d10dfec16f1b626292b342a5e86afd1858fcaed7c29d4e68e7cf821183d7" + }, + "verification_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "verifications": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } + }, + "mail_sent": false, + "production_restored": false, + "application_restore_verified": true, + "recovery_image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "network": "disabled", + "checks": { + "accepted_verifications": 0, + "unknown_verifications": 0, + "invalidated_handles": 0, + "suppressions": 1, + "accepted_deliveries": 2, + "unknown_sends": 0 + }, + "uid": 10001, + "evidence_ceiling": "isolated_ledger_rehearsal", + "offline_image_binding_verified": false, + "elapsed_seconds": 1.61 + }, + "independent_identity_escrow_admitted": false, + "separately_scoped_recovery_caller": false, + "schedules_enabled": false, + "production_rto_measured": false, + "offline_kit_rehearsal": { + "status": "application_restored_verified", + "application": "email-connect", + "object_key": "platform-pg/application-archives/email-connect/2026/10/09/20261009T181751Z-7e1211057348f51569d8b9e4.tar.age", + "version_id": "1791569930973785", + "ciphertext_sha256": "1a7ae09129d8252053ea210cfe37d8a27d25bc4ee615bfe85f65c207b3e5f38e", + "database_sha256": "6694bddc3b57b5918852fbdd79d569d5bf1393831ed55aca25bb19a59e601feb", + "ledger_fingerprint": { + "schema_sha256": "13148e5a9c6e01cc593bb07278a8649b13e43b26c30b0ed1a6e15995f7b00732", + "user_version": 0, + "tables": { + "caller_buckets": { + "rows": 1, + "sha256": "0ce5a0b678fdcf55de7a4b2314a02d12f168e0f08b2a0d42d82c3e1dc514778c" + }, + "deliveries": { + "rows": 2, + "sha256": "a0387716815b2ec191187cb34bb3252b9cf51f5fb15a2019918a9cf1737c061e" + }, + "send_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "suppressions": { + "rows": 1, + "sha256": "ae26d10dfec16f1b626292b342a5e86afd1858fcaed7c29d4e68e7cf821183d7" + }, + "verification_attempts": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "verifications": { + "rows": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + } + }, + "mail_sent": false, + "production_restored": false, + "application_restore_verified": true, + "recovery_image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "network": "disabled", + "checks": { + "accepted_verifications": 0, + "unknown_verifications": 0, + "invalidated_handles": 0, + "suppressions": 1, + "accepted_deliveries": 2, + "unknown_sends": 0 + }, + "uid": 10001, + "evidence_ceiling": "isolated_ledger_rehearsal", + "offline_image_binding_verified": true, + "elapsed_seconds": 0.91 + }, + "recovery_kit": { + "status": "recovery_kit_prepared", + "kit_sha256": "3ce8788375640398cd464996ea371a3d3d61a9252b2fb06a2f4192ad70f3811b", + "kit_bytes": 47624219, + "image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "image_id": "sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "private_identity_included": false, + "provider_upload_verified": false, + "protected_version": "1791569930973785" + } +} diff --git a/docs/scoped-caller-live-2026-10-09.json b/docs/scoped-caller-live-2026-10-09.json new file mode 100644 index 0000000..8092fd6 --- /dev/null +++ b/docs/scoped-caller-live-2026-10-09.json @@ -0,0 +1,85 @@ +{ + "schema": "email-connect.scoped-caller-live.v1", + "date": "2026-10-09", + "status": "activated_verified", + "activation_decision_id": "8edbff8c-f62d-4357-8465-8eb37300f311", + "activation_plan_sha256": "8e529e2ffdb6ee3d53a768d65b2d025b95e788734c48a29f2f8b975825dbaed7", + "receiver_image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "user_engine_image": "forgejo.coulomb.social/coulomb/user-engine@sha256:312dcb062b237c518148e1b4f09798a4e6ad08bc60d4add6834aab3f246f2002", + "caller_delivery": { + "status": "reader_delivery_verified", + "projection_equal": true, + "credential_values_printed": false, + "kv_version": 1, + "reader_lease_seconds": 900, + "exact_reader_policy": "email-connect-scoped-caller-reader", + "readers": [ + "email-connect/email-connect-scoped-caller-eso", + "user-engine/user-engine-scoped-mail-eso" + ], + "each_reader_boundary_denials": 5, + "negative_login_denials": [ + "wrong_bound_identity", + "wrong_account", + "wrong_audience" + ], + "all_reader_self_revocations_verified": true, + "initial_writer_self_revoke_cli_failed": true, + "parent_login_session_revocation_confirmed_by_user": true + }, + "release": { + "status": "coordinated_scoped_rollout_ready", + "ledger_rows_preserved": true, + "accepted_live_deliveries_checked": 2, + "legacy_token_denied": true, + "suppression_denied": true, + "foreign_tenant_denied": true, + "payload_cap_denied": true, + "burst_denied": true, + "durable_nonquota_tables_unchanged": true, + "probe_mail_sent": false, + "pre_release_version": "1791569299633609", + "user_engine_ready": true, + "elapsed_probe_and_release_seconds": 39.58 + }, + "user_engine_environment": { + "both_transport_credentials_match_projection": true, + "binding_configuration_matches": true + }, + "canonical_receiver_server_dry_run": "passed", + "backup_source_and_runtime_pins_match_receiver": true, + "backup_timers_enabled": false, + "security_incident": { + "legacy_ingest_bearer_rendered_in_probe_error": true, + "new_caller_credential_exposed": false, + "legacy_bearer_live_denial_verified": true, + "platform_root_notice_id": "b78dfaf0-953e-410c-b9fb-0309af4c42ae", + "rollback_to_legacy_bearer_forbidden": true, + "provider_credentials_exposed": false + }, + "corrected_revocation_test": { + "actual_cli_against_local_synthetic_endpoint": "passed", + "focused_backup_notification_custody_tests_passed": 41 + }, + "diagnostics": { + "http_status": 200, + "database": "ok", + "smtp_authentication": "ok", + "provider_mail_submission": false + }, + "current_image_offsite_backup_version": "1791569930973785", + "current_image_recovery_evidence": "docs/ledger-scoped-image-recovery-2026-10-09.json", + "recovery_kit": { + "status": "recovery_kit_prepared", + "kit_sha256": "3ce8788375640398cd464996ea371a3d3d61a9252b2fb06a2f4192ad70f3811b", + "kit_bytes": 47624219, + "image": "forgejo.coulomb.social/coulomb/email-connect@sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "image_id": "sha256:5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d", + "private_identity_included": false, + "provider_upload_verified": false, + "protected_version": "1791569930973785" + }, + "current_plan_sha256": "5a847f4e5f05557d6740f8c62d3891b13854540eb6835d9cd9cca3897a2abcda", + "offline_kit_rehearsal_verified": true, + "offline_kit_application_rehearsal_seconds": 0.91 +} diff --git a/scripts/admit-scoped-caller.py b/scripts/admit-scoped-caller.py index 95ad282..d38220b 100644 --- a/scripts/admit-scoped-caller.py +++ b/scripts/admit-scoped-caller.py @@ -26,6 +26,20 @@ READERS = [ ("email-connect", "email-connect-scoped-caller-eso"), ("user-engine", "user-engine-scoped-mail-eso"), ] +FAILURE_CODES = frozenset({ + "attended_bao_operation_failed", "reviewed_plan_hash_required", + "reviewed_artifact_changed", "founder_exact_plan_decision_required", + "exact_caller_contract_required", "existing_policy_differs", + "policy_readback_differs", "bounded_writer_required", "initial_cas_write_required", + "caller_record_changed", "caller_credential_invalid", "caller_readback_differs", + "existing_reader_role_differs", "private_receipt_path_required", "fresh_receipt_required", +}) + + +def failure_receipt(error): + code = str(error) if isinstance(error, Refused) else None + return {"status": "failed", "failure_type": type(error).__name__, + "failure_code": code if code in FAILURE_CODES else "contained_operation_failed"} def role(namespace, account): @@ -123,7 +137,7 @@ def provision(transport=bao): return {"kv_version": version, "created": created, "readback_equal": True, "credential_values_printed": False} finally: - transport(["write", "auth/token/revoke-self"], token=token) + transport(["token", "revoke", "-self"], token=token) def execute(plan, reviewed, decision, base): @@ -170,7 +184,7 @@ def main(): try: receipt = execute(plan, args.reviewed_sha256, args.decision_id, args.api_base) except Exception as error: - checkpoint(args.receipt, {"status": "failed", "failure_type": type(error).__name__}) + checkpoint(args.receipt, failure_receipt(error)) raise checkpoint(args.receipt, receipt) diff --git a/scripts/capture-ledger.py b/scripts/capture-ledger.py index 784d3cb..153458d 100644 --- a/scripts/capture-ledger.py +++ b/scripts/capture-ledger.py @@ -20,7 +20,7 @@ NS = "email-connect" CLUSTER_UID = "a553c742-0115-43d4-99a4-a5ca56fe0786" IMAGE = ( "forgejo.coulomb.social/coulomb/email-connect@sha256:" - "eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2" + "5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d" ) LOCK_PATH = Path("/var/lib/email-connect-backup/capture.lock") SELECTOR = "app.kubernetes.io/name=email-connect" diff --git a/scripts/rehearse-ledger.py b/scripts/rehearse-ledger.py index 83c20c6..910f509 100644 --- a/scripts/rehearse-ledger.py +++ b/scripts/rehearse-ledger.py @@ -22,7 +22,7 @@ from ledger_archive import ( IMAGE = ( "forgejo.coulomb.social/coulomb/email-connect@sha256:" - "eb3e7b68d21209e9eb71b928a5b0bc987e8e3164f21dca4ff712f946d0c103f2" + "5d79fd1fec15d3a279383820f170d263de3a55f5308df01a0a34732ebaea6d3d" ) CODE = """import io,json,os,sys diff --git a/tests/test_closure_custody.py b/tests/test_closure_custody.py index b517c0a..e6a7398 100644 --- a/tests/test_closure_custody.py +++ b/tests/test_closure_custody.py @@ -29,7 +29,7 @@ def test_caller_writer_uses_cas0_and_revokes_on_every_outcome(monkeypatch, failu if args[0] == "token": return json.dumps({"auth": {"client_token": "synthetic-writer", "policies": [caller.WRITER], "lease_duration": 300}}).encode() - if "auth/token/revoke-self" in args: + if args == ["token", "revoke", "-self"]: return b"{}" if "platform/metadata/workloads/email-connect/callers/user-engine" in args: return None @@ -48,7 +48,7 @@ def test_caller_writer_uses_cas0_and_revokes_on_every_outcome(monkeypatch, failu receipt = caller.provision(transport) assert receipt["kv_version"] == 1 and receipt["readback_equal"] assert value not in json.dumps(receipt) - assert calls[-1][0] == ["write", "auth/token/revoke-self"] + assert calls[-1][0] == ["token", "revoke", "-self"] assert calls[-1][2] == "synthetic-writer" @@ -64,7 +64,7 @@ def test_caller_writer_rejects_inherited_authority_before_any_secret_access(monk return b"{}" with pytest.raises(caller.Refused, match="bounded_writer_required"): caller.provision(transport) - assert len(seen) == 2 and seen[-1] == ["write", "auth/token/revoke-self"] + assert len(seen) == 2 and seen[-1] == ["token", "revoke", "-self"] def test_unreviewed_plan_refuses_before_decision_network_call(monkeypatch): @@ -74,6 +74,53 @@ def test_unreviewed_plan_refuses_before_decision_network_call(monkeypatch): caller.approved_plan({}, "unreviewed", None, "http://synthetic.invalid") +def test_caller_self_revocation_reaches_actual_bao_cli_endpoint(monkeypatch): + import shutil + from http.server import BaseHTTPRequestHandler, HTTPServer + from threading import Thread + + if not shutil.which("bao"): + pytest.skip("OpenBao CLI integration requires installed bao") + caller = module("admit-scoped-caller", monkeypatch) + seen = [] + + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + seen.append((self.path, self.headers.get("X-Vault-Token"))) + self.send_response(204) + self.end_headers() + + def do_PUT(self): + self.do_POST() + + def log_message(self, *args): + pass + + server = HTTPServer(("127.0.0.1", 0), Handler) + worker = Thread(target=server.serve_forever, daemon=True) + worker.start() + monkeypatch.setenv("BAO_ADDR", f"http://127.0.0.1:{server.server_port}") + monkeypatch.setenv("VAULT_ADDR", f"http://127.0.0.1:{server.server_port}") + monkeypatch.delenv("BAO_NAMESPACE", raising=False) + monkeypatch.delenv("VAULT_NAMESPACE", raising=False) + try: + caller.bao(["token", "revoke", "-self"], token="synthetic-only-writer") + finally: + server.shutdown() + server.server_close() + worker.join(timeout=2) + assert seen == [("/v1/auth/token/revoke-self", "synthetic-only-writer")] + + +def test_caller_failure_receipt_allows_only_static_diagnostic_codes(monkeypatch): + caller = module("admit-scoped-caller", monkeypatch) + assert caller.failure_receipt(caller.Refused("caller_readback_differs"))["failure_code"] == "caller_readback_differs" + for error in [caller.Refused("synthetic-sensitive-value"), ValueError("synthetic-sensitive-value")]: + receipt = caller.failure_receipt(error) + assert receipt["failure_code"] == "contained_operation_failed" + assert "synthetic-sensitive-value" not in json.dumps(receipt) + + @pytest.mark.parametrize("protected,ephemeral,corrupt", [ (True, None, False), (False, None, False), (True, {}, False), (True, None, True), ]) diff --git a/workplans/EMAIL-WP-0007-ledger-backup-recovery.md b/workplans/EMAIL-WP-0007-ledger-backup-recovery.md index 538c899..6d714da 100644 --- a/workplans/EMAIL-WP-0007-ledger-backup-recovery.md +++ b/workplans/EMAIL-WP-0007-ledger-backup-recovery.md @@ -236,3 +236,24 @@ Actual pre-capture capacity check passes: retained local files 148,994 bytes, Receiver image/pins remain eb3e7b68 and both timers remain disabled. Receipt: deploy/backup/runtime-update-2026-10-09.json. Earlier preparation evidence retains its historical statement that node helpers had not yet changed. + +## Scoped receiver recovery point — 2026-10-09 + +Capture/rehearsal source and installed runtime now bind the live scoped receiver +5d79fd1f. Actual new offsite version 1791569930973785 has ciphertext SHA256 +1a7ae09129d8252053ea210cfe37d8a27d25bc4ee615bfe85f65c207b3e5f38e. +Capture/resume took 57.47 seconds. Private retrieval to the workstation, existing +age identity decrypt and UID10001/network-none application rehearsal preserve all +six tables, including one caller quota bucket, two accepted deliveries and one +suppression. Other transactional tables remain empty in this live point. +Proof: docs/ledger-scoped-image-recovery-2026-10-09.json. + +Refreshed private recovery kit: 47,624,219 bytes, SHA256 +3ce8788375640398cd464996ea371a3d3d61a9252b2fb06a2f4192ad70f3811b, +with the current receiver image/tools and ciphertext, excluding the private age +identity. It has not been uploaded into independent durable custody. Existing +publisher readback is not a separate recovery IAM caller. Both deployments resume +Ready. EMAIL-WP-0008 is finished; T03/T04 here remain progress for independent +Scaleway custody/grants, retention/schedule and recurring operation. Timers remain +disabled. The user was asked for an existing Scaleway operator lane or Console +custody setup, since the existing publisher cannot administer escrow resources. diff --git a/workplans/EMAIL-WP-0008-scoped-transactional-callers.md b/workplans/EMAIL-WP-0008-scoped-transactional-callers.md index edede1f..2cceba2 100644 --- a/workplans/EMAIL-WP-0008-scoped-transactional-callers.md +++ b/workplans/EMAIL-WP-0008-scoped-transactional-callers.md @@ -4,7 +4,7 @@ type: workplan title: "Isolate explicitly admitted transactional mail callers" domain: infotech repo: email-connect -status: active +status: finished owner: codex topic_slug: netkingdom created: "2026-10-08" @@ -16,7 +16,8 @@ state_hub_workstream_id: "3a0f264d-e441-5b12-964e-dab8fe5b3828" CSOC-WP-0010-T04 needs independent caller credentials, bounded template/tenant permissions, durable limits and scoped diagnostics. This record owns receiver -source and its admission; current live single-caller SMTP remains unchanged. +source and its admission. Scoped original-caller activation is complete; +productive journey acceptance remains in CSOC-WP-0015. ## T01 — Implement explicit caller policies and durable isolation @@ -57,10 +58,9 @@ receiver and caller module/tests. T03 remains waiting for runtime admission. ```task id: EMAIL-WP-0008-T03 -status: progress +status: done priority: high -needs_human: true -intervention_note: "User approved compatible rollout preparation, proposed limits and pattern support. Glob allow-list, legacy invitation adoption, user-engine transport, candidate image and manifests are prepared. Separate token custody and coordinated runtime activation remain open; productive sender/reply/bounce acceptance stays in CSOC-WP-0015." +needs_human: false state_hub_task_id: "9b4c7260-a6bf-54f9-82f8-2e98c368d294" ``` @@ -180,3 +180,56 @@ OpenBao edge is initialized/unsealed. The private receipt directory exists and the exact silent attended command was supplied to the user. No completed login, credential issue or native reader proof is claimed; awaiting the actual receipt. Current receiver/user-engine deployments remain Ready at their baseline images. + +## Scoped activation complete — 2026-10-09 + +EMAIL-WP-0008 now runs the published scoped receiver and compatible user-engine +images, with one Ready replica each. Decision +8edbff8c-f62d-4357-8465-8eb37300f311 admitted continuation of the reviewed package. +The attended child installed both exact policies/roles and created/read back +caller KV version 1, then failed because its writer self-revocation CLI command +was invalid. User confirmed parent login revocation. Corrected to `bao token +revoke -self`; a real CLI test against a synthetic local HTTP endpoint verifies +that call. Failure receipts now retain only allow-listed diagnostic codes. +No second administrator session was used. + +Both approved ESO readers deliver equal credentials. Exact policy/900-second +bounds and own-leaf read pass; sibling data, metadata, parent list, administrative +policy read and a non-mutating CAS0 write to the existing leaf deny. Wrong bound +identity, account and audience logins deny. Both proof tokens self-revoke and +subsequent lookup denies. User-engine's invitation/registration credentials match +its own projection and its binding JSON matches the reviewed exact/glob pairs. + +Quiesced pre-release offsite capture version 1791569299633609 precedes coordinated +activation. A probe initially rejected a newline in the retired legacy bearer; +Python rendered that bearer in its error. Automatic rollback restored baseline, +then dispatch was fenced and scoped mode restored immediately. The corrected +contained probe strips/rejects invalid header characters. The live receiver now +returns 401 for the exposed legacy bearer. New caller/provider credentials were +not exposed. Platform-root notice: b78dfaf0-953e-410c-b9fb-0309af4c42ae. Never restore +the retired bearer or the old single-caller rollback spec. Its custody record was +not deleted; retirement is enforced by the active scoped policy and canonical +receiver manifests. Productive provider traffic remains separately owned. + +Live no-send probes verify forbidden suppression, foreign tenant, 8192-byte cap, +burst five and preservation of both existing accepted deliveries. Non-quota +ledger tables remain unchanged; SMTP authentication/database diagnostics pass. +The synthetic recovery/package tests retain unknown-outcome, consumed/cancelled +handle and legacy adoption coverage; those live state tables were empty. + +Canonical receiver kustomization now retains the scoped image, immutable policy +and exact receiver-side ESO projection and passes server dry-run. The user-engine +activation patch remains deploy/closure/user-engine.patch.json. Capture/rehearsal +pins are changed together in source and installed runtime to the actual scoped +image. Fresh offsite version 1791569930973785 and isolated UID10001/network-none +recovery preserve all six tables, including the populated caller quota bucket. +A refreshed private kit bundles the image/tools/age ciphertext/public receipts +and excludes the private age identity. It remains local, not independent escrow. + +Public proofs: docs/scoped-caller-live-2026-10-09.json and + docs/ledger-scoped-image-recovery-2026-10-09.json. Focused backup/notification/ +custody regressions: 41 passed; focused syntax checks pass. Earlier full email +regression remains 193 passed, zero skips. EMAIL-WP-0008-T03 is done. Independent +Scaleway custody, generation retention and recurring backup activation remain +EMAIL-WP-0007; backup timers stay disabled. Productive/browser sender/reply/bounce +acceptance remains CSOC-WP-0015.