From 8434134391f2b7715fbeefd965edbd4bcd588910 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 01:33:51 +0100 Subject: [PATCH 01/16] Record founder-verified Coulomb storage release revision 18 Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- .../2026-10-31-coulomb-storage-release.json | 68 +++++++++++++++++++ helm/coulomb-social-values.yaml | 4 +- 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 docs/evidence/2026-10-31-coulomb-storage-release.json diff --git a/docs/evidence/2026-10-31-coulomb-storage-release.json b/docs/evidence/2026-10-31-coulomb-storage-release.json new file mode 100644 index 0000000..ee3265a --- /dev/null +++ b/docs/evidence/2026-10-31-coulomb-storage-release.json @@ -0,0 +1,68 @@ +{ + "completed_at": "2026-10-31T00:29:28.113460+00:00", + "elapsed_seconds": 99.527, + "fresh_paired_point": { + "age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "application": "coulomb-social", + "application_restore_verified": false, + "captured_at": "2026-10-31T00:27:56Z", + "ciphertext_bytes": 2181848, + "ciphertext_sha256": "f7be502c7e122283e70b10d2fd8a2cbf9550d2261192ba11f94d945a9f39af51", + "completed_at": "2026-10-31T00:28:53.504056+00:00", + "content_sha256": "22b694a56178a70274df55bdfc60f2cc0fe24ec66563c2e952058e5d8cf2154a", + "database_sha256": "99e704023d0832aa29dcfd7695dd436dea58038ae48e3ab142d0ec2b916eb574", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T002756Z-c31b84b48b3874bdaa87cbf5.tar.age", + "encrypted_bytes": 2181848, + "encrypted_sha256": "f7be502c7e122283e70b10d2fd8a2cbf9550d2261192ba11f94d945a9f39af51", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:9a646d8e01a61ba5316d3c2eb0273064e17f2d0300ad07af766ccedda65ecb18", + "immutable_create": true, + "local_archive": "20261031T002756Z-c31b84b48b3874bdaa87cbf5.tar.age", + "object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T002756Z-c31b84b48b3874bdaa87cbf5.tar.age", + "pause_and_capture_seconds": 56.63, + "remote_retrieval_verified": true, + "schema": "platform.application-primary-archive.v1", + "status": "offsite_fetched_verified", + "version_id": "1793406532815253" + }, + "global_constraint_applied": true, + "helm_revision": 18, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "pid": 2724118, + "postflight": { + "actual_owner_pages_rendered": 40, + "business_rows": 92, + "business_rows_sha256": "b65c4a32d554d741569df5622c09f31423f6c18aa1c4edc5154c4a95e3511508", + "business_tables": 12, + "content_files": 86, + "content_files_sha256": "fd9ea2fa6890ad960e45dff308713ccd1aa303c502dd0ceb0cc26908326216a4", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "production_identity_or_grant_created": false, + "tenant_slug_constraint": true, + "unsaved_outsider_page_denials": 40 + }, + "preflight": { + "business_rows": 92, + "business_rows_sha256": "b65c4a32d554d741569df5622c09f31423f6c18aa1c4edc5154c4a95e3511508", + "business_tables": 12, + "content_files": 86, + "content_files_sha256": "fd9ea2fa6890ad960e45dff308713ccd1aa303c502dd0ceb0cc26908326216a4", + "duplicate_slug_groups": 0, + "global_slug_constraint": false, + "migration_0003_applied": false, + "tenant_slug_constraint": true + }, + "private_values_printed": false, + "production_data_restored": false, + "proposal_digest": "sha256:5fd7ccd8f68057346367d81efb82b48280a16dd31d87683a5bd83e60e3d1837e", + "public_health_landing_static_verified": true, + "schema": "coulomb.guarded-storage-release.v1", + "snapshot_pin_refreshed": true, + "started_at": "2026-10-31T00:27:48.578472+00:00", + "status": "storage_candidate_deployed_verified", + "timers_activated": false +} diff --git a/helm/coulomb-social-values.yaml b/helm/coulomb-social-values.yaml index bc34981..48b243c 100644 --- a/helm/coulomb-social-values.yaml +++ b/helm/coulomb-social-values.yaml @@ -3,8 +3,8 @@ image: repository: forgejo.coulomb.social/coulomb/coulomb-social - tag: "dd83c90" - digest: "sha256:9a646d8e01a61ba5316d3c2eb0273064e17f2d0300ad07af766ccedda65ecb18" + tag: "17d0eb6" + digest: "sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd" # Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). # Apex cutover is deferred until data + self-registration are ready. From 84a5aa0c9281bf2ef6b8e8f734998731cea9025b Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 04:47:32 +0100 Subject: [PATCH 02/16] Prepare supervised Coulomb identity and export image release Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- WORK-RECORDS.md | 1 + docs/coulomb-image-release-2026-10-31.md | 38 ++ ...10-31-coulomb-image-release-preflight.json | 25 + release/coulomb-identity-export-23ffe2c.json | 51 ++ ...ity-export-candidate-build-2026-10-31.json | 95 ++++ ...xport-candidate-provenance-2026-10-31.json | 20 + ...-export-candidate-recovery-2026-10-31.json | 63 +++ ...ity-export-candidate-tests-2026-10-31.json | 17 + release/identity-export-snapshot.py | 175 ++++++ release/identity-export-values.yaml | 50 ++ tests/test_coulomb_image_release.py | 194 +++++++ tools/coulomb-image-release.py | 185 ++++++ tools/coulomb_image_probe.py | 134 +++++ tools/coulomb_image_release_node.py | 525 ++++++++++++++++++ ...P-0015-coulomb-personal-content-release.md | 44 +- 15 files changed, 1616 insertions(+), 1 deletion(-) create mode 100644 docs/coulomb-image-release-2026-10-31.md create mode 100644 docs/evidence/2026-10-31-coulomb-image-release-preflight.json create mode 100644 release/coulomb-identity-export-23ffe2c.json create mode 100644 release/identity-export-evidence/identity-export-candidate-build-2026-10-31.json create mode 100644 release/identity-export-evidence/identity-export-candidate-provenance-2026-10-31.json create mode 100644 release/identity-export-evidence/identity-export-candidate-recovery-2026-10-31.json create mode 100644 release/identity-export-evidence/identity-export-candidate-tests-2026-10-31.json create mode 100644 release/identity-export-snapshot.py create mode 100644 release/identity-export-values.yaml create mode 100644 tests/test_coulomb_image_release.py create mode 100644 tools/coulomb-image-release.py create mode 100644 tools/coulomb_image_probe.py create mode 100644 tools/coulomb_image_release_node.py diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 54ea2a7..dc3c219 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -41,6 +41,7 @@ | task | RAPPS-WP-0015-T02 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T03 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T05 | progress | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/docs/coulomb-image-release-2026-10-31.md b/docs/coulomb-image-release-2026-10-31.md new file mode 100644 index 0000000..2a16234 --- /dev/null +++ b/docs/coulomb-image-release-2026-10-31.md @@ -0,0 +1,38 @@ +# Prepared Coulomb identity/export image release + +The exact supervised proposal is +`sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f`. +Open decision: `75b94bb6-4709-495f-91fe-43e53e2867ed`. +The full founder packet is +[coulomb-social release review](/home/worsch/coulomb-social/docs/release/identity-export-release-review-2026-10-31.md). + +`tools/coulomb-image-release.py` prepares the capsule by default. `--preflight` +checks the live target without capture or deployment. `--execute --expect-digest` +requires the founder's own interactive terminal and the exact typed proposal. +The node controller freezes verified source bytes into its private durable +journal before execution. An existing attempt refuses blind replay. + +This distinct controller adapts the previously reviewed storage controller +from owner source 45d6178. It requires migration 0003 and the global constraint +already present, adds exact migration-history equality, verifies installed +helpers during preflight, verifies the desired complete spec through an image-only +server-dry-run patch and verifies rollback readiness/specification. Unknown +helper changes refuse overwrite. Historical storage commands/capsules remain +unchanged and must not be replayed. + +Candidate values live in `release/identity-export-values.yaml`; the current +`helm/coulomb-social-values.yaml` remains pinned to the actual revision 18 image. +The new snapshot-helper bytes are separate review material until execution. +No unit/timer, sender, grant, apex or data-restore operation is included. + +Validation: 19 release safety cases pass; Ruff, Python compile and Helm lint pass. +The sealed native preflight is retained in +`docs/evidence/2026-10-31-coulomb-image-release-preflight.json`. +Four hash-bound candidate evidence files travel inside the capsule. The product +review names source 441 tests, actual packaged 25 tests, all 80 packaged source matches +and the retained isolated recovery/export/view checks with their limits. + +After founder execution, inspect the durable receipt and independent current +state before reconciling the live values and snapshot pin. The product then +mirrors/retrieves the newly captured point and verifies packaged export on that +coherent post-correction data. Source publication alone does not finish T05. diff --git a/docs/evidence/2026-10-31-coulomb-image-release-preflight.json b/docs/evidence/2026-10-31-coulomb-image-release-preflight.json new file mode 100644 index 0000000..0402fbf --- /dev/null +++ b/docs/evidence/2026-10-31-coulomb-image-release-preflight.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.guarded-image-release.v1", + "proposal_digest": "sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f", + "started_at": "2026-10-31T03:41:35.362323+00:00", + "status": "live_release_preflight_verified", + "production_data_restored": false, + "timers_activated": false, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "preflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "live_changes": false +} \ No newline at end of file diff --git a/release/coulomb-identity-export-23ffe2c.json b/release/coulomb-identity-export-23ffe2c.json new file mode 100644 index 0000000..c89ca3d --- /dev/null +++ b/release/coulomb-identity-export-23ffe2c.json @@ -0,0 +1,51 @@ +{ + "schema": "coulomb.reviewed-image-release.v1", + "proposal_id": "CSOC-IDENTITY-EXPORT-20261031", + "scope": "ADMINISTER @ realm:kubernetes/railiance01; supervised founder execution only", + "operation": "image_only_existing_migration_0003", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "baseline_revision": 18, + "baseline_helm_manifest_sha256": "1f74302e7be7211b3ae8728f58ef6b8ec243ffa342306b25bbf42dd6f27b5ef7", + "candidate_render_sha256": "98ffff162a17a9ff4b3922364d002ddbc571f9c79af95191c84d7c0b47c9a16f", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_spec_sha256": "013aca79ec910b52ed2c2df7a18050c436751005320d1dec616590962da30b8e", + "candidate_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "product_source": "23ffe2cf4b214666754a2420558943438e54251a", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "public_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "installed_helper_hashes": { + "/opt/coulomb-social-backup/paired-backup.py": "43e119acb6a4933b4e144c349a091e074e1a91ab61dbe15a65c8264c1d47661a", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "4ff81b791daf6c8b976a4f89ef152e52f385fc1656986e2ea28bf555b9bd0d8a", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d" + }, + "clipboard_static_sha256": "347eabe0c8d93bf439df02b5a08a7ea71004600e6ff4e8a42cecbee52a9f0413", + "fresh_backup_before_helm": true, + "requires_dataset_equality_before_and_after": true, + "node_request_percent_limit": 90, + "helm_atomic_timeout_seconds": 300, + "rollback": "Restore Helm revision18/image and verified snapshot pin; preserve all post-capture data. No production data restore or schema downgrade.", + "approval_status": "prepared_not_admitted", + "recovery_rehearsal_scope": "retained October30 point; fresh post-correction primary backup/readback occurs before Helm; independent retrieval/export of that new point remains subsequent consumer acceptance", + "rehearsal_evidence_sha256": { + "identity-export-candidate-build-2026-10-31.json": "12e8709529f27323274b40c68bd701c39e1cd2aa7428f42e061d8b5c2d410176", + "identity-export-candidate-provenance-2026-10-31.json": "71a8e1b2e60d0da6948fb74d9d1be8a8b892cef4be0d6dd0b98fa45d8983551c", + "identity-export-candidate-recovery-2026-10-31.json": "ccfe90ff68ecc80c02ef673a1d1efd21291e7c829c1dc17dc8bd0c902b9cb8b1", + "identity-export-candidate-tests-2026-10-31.json": "bd9d7535f42c7fe79752b46672f8da67492baf0dd06893b8a3288ac9eb3edaae" + }, + "new_resources_committed": 0, + "autopilot_or_eur_budget_claimed": false +} diff --git a/release/identity-export-evidence/identity-export-candidate-build-2026-10-31.json b/release/identity-export-evidence/identity-export-candidate-build-2026-10-31.json new file mode 100644 index 0000000..51aa01b --- /dev/null +++ b/release/identity-export-evidence/identity-export-candidate-build-2026-10-31.json @@ -0,0 +1,95 @@ +{ + "schema": "coulomb.identity-export-candidate-build.v1", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "tag": "forgejo.coulomb.social/coulomb/coulomb-social:23ffe2cf4b21", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "registry_digest_independently_verified": true, + "source_revision_label_verified": true, + "build_context": "git archive of exact source; Dockerfile, dockerignore, lock, project, manage.py, app and entrypoint only", + "build_inputs": { + "Dockerfile": "d0e197a18db8980eb3e51cca842d693e10ab0b4208627bba28691d10719d4a00", + ".dockerignore": "901c8b95d95549d0e78c1c3d6d0538bf0252c8a77e9409129e0cadfa55c7c56c", + "pyproject.toml": "5758c56fd1be89b973861ccc5ecbadb495e742ee13ebd7e19a221a0f119c54ee", + "uv.lock": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887", + "scripts/docker-entrypoint.sh": "70100256ca41ae6afe77078c810a8b6012472f0a741919f403f32088f98ab463" + }, + "runtime": { + "live": { + "python": "3.12.16", + "distributions": { + "anyio": "4.14.2", + "asgiref": "3.12.1", + "authlib": "1.7.2", + "bleach": "6.4.0", + "certifi": "2026.7.22", + "cffi": "2.1.1", + "cryptography": "50.0.0", + "dj-database-url": "3.1.2", + "django": "6.1", + "gunicorn": "26.0.0", + "h11": "0.16.0", + "httpcore": "1.0.9", + "httpx": "0.28.1", + "idna": "3.18", + "joserfc": "1.7.4", + "markdown": "3.10.3", + "packaging": "26.3", + "psycopg": "3.3.4", + "psycopg-binary": "3.3.4", + "pycparser": "3.0", + "python-decouple": "3.8", + "sqlparse": "0.5.5", + "typing_extensions": "4.16.0", + "webencodings": "0.5.1", + "whitenoise": "6.12.0" + } + }, + "candidate": { + "python": "3.12.16", + "distributions": { + "anyio": "4.14.2", + "asgiref": "3.12.1", + "authlib": "1.7.2", + "bleach": "6.4.0", + "certifi": "2026.7.22", + "cffi": "2.1.1", + "cryptography": "50.0.0", + "dj-database-url": "3.1.2", + "django": "6.1", + "gunicorn": "26.0.0", + "h11": "0.16.0", + "httpcore": "1.0.9", + "httpx": "0.28.1", + "idna": "3.18", + "joserfc": "1.7.4", + "markdown": "3.10.3", + "packaging": "26.3", + "psycopg": "3.3.4", + "psycopg-binary": "3.3.4", + "pycparser": "3.0", + "python-decouple": "3.8", + "sqlparse": "0.5.5", + "typing_extensions": "4.16.0", + "webencodings": "0.5.1", + "whitenoise": "6.12.0" + } + }, + "runtime_packages_equal": true, + "python_versions_equal": true + }, + "production_deployed": false, + "live_release_pins_modified": false, + "new_database_migrations": false, + "not_an_offboarding_or_transfer_acceptance": true, + "source_manifest_sha256": "a90b0217c6424a89e166a7df514fe4a86c9643b502ecdc9ebf0b64b76c06b739", + "packaged_django_check": "passed", + "packaged_migration_drift_check": "passed_no_changes_detected", + "source_gate": { + "command": "make check", + "tests": 441, + "failures": 0, + "errors": 0, + "skipped": 0, + "junit_sha256": "1e5b3c63d85a23f176420f2004831b59406d294c1528b3be80d49669edbb116f" + } +} diff --git a/release/identity-export-evidence/identity-export-candidate-provenance-2026-10-31.json b/release/identity-export-evidence/identity-export-candidate-provenance-2026-10-31.json new file mode 100644 index 0000000..2b769b0 --- /dev/null +++ b/release/identity-export-evidence/identity-export-candidate-provenance-2026-10-31.json @@ -0,0 +1,20 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "network": "none", + "read_only_root": true, + "entrypoint_bypassed": true, + "database_contact": false, + "production_changes": false, + "comparisons": [ + { + "commit": "23ffe2cf4b214666754a2420558943438e54251a", + "packaged_file_count": 80, + "source_file_count": 80, + "different_files": 0, + "all_packaged_source_bytes_match": true + } + ], + "actual_packaged_manifest_sha256": "6b5a7b339d88f3c5eafb70a33aa28a7d3972988645b246ae1a92a3f159d72528", + "dependency_or_build_reproducibility_proved": false +} diff --git a/release/identity-export-evidence/identity-export-candidate-recovery-2026-10-31.json b/release/identity-export-evidence/identity-export-candidate-recovery-2026-10-31.json new file mode 100644 index 0000000..6714caf --- /dev/null +++ b/release/identity-export-evidence/identity-export-candidate-recovery-2026-10-31.json @@ -0,0 +1,63 @@ +{ + "schema": "coulomb.packaged-identity-export-recovery.v1", + "status": "packaged_recovered_client_export_and_views_verified", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "image_id": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "restore_receipt_sha256": "b648348780f87f81094b40876a6b0ebabaefff7986cf20fb426c3d7456a0bc84", + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "8efa82e7c29c95840d61d1290e20cac4b8acf5ae33d2c5b632382a7f6b779c37", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "candidate_application": { + "status": "isolated_application_recovery_verified", + "recovered_spaces_with_content": 1, + "actual_pages_rendered_through_view": 40, + "restored_owner_access_verified": true, + "synthetic_viewer_reads": 1, + "synthetic_viewer_mutation_denials": 1, + "same_tenant_ungranted_page_denials": 40, + "fixture_transaction_rolled_back": true, + "production_access": false, + "external_identity_provider_used": false, + "real_browser_journey_verified": false + }, + "previous_image_application": { + "status": "isolated_application_recovery_verified", + "recovered_spaces_with_content": 1, + "actual_pages_rendered_through_view": 40, + "restored_owner_access_verified": true, + "synthetic_viewer_reads": 1, + "synthetic_viewer_mutation_denials": 1, + "same_tenant_ungranted_page_denials": 40, + "fixture_transaction_rolled_back": true, + "production_access": false, + "external_identity_provider_used": false, + "real_browser_journey_verified": false + }, + "source_content_files_unchanged": 86, + "application_source_overlay": false, + "export_archive_removed": true, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "post_date_correction_data_claimed": false, + "driver_sha256": "9c370b6e7e1acb442cb660cc3709e3ffd3676c87419a217a7656174c8a347f95" +} diff --git a/release/identity-export-evidence/identity-export-candidate-tests-2026-10-31.json b/release/identity-export-evidence/identity-export-candidate-tests-2026-10-31.json new file mode 100644 index 0000000..bda3298 --- /dev/null +++ b/release/identity-export-evidence/identity-export-candidate-tests-2026-10-31.json @@ -0,0 +1,17 @@ +{ + "schema": "coulomb.packaged-identity-export-tests.v1", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "image_id": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "tests": 25, + "failures": 0, + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "test_tooling_injected": true, + "application_modules_from_image_verified": true, + "production_access": false, + "junit_sha256": "85aac22d33a101d68e478f41f61bc3c7e7adb4f3d7f893a2810f7eb25fce5a39", + "driver_sha256": "a19f94f3dbc71dfd80a2d380f03e5f737c7453b64df022b6e5c2ede8141976b3" +} diff --git a/release/identity-export-snapshot.py b/release/identity-export-snapshot.py new file mode 100644 index 0000000..a928c1b --- /dev/null +++ b/release/identity-export-snapshot.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Node-side quiesced coulomb-social snapshot. Binary output stays private. + +Run on railiance01 after reviewed live approval. Does not restore production. +Default is server dry-run; --execute briefly pauses the app and captures a +paired pg_dump/content archive. A separate isolated restore must validate it. +""" +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import signal +import stat +import subprocess +import sys +import time +from pathlib import Path + +NS = 'coulomb-social' +IMAGE = ('forgejo.coulomb.social/coulomb/coulomb-social@sha256:' + '522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed') +CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' +LOCK_PATH = Path('/var/lib/coulomb-social-backup/snapshot.lock') + + +class Refused(RuntimeError): + pass + + +def run(args: list[str], *, payload: bytes | None = None, timeout: int = 180) -> bytes: + p = subprocess.run(args, input=payload, capture_output=True, timeout=timeout) + if p.returncode: + raise Refused('command_failed') + return p.stdout + + +def k(*args: str, payload: bytes | None = None) -> bytes: + return run(['kubectl', *args], payload=payload) + + +def document(*args: str) -> dict: + return json.loads(k(*args, '-o', 'json')) + + +def private_write(path: Path, value: bytes) -> None: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, 'wb') as f: + f.write(value) + + +def snapshot() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--execute', action='store_true') + parser.add_argument('--destination', type=Path, required=True) + args = parser.parse_args() + os.environ['KUBECONFIG'] = '/etc/rancher/k3s/k3s.yaml' + started = time.monotonic() + if document('get', 'namespace', 'kube-system')['metadata']['uid'] != CLUSTER_UID: + raise Refused('wrong_cluster') + dep = document('get', 'deployment', 'coulomb-social', '-n', NS) + spec = dep['spec']['template']['spec'] + if dep['spec']['replicas'] != 1 or spec['containers'][0]['image'] != IMAGE: + raise Refused('unexpected_release') + if document('get', 'pvc', 'coulomb-social-content', '-n', NS)['status']['phase'] != 'Bound': + raise Refused('pvc_not_bound') + cluster = document('get', 'cluster', 'apps-pg', '-n', 'databases') + primary = cluster['status']['currentPrimary'] + name = 'csoc-paired-snapshot-' + time.strftime('%Y%m%d%H%M%S', time.gmtime()) + pod = { + 'apiVersion': 'v1', 'kind': 'Pod', + 'metadata': {'name': name, 'namespace': NS, 'labels': {'csoc-recovery': name}}, + 'spec': { + 'restartPolicy': 'Never', 'automountServiceAccountToken': False, + 'securityContext': {'runAsNonRoot': True, 'runAsUser': 10001, + 'runAsGroup': 10001, 'fsGroup': 10001, + 'fsGroupChangePolicy': 'OnRootMismatch'}, + 'containers': [{'name': 'snapshot', 'image': IMAGE, + 'command': ['python', '-c', 'import time;time.sleep(1800)'], + 'resources': {'requests': {'cpu': '10m', 'memory': '64Mi'}, + 'limits': {'cpu': '100m', 'memory': '256Mi'}}, + 'securityContext': {'allowPrivilegeEscalation': False, + 'readOnlyRootFilesystem': True, + 'capabilities': {'drop': ['ALL']}}, + 'volumeMounts': [{'name': 'content', 'mountPath': '/snapshot', 'readOnly': True}]}], + 'volumes': [{'name': 'content', 'persistentVolumeClaim': + {'claimName': 'coulomb-social-content', 'readOnly': True}}], + }, + } + body = json.dumps(pod).encode() + k('create', '--dry-run=server', '-f', '-', payload=body) + if not args.execute: + print(json.dumps({'status': 'reviewed', 'server_dry_run': True, + 'image_pinned': True, 'pvc_read_only': True, + 'db_scope': 'coulomb_social_db', 'replicas_restored_to': 1})) + return 0 + dest = args.destination + if not dest.is_absolute() or dest.exists(): + raise Refused('destination_must_be_new_absolute_directory') + dest.mkdir(mode=0o700) + paused = False + created = False + receipt = {'status': 'incomplete', 'app': 'coulomb-social', + 'database': 'coulomb_social_db', 'image': IMAGE, + 'started_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} + try: + paused = True + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=0') + k('wait', '--for=delete', 'pod', '-n', NS, + '-l', 'app.kubernetes.io/instance=coulomb-social', '--timeout=120s') + created = True + k('create', '-f', '-', payload=body) + k('wait', '--for=condition=Ready', 'pod/' + name, '-n', NS, '--timeout=120s') + dump = k('exec', '-n', 'databases', primary, '-c', 'postgres', '--', + 'pg_dump', '-U', 'postgres', '-d', 'coulomb_social_db', '-Fc') + if not dump.startswith(b'PGDMP'): + raise Refused('invalid_pg_dump') + private_write(dest / 'database.dump', dump) + archive_code = ('import io,sys,tarfile; b=io.BytesIO(); ' + 't=tarfile.open(fileobj=b,mode="w:gz"); ' + 't.add("/snapshot",arcname="content"); ' + 't.close();sys.stdout.buffer.write(b.getvalue())') + content = k('exec', '-n', NS, name, '--', 'python', '-c', archive_code) + private_write(dest / 'content.tar.gz', content) + receipt.update(status='captured', writers_quiesced=True, + database_sha256=hashlib.sha256(dump).hexdigest(), + content_sha256=hashlib.sha256(content).hexdigest(), + database_bytes=len(dump), content_bytes=len(content)) + finally: + try: + if created: + k('delete', 'pod', name, '-n', NS, '--wait=true', '--timeout=120s') + finally: + if paused: + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=1') + k('rollout', 'status', 'deployment/coulomb-social', '-n', NS, '--timeout=180s') + receipt['app_resumed'] = True + receipt['elapsed_seconds'] = round(time.monotonic() - started, 2) + private_write(dest / 'receipt.json', json.dumps(receipt, indent=2).encode()) + print(json.dumps(receipt)) + return 0 + + +def main() -> int: + if '--execute' not in sys.argv: + return snapshot() + LOCK_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + parent = LOCK_PATH.parent.lstat() + if (not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.geteuid() + or parent.st_mode & 0o077): + raise Refused('lock_directory_not_private') + fd = os.open(LOCK_PATH, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'w') as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused('snapshot_already_running') from None + def terminate(signum, frame): + raise Refused('snapshot_interrupted') + previous = signal.signal(signal.SIGTERM, terminate) + try: + return snapshot() + finally: + signal.signal(signal.SIGTERM, previous) + + +if __name__ == '__main__': + try: + sys.exit(main()) + except Exception as exc: + # Never expose child output, kubectl objects, credentials or raw exception messages. + print(json.dumps({'status': 'failed', 'failure_type': type(exc).__name__})) + sys.exit(1) diff --git a/release/identity-export-values.yaml b/release/identity-export-values.yaml new file mode 100644 index 0000000..56d4ded --- /dev/null +++ b/release/identity-export-values.yaml @@ -0,0 +1,50 @@ +# Production overrides for coulomb-social. +# Non-secret values only. Secrets live in K8s Secret coulomb-social-env. + +image: + repository: forgejo.coulomb.social/coulomb/coulomb-social + tag: "23ffe2cf4b21" + digest: "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" + +# Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). +# Apex cutover is deferred until data + self-registration are ready. +env: + DJANGO_SETTINGS_MODULE: coulomb_social.settings.prod + ALLOWED_HOSTS: app.coulomb.social,localhost + CSRF_TRUSTED_ORIGINS: https://app.coulomb.social + CONTENT_ROOT: /app/var/content + DEFAULT_TENANT_ID: "tenant:coulomb" + OIDC_ENABLED: "true" + OIDC_ISSUER: https://kc.coulomb.social + OIDC_CLIENT_ID: coulomb-social + OIDC_REDIRECT_URI: https://app.coulomb.social/auth/callback/ + OIDC_SCOPES: "openid profile email groups" + USER_ENGINE_BASE_URL: https://users.coulomb.social + USER_ENGINE_APPLICATION_ID: coulomb-social + USER_ENGINE_EXPECTED_AUDIENCE: user-engine-portal + NETKINGDOM_REGISTRATION_URL: https://users.92-205-62-239.nip.io/register + +probes: + hostHeader: app.coulomb.social + path: /healthz + +# One writable local-path volume; Recreate prevents concurrent old/new writers. +replicaCount: 1 +deploymentStrategy: Recreate +persistence: + content: + enabled: true + storageClass: local-path + size: 2Gi + mountPath: /app/var/content + retain: true + +# Wave-2 right-sizing (CSOC-WP-0017-T01, 2026-10-21 node budget): 7d p95 +# 0.4m CPU / 146Mi. Limits stay at the chart defaults (1000m/1Gi). +resources: + requests: + cpu: 15m + memory: 176Mi + limits: + cpu: 1000m + memory: 1Gi diff --git a/tests/test_coulomb_image_release.py b/tests/test_coulomb_image_release.py new file mode 100644 index 0000000..e733c02 --- /dev/null +++ b/tests/test_coulomb_image_release.py @@ -0,0 +1,194 @@ +"""Synthetic release safety cases; no cluster, credentials or real content.""" +import copy +import importlib.util +import json +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +SOURCE = Path(__file__).resolve().parents[1] / "tools/coulomb_image_release_node.py" +spec = importlib.util.spec_from_file_location("storage_release", SOURCE) +release = importlib.util.module_from_spec(spec) +spec.loader.exec_module(release) + +launcher_spec = importlib.util.spec_from_file_location("storage_launcher", SOURCE.with_name("coulomb-image-release.py")) +launcher = importlib.util.module_from_spec(launcher_spec) +launcher_spec.loader.exec_module(launcher) + + +def capsule(tmp_path): + root = tmp_path / "capsule" + root.mkdir() + (root / "probe.py").write_bytes(b"synthetic read-only probe") + plan = {"files": {"probe.py": release.sha((root / "probe.py").read_bytes())}} + (root / "manifest.json").write_text(json.dumps(plan)) + return root, "sha256:" + release.sha(release.canonical(plan)) + + +def test_capsule_tampering_refuses_before_privileged_work(tmp_path, monkeypatch): + root, digest = capsule(tmp_path) + monkeypatch.setattr(release.os, "geteuid", lambda: 0) + monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal") + (root / "probe.py").write_bytes(b"changed") + with pytest.raises(release.Refused, match="capsule_file_differs"): + release.Release(root, digest) + assert not (tmp_path / "journal").exists() + + +def test_preflight_never_creates_an_execution_journal(tmp_path, monkeypatch): + root, digest = capsule(tmp_path) + monkeypatch.setattr(release.os, "geteuid", lambda: 0) + monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal") + instance = release.Release(root, digest, read_only=True) + instance.save() + assert not (tmp_path / "journal").exists() + + +def test_prior_attempt_is_not_blindly_replayed(tmp_path, monkeypatch): + root, digest = capsule(tmp_path) + journal = tmp_path / "journal" + (journal / digest.removeprefix("sha256:")).mkdir(parents=True) + monkeypatch.setattr(release.os, "geteuid", lambda: 0) + monkeypatch.setattr(release, "JOURNAL_ROOT", journal) + with pytest.raises(release.Refused, match="prior_execution_requires_review"): + release.Release(root, digest) + + +def test_mutable_staging_is_frozen_before_live_work(tmp_path, monkeypatch): + root, digest = capsule(tmp_path) + monkeypatch.setattr(release.os, "geteuid", lambda: 0) + monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal") + instance = release.Release(root, digest) + (root / "probe.py").write_bytes(b"changed after verification") + assert (instance.root / "probe.py").read_bytes() == b"synthetic read-only probe" + assert (instance.journal / "receipt.json").is_file() + + +@pytest.mark.parametrize("field", ["business_rows_sha256", "content_files_sha256", "migration_rows_sha256"]) +def test_dataset_drift_is_not_accepted(field): + instance = object.__new__(release.Release) + baseline = {"business_tables": 2, "business_rows": 3, "business_rows_sha256": "synthetic-a", + "content_files": 4, "content_files_sha256": "synthetic-b", + "migration_rows_sha256": "synthetic-migrations"} + instance.plan = {"expected_data": baseline} + changed = dict(baseline, duplicate_slug_groups=0, tenant_slug_constraint=True) + changed[field] = "different" + with pytest.raises(release.Refused, match="dataset_drifted"): + instance.same_data(changed) + + +def test_failed_preflight_never_captures_or_upgrades(): + instance = object.__new__(release.Release) + instance.preflight = Mock(side_effect=release.Refused("guard stopped")) + instance.capture = Mock() + instance.command = Mock() + with pytest.raises(release.Refused): + instance.execute() + instance.capture.assert_not_called() + instance.command.assert_not_called() + + +@pytest.mark.parametrize("rollback_spec,rollback_ready,verified", [ + ({"synthetic": "old"}, 1, True), + ({"synthetic": "candidate"}, 1, False), + ({"synthetic": "old"}, 0, False), +]) +def test_postflight_failure_rolls_back_image_without_restoring_data( + tmp_path, monkeypatch, rollback_spec, rollback_ready, verified): + pin = tmp_path / "snapshot.py" + pin.write_bytes(b"old pin") + monkeypatch.setattr(release, "SNAPSHOT", pin) + instance = object.__new__(release.Release) + instance.receipt = {} + instance.root = tmp_path + spec = {"synthetic": "candidate"} + baseline = {"business_tables": 2, "business_rows": 3, "business_rows_sha256": "synthetic-a", + "content_files": 4, "content_files_sha256": "synthetic-b", + "migration_rows_sha256": "synthetic-migrations"} + instance.plan = {"candidate_spec_sha256": release.sha(release.canonical(spec)), "baseline_revision": 18, + "baseline_spec_sha256": release.sha(release.canonical({"synthetic": "old"})), + "expected_data": baseline} + instance.preflight = Mock() + instance.capture = Mock() + instance.save = Mock() + instance.deployment = Mock(side_effect=[ + {"spec": copy.deepcopy(spec), "status": {"readyReplicas": 1}}, + {"spec": rollback_spec, "status": {"readyReplicas": rollback_ready}}, + ]) + instance.probe = lambda mode: dict(baseline, business_rows_sha256="changed") + instance.command = Mock(return_value=SimpleNamespace(stdout=b"", returncode=0)) + instance.restore_pin = Mock() + with pytest.raises(release.Refused, match="dataset_drifted"): + instance.execute() + commands = [call.args[0] for call in instance.command.call_args_list] + assert commands[0][:2] == ["helm", "upgrade"] and "--atomic" in commands[0] + assert commands[1][:4] == ["helm", "rollback", "coulomb-social", "18"] + assert not any("pg_restore" in command for command in commands) + instance.restore_pin.assert_called_once_with(b"old pin") + assert instance.receipt["image_rollback_completed"] is verified + + +def test_live_launcher_rejects_unattended_execution_before_ssh(monkeypatch): + _, digest, _ = launcher.prepare() + monkeypatch.setattr(launcher.sys, "argv", ["release", "--execute", "--expect-digest", digest]) + monkeypatch.setattr(launcher.sys, "stdin", SimpleNamespace(isatty=lambda: False)) + command = Mock() + monkeypatch.setattr(launcher.subprocess, "run", command) + with pytest.raises(SystemExit, match="own terminal"): + launcher.main() + command.assert_not_called() + + +def test_timeout_gives_the_existing_capture_resume_window(monkeypatch): + instance = object.__new__(release.Release) + instance.env = {} + process = Mock() + process.communicate.side_effect = release.subprocess.TimeoutExpired(["synthetic"], 1) + monkeypatch.setattr(release.subprocess, "Popen", Mock(return_value=process)) + with pytest.raises(release.Refused, match="cleanup_window"): + instance.command(["synthetic"], timeout=1) + process.terminate.assert_called_once() + process.wait.assert_called_once_with(timeout=450) + process.kill.assert_not_called() + + +@pytest.mark.parametrize("field", ["global_slug_constraint", "migration_0003_applied"]) +@pytest.mark.parametrize("value", [False, None]) +def test_image_only_release_requires_existing_migration_and_constraint(field, value): + instance = object.__new__(release.Release) + baseline = {"business_tables": 2, "business_rows": 3, + "business_rows_sha256": "rows", "content_files": 4, + "content_files_sha256": "files", "migration_rows_sha256": "migrations"} + instance.plan = {"expected_data": baseline} + observed = dict(baseline, duplicate_slug_groups=0, tenant_slug_constraint=True, + global_slug_constraint=True, migration_0003_applied=True) + instance.same_data(observed) + observed[field] = value + with pytest.raises(release.Refused, match="already_migrated_baseline_required"): + instance.same_data(observed) + + +def test_rollback_does_not_overwrite_unrecognized_snapshot_helper(tmp_path, monkeypatch): + pin = tmp_path / "snapshot.py" + pin.write_bytes(b"concurrent owner change") + monkeypatch.setattr(release, "SNAPSHOT", pin) + instance = object.__new__(release.Release) + instance.plan = {"files": {"release/identity-export-snapshot.py": release.sha(b"new pin")}} + with pytest.raises(release.Refused, match="snapshot_pin_changed_during_rollback"): + instance.restore_pin(b"old pin") + assert pin.read_bytes() == b"concurrent owner change" + + +def test_known_candidate_helper_can_be_rolled_back(tmp_path, monkeypatch): + pin = tmp_path / "snapshot.py" + pin.write_bytes(b"new pin") + monkeypatch.setattr(release, "SNAPSHOT", pin) + instance = object.__new__(release.Release) + instance.plan = {"files": {"release/identity-export-snapshot.py": release.sha(b"new pin")}} + instance.restore_pin(b"old pin") + assert pin.read_bytes() == b"old pin" + # A completed inverse can be checked again without changing it. + instance.restore_pin(b"old pin") + assert pin.read_bytes() == b"old pin" diff --git a/tools/coulomb-image-release.py b/tools/coulomb-image-release.py new file mode 100644 index 0000000..1b2efc7 --- /dev/null +++ b/tools/coulomb-image-release.py @@ -0,0 +1,185 @@ +"""Prepare/read-only-check a sealed release; live mode is attended founder work. + +This launcher does not mint an approval or bypass gated-executor. The existing +gated-executor profile covers Kubernetes, not the root-owned backup helper refresh. +Only the founder runs --execute in their own terminal after reviewing the packet. +""" + +from __future__ import annotations + +import argparse +import hashlib +import io +import json +import os +import shlex +import subprocess +import sys +import tarfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PLAN = ROOT / "release/coulomb-identity-export-23ffe2c.json" + + +def canonical(value): + return json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + + +def prepare(): + plan = json.loads(PLAN.read_text()) + paths = [ + ROOT / name + for name in ( + "tools/coulomb-image-release.py", + "tools/coulomb_image_release_node.py", + "tools/coulomb_image_probe.py", + "release/identity-export-values.yaml", + "release/identity-export-snapshot.py", + ) + ] + paths += sorted((ROOT / "charts/coulomb-social").rglob("*")) + paths += sorted((ROOT / "release/identity-export-evidence").glob("*.json")) + paths = [p for p in paths if p.is_file()] + if any(p.is_symlink() or not p.resolve().is_relative_to(ROOT) for p in paths): + raise RuntimeError("regular_reviewed_files_required") + content = {str(p.relative_to(ROOT)): p.read_bytes() for p in paths} + plan["files"] = { + name: hashlib.sha256(data).hexdigest() for name, data in content.items() + } + for name, expected in plan["rehearsal_evidence_sha256"].items(): + if plan["files"].get("release/identity-export-evidence/" + name) != expected: + raise RuntimeError("reviewed_candidate_evidence_changed") + digest = "sha256:" + hashlib.sha256(canonical(plan)).hexdigest() + content["manifest.json"] = canonical(plan) + return plan, digest, content + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--preflight", action="store_true") + parser.add_argument("--execute", action="store_true") + parser.add_argument("--expect-digest") + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.preflight and args.execute: + raise SystemExit("choose read-only preflight or attended execution") + plan, digest, content = prepare() + if args.expect_digest and args.expect_digest != digest: + raise SystemExit("reviewed proposal changed; do not execute") + if not args.preflight and not args.execute: + print( + json.dumps( + { + "status": "prepared", + "proposal_digest": digest, + "file_count": len(content), + "candidate": plan["candidate_image"], + "live_changes": False, + } + ) + ) + return + if args.execute: + if not args.expect_digest or not sys.stdin.isatty(): + raise SystemExit( + "founder must review and run the exact digest in their own terminal" + ) + print("Reviewed proposal: " + digest) + print( + "Coulomb-only image update; existing database schema retained, fresh paired backup, postflight and backup image-pin refresh." + ) + if ( + input("Type DEPLOY followed by the full digest to proceed: ").strip() + != "DEPLOY " + digest + ): + raise SystemExit("not approved; no live change") + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w:") as archive: + for name, data in content.items(): + entry = tarfile.TarInfo(name) + entry.size, entry.mode = len(data), 0o600 + archive.addfile(entry, io.BytesIO(data)) + remote = "/tmp/csoc-reviewed-image-" + digest.removeprefix("sha256:") + receiver = r""" +import hashlib,io,json,os,pathlib,sys,tarfile +root=pathlib.Path(sys.argv[1]);expected=sys.argv[2] +raw=sys.stdin.buffer.read(2*1024**2+1);assert len(raw)<=2*1024**2 +with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive: + entries=archive.getmembers();assert len({e.name for e in entries})==len(entries) + assert all(e.isfile() and not pathlib.Path(e.name).is_absolute() and '..' not in pathlib.Path(e.name).parts for e in entries) + data={e.name:archive.extractfile(e).read() for e in entries} + plan=json.loads(data['manifest.json']) + canonical=json.dumps(plan,sort_keys=True,separators=(',',':')).encode() + assert 'sha256:'+hashlib.sha256(canonical).hexdigest()==expected + assert set(data)==set(plan['files'])|{'manifest.json'} + for name,digest in plan['files'].items():assert hashlib.sha256(data[name]).hexdigest()==digest + root.mkdir(mode=0o700,exist_ok=True) + assert root.lstat().st_uid==os.getuid() and root.lstat().st_mode&0o077==0 + for name,value in data.items(): + target=root/name;target.parent.mkdir(parents=True,exist_ok=True,mode=0o700) + if target.exists():assert not target.is_symlink() and target.read_bytes()==value;continue + fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'wb') as stream:stream.write(value) +""" + staged = subprocess.run( + [ + "ssh", + "railiance01", + "python3 -c " + + shlex.quote(receiver) + + " " + + shlex.quote(remote) + + " " + + shlex.quote(digest), + ], + input=buffer.getvalue(), + capture_output=True, + timeout=60, + check=False, + ) + if staged.returncode: + raise SystemExit("reviewed capsule staging refused; no live change") + node_hash = plan["files"]["tools/coulomb_image_release_node.py"] + guard = ( + "import hashlib,pathlib,sys;p=pathlib.Path(" + + repr(remote + "/tools/coulomb_image_release_node.py") + + ");" + "b=p.read_bytes();assert hashlib.sha256(b).hexdigest()==" + + repr(node_hash) + + ";" + "sys.argv=['reviewed-release','--capsule'," + + repr(remote) + + ",'--expect-digest'," + + repr(digest) + + "," + + repr("--execute" if args.execute else "--preflight") + + "];exec(compile(b,str(p),'exec'),{'__name__':'__main__'})" + ) + result = subprocess.run( + ["ssh", "railiance01", "sudo -n python3 -c " + shlex.quote(guard)], + capture_output=True, + timeout=3600, + check=False, + ) + try: + receipt = json.loads(result.stdout) + except (ValueError, TypeError): + receipt = { + "status": "remote_execution_unconfirmed", + "proposal_digest": digest, + "journal": "/var/lib/coulomb-social-backup/releases/" + + digest.removeprefix("sha256:") + + "/receipt.json", + } + if args.output: + fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "w") as stream: + json.dump(receipt, stream, indent=2) + print(json.dumps(receipt)) + if result.returncode: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/coulomb_image_probe.py b/tools/coulomb_image_probe.py new file mode 100644 index 0000000..8d7b028 --- /dev/null +++ b/tools/coulomb_image_probe.py @@ -0,0 +1,134 @@ +"""Read-only data/schema and view probe; runs inside the existing app container.""" + +import hashlib +import json +import sys +import uuid +from pathlib import Path + +import django + +django.setup() +from coulomb_social.apps.members.models import Member, User +from coulomb_social.apps.spaces.models import Space +from django.conf import settings +from django.db import connection +from django.db.models import Count + +assert connection.vendor == "postgresql" +assert connection.settings_dict["NAME"] in { + "coulomb_social_db", + "coulomb_social_restore", +} +tables = {} +with connection.cursor() as cursor: + cursor.execute( + "SELECT tablename FROM pg_tables WHERE schemaname='public' " + "AND tablename!='django_migrations' ORDER BY tablename" + ) + names = [row[0] for row in cursor.fetchall()] + for table in names: + cursor.execute("SELECT * FROM " + connection.ops.quote_name(table)) + rows = sorted( + json.dumps(row, default=str, separators=(",", ":")) + for row in cursor.fetchall() + ) + tables[table] = { + "rows": len(rows), + "sha256": hashlib.sha256("\n".join(rows).encode()).hexdigest(), + } + cursor.execute( + "SELECT conname FROM pg_constraint WHERE conrelid='spaces_space'::regclass" + ) + constraints = {row[0] for row in cursor.fetchall()} + cursor.execute( + "SELECT COUNT(*) FROM django_migrations WHERE app='spaces' AND name='0003_space_slug_global_uniq'" + ) + applied = cursor.fetchone()[0] == 1 + cursor.execute("SELECT * FROM django_migrations") + migration_rows = sorted(json.dumps(row, default=str, separators=(",", ":")) + for row in cursor.fetchall()) +root = Path(settings.CONTENT_ROOT) +files = { + str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() + for p in root.rglob("*") + if p.is_file() +} +result = { + "business_tables": len(tables), + "migration_rows_sha256": hashlib.sha256("\n".join(migration_rows).encode()).hexdigest(), + "business_rows": sum(t["rows"] for t in tables.values()), + "business_rows_sha256": hashlib.sha256( + json.dumps(tables, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest(), + "content_files": len(files), + "content_files_sha256": hashlib.sha256( + json.dumps(files, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest(), + "global_slug_constraint": "spaces_space_slug_global_uniq" in constraints, + "tenant_slug_constraint": "spaces_space_tenant_slug_uniq" in constraints, + "migration_0003_applied": applied, + "duplicate_slug_groups": Space.objects.values("slug") + .annotate(n=Count("pk")) + .filter(n__gt=1) + .count(), +} +if len(sys.argv) > 1 and sys.argv[1] == "views": + from coulomb_social.apps.spaces import views + from coulomb_social.apps.spaces.content import load_space_page + from django.test import RequestFactory + from django.urls import reverse + + assert not settings.FLEX_AUTH_BASE_URL + factory = RequestFactory() + rendered_count, denied_count = 0, 0 + for space in Space.objects.filter(is_active=True): + paths = sorted((root / "spaces" / space.slug / "pages").glob("*.md")) + if not paths: + continue + owner = space.created_by + if owner is None: + owner = ( + space.memberships.filter(role="owner") + .select_related("member__user") + .first() + .member + ) + token = "unsaved-release-check-" + uuid.uuid4().hex + outsider = Member( + user=User(username=token), + tenant_id=space.tenant_id, + subject=token, + issuer="https://release-check.invalid", + user_engine_user_id=token, + ) + for path in paths: + page = path.stem + loaded = load_space_page(space, page) + assert loaded.error is None + + def request(user, slug=space.slug, page_slug=page): + req = factory.get( + reverse("spaces:detail", kwargs={"slug": slug}), + {"page": page_slug}, + ) + req.user, req.session = user, {} + return req + + response = views.space_detail(request(owner.user), slug=space.slug) + assert ( + response.status_code == 200 and loaded.html.encode() in response.content + ) + rendered_count += 1 + denied = views.space_detail(request(outsider.user), slug=space.slug) + assert denied.status_code == 404 + if loaded.html: + assert loaded.html.encode() not in denied.content + denied_count += 1 + assert rendered_count > 0 and denied_count == rendered_count + result.update( + actual_owner_pages_rendered=rendered_count, + unsaved_outsider_page_denials=denied_count, + production_identity_or_grant_created=False, + ) +print(json.dumps(result)) diff --git a/tools/coulomb_image_release_node.py b/tools/coulomb_image_release_node.py new file mode 100644 index 0000000..d84b525 --- /dev/null +++ b/tools/coulomb_image_release_node.py @@ -0,0 +1,525 @@ +"""Attended founder-only node release; argv/data are sealed by the reviewed capsule. + +This is a supervisor command, not a new agent credential or approval boundary. +No Secret object/output is fetched. Existing approved backup custody stays local. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import stat +import subprocess +import tempfile +import time +import urllib.request +from datetime import UTC, datetime +from pathlib import Path + +JOURNAL_ROOT = Path("/var/lib/coulomb-social-backup/releases") +SNAPSHOT = Path("/opt/coulomb-social-backup/snapshot-content-and-db.py") + + +def canonical(value): + return json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +class Refused(RuntimeError): + pass + + +class Release: + def __init__(self, capsule, expected, read_only=False): + self.root = capsule + self.plan = json.loads((capsule / "manifest.json").read_text()) + frozen = {} + if "sha256:" + sha(canonical(self.plan)) != expected: + raise Refused("proposal_digest_differs") + for name, digest in self.plan["files"].items(): + path = capsule / name + if ( + not path.resolve().is_relative_to(capsule.resolve()) + or Path(name).is_absolute() + or ".." in Path(name).parts + or path.is_symlink() + or not path.is_file() + or sha(path.read_bytes()) != digest + ): + raise Refused("capsule_file_differs") + frozen[name] = path.read_bytes() + if sha(frozen[name]) != digest: + raise Refused("capsule_changed_during_verification") + if os.geteuid() != 0: + raise Refused("attended_founder_root_execution_required") + self.env = dict(os.environ, KUBECONFIG="/etc/rancher/k3s/k3s.yaml") + self.read_only = read_only + self.receipt = { + "schema": "coulomb.guarded-image-release.v1", + "proposal_digest": expected, + "started_at": datetime.now(UTC).isoformat(), + "status": "preflight", + "production_data_restored": False, + "timers_activated": False, + } + self.journal = JOURNAL_ROOT / expected.removeprefix("sha256:") + if read_only: + return + if self.journal.exists(): + raise Refused("prior_execution_requires_review") + self.journal.mkdir(parents=True, mode=0o700) + self.root = self.journal / "capsule" + self.root.mkdir(mode=0o700) + for name, data in frozen.items(): + target = self.root / name + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, "wb") as stream: + stream.write(data) + self.receipt["pid"] = os.getpid() + self.save() + + def save(self): + if self.read_only: + return + fd, name = tempfile.mkstemp(prefix=".receipt-", dir=self.journal) + with os.fdopen(fd, "w") as stream: + json.dump(self.receipt, stream, sort_keys=True, indent=2) + stream.flush() + os.fsync(stream.fileno()) + os.replace(name, self.journal / "receipt.json") + directory = os.open(self.journal, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + + def command(self, arguments, payload=None, env=None, timeout=180, allowed=(0,)): + # In particular, give the paired capture its existing app-resume window + # on timeout; subprocess.run would SIGKILL it before that cleanup. + with tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr: + process = subprocess.Popen( + arguments, + env=env or self.env, + stdin=subprocess.PIPE if payload is not None else subprocess.DEVNULL, + stdout=stdout, + stderr=stderr, + ) + try: + process.communicate(payload, timeout=timeout) + except subprocess.TimeoutExpired: + process.terminate() + try: + process.wait(timeout=450) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + raise Refused("private_child_timed_out_after_cleanup_window") from None + stdout.seek(0) + body = stdout.read(8 * 1024**2 + 1) + if len(body) > 8 * 1024**2: + raise Refused("bounded_child_output_required") + result = subprocess.CompletedProcess(arguments, process.returncode, body) + if result.returncode not in allowed: + raise Refused("private_child_refused") + return result + + def kdoc(self, *arguments): + return json.loads(self.command(["kubectl", *arguments, "-o", "json"]).stdout) + + def history(self): + return json.loads( + self.command( + [ + "helm", + "history", + "coulomb-social", + "-n", + "coulomb-social", + "-o", + "json", + ] + ).stdout + ) + + def deployment(self): + return self.kdoc("get", "deployment", "coulomb-social", "-n", "coulomb-social") + + def probe(self, mode="data"): + source = (self.root / "tools/coulomb_image_probe.py").read_text() + result = self.command( + [ + "kubectl", + "exec", + "-n", + "coulomb-social", + "deploy/coulomb-social", + "--", + "python", + "-c", + source, + mode, + ] + ) + return json.loads(result.stdout) + + def same_data(self, actual): + for name in ( + "business_tables", + "business_rows", + "business_rows_sha256", + "content_files", + "content_files_sha256", + "migration_rows_sha256", + ): + if actual[name] != self.plan["expected_data"][name]: + raise Refused("approved_recovered_dataset_drifted") + if actual["duplicate_slug_groups"] != 0 or actual["tenant_slug_constraint"] is not True: + raise Refused("storage_invariant_refused") + if (actual.get("global_slug_constraint") is not True + or actual.get("migration_0003_applied") is not True): + raise Refused("already_migrated_baseline_required") + + def preflight(self): + self.verify_installed_helpers() + if ( + self.kdoc("get", "namespace", "kube-system")["metadata"]["uid"] + != self.plan["cluster_uid"] + ): + raise Refused("wrong_cluster") + node = self.kdoc("get", "nodes")["items"] + if len(node) != 1 or not any( + a["address"] == "92.205.62.239" for a in node[0]["status"]["addresses"] + ): + raise Refused("wrong_node") + conditions = {v["type"]: v["status"] for v in node[0]["status"]["conditions"]} + if conditions.get("Ready") != "True" or any( + conditions.get(k) != "False" + for k in ("MemoryPressure", "DiskPressure", "PIDPressure") + ): + raise Refused("node_pressure") + allocated = self.command( + ["kubectl", "describe", "node", node[0]["metadata"]["name"]] + ).stdout.decode() + requests = {} + for resource in ("cpu", "memory"): + found = re.search( + r"(?m)^\s+" + resource + r"\s+\S+\s+\((\d+)%\)", allocated + ) + if not found or int(found[1]) > 90: + raise Refused("node_request_headroom_refused") + requests[resource] = int(found[1]) + self.receipt["node_request_percent"] = requests + history = self.history() + if ( + history[-1]["revision"] != self.plan["baseline_revision"] + or history[-1]["status"] != "deployed" + ): + raise Refused("helm_baseline_changed") + manifest = self.command( + ["helm", "get", "manifest", "coulomb-social", "-n", "coulomb-social"] + ).stdout + if sha(manifest) != self.plan["baseline_helm_manifest_sha256"]: + raise Refused("helm_manifest_changed") + deployment = self.deployment() + if ( + deployment["metadata"]["uid"] != self.plan["deployment_uid"] + or sha(canonical(deployment["spec"])) != self.plan["baseline_spec_sha256"] + ): + raise Refused("live_spec_changed") + if ( + deployment["status"].get("readyReplicas") != 1 + or deployment["spec"]["strategy"]["type"] != "Recreate" + ): + raise Refused("single_ready_writer_required") + # Helm's three-way merge preserves unmanaged pod metadata. Bind the + # complete desired live spec rather than treating kubectl apply's + # last-applied projection as Helm's result. + image_patch = [ + {"op": "test", "path": "/spec", "value": deployment["spec"]}, + {"op": "replace", "path": "/spec/template/spec/containers/0/image", + "value": self.plan["candidate_image"]}, + ] + preview = self.kdoc( + "patch", "deployment", "coulomb-social", "-n", "coulomb-social", + "--type=json", "--dry-run=server", "-p", json.dumps(image_patch), + ) + if sha(canonical(preview["spec"])) != self.plan["candidate_spec_sha256"]: + raise Refused("image_only_server_preview_differs") + pvc = self.kdoc("get", "pvc", "coulomb-social-content", "-n", "coulomb-social") + if ( + pvc["metadata"]["uid"] != self.plan["pvc_uid"] + or pvc["status"]["phase"] != "Bound" + ): + raise Refused("pvc_changed") + apps = self.kdoc("get", "applications.argoproj.io", "-A")["items"] + if any( + a.get("spec", {}).get("destination", {}).get("namespace") + == "coulomb-social" + for a in apps + ): + raise Refused("gitops_adoption_requires_owner_path") + rendered = self.command( + [ + "helm", + "template", + "coulomb-social", + str(self.root / "charts/coulomb-social"), + "-f", + str(self.root / "release/identity-export-values.yaml"), + "-n", + "coulomb-social", + ] + ).stdout + if sha(rendered) != self.plan["candidate_render_sha256"]: + raise Refused("candidate_render_changed") + self.command(["kubectl", "diff", "-f", "-"], payload=rendered, allowed=(0, 1)) + self.command( + ["kubectl", "apply", "--dry-run=server", "-f", "-"], payload=rendered + ) + self.command( + [ + "helm", + "upgrade", + "coulomb-social", + str(self.root / "charts/coulomb-social"), + "-f", + str(self.root / "release/identity-export-values.yaml"), + "-n", + "coulomb-social", + "--dry-run=server", + ] + ) + before = self.probe() + self.same_data(before) + self.receipt["preflight"] = before + self.save() + + def verify_installed_helpers(self): + for path, expected in self.plan["installed_helper_hashes"].items(): + metadata = Path(path).lstat() + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid != 0 + or metadata.st_mode & 0o022 + or sha(Path(path).read_bytes()) != expected + ): + raise Refused("installed_backup_helper_changed") + def capture(self): + self.verify_installed_helpers() + environment = dict(self.env) + for name in ("primary.env", "recipient.env"): + path = Path("/etc/coulomb-social-backup") / name + metadata = path.lstat() + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid != 0 + or metadata.st_mode & 0o077 + ): + raise Refused("private_own_backup_configuration_required") + environment.update( + dict( + line.split("=", 1) + for line in path.read_text().splitlines() + if line and not line.startswith("#") + ) + ) + output = self.command( + ["python3", "/opt/coulomb-social-backup/paired-backup.py", "--execute"], + env=environment, + timeout=1200, + ) + point = json.loads(output.stdout) + if ( + point.get("status") != "offsite_fetched_verified" + or not point.get("remote_retrieval_verified") + or not point.get("version_id") + or point.get("version_id") == "null" + or point.get("image") != self.plan["old_image"] + or point.get("age_recipient") != self.plan["public_recipient"] + or not point.get("immutable_create") + or not point.get("destination", "").startswith( + "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/" + ) + ): + raise Refused("fresh_paired_primary_readback_required") + self.receipt["fresh_paired_point"] = point + self.save() + self.same_data(self.probe()) + + def restore_pin(self, old): + path = SNAPSHOT + if ( + sha(path.read_bytes()) + == self.plan["files"]["release/identity-export-snapshot.py"] + ): + self.replace_pin(path, old) + elif sha(path.read_bytes()) != sha(old): + raise Refused("snapshot_pin_changed_during_rollback") + + def replace_pin(self, path, value): + fd, name = tempfile.mkstemp(prefix=".reviewed-snapshot-", dir=path.parent) + try: + with os.fdopen(fd, "wb") as stream: + stream.write(value) + stream.flush() + os.fsync(stream.fileno()) + os.replace(name, path) + directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY) + try: + os.fsync(directory) + finally: + os.close(directory) + finally: + if os.path.exists(name): + os.unlink(name) + + def execute(self): + began = time.monotonic() + self.preflight() + self.capture() + old_pin = SNAPSHOT.read_bytes() + self.receipt["status"] = "upgrade_started" + self.save() + try: + self.command( + [ + "helm", + "upgrade", + "coulomb-social", + str(self.root / "charts/coulomb-social"), + "-f", + str(self.root / "release/identity-export-values.yaml"), + "-n", + "coulomb-social", + "--atomic", + "--wait", + "--timeout", + "5m", + ], + timeout=660, + ) + after = self.deployment() + if ( + sha(canonical(after["spec"])) != self.plan["candidate_spec_sha256"] + or after["status"].get("readyReplicas") != 1 + ): + raise Refused("candidate_spec_or_readiness_differs") + probe = self.probe("views") + self.same_data(probe) + for suffix in ("/healthz", "/", "/static/spaces/copy-content.js"): + with urllib.request.urlopen( + "https://app.coulomb.social" + suffix, timeout=20 + ) as response: + body = response.read(1024**2) + if response.status != 200: + raise Refused("public_http_postflight_refused") + if ( + suffix.endswith("copy-content.js") + and sha(body) != self.plan["clipboard_static_sha256"] + ): + raise Refused("served_static_differs") + path = SNAPSHOT + if ( + sha(path.read_bytes()) + != self.plan["installed_helper_hashes"][str(path)] + ): + raise Refused("snapshot_pin_drifted") + self.replace_pin( + path, (self.root / "release/identity-export-snapshot.py").read_bytes() + ) + self.command( + [ + "python3", + str(path), + "--destination", + "/tmp/csoc-post-image-server-review", + ] + ) + self.receipt.update( + status="image_candidate_deployed_verified", + postflight=probe, + helm_revision=self.history()[-1]["revision"], + snapshot_pin_refreshed=True, + public_health_landing_static_verified=True, + existing_global_constraint_preserved=True, + ) + except Exception: + self.receipt["rollback_requested"] = True + try: + self.command( + [ + "helm", + "rollback", + "coulomb-social", + str(self.plan["baseline_revision"]), + "-n", + "coulomb-social", + "--wait", + "--timeout", + "5m", + ], + timeout=360, + ) + self.restore_pin(old_pin) + restored = self.deployment() + if (sha(canonical(restored["spec"])) != self.plan["baseline_spec_sha256"] + or restored["status"].get("readyReplicas") != 1): + raise Refused("rollback_spec_or_readiness_differs") + self.receipt["image_rollback_completed"] = True + except Exception: # noqa: BLE001 -- retain explicit unconfirmed rollback outcome + self.receipt["image_rollback_completed"] = False + raise + self.receipt.update( + elapsed_seconds=round(time.monotonic() - began, 3), + completed_at=datetime.now(UTC).isoformat(), + private_values_printed=False, + ) + self.save() + return self.receipt + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--capsule", type=Path, required=True) + parser.add_argument("--expect-digest", required=True) + parser.add_argument("--execute", action="store_true") + parser.add_argument("--preflight", action="store_true") + args = parser.parse_args() + if not args.execute and not args.preflight: + print(json.dumps({"status": "prepared", "live_changes": False})) + return + if args.execute and args.preflight: + raise SystemExit("choose read-only preflight or attended execution") + release = Release(args.capsule, args.expect_digest, read_only=args.preflight) + try: + if args.preflight: + release.preflight() + print( + json.dumps( + dict( + release.receipt, + status="live_release_preflight_verified", + live_changes=False, + ) + ) + ) + return + print(json.dumps(release.execute())) + except Exception as error: # noqa: BLE001 -- private child diagnostics are suppressed + release.receipt.update( + status="release_refused_or_failed", failure_type=type(error).__name__ + ) + release.save() + print(json.dumps(release.receipt)) + raise SystemExit(1) from None + + +if __name__ == "__main__": + main() diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index fd4cac8..b08998c 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: railiance-apps created: "2026-10-06" -updated: "2026-10-08" +updated: "2026-10-31" related: - CSOC-WP-0008 state_hub_workstream_id: "79abacf8-361e-51ab-829d-40dec8e142d1" @@ -340,3 +340,45 @@ server dry-run pass. No Secret, issuer, mail, caller, PVC, offsite schedule or Bubble/apex change. T04 remains wait for productive complete user acceptance; product WP-0008/WP-0010 retain their broader open tasks. Product receipt: docs/release/sharing-portability-review-2026-10-08.md. + +## T05 — Deliver the identity-binding and operator-export image + +```task +id: RAPPS-WP-0015-T05 +status: progress +priority: high +needs_human: true +decision_id: "75b94bb6-4709-495f-91fe-43e53e2867ed" +intervention_note: "Exact image-only proposal9f1daf6c passes native read-only preflight. Founder terminal execution remains required; then verify durable receipt, current source/values/helper pins and consumer recovery/export follow-through." +``` + +Owner execution dependency for CSOC-WP-0012-T03 and CSOC-WP-0015-T04. This is +separate from completed storage release 5fd7ccd8 and date correction 47829c7a. +Candidate source 23ffe2c/image 522bcd7f is published and packaged-source verified; +441 source tests and 25 packaged identity/export tests pass. The retained isolated +recovery verifies 85 client files and 40 owner/40 outsider views without app overlays. +Current production remains revision 18/image 51281946; default live Helm values are +unchanged until the verified execution return. + +Prepared separate image-only launcher/node/probe, candidate values and snapshot +pin. The controller requires both existing slug constraints and migration 0003, +compares complete business/migration-history/content fingerprints, makes a fresh +admitted primary backup/readback, then performs atomic Helm upgrade and postflight. +Rollback verifies the previous live specification, Ready replica and recognized +helper; no data restore/schema downgrade or automatic replay. The old storage +launcher is not reused. 19 synthetic safety tests, Ruff, compile and Helm lint pass. + +Sealed read-only preflight 9f1daf6c passes at 2026-10-31T03:41:35Z: revision 18, +12 business tables/92 rows/86 files, exact migration history, unchanged helper pins, +CPU 68%/memory 76%, image-only chart/server-preview checks and no Argo adoption. +The expected live spec preserves the unmanaged restart annotation; raw kubectl +apply remains dry-run only. Receipt: docs/evidence/2026-10-31-coulomb-image-release-preflight.json. +Founder packet: coulomb-social/docs/release/identity-export-release-review-2026-10-31.md. +The new execution has not occurred. No node helper, Helm release, timer or data +changed. Existing recovery rehearsal predates the date correction; independent +retrieval/restore/export of the new execution point remains consumer follow-through. + +Done when founder execution and independent postflight are verified, the native +receipt and actual image/helper pins are reconciled into owner/product source, +and the product records reference that exact release. This does not accept the +remaining productive journeys or service/offboarding obligations for the consumer. From a70e3b2e9f477fe0d6380b6650a1964da9a0f044 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 04:52:12 +0100 Subject: [PATCH 03/16] Record published identity export release handoff Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index b08998c..fea3cfc 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -350,6 +350,7 @@ priority: high needs_human: true decision_id: "75b94bb6-4709-495f-91fe-43e53e2867ed" intervention_note: "Exact image-only proposal9f1daf6c passes native read-only preflight. Founder terminal execution remains required; then verify durable receipt, current source/values/helper pins and consumer recovery/export follow-through." +state_hub_task_id: "9f657a95-ce0e-5805-b754-a64e38827c73" ``` Owner execution dependency for CSOC-WP-0012-T03 and CSOC-WP-0015-T04. This is @@ -382,3 +383,8 @@ Done when founder execution and independent postflight are verified, the native receipt and actual image/helper pins are reconciled into owner/product source, and the product records reference that exact release. This does not accept the remaining productive journeys or service/offboarding obligations for the consumer. + +2026-10-31 handoff: controller commit `84a5aa0` is published on owner main. +The canonical workstation launcher reproduces proposal `9f1daf6c` with +`live_changes: false`; the founder packet is ready for attended execution. +Consumer follow-through remains CSOC-WP-0012-T03 and CSOC-WP-0015-T04. From d795120f982264290ed180d0ebc7a7a987e12725 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 08:35:54 +0100 Subject: [PATCH 04/16] Reconcile verified Coulomb revision 19 and complete image delivery Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- WORK-RECORDS.md | 2 +- docs/coulomb-image-release-2026-10-31.md | 17 ++--- ...-image-release-independent-postflight.json | 49 +++++++++++++ ...6-10-31-coulomb-image-release-receipt.json | 70 +++++++++++++++++++ ...ulomb-image-release-source-provenance.json | 10 +++ helm/coulomb-social-values.yaml | 4 +- ...P-0015-coulomb-personal-content-release.md | 17 ++++- 7 files changed, 155 insertions(+), 14 deletions(-) create mode 100644 docs/evidence/2026-10-31-coulomb-image-release-independent-postflight.json create mode 100644 docs/evidence/2026-10-31-coulomb-image-release-receipt.json create mode 100644 docs/evidence/2026-10-31-coulomb-image-release-source-provenance.json diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index dc3c219..c255f81 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -41,7 +41,7 @@ | task | RAPPS-WP-0015-T02 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T03 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | -| task | RAPPS-WP-0015-T05 | progress | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/docs/coulomb-image-release-2026-10-31.md b/docs/coulomb-image-release-2026-10-31.md index 2a16234..7d654e5 100644 --- a/docs/coulomb-image-release-2026-10-31.md +++ b/docs/coulomb-image-release-2026-10-31.md @@ -1,8 +1,8 @@ -# Prepared Coulomb identity/export image release +# Completed Coulomb identity/export image release The exact supervised proposal is `sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f`. -Open decision: `75b94bb6-4709-495f-91fe-43e53e2867ed`. +Resolved execution decision: `75b94bb6-4709-495f-91fe-43e53e2867ed`. The full founder packet is [coulomb-social release review](/home/worsch/coulomb-social/docs/release/identity-export-release-review-2026-10-31.md). @@ -21,8 +21,8 @@ helper changes refuse overwrite. Historical storage commands/capsules remain unchanged and must not be replayed. Candidate values live in `release/identity-export-values.yaml`; the current -`helm/coulomb-social-values.yaml` remains pinned to the actual revision 18 image. -The new snapshot-helper bytes are separate review material until execution. +`helm/coulomb-social-values.yaml` now matches verified revision 19/image522bcd7f. +The installed snapshot helper matches the reviewed bytes (dcc8dd2a). No unit/timer, sender, grant, apex or data-restore operation is included. Validation: 19 release safety cases pass; Ruff, Python compile and Helm lint pass. @@ -32,7 +32,8 @@ Four hash-bound candidate evidence files travel inside the capsule. The product review names source 441 tests, actual packaged 25 tests, all 80 packaged source matches and the retained isolated recovery/export/view checks with their limits. -After founder execution, inspect the durable receipt and independent current -state before reconciling the live values and snapshot pin. The product then -mirrors/retrieves the newly captured point and verifies packaged export on that -coherent post-correction data. Source publication alone does not finish T05. +Founder execution and independent postflight are verified at revision19. The +product independently retrieved/restored the fresh point and verified packaged +client export on the corrected data. T05 is complete. Native receipt, independent +postflight and live packaged-source proof are under docs/evidence/2026-10-31-coulomb-image-release-*.json. +The historical launcher/capsule is retained for audit and must not be replayed. diff --git a/docs/evidence/2026-10-31-coulomb-image-release-independent-postflight.json b/docs/evidence/2026-10-31-coulomb-image-release-independent-postflight.json new file mode 100644 index 0000000..f93afb7 --- /dev/null +++ b/docs/evidence/2026-10-31-coulomb-image-release-independent-postflight.json @@ -0,0 +1,49 @@ +{ + "schema": "coulomb.independent-image-release-postflight.v1", + "status": "live_image_release_verified", + "observed_at": "2026-10-31T07:31:19.932895+00:00", + "proposal_digest": "sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f", + "helm_revision": 19, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "cluster_deployment_pvc_identity_verified": true, + "complete_deployment_spec_verified": true, + "sole_ready_pvc_writer": true, + "data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "installed_helper_sha256": { + "/opt/coulomb-social-backup/paired-backup.py": "43e119acb6a4933b4e144c349a091e074e1a91ab61dbe15a65c8264c1d47661a", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d" + }, + "snapshot_server_dry_run": "passed", + "public_status": { + "/healthz": 200, + "/": 200, + "/static/spaces/copy-content.js": 200 + }, + "timers": { + "coulomb-social-paired-backup.timer": { + "LoadState": "not-found", + "ActiveState": "inactive" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "not-found", + "ActiveState": "inactive" + } + }, + "production_data_modified": false +} diff --git a/docs/evidence/2026-10-31-coulomb-image-release-receipt.json b/docs/evidence/2026-10-31-coulomb-image-release-receipt.json new file mode 100644 index 0000000..0799b60 --- /dev/null +++ b/docs/evidence/2026-10-31-coulomb-image-release-receipt.json @@ -0,0 +1,70 @@ +{ + "completed_at": "2026-10-31T07:15:24.659887+00:00", + "elapsed_seconds": 99.14, + "existing_global_constraint_preserved": true, + "fresh_paired_point": { + "age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "application": "coulomb-social", + "application_restore_verified": false, + "captured_at": "2026-10-31T07:13:52Z", + "ciphertext_bytes": 2181848, + "ciphertext_sha256": "840e633047fddd88c6e6a1a3d6909d9af1ad80eaba8c90f83d8e81188dfc17a4", + "completed_at": "2026-10-31T07:14:50.236959+00:00", + "content_sha256": "644db8f09684bd4a79c126a01a329904351c30bb340295b29c8bb0ca71092dfe", + "database_sha256": "498a11e221dcdef38075d23ef89a26f213316c6164addb70d47bfcb98a1999a7", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T071352Z-abf5997f01a14d9cb05e4bba.tar.age", + "encrypted_bytes": 2181848, + "encrypted_sha256": "840e633047fddd88c6e6a1a3d6909d9af1ad80eaba8c90f83d8e81188dfc17a4", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd", + "immutable_create": true, + "local_archive": "20261031T071352Z-abf5997f01a14d9cb05e4bba.tar.age", + "object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T071352Z-abf5997f01a14d9cb05e4bba.tar.age", + "pause_and_capture_seconds": 56.34, + "remote_retrieval_verified": true, + "schema": "platform.application-primary-archive.v1", + "status": "offsite_fetched_verified", + "version_id": "1793430889823443" + }, + "helm_revision": 19, + "node_request_percent": { + "cpu": 68, + "memory": 76 + }, + "pid": 3639216, + "postflight": { + "actual_owner_pages_rendered": 40, + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "business_tables": 12, + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "production_identity_or_grant_created": false, + "tenant_slug_constraint": true, + "unsaved_outsider_page_denials": 40 + }, + "preflight": { + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "business_tables": 12, + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "duplicate_slug_groups": 0, + "global_slug_constraint": true, + "migration_0003_applied": true, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "tenant_slug_constraint": true + }, + "private_values_printed": false, + "production_data_restored": false, + "proposal_digest": "sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f", + "public_health_landing_static_verified": true, + "schema": "coulomb.guarded-image-release.v1", + "snapshot_pin_refreshed": true, + "started_at": "2026-10-31T07:13:45.511369+00:00", + "status": "image_candidate_deployed_verified", + "timers_activated": false +} diff --git a/docs/evidence/2026-10-31-coulomb-image-release-source-provenance.json b/docs/evidence/2026-10-31-coulomb-image-release-source-provenance.json new file mode 100644 index 0000000..255572a --- /dev/null +++ b/docs/evidence/2026-10-31-coulomb-image-release-source-provenance.json @@ -0,0 +1,10 @@ +{ + "schema": "coulomb.live-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "packaged_files": 80, + "all_packaged_source_bytes_match": true, + "packaged_manifest_sha256": "6b5a7b339d88f3c5eafb70a33aa28a7d3972988645b246ae1a92a3f159d72528", + "live_read_only_probe": true, + "production_data_changed": false +} diff --git a/helm/coulomb-social-values.yaml b/helm/coulomb-social-values.yaml index 48b243c..56d4ded 100644 --- a/helm/coulomb-social-values.yaml +++ b/helm/coulomb-social-values.yaml @@ -3,8 +3,8 @@ image: repository: forgejo.coulomb.social/coulomb/coulomb-social - tag: "17d0eb6" - digest: "sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd" + tag: "23ffe2cf4b21" + digest: "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" # Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). # Apex cutover is deferred until data + self-registration are ready. diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index fea3cfc..119f326 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -345,11 +345,11 @@ docs/release/sharing-portability-review-2026-10-08.md. ```task id: RAPPS-WP-0015-T05 -status: progress +status: done priority: high -needs_human: true +needs_human: false decision_id: "75b94bb6-4709-495f-91fe-43e53e2867ed" -intervention_note: "Exact image-only proposal9f1daf6c passes native read-only preflight. Founder terminal execution remains required; then verify durable receipt, current source/values/helper pins and consumer recovery/export follow-through." +intervention_note: "Founder executed9f1daf6c; revision19 and complete independent postflight, source/helper/values reconciliation and current-point consumer recovery/export are verified. No rerun required." state_hub_task_id: "9f657a95-ce0e-5805-b754-a64e38827c73" ``` @@ -388,3 +388,14 @@ remaining productive journeys or service/offboarding obligations for the consume The canonical workstation launcher reproduces proposal `9f1daf6c` with `live_changes: false`; the founder packet is ready for attended execution. Consumer follow-through remains CSOC-WP-0012-T03 and CSOC-WP-0015-T04. + +2026-10-31 founder image-release return: proposal9f1daf6c completed at07:15:24Z, +Helm revision19/image522bcd7f. Independent complete-spec/Ready/identity checks, +all80packaged source hashes, unchanged92business rows/86files/migration history, +40owner renders/40outsider denials, public endpoints and refreshed helperdcc8dd2a +all pass. Live owner values/product pins are reconciled. Fresh07:13 primary point +is mirrored, independently retrieved and restored (13tables/114rows/86files); +packaged client export verifies85files without source mutation. Full make check +441passes, zero skips. Release decision75b94bb6 is resolved only for that exact +execution; RAPPS-WP-0015-T05 is complete. Productive flow and service/offboarding +acceptance remain open. Return: docs/release/identity-export-release-return-2026-10-31.md. From 904dcb3ce32c446fe4236d3e2c5e42b409a5fa7b Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 12:44:59 +0100 Subject: [PATCH 05/16] Keep future Coulomb release evidence directories private Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- WORK-RECORDS.md | 1 + tests/test_coulomb_image_release.py | 44 +++++++++++++++++++ tools/coulomb_image_release_node.py | 33 +++++++++++++- ...P-0015-coulomb-personal-content-release.md | 23 ++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index c255f81..1a75a6c 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -42,6 +42,7 @@ | task | RAPPS-WP-0015-T03 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/tests/test_coulomb_image_release.py b/tests/test_coulomb_image_release.py index e733c02..703326e 100644 --- a/tests/test_coulomb_image_release.py +++ b/tests/test_coulomb_image_release.py @@ -2,6 +2,8 @@ import copy import importlib.util import json +import os +import stat from pathlib import Path from types import SimpleNamespace from unittest.mock import Mock @@ -66,6 +68,48 @@ def test_mutable_staging_is_frozen_before_live_work(tmp_path, monkeypatch): assert (instance.journal / "receipt.json").is_file() +def test_nested_capsule_and_release_root_stay_private_under_normal_umask(tmp_path, monkeypatch): + root, _ = capsule(tmp_path) + nested = root / "charts/coulomb/templates/deployment.yaml" + nested.parent.mkdir(parents=True) + nested.write_bytes(b"synthetic chart") + plan = {"files": {str(p.relative_to(root)): release.sha(p.read_bytes()) + for p in root.rglob("*") if p.is_file() and p.name != "manifest.json"}} + (root / "manifest.json").write_text(json.dumps(plan)) + monkeypatch.setattr(release.os, "geteuid", lambda: 0) + monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal") + previous = os.umask(0o022) + try: + instance = release.Release(root, "sha256:" + release.sha(release.canonical(plan))) + finally: + os.umask(previous) + for path in [tmp_path / "journal", *(tmp_path / "journal").rglob("*")]: + assert stat.S_IMODE(path.stat().st_mode) == (0o700 if path.is_dir() else 0o600) + assert (instance.root / nested.relative_to(root)).read_bytes() == b"synthetic chart" + + +@pytest.mark.parametrize("problem", ["broad", "link", "ancestor_link", "file"]) +def test_unsafe_release_parent_is_refused_without_changing_it(tmp_path, monkeypatch, problem): + root, digest = capsule(tmp_path) + journal = tmp_path / "journal" + if problem == "broad": + journal.mkdir(mode=0o755) + elif problem == "file": + journal.write_text("untouched") + elif problem == "link": + journal.symlink_to(root, target_is_directory=True) + else: + alias = tmp_path / "alias" + alias.symlink_to(root, target_is_directory=True) + journal = alias / "journal" + monkeypatch.setattr(release.os, "geteuid", lambda: 0) + monkeypatch.setattr(release, "JOURNAL_ROOT", journal) + before = {str(p): p.lstat() for p in tmp_path.rglob("*")} + with pytest.raises((release.Refused, OSError)): + release.Release(root, digest) + assert before == {str(p): p.lstat() for p in tmp_path.rglob("*")} + + @pytest.mark.parametrize("field", ["business_rows_sha256", "content_files_sha256", "migration_rows_sha256"]) def test_dataset_drift_is_not_accepted(field): instance = object.__new__(release.Release) diff --git a/tools/coulomb_image_release_node.py b/tools/coulomb_image_release_node.py index d84b525..10381ea 100644 --- a/tools/coulomb_image_release_node.py +++ b/tools/coulomb_image_release_node.py @@ -16,6 +16,7 @@ import subprocess import tempfile import time import urllib.request +from contextlib import ExitStack from datetime import UTC, datetime from pathlib import Path @@ -35,6 +36,33 @@ class Refused(RuntimeError): pass +def private_directories(root, relative): + """Create every component privately; never repair existing unsafe state.""" + if not root.is_absolute() or relative.is_absolute() or ".." in relative.parts: + raise Refused("private_directory_path_required") + with ExitStack() as stack: + def opened(name, parent=None): + fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=parent) + stack.callback(os.close, fd) + return fd + + fd = opened("/") + for component in root.parts[1:]: + fd = opened(component, fd) + for component in (*relative.parts, None): + info = os.fstat(fd) + if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700: + raise Refused("private_owned_directory_required") + if component is None: + break + try: + os.mkdir(component, mode=0o700, dir_fd=fd) + os.fsync(fd) + except FileExistsError: + pass + fd = opened(component, fd) + + class Release: def __init__(self, capsule, expected, read_only=False): self.root = capsule @@ -73,12 +101,13 @@ class Release: return if self.journal.exists(): raise Refused("prior_execution_requires_review") - self.journal.mkdir(parents=True, mode=0o700) + private_directories(JOURNAL_ROOT.parent, Path(JOURNAL_ROOT.name)) + self.journal.mkdir(mode=0o700) self.root = self.journal / "capsule" self.root.mkdir(mode=0o700) for name, data in frozen.items(): target = self.root / name - target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + private_directories(self.root, Path(name).parent) fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "wb") as stream: stream.write(data) diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 119f326..d91b2c4 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -399,3 +399,26 @@ packaged client export verifies85files without source mutation. Full make check 441passes, zero skips. Release decision75b94bb6 is resolved only for that exact execution; RAPPS-WP-0015-T05 is complete. Productive flow and service/offboarding acceptance remain open. Return: docs/release/identity-export-release-return-2026-10-31.md. + +## T06 — Preserve private custody when freezing future release evidence + +```task +id: RAPPS-WP-0015-T06 +status: done +priority: high +needs_human: false +``` + +Source correction for CSOC-WP-0012-T13 after backup installation preflight exposed +0755 intermediate capsule directories from completed releases. The current image +controller creates each component with mode0700 through no-follow directory +file descriptors, checks private ownership/mode and refuses unsafe existing state. +It does not chmod historical directories or replay the completed release. Files +remain0600. The old storage capsule remains frozen; it is not a current release path. + +Validation: 24 focused release safety cases pass, including a nested chart under +umask022 and refusal of broad, symlinked, ancestor-symlinked and non-directory +parents without mutation. Ruff and git diff --check pass. Product receipt: +coulomb-social/docs/release/backup-producer-custody-validation-2026-10-31.json. +The historical eight-entry repair and recurring service acceptance remain product +T04/T09. No cluster operation occurred; future release proposals bind the new source. From 169dd3e0743a164abed4c0cc9505d61b9d678fa1 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 12:46:11 +0100 Subject: [PATCH 06/16] Record State Hub identity for release custody correction Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index d91b2c4..35fe049 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -407,6 +407,7 @@ id: RAPPS-WP-0015-T06 status: done priority: high needs_human: false +state_hub_task_id: "5357f194-ae4d-599e-9a87-08e8d585a124" ``` Source correction for CSOC-WP-0012-T13 after backup installation preflight exposed From d3c46421b8afe79a8204d12fe4d98f47ed9539e8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 12:58:59 +0100 Subject: [PATCH 07/16] Track release delivery for the content recovery correction Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- WORK-RECORDS.md | 1 + ...P-0015-coulomb-personal-content-release.md | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 1a75a6c..00cca4b 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -43,6 +43,7 @@ | task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T07 | todo | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 35fe049..f78e10e 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -423,3 +423,32 @@ parents without mutation. Ruff and git diff --check pass. Product receipt: coulomb-social/docs/release/backup-producer-custody-validation-2026-10-31.json. The historical eight-entry repair and recurring service acceptance remain product T04/T09. No cluster operation occurred; future release proposals bind the new source. + +## T07 — Deliver the attachment crash-recovery correction + +```task +id: RAPPS-WP-0015-T07 +status: todo +priority: high +needs_human: false +``` + +Deployment-owner follow-through for CSOC-WP-0013-T05. Candidate source f306dbf is +published at `sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec`. +Only packaged pageops.py differs from verified revision19/image522bcd7f. All80 +packaged sources match the commit; Python/runtime distribution versions match; +no migration is added. Product full gate546 and packaged lifecycle29 pass. The +previous image reproduces exactly four partial-attachment recovery failures under +real child-process exits. Product evidence: docs/release/content-lifecycle-candidate- +{build,provenance,tests}-2026-10-31.json and the baseline-tests receipt. + +Prepare an exact current-target image-only packet using the existing release +controller, including its private directory fix from T06. Verify current release, +full dataset/migration fingerprints and complete candidate/previous-image recovery +checks. Bind fresh paired rollback capture, postflight and snapshot image-pin +refresh. Keep historical capsules and existing pending backup repair/installation/ +route packets intact. Then obtain the attended execution and independently verify +its durable return. Current verified revision19 remains authoritative until that +return; no image deploy is implied by publication. Productive review stays with +CSOC-WP-0013-T05/T03. Retention policy c3a191f2 is accepted; no cutoff or purge was +authorized. Do not replay the completed identity image release. From 75343cfec199dbeb3198351739fba7ef7f2231d0 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 13:00:13 +0100 Subject: [PATCH 08/16] Record deployment-owner task identity for content recovery release Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index f78e10e..c8e5e2b 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -431,6 +431,7 @@ id: RAPPS-WP-0015-T07 status: todo priority: high needs_human: false +state_hub_task_id: "8f448934-bdfe-539f-8cab-a739ec752491" ``` Deployment-owner follow-through for CSOC-WP-0013-T05. Candidate source f306dbf is From 9c748846413c6db3f13ae3e55627fe3f6ad89194 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 13:20:46 +0100 Subject: [PATCH 09/16] Prepare guarded content release after inactive backup installation Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3 --- WORK-RECORDS.md | 2 +- ...t-lifecycle-baseline-tests-2026-10-31.json | 25 +++ ...-lifecycle-candidate-build-2026-10-31.json | 119 ++++++++++++ ...cycle-candidate-provenance-2026-10-31.json | 20 ++ ...fecycle-candidate-recovery-2026-10-31.json | 65 +++++++ ...-lifecycle-candidate-tests-2026-10-31.json | 20 ++ ...ifecycle-release-row-drift-2026-10-31.json | 61 ++++++ ...t-lifecycle-release-target-2026-10-31.json | 62 +++++++ ...ntent-lifecycle-validation-2026-10-31.json | 58 ++++++ release/content-lifecycle-snapshot.py | 175 ++++++++++++++++++ release/content-lifecycle-values.yaml | 50 +++++ .../coulomb-content-lifecycle-f306dbf.json | 69 +++++++ tests/test_coulomb_image_release.py | 126 +++++++++++++ tools/coulomb-image-release.py | 27 ++- tools/coulomb_image_release_node.py | 82 ++++++-- ...P-0015-coulomb-personal-content-release.md | 31 +++- 16 files changed, 972 insertions(+), 20 deletions(-) create mode 100644 release/content-lifecycle-evidence/content-lifecycle-baseline-tests-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-candidate-build-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-candidate-provenance-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-candidate-recovery-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-candidate-tests-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-release-row-drift-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-release-target-2026-10-31.json create mode 100644 release/content-lifecycle-evidence/content-lifecycle-validation-2026-10-31.json create mode 100644 release/content-lifecycle-snapshot.py create mode 100644 release/content-lifecycle-values.yaml create mode 100644 release/coulomb-content-lifecycle-f306dbf.json diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 00cca4b..af8ac76 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -43,7 +43,7 @@ | task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | -| task | RAPPS-WP-0015-T07 | todo | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T07 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/release/content-lifecycle-evidence/content-lifecycle-baseline-tests-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-baseline-tests-2026-10-31.json new file mode 100644 index 0000000..6b3e676 --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-baseline-tests-2026-10-31.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-tests.v1", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "image_id": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "tests": 29, + "failures": 4, + "expected_baseline_regressions": true, + "failed_nodes": [ + "test_process_death_during_copy_or_move_is_recoverable[copy_page-during_first_asset]", + "test_process_death_during_copy_or_move_is_recoverable[copy_page-during_second_asset]", + "test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_first_asset]", + "test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_second_asset]" + ], + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "test_tooling_injected": true, + "application_modules_from_image_verified": true, + "production_access": false, + "junit_sha256": "edc52bd3a321d028d255b3d478020b3383d94747957ffe2985583b1f0741cb85", + "driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36", + "test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-candidate-build-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-candidate-build-2026-10-31.json new file mode 100644 index 0000000..07207bc --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-candidate-build-2026-10-31.json @@ -0,0 +1,119 @@ +{ + "schema": "coulomb.content-lifecycle-candidate-build.v1", + "completed_at": "2026-10-31T11:58:26.697591+00:00", + "source": "f306dbfa2604e4815523667740be2925c333b84e", + "tag": "forgejo.coulomb.social/coulomb/coulomb-social:f306dbfa2604", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "registry_digest_independently_verified": true, + "source_revision_label_verified": true, + "build_context": "Clean git archive restricted to Dockerfile, dockerignore, lock, project, manage.py, app and entrypoint.", + "build_inputs": [ + { + "path": "Dockerfile", + "sha256": "d0e197a18db8980eb3e51cca842d693e10ab0b4208627bba28691d10719d4a00" + }, + { + "path": ".dockerignore", + "sha256": "901c8b95d95549d0e78c1c3d6d0538bf0252c8a77e9409129e0cadfa55c7c56c" + }, + { + "path": "pyproject.toml", + "sha256": "5758c56fd1be89b973861ccc5ecbadb495e742ee13ebd7e19a221a0f119c54ee" + }, + { + "path": "uv.lock", + "sha256": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887" + }, + { + "path": "scripts/docker-entrypoint.sh", + "sha256": "70100256ca41ae6afe77078c810a8b6012472f0a741919f403f32088f98ab463" + } + ], + "runtime": { + "previous": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "candidate": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "runtime_packages_equal": true, + "python_versions_equal": true, + "django_check": { + "returncode": 0, + "output": "System check identified no issues (0 silenced)." + }, + "migration_drift": { + "returncode": 0, + "output": "No changes detected" + } + }, + "source_delta_from_revision_19": [ + "coulomb_social/apps/spaces/pageops.py" + ], + "new_database_migrations": false, + "production_deployed": false, + "live_release_pins_modified": false, + "source_full_gate_tests": 546, + "packaged_lifecycle_tests": 29, + "previous_image_expected_lifecycle_failures": 4, + "owner_release_task": "RAPPS-WP-0015-T07", + "remaining": [ + "Exact current-target release packet, paired rollback evidence and candidate/previous-image restored-store checks.", + "Attended image release and independent native postflight.", + "Productive lifecycle verdict; retention policy already accepted under c3a191f2-ce06-483f-82d7-ef8c54ea0d5e." + ] +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-candidate-provenance-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-candidate-provenance-2026-10-31.json new file mode 100644 index 0000000..60aefca --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-candidate-provenance-2026-10-31.json @@ -0,0 +1,20 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "network": "none", + "read_only_root": true, + "entrypoint_bypassed": true, + "database_contact": false, + "production_changes": false, + "comparisons": [ + { + "commit": "f306dbfa2604e4815523667740be2925c333b84e", + "packaged_file_count": 80, + "source_file_count": 80, + "different_files": 0, + "all_packaged_source_bytes_match": true + } + ], + "actual_packaged_manifest_sha256": "b5c8a16b4b3d30cc9d8a7a9f3ed61bd8cb0e182589aceb5e4127fff27a8ae482", + "dependency_or_build_reproducibility_proved": false +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-candidate-recovery-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-candidate-recovery-2026-10-31.json new file mode 100644 index 0000000..72b4365 --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-candidate-recovery-2026-10-31.json @@ -0,0 +1,65 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-recovery.v1", + "status": "packaged_recovered_client_export_and_views_verified", + "source": "f306dbfa2604e4815523667740be2925c333b84e", + "image_id": "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "restore_receipt_sha256": "966d6640e42e2f4940c66d1cd5c8a802cd950de0305572f3c8b720ee3d81ae2b", + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "e364a61570e0083e729a3f314eb707c57347200438bd98170c77b8f7fcc2b337", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "candidate_application": { + "status": "isolated_application_recovery_verified", + "recovered_spaces_with_content": 1, + "actual_pages_rendered_through_view": 40, + "restored_owner_access_verified": true, + "synthetic_viewer_reads": 1, + "synthetic_viewer_mutation_denials": 1, + "same_tenant_ungranted_page_denials": 40, + "fixture_transaction_rolled_back": true, + "production_access": false, + "external_identity_provider_used": false, + "real_browser_journey_verified": false + }, + "previous_image_application": { + "status": "isolated_application_recovery_verified", + "recovered_spaces_with_content": 1, + "actual_pages_rendered_through_view": 40, + "restored_owner_access_verified": true, + "synthetic_viewer_reads": 1, + "synthetic_viewer_mutation_denials": 1, + "same_tenant_ungranted_page_denials": 40, + "fixture_transaction_rolled_back": true, + "production_access": false, + "external_identity_provider_used": false, + "real_browser_journey_verified": false + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged_after_both_view_checks": true, + "application_source_overlay": false, + "export_archive_removed": true, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "post_date_correction_data_claimed": true, + "retained_point_captured_at": "2026-10-31T07:13:52Z", + "driver_sha256": "92b66225e5f9bdf03d0a0999b613370c42341a2ceef468f1743fb4301884cb25" +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-candidate-tests-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-candidate-tests-2026-10-31.json new file mode 100644 index 0000000..a0748e4 --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-candidate-tests-2026-10-31.json @@ -0,0 +1,20 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-tests.v1", + "source": "f306dbfa2604e4815523667740be2925c333b84e", + "image_id": "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "tests": 29, + "failures": 0, + "expected_baseline_regressions": false, + "failed_nodes": [], + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "test_tooling_injected": true, + "application_modules_from_image_verified": true, + "production_access": false, + "junit_sha256": "f2968ef1048ec9af68a187b967d558db0f6ee76e765a8106fb7fab284953511f", + "driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36", + "test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-release-row-drift-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-release-row-drift-2026-10-31.json new file mode 100644 index 0000000..8c8aeec --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-release-row-drift-2026-10-31.json @@ -0,0 +1,61 @@ +{ + "changed_columns": { + "django_session": [ + "session_key", + "session_data", + "expire_date" + ], + "members_member": [ + "updated_at", + "last_login_at" + ], + "members_user": [ + "last_login" + ] + }, + "file_changes": [], + "retained_fingerprint": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "schema": "coulomb.content-release-row-drift.v1", + "changed_tables": [ + { + "table": "django_session", + "retained_rows": 42, + "current_rows": 43, + "retained_sha256": "c958e33ec782f9f2fd1b3cceba4a64da0038fde51050f25905ae42de91ca6dde", + "current_sha256": "594e246ef6297d9be9b6e8d9eda076a5b2ab36afb9263606b719f64f51293ba6" + }, + { + "table": "members_member", + "retained_rows": 1, + "current_rows": 1, + "retained_sha256": "4d465d2b9d1e6311e505f861f2ed4b67763a545a965f9a7d0c586a3d07e3e9a3", + "current_sha256": "ab82b3f2c5e49f8634c598c9642894747f4b6fcff0f5230ac27f41939feb8640" + }, + { + "table": "members_user", + "retained_rows": 1, + "current_rows": 1, + "retained_sha256": "81ccb3a40191e910a68c101896df518a588c00231728934c4c900577f8ae4f12", + "current_sha256": "0fc7a71b4b80a71fbedce527bbd2cae3858cf6a062e5714fc231be13ff987256" + } + ], + "all_other_business_tables_equal": true, + "migration_history_equal": true, + "content_equal": true, + "retained_point": "2026-10-31T07:13:52Z", + "current_business_rows": 93, + "current_business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "private_row_values_printed": false, + "production_changes": false +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-release-target-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-release-target-2026-10-31.json new file mode 100644 index 0000000..a3e6e05 --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-release-target-2026-10-31.json @@ -0,0 +1,62 @@ +{ + "schema": "coulomb.content-release-target-observation.v1", + "observed_at": "2026-10-31T12:07:24.322343+00:00", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "c65cb691bab7054ae25de58cdb75933d383621ec4e049d2daa1b1bf52dce0bba", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "sole_ready_pvc_writer": true, + "existing_unmanaged_metadata_preserved": true, + "candidate_image_only_server_preview_verified": true, + "data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "matches_verified_revision19_data": false, + "installed_helper_sha256": { + "/opt/coulomb-social-backup/paired-backup.py": "43e119acb6a4933b4e144c349a091e074e1a91ab61dbe15a65c8264c1d47661a", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d" + }, + "timers": { + "coulomb-social-paired-backup.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "UnitFileState": "" + } + }, + "prerequisites": { + "custody": { + "exists": false + }, + "installation": { + "exists": false + }, + "route": { + "exists": false + } + }, + "production_data_modified": false, + "release_executed": false +} diff --git a/release/content-lifecycle-evidence/content-lifecycle-validation-2026-10-31.json b/release/content-lifecycle-evidence/content-lifecycle-validation-2026-10-31.json new file mode 100644 index 0000000..f426e59 --- /dev/null +++ b/release/content-lifecycle-evidence/content-lifecycle-validation-2026-10-31.json @@ -0,0 +1,58 @@ +{ + "schema": "coulomb.content-lifecycle-validation.v1", + "recorded_at": "2026-10-31T11:54:24.215512+00:00", + "status": "source_fix_verified", + "workplan_task": "CSOC-WP-0013-T05", + "bug": "A process exit during attachment creation left bytes absent from the copy/move cleanup journal; the orphan blocked retry.", + "regression_observed_before_fix": { + "baseline": "f70708839cd5ca43310f6891c8d94aaff915139c", + "operations": [ + "copy_page", + "transfer_page" + ], + "interruption": "partial first attachment flushed, immediate os._exit(86)", + "result": "both failed because the partial destination survived recovery" + }, + "fix": [ + "Refuse existing attachment entries before recording cleanup authority.", + "Persist intended new attachment name before exclusive creation under the content writer lock.", + "Flush recovery and attachment directory entries before destination publication." + ], + "focused_cases": { + "total": 29, + "abrupt_child_process_exit": 14, + "existing_destination_collision": 8, + "prior_retention_and_recovery": 7 + }, + "full_gate": { + "command": "make check", + "tests": 546, + "failures": 0, + "errors": 0, + "skipped": 0, + "warnings": 69, + "junit_sha256": "dd3d0433c8109f00f6dddd119cfdf8281915f94da2da1f5e6009c31cfe76c65b", + "log_sha256": "eb59a394c95c5307c17069874640d1655ef61c5aa7bb426ef6d21db4929f748e" + }, + "files": [ + { + "path": "coulomb_social/apps/spaces/pageops.py", + "sha256": "055ef25b5efc75798cf17ffa68739de4d4099d2cab4d041e7c7eacdfaf43803e" + }, + { + "path": "tests/test_content_recovery.py", + "sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" + } + ], + "policy": "User accepted retain-all default and separately reviewed manual cutoff; keep live page and all history of deleted pages.", + "live_purge_authorized": false, + "live_purge_executed": false, + "production_deployed": false, + "productive_acceptance": false, + "limits": [ + "Child process termination on synthetic local files is proved; physical power-loss durability is not experimentally proved.", + "Cooperating application/import writers must use the content writer lock.", + "Release delivery and productive copy/move review remain open." + ], + "policy_decision_id": "c3a191f2-ce06-483f-82d7-ef8c54ea0d5e" +} diff --git a/release/content-lifecycle-snapshot.py b/release/content-lifecycle-snapshot.py new file mode 100644 index 0000000..de340de --- /dev/null +++ b/release/content-lifecycle-snapshot.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Node-side quiesced coulomb-social snapshot. Binary output stays private. + +Run on railiance01 after reviewed live approval. Does not restore production. +Default is server dry-run; --execute briefly pauses the app and captures a +paired pg_dump/content archive. A separate isolated restore must validate it. +""" +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import signal +import stat +import subprocess +import sys +import time +from pathlib import Path + +NS = 'coulomb-social' +IMAGE = ('forgejo.coulomb.social/coulomb/coulomb-social@sha256:' + 'd3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec') +CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' +LOCK_PATH = Path('/var/lib/coulomb-social-backup/snapshot.lock') + + +class Refused(RuntimeError): + pass + + +def run(args: list[str], *, payload: bytes | None = None, timeout: int = 180) -> bytes: + p = subprocess.run(args, input=payload, capture_output=True, timeout=timeout) + if p.returncode: + raise Refused('command_failed') + return p.stdout + + +def k(*args: str, payload: bytes | None = None) -> bytes: + return run(['kubectl', *args], payload=payload) + + +def document(*args: str) -> dict: + return json.loads(k(*args, '-o', 'json')) + + +def private_write(path: Path, value: bytes) -> None: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, 'wb') as f: + f.write(value) + + +def snapshot() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--execute', action='store_true') + parser.add_argument('--destination', type=Path, required=True) + args = parser.parse_args() + os.environ['KUBECONFIG'] = '/etc/rancher/k3s/k3s.yaml' + started = time.monotonic() + if document('get', 'namespace', 'kube-system')['metadata']['uid'] != CLUSTER_UID: + raise Refused('wrong_cluster') + dep = document('get', 'deployment', 'coulomb-social', '-n', NS) + spec = dep['spec']['template']['spec'] + if dep['spec']['replicas'] != 1 or spec['containers'][0]['image'] != IMAGE: + raise Refused('unexpected_release') + if document('get', 'pvc', 'coulomb-social-content', '-n', NS)['status']['phase'] != 'Bound': + raise Refused('pvc_not_bound') + cluster = document('get', 'cluster', 'apps-pg', '-n', 'databases') + primary = cluster['status']['currentPrimary'] + name = 'csoc-paired-snapshot-' + time.strftime('%Y%m%d%H%M%S', time.gmtime()) + pod = { + 'apiVersion': 'v1', 'kind': 'Pod', + 'metadata': {'name': name, 'namespace': NS, 'labels': {'csoc-recovery': name}}, + 'spec': { + 'restartPolicy': 'Never', 'automountServiceAccountToken': False, + 'securityContext': {'runAsNonRoot': True, 'runAsUser': 10001, + 'runAsGroup': 10001, 'fsGroup': 10001, + 'fsGroupChangePolicy': 'OnRootMismatch'}, + 'containers': [{'name': 'snapshot', 'image': IMAGE, + 'command': ['python', '-c', 'import time;time.sleep(1800)'], + 'resources': {'requests': {'cpu': '10m', 'memory': '64Mi'}, + 'limits': {'cpu': '100m', 'memory': '256Mi'}}, + 'securityContext': {'allowPrivilegeEscalation': False, + 'readOnlyRootFilesystem': True, + 'capabilities': {'drop': ['ALL']}}, + 'volumeMounts': [{'name': 'content', 'mountPath': '/snapshot', 'readOnly': True}]}], + 'volumes': [{'name': 'content', 'persistentVolumeClaim': + {'claimName': 'coulomb-social-content', 'readOnly': True}}], + }, + } + body = json.dumps(pod).encode() + k('create', '--dry-run=server', '-f', '-', payload=body) + if not args.execute: + print(json.dumps({'status': 'reviewed', 'server_dry_run': True, + 'image_pinned': True, 'pvc_read_only': True, + 'db_scope': 'coulomb_social_db', 'replicas_restored_to': 1})) + return 0 + dest = args.destination + if not dest.is_absolute() or dest.exists(): + raise Refused('destination_must_be_new_absolute_directory') + dest.mkdir(mode=0o700) + paused = False + created = False + receipt = {'status': 'incomplete', 'app': 'coulomb-social', + 'database': 'coulomb_social_db', 'image': IMAGE, + 'started_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} + try: + paused = True + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=0') + k('wait', '--for=delete', 'pod', '-n', NS, + '-l', 'app.kubernetes.io/instance=coulomb-social', '--timeout=120s') + created = True + k('create', '-f', '-', payload=body) + k('wait', '--for=condition=Ready', 'pod/' + name, '-n', NS, '--timeout=120s') + dump = k('exec', '-n', 'databases', primary, '-c', 'postgres', '--', + 'pg_dump', '-U', 'postgres', '-d', 'coulomb_social_db', '-Fc') + if not dump.startswith(b'PGDMP'): + raise Refused('invalid_pg_dump') + private_write(dest / 'database.dump', dump) + archive_code = ('import io,sys,tarfile; b=io.BytesIO(); ' + 't=tarfile.open(fileobj=b,mode="w:gz"); ' + 't.add("/snapshot",arcname="content"); ' + 't.close();sys.stdout.buffer.write(b.getvalue())') + content = k('exec', '-n', NS, name, '--', 'python', '-c', archive_code) + private_write(dest / 'content.tar.gz', content) + receipt.update(status='captured', writers_quiesced=True, + database_sha256=hashlib.sha256(dump).hexdigest(), + content_sha256=hashlib.sha256(content).hexdigest(), + database_bytes=len(dump), content_bytes=len(content)) + finally: + try: + if created: + k('delete', 'pod', name, '-n', NS, '--wait=true', '--timeout=120s') + finally: + if paused: + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=1') + k('rollout', 'status', 'deployment/coulomb-social', '-n', NS, '--timeout=180s') + receipt['app_resumed'] = True + receipt['elapsed_seconds'] = round(time.monotonic() - started, 2) + private_write(dest / 'receipt.json', json.dumps(receipt, indent=2).encode()) + print(json.dumps(receipt)) + return 0 + + +def main() -> int: + if '--execute' not in sys.argv: + return snapshot() + LOCK_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + parent = LOCK_PATH.parent.lstat() + if (not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.geteuid() + or parent.st_mode & 0o077): + raise Refused('lock_directory_not_private') + fd = os.open(LOCK_PATH, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'w') as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused('snapshot_already_running') from None + def terminate(signum, frame): + raise Refused('snapshot_interrupted') + previous = signal.signal(signal.SIGTERM, terminate) + try: + return snapshot() + finally: + signal.signal(signal.SIGTERM, previous) + + +if __name__ == '__main__': + try: + sys.exit(main()) + except Exception as exc: + # Never expose child output, kubectl objects, credentials or raw exception messages. + print(json.dumps({'status': 'failed', 'failure_type': type(exc).__name__})) + sys.exit(1) diff --git a/release/content-lifecycle-values.yaml b/release/content-lifecycle-values.yaml new file mode 100644 index 0000000..3c0e2f6 --- /dev/null +++ b/release/content-lifecycle-values.yaml @@ -0,0 +1,50 @@ +# Production overrides for coulomb-social. +# Non-secret values only. Secrets live in K8s Secret coulomb-social-env. + +image: + repository: forgejo.coulomb.social/coulomb/coulomb-social + tag: "f306dbfa2604" + digest: "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec" + +# Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). +# Apex cutover is deferred until data + self-registration are ready. +env: + DJANGO_SETTINGS_MODULE: coulomb_social.settings.prod + ALLOWED_HOSTS: app.coulomb.social,localhost + CSRF_TRUSTED_ORIGINS: https://app.coulomb.social + CONTENT_ROOT: /app/var/content + DEFAULT_TENANT_ID: "tenant:coulomb" + OIDC_ENABLED: "true" + OIDC_ISSUER: https://kc.coulomb.social + OIDC_CLIENT_ID: coulomb-social + OIDC_REDIRECT_URI: https://app.coulomb.social/auth/callback/ + OIDC_SCOPES: "openid profile email groups" + USER_ENGINE_BASE_URL: https://users.coulomb.social + USER_ENGINE_APPLICATION_ID: coulomb-social + USER_ENGINE_EXPECTED_AUDIENCE: user-engine-portal + NETKINGDOM_REGISTRATION_URL: https://users.92-205-62-239.nip.io/register + +probes: + hostHeader: app.coulomb.social + path: /healthz + +# One writable local-path volume; Recreate prevents concurrent old/new writers. +replicaCount: 1 +deploymentStrategy: Recreate +persistence: + content: + enabled: true + storageClass: local-path + size: 2Gi + mountPath: /app/var/content + retain: true + +# Wave-2 right-sizing (CSOC-WP-0017-T01, 2026-10-21 node budget): 7d p95 +# 0.4m CPU / 146Mi. Limits stay at the chart defaults (1000m/1Gi). +resources: + requests: + cpu: 15m + memory: 176Mi + limits: + cpu: 1000m + memory: 1Gi diff --git a/release/coulomb-content-lifecycle-f306dbf.json b/release/coulomb-content-lifecycle-f306dbf.json new file mode 100644 index 0000000..9937810 --- /dev/null +++ b/release/coulomb-content-lifecycle-f306dbf.json @@ -0,0 +1,69 @@ +{ + "schema": "coulomb.reviewed-image-release.v1", + "proposal_id": "CSOC-CONTENT-LIFECYCLE-20261031", + "scope": "ADMINISTER @ realm:kubernetes/railiance01; supervised founder execution only", + "operation": "image_only_existing_migration_0003", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "candidate_render_sha256": "1606725273089b83689850ad1c25d1834faa15e00c8cf8aa24e1053273af71bc", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "c65cb691bab7054ae25de58cdb75933d383621ec4e049d2daa1b1bf52dce0bba", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec", + "product_source": "f306dbfa2604e4815523667740be2925c333b84e", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "public_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "installed_helper_hashes": { + "/opt/coulomb-social-backup/paired-backup.py": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "/opt/coulomb-social-backup/mirror-paired-backup.py": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "/opt/coulomb-social-backup/check-paired-backup-health.py": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "/opt/coulomb-social-backup/backup_capacity.py": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "/opt/railiance-platform/scripts/application_recovery_identities.py": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "/opt/railiance-platform/scripts/application_recovery_mirror.py": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "/opt/railiance-platform/scripts/application_recovery_resources.py": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "/opt/coulomb-social-backup/backup_alerts.py": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "/opt/coulomb-social-backup/notify-backup-alert.py": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "/opt/coulomb-social-backup/observe-backup-alerts.py": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664" + }, + "clipboard_static_sha256": "347eabe0c8d93bf439df02b5a08a7ea71004600e6ff4e8a42cecbee52a9f0413", + "fresh_backup_before_helm": true, + "requires_dataset_equality_before_and_after": true, + "node_request_percent_limit": 90, + "helm_atomic_timeout_seconds": 300, + "rollback": "Restore Helm revision19/image522bcd7f and verified snapshot pin; preserve all post-capture data. No production data restore or schema downgrade.", + "approval_status": "prepared_not_admitted", + "recovery_rehearsal_scope": "Retained independently recovered 2026-10-31T07:13:52Z point; candidate and previous image preserve all 13 source tables and 86 files. Live observation now has93 business rows versus92 in that point; exact fresh paired backup/readback is required before Helm. Independent restore of the new point remains consumer follow-through. Read-only drift comparison confines differences to sessions and user/member login timestamps; all content and migration history match the retained point.", + "rehearsal_evidence_sha256": { + "content-lifecycle-candidate-build-2026-10-31.json": "83ad2ec41c85f8bb5087a250fdb91126ed92f12351cdc72b353c6f0e1a26452b", + "content-lifecycle-candidate-provenance-2026-10-31.json": "35ed909e0a2fa5c57cea1fcc4fbebb877eae559ca9dc753c3da00f84806d7b73", + "content-lifecycle-candidate-tests-2026-10-31.json": "27a75cfee6bbda41f377b73d6a6c7ecde0c54806c113f1be5223dd2f2cf39b3b", + "content-lifecycle-baseline-tests-2026-10-31.json": "de14e4e10337990ccec1c483f7d943bdc7e11b03aa813aff47d00a9b38a07b65", + "content-lifecycle-candidate-recovery-2026-10-31.json": "8d15172e5e330d01f153336e67f6bffb4363d571530c31ea05db872ca10807a9", + "content-lifecycle-validation-2026-10-31.json": "cbb093af470588ca6e8eb3ec5bc1ccee0be537ab37a46625b6ac267c17343d99", + "content-lifecycle-release-target-2026-10-31.json": "e9b50be3c8284c5a9ae96d75b800cbc4090580fa811c2ecd098d73ce8cbee74e", + "content-lifecycle-release-row-drift-2026-10-31.json": "b10f65bf55e900199ae6037767f32e8901aaccb6f7f3a86461f13dd02ec8efce" + }, + "new_resources_committed": 0, + "autopilot_or_eur_budget_claimed": false, + "values_file": "release/content-lifecycle-values.yaml", + "snapshot_file": "release/content-lifecycle-snapshot.py", + "evidence_directory": "release/content-lifecycle-evidence", + "prerequisite_installation_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "requires_inactive_backup_units": true +} diff --git a/tests/test_coulomb_image_release.py b/tests/test_coulomb_image_release.py index 703326e..d4e5fae 100644 --- a/tests/test_coulomb_image_release.py +++ b/tests/test_coulomb_image_release.py @@ -236,3 +236,129 @@ def test_known_candidate_helper_can_be_rolled_back(tmp_path, monkeypatch): # A completed inverse can be checked again without changing it. instance.restore_pin(b"old pin") assert pin.read_bytes() == b"old pin" + + +def test_missing_installation_refuses_before_execution_journal(tmp_path, monkeypatch): + root, _ = capsule(tmp_path) + plan = json.loads((root / 'manifest.json').read_text()) + plan['prerequisite_installation_digest'] = 'sha256:' + 'a' * 64 + (root / 'manifest.json').write_text(json.dumps(plan)) + digest = 'sha256:' + release.sha(release.canonical(plan)) + monkeypatch.setattr(release.os, 'geteuid', lambda: 0) + monkeypatch.setattr(release, 'JOURNAL_ROOT', tmp_path / 'journal') + monkeypatch.setattr(release, 'INSTALL_RECEIPT', tmp_path / 'missing.json') + with pytest.raises(release.Refused, match='installation_required_before_image'): + release.Release(root, digest) + assert not (tmp_path / 'journal').exists() + + +@pytest.fixture +def installed(monkeypatch): + instance = object.__new__(release.Release) + digest = 'sha256:' + 'a' * 64 + instance.plan = {'prerequisite_installation_digest': digest} + receipt = {'schema': 'coulomb.backup-source-supervised-installation.v1', + 'proposal_digest': digest, 'status': 'sources_installed_units_inactive_verified', + 'verified_targets': 19, 'timers_activated': False} + path = SimpleNamespace(parents=[], lstat=lambda: SimpleNamespace( + st_mode=stat.S_IFREG | 0o600, st_uid=0, st_nlink=1), + read_bytes=lambda: json.dumps(receipt).encode()) + monkeypatch.setattr(release, 'INSTALL_RECEIPT', path) + state = {'active': 'inactive', 'enabled': 'disabled', 'jobs': b''} + def command(arguments): + if arguments[1] == 'list-jobs': + return SimpleNamespace(stdout=state['jobs']) + return SimpleNamespace(stdout=(f"LoadState=loaded\nActiveState={state['active']}\n" + f"UnitFileState={state['enabled']}\n").encode()) + instance.command = Mock(side_effect=command) + return instance, receipt, state + + +def test_completed_installer_and_inactive_units_admit_release_prerequisite(installed): + instance, _, _ = installed + instance.verify_installation_prerequisite() + assert instance.command.call_count == 5 + + +@pytest.mark.parametrize('field,value', [ + ('status', 'installation_requires_inspection'), ('verified_targets', 18), + ('proposal_digest', 'sha256:' + 'b' * 64), ('timers_activated', True), +]) +def test_wrong_or_incomplete_installation_never_checks_or_changes_units(installed, field, value): + instance, receipt, _ = installed + receipt[field] = value + with pytest.raises(release.Refused, match='verified_inactive_backup_installation'): + instance.verify_installation_prerequisite() + instance.command.assert_not_called() + + +@pytest.mark.parametrize('field,value', [('active', 'active'), ('enabled', 'enabled'), + ('jobs', b'17 coulomb-social-paired-backup.service start waiting\n'), + ('jobs', b'unrecognized job format\n')]) +def test_active_enabled_or_queued_backup_refuses_image_release(installed, field, value): + instance, _, state = installed + state[field] = value + with pytest.raises(release.Refused): + instance.verify_installation_prerequisite() + assert all(c.args[0][1] in {'show', 'list-jobs'} for c in instance.command.call_args_list) + + +def test_an_explicit_release_plan_seals_its_own_artifacts(tmp_path, monkeypatch): + monkeypatch.setattr(launcher, 'ROOT', tmp_path) + paths = ['tools/coulomb-image-release.py', 'tools/coulomb_image_release_node.py', + 'tools/coulomb_image_probe.py', 'release/next-values.yaml', + 'release/next-snapshot.py', 'charts/coulomb-social/Chart.yaml', + 'release/next-evidence/proof.json'] + for name in paths: + path = tmp_path / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(('synthetic ' + name).encode()) + plan = {'values_file': paths[3], 'snapshot_file': paths[4], + 'evidence_directory': 'release/next-evidence', + 'rehearsal_evidence_sha256': {'proof.json': release.sha((tmp_path / paths[-1]).read_bytes())}} + path = tmp_path / 'release/next.json' + path.write_text(json.dumps(plan)) + prepared, digest, content = launcher.prepare(path) + assert set(content) == set(paths) | {'manifest.json'} + assert digest == 'sha256:' + release.sha(release.canonical(prepared)) + plan['values_file'] = '../outside.yaml' + path.write_text(json.dumps(plan)) + with pytest.raises(RuntimeError, match='release_paths'): + launcher.prepare(path) + + +@pytest.mark.parametrize('name', ['release/missing.yaml', '../outside.yaml', '/outside.yaml']) +def test_node_refuses_unsealed_artifact_reference_before_journal(tmp_path, monkeypatch, name): + root, _ = capsule(tmp_path) + plan = json.loads((root / 'manifest.json').read_text()) + plan['values_file'] = name + (root / 'manifest.json').write_text(json.dumps(plan)) + monkeypatch.setattr(release.os, 'geteuid', lambda: 0) + monkeypatch.setattr(release, 'JOURNAL_ROOT', tmp_path / 'journal') + with pytest.raises(release.Refused, match='reviewed_release_artifact'): + release.Release(root, 'sha256:' + release.sha(release.canonical(plan))) + assert not (tmp_path / 'journal').exists() + + +def test_initializer_refusal_is_structured_and_never_calls_execute(monkeypatch, capsys): + monkeypatch.setattr(release, 'Release', Mock(side_effect=release.Refused( + 'inactive_backup_installation_required_before_image_release'))) + monkeypatch.setattr(release.argparse.ArgumentParser, 'parse_args', lambda _: SimpleNamespace( + capsule=Path('/synthetic'), expect_digest='sha256:' + 'a' * 64, + execute=False, preflight=True)) + with pytest.raises(SystemExit): + release.main() + result = json.loads(capsys.readouterr().out) + assert result['execution_started'] is False + assert result['failure_reason'] == 'inactive_backup_installation_required_before_image_release' + + +def test_custom_snapshot_artifact_is_used_for_recognized_pin_rollback(tmp_path, monkeypatch): + path = tmp_path / 'snapshot.py' + path.write_bytes(b'new reviewed pin') + monkeypatch.setattr(release, 'SNAPSHOT', path) + instance = object.__new__(release.Release) + instance.plan = {'snapshot_file': 'release/next-snapshot.py', + 'files': {'release/next-snapshot.py': release.sha(path.read_bytes())}} + instance.restore_pin(b'previous pin') + assert path.read_bytes() == b'previous pin' diff --git a/tools/coulomb-image-release.py b/tools/coulomb-image-release.py index 1b2efc7..308adb0 100644 --- a/tools/coulomb-image-release.py +++ b/tools/coulomb-image-release.py @@ -26,20 +26,32 @@ def canonical(value): return json.dumps(value, sort_keys=True, separators=(",", ":")).encode() -def prepare(): - plan = json.loads(PLAN.read_text()) +def prepare(plan_path=None): + plan_path = plan_path or PLAN + if plan_path.is_symlink() or not plan_path.resolve().is_relative_to(ROOT.resolve()): + raise RuntimeError("reviewed_plan_inside_owner_repository_required") + plan = json.loads(plan_path.read_text()) + values = plan.get("values_file", "release/identity-export-values.yaml") + snapshot = plan.get("snapshot_file", "release/identity-export-snapshot.py") + evidence = plan.get("evidence_directory", "release/identity-export-evidence") + for name in (values, snapshot, evidence): + if (not isinstance(name, str) or Path(name).is_absolute() + or ".." in Path(name).parts or not name.startswith("release/")): + raise RuntimeError("reviewed_release_paths_required") + if not all((ROOT / name).is_file() for name in (values, snapshot)): + raise RuntimeError("reviewed_release_artifacts_missing") paths = [ ROOT / name for name in ( "tools/coulomb-image-release.py", "tools/coulomb_image_release_node.py", "tools/coulomb_image_probe.py", - "release/identity-export-values.yaml", - "release/identity-export-snapshot.py", + values, + snapshot, ) ] paths += sorted((ROOT / "charts/coulomb-social").rglob("*")) - paths += sorted((ROOT / "release/identity-export-evidence").glob("*.json")) + paths += sorted((ROOT / evidence).glob("*.json")) paths = [p for p in paths if p.is_file()] if any(p.is_symlink() or not p.resolve().is_relative_to(ROOT) for p in paths): raise RuntimeError("regular_reviewed_files_required") @@ -48,7 +60,7 @@ def prepare(): name: hashlib.sha256(data).hexdigest() for name, data in content.items() } for name, expected in plan["rehearsal_evidence_sha256"].items(): - if plan["files"].get("release/identity-export-evidence/" + name) != expected: + if plan["files"].get(evidence + "/" + name) != expected: raise RuntimeError("reviewed_candidate_evidence_changed") digest = "sha256:" + hashlib.sha256(canonical(plan)).hexdigest() content["manifest.json"] = canonical(plan) @@ -61,10 +73,11 @@ def main(): parser.add_argument("--execute", action="store_true") parser.add_argument("--expect-digest") parser.add_argument("--output", type=Path) + parser.add_argument("--plan", type=Path, default=PLAN) args = parser.parse_args() if args.preflight and args.execute: raise SystemExit("choose read-only preflight or attended execution") - plan, digest, content = prepare() + plan, digest, content = prepare(args.plan) if args.expect_digest and args.expect_digest != digest: raise SystemExit("reviewed proposal changed; do not execute") if not args.preflight and not args.execute: diff --git a/tools/coulomb_image_release_node.py b/tools/coulomb_image_release_node.py index 10381ea..df49bc9 100644 --- a/tools/coulomb_image_release_node.py +++ b/tools/coulomb_image_release_node.py @@ -22,6 +22,9 @@ from pathlib import Path JOURNAL_ROOT = Path("/var/lib/coulomb-social-backup/releases") SNAPSHOT = Path("/opt/coulomb-social-backup/snapshot-content-and-db.py") +INSTALL_RECEIPT = Path( + "/var/lib/coulomb-social-backup-installations/20261031bacc0001/receipt.json" +) def canonical(value): @@ -88,6 +91,12 @@ class Release: raise Refused("attended_founder_root_execution_required") self.env = dict(os.environ, KUBECONFIG="/etc/rancher/k3s/k3s.yaml") self.read_only = read_only + for field in ("values_file", "snapshot_file"): + if field in self.plan: + name = self.plan[field] + if (not isinstance(name, str) or not name.startswith("release/") + or ".." in Path(name).parts or name not in frozen): + raise Refused("reviewed_release_artifact_required") self.receipt = { "schema": "coulomb.guarded-image-release.v1", "proposal_digest": expected, @@ -97,6 +106,7 @@ class Release: "timers_activated": False, } self.journal = JOURNAL_ROOT / expected.removeprefix("sha256:") + self.verify_installation_prerequisite() if read_only: return if self.journal.exists(): @@ -215,7 +225,52 @@ class Release: or actual.get("migration_0003_applied") is not True): raise Refused("already_migrated_baseline_required") + def artifact_name(self, field): + return self.plan.get(field, { + "values_file": "release/identity-export-values.yaml", + "snapshot_file": "release/identity-export-snapshot.py", + }[field]) + + def verify_installation_prerequisite(self): + expected = self.plan.get("prerequisite_installation_digest") + if expected is None: + return + if not re.fullmatch(r"sha256:[a-f0-9]{64}", expected): + raise Refused("reviewed_installation_digest_required") + try: + info = INSTALL_RECEIPT.lstat() + except FileNotFoundError: + raise Refused("inactive_backup_installation_required_before_image_release") from None + if (not stat.S_ISREG(info.st_mode) or info.st_uid != 0 + or info.st_mode & 0o077 or info.st_nlink != 1 + or any(p.is_symlink() for p in INSTALL_RECEIPT.parents)): + raise Refused("private_installation_receipt_required") + receipt = json.loads(INSTALL_RECEIPT.read_bytes()) + if (receipt.get("schema") != "coulomb.backup-source-supervised-installation.v1" + or receipt.get("proposal_digest") != expected + or receipt.get("status") != "sources_installed_units_inactive_verified" + or receipt.get("verified_targets") != 19 + or receipt.get("timers_activated") is not False): + raise Refused("verified_inactive_backup_installation_required") + for name in ("paired-backup", "backup-freshness"): + for suffix in ("service", "timer"): + unit = f"coulomb-social-{name}.{suffix}" + raw = self.command(["systemctl", "show", unit, + "--property=LoadState,ActiveState,UnitFileState"]).stdout + state = dict(line.split("=", 1) for line in raw.decode().splitlines()) + if (state.get("LoadState") != "loaded" or state.get("ActiveState") != "inactive" + or state.get("UnitFileState") not in {"disabled", "static"}): + raise Refused("backup_units_must_remain_inactive_for_image_release") + jobs = self.command(["systemctl", "list-jobs", "--no-legend", "--no-pager", + "--plain"]).stdout.decode().splitlines() + for line in jobs: + fields = line.split() + if (len(fields) != 4 or not fields[0].isdecimal() + or fields[1].startswith("coulomb-social-")): + raise Refused("queued_backup_job_requires_review") + def preflight(self): + self.verify_installation_prerequisite() self.verify_installed_helpers() if ( self.kdoc("get", "namespace", "kube-system")["metadata"]["uid"] @@ -301,7 +356,7 @@ class Release: "coulomb-social", str(self.root / "charts/coulomb-social"), "-f", - str(self.root / "release/identity-export-values.yaml"), + str(self.root / self.artifact_name("values_file")), "-n", "coulomb-social", ] @@ -319,7 +374,7 @@ class Release: "coulomb-social", str(self.root / "charts/coulomb-social"), "-f", - str(self.root / "release/identity-export-values.yaml"), + str(self.root / self.artifact_name("values_file")), "-n", "coulomb-social", "--dry-run=server", @@ -386,7 +441,7 @@ class Release: path = SNAPSHOT if ( sha(path.read_bytes()) - == self.plan["files"]["release/identity-export-snapshot.py"] + == self.plan["files"][self.artifact_name("snapshot_file")] ): self.replace_pin(path, old) elif sha(path.read_bytes()) != sha(old): @@ -424,7 +479,7 @@ class Release: "coulomb-social", str(self.root / "charts/coulomb-social"), "-f", - str(self.root / "release/identity-export-values.yaml"), + str(self.root / self.artifact_name("values_file")), "-n", "coulomb-social", "--atomic", @@ -461,7 +516,7 @@ class Release: ): raise Refused("snapshot_pin_drifted") self.replace_pin( - path, (self.root / "release/identity-export-snapshot.py").read_bytes() + path, (self.root / self.artifact_name("snapshot_file")).read_bytes() ) self.command( [ @@ -526,8 +581,9 @@ def main(): return if args.execute and args.preflight: raise SystemExit("choose read-only preflight or attended execution") - release = Release(args.capsule, args.expect_digest, read_only=args.preflight) + release = None try: + release = Release(args.capsule, args.expect_digest, read_only=args.preflight) if args.preflight: release.preflight() print( @@ -542,11 +598,15 @@ def main(): return print(json.dumps(release.execute())) except Exception as error: # noqa: BLE001 -- private child diagnostics are suppressed - release.receipt.update( - status="release_refused_or_failed", failure_type=type(error).__name__ - ) - release.save() - print(json.dumps(release.receipt)) + receipt = release.receipt if release is not None else { + "proposal_digest": args.expect_digest, "execution_started": False, + } + receipt.update(status="release_refused_or_failed", failure_type=type(error).__name__) + if isinstance(error, Refused): + receipt["failure_reason"] = str(error) + if release is not None: + release.save() + print(json.dumps(receipt)) raise SystemExit(1) from None diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index c8e5e2b..472fc6f 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -428,9 +428,13 @@ T04/T09. No cluster operation occurred; future release proposals bind the new so ```task id: RAPPS-WP-0015-T07 -status: todo +status: wait priority: high needs_human: false +wait_kind: external +blocking_reason: "Prepared image packet c9c32316 waits for CSOC-WP-0012-T08 inactive installation8f442bf0; native preflight confirms its receipt is absent." +depends_on: +- CSOC-WP-0012-T08 state_hub_task_id: "8f448934-bdfe-539f-8cab-a739ec752491" ``` @@ -453,3 +457,28 @@ its durable return. Current verified revision19 remains authoritative until that return; no image deploy is implied by publication. Productive review stays with CSOC-WP-0013-T05/T03. Retention policy c3a191f2 is accepted; no cutoff or purge was authorized. Do not replay the completed identity image release. + +2026-10-31 preparation return: exact20-member proposal +`sha256:c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08` +is prepared with a separate plan/values/snapshot/evidence set. The existing launcher +accepts an explicit reviewed plan; historical frozen capsules stay intact.40 +controller tests, Ruff and Helm lint pass. Native image-only server preview preserves +the complete current specification and unmanaged metadata. Candidate/previous-image +checks on the independent07:13 recovery point render40owner pages, deny40outsiders, +verify85export files, and preserve all13tables/86files. Current target remains +revision19;93business rows reflect a new session plus login timestamp changes. +Content and migration history match the retained point; private values are not exposed. + +Ordering is material: the pending installer8f442bf0 pins snapshotdcc8dd2a. Deploying +this image first would stale that reviewed packet. The new controller therefore +requires that exact completed installation receipt and currently inactive/disabled +backup units before creating its release journal, then pins all12 installed Python +helpers. Native full preflight correctly refuses the absent installation receipt; +it is not a pass and no image execution occurred. Historical custodyfa2763d1, +route4d848106 and installer8f442bf0 packets are unchanged. After their returns, +re-run the full image preflight before admitting execution; finish native service +proof/schedule activation afterward. Consumer review commands and exact scope: +coulomb-social/docs/usecases/sharing-portability-review.md, lifecycle release section. +Evidence: content-lifecycle-release-{target,row-drift,preflight,proposal,preparation} +JSON files in the consumer docs/release directory. T07 is now external wait for T08; +product policy is settled, and productive lifecycle acceptance remains open. From c733e86b0fac4802445484233d962181eb57950c Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 14:19:03 +0100 Subject: [PATCH 10/16] Answer NK-WP-0056-T02 owner request: rapp-bounded-executor intake Owner answer for who creates the rapp-bounded-executor repository and which Railiance rapp conventions apply. Proposal only. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 994874@bnt-lap001 Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87 --- docs/rapp-bounded-executor-intake.md | 151 +++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/rapp-bounded-executor-intake.md diff --git a/docs/rapp-bounded-executor-intake.md b/docs/rapp-bounded-executor-intake.md new file mode 100644 index 0000000..fffef84 --- /dev/null +++ b/docs/rapp-bounded-executor-intake.md @@ -0,0 +1,151 @@ +# rapp-bounded-executor intake (owner answer to NK-WP-0056-T02) + +Status: owner answer from `railiance-apps`, 2026-10-31. Proposal only. Creating +the repository, its Forgejo remote and any live deploy remain founder decisions. + +Request: `net-kingdom/docs/proposals/2026-10-31-nk56-t02-owner-requests.md` +(section "railiance-apps"), hub message `41dbcc41`. Founder decision hub +`6d1adce9`: NetKingdom writes a bounded executor runtime (standard +`net-kingdom/canon/standards/bounded-execution-admission_v0.1.md`); a new +`rapp-*` package deploys it in namespace `sso` with ServiceAccount +`nk-bind-executor`. + +## Short answer + +1. **`railiance-apps` does not create or own the repository.** Under ADR-0015 + and the Railiance family contract, a `rapp-*` is its own repository. This + repository (S5) owns shared app-release lessons and the apps it still hosts + (Path C); it is not the factory for rapp repos. +2. **Who creates it:** the founder, by admitting the creation. The tool is + `repo-manager` (`rmgr rapp wrap`), run from a workstation checkout with + `--ownership-repo net-kingdom`. `wrap` writes local files only. Creating + the Forgejo remote (`coulomb/rapp-bounded-executor`) is a separate + founder-run step. Every existing NetKingdom rapp was bootstrapped this way + (for example `rapp-user-engine`, first commit 2026-08-18, "bootstrap managed + package from NetKingdom apply home"). NetKingdom's agent may run `rmgr rapp + wrap` locally once the founder says the repo should exist. +3. **The image is not built by the rapp.** Correction to the request wording + ("rapp-bounded-executor builds the image"). The fleet rule is that the + **app repo** builds and publishes the image digest + (`.forgejo/workflows/image.yaml`), and the rapp **pins** that digest. + Here the app repo is `net-kingdom`, unless the executor gets its own source + repository (see founder item F2). +4. **Deploy path:** `kubectl` server-side apply of digest-pinned manifests + through the rapp's Makefile (`render`, `server-dry-run`, `deploy`, + `verify-live`, `rollback`). There is no ArgoCD or other GitOps controller + in this estate. Helm is optional; the default for a new first-party rapp is + `manifest-managed-platform-service`. Live apply is founder-run. + +## Conventions that apply (with sources) + +| Topic | Convention | Source | +| --- | --- | --- | +| What a rapp is | One Railiance-managed workload; packaging, not ownership | `railiance-master/docs/repository-axes.md`, ADR-0015 | +| Declaration | `declarations/rapp.yaml`, validated against the schema; there is no `register-rapp` command | `railiance-master/schemas/rapp.schema.json`, `railiance-master/docs/adr/ADR-0007-rapp-declaration-contract.md` | +| Repo baseline | `.repo-classification.yaml`, README, INTENT, SCOPE, AGENTS, CLAUDE, LICENSE, Makefile, `declarations/rapp.yaml`, `workplans/`, `workplans/archived/` | `railiance-master/docs/repo-family-bootstrap-contract.md`; `repo-manager/docs/RailianceAppDeploymentGuide.md` section 6 | +| Greenfield path | `rmgr rapp wrap` → human review → CI digest → `rmgr rapp pin-image` → `rmgr rapp place` → operator deploy | Deployment Guide section 0 | +| Ownership split | Code, tests, Containerfile, image build: app repo. Manifests, ServiceAccount, NetworkPolicy, Makefile, smoke, digest pin: rapp. Secret values: OpenBao via platform. | Deployment Guide section 5 | +| Image | Forge registry `forgejo.coulomb.social/coulomb/@sha256:...`; never a tag; check pull with `railiance-apps/tools/check-oci-image.sh` | Deployment Guide sections 0, 9.4; `docs/s5-app-onboarding-checklist.md` | +| Reef binding | `bound_reefs: []` at `declared`; add `reef-railiance` binding in `reef-railiance/bindings/rapps.yaml` only with an explicit substrate decision | Deployment Guide 10.2; `reef-railiance/bindings/rapps.yaml` notes | +| Readiness | `draft` → `declared` → `deployed`/`verified` only with evidence | Deployment Guide section 6 table | +| Exposure | Omit (private). The executor has no Ingress. | Deployment Guide section 4 | +| Closest model | `rapp-user-engine` (manifest-managed, digest pin, `make verify-live`, `make rollback DIGEST=`) | `~/rapp-user-engine` | + +## Proposed declaration values + +| Field | Proposed value | Why | +| --- | --- | --- | +| `rapp_id` / `repo` | `rapp-bounded-executor` | working name from the request | +| `workload_identity.name` | `bounded-executor` (or `nk-bind-executor`) | must not carry `rapp-` | +| `ownership_repo` | `net-kingdom` | executor runtime is written there | +| `package_type` | `manifest-managed-platform-service` | no existing chart | +| `primary_rail` | `rail-kubernetes` | Job semantics; no scale-to-zero need | +| `bound_reefs` | `[]` until placement is admitted | | +| `data_classification` | `confidential` (consider `restricted`) | it handles directory credentials | +| `criticality` | `high` | it writes identity credentials | +| `runtime_dependencies` | `kubernetes-api`, `forgejo-oci-registry`, `openbao`, `approval-engine`, `lldap` | from the T02 request | +| `secret_references` | the OpenBao paths the secrets-engine answer grants (paths only) | | +| exposure | omitted | | + +## Workload shape: Job, not Deployment + +The standard says one admission is consumed once, before the first step. A +`Job` per admission (`backoffLimit: 0`, `activeDeadlineSeconds` equal to or +shorter than the admission window, `ttlSecondsAfterFinished`) matches that. +A long-running Deployment would hold OpenBao and directory reach with no +admission in hand. Proposed: the rapp ships a Job template and a founder-run +`make run ADMISSION_ID=` that renders the Job with the pinned digest and the +admission id and applies it. No CronJob. + +## Namespace `sso` implications + +- The `sso` Namespace object, and its namespace-wide NetworkPolicy + (`net-kingdom/sso-mfa/k8s/network-policies/netpol-sso.yaml`), currently live + in `net-kingdom`'s migration apply home. The rapp must **not** redeclare the + Namespace. It applies only its own objects in `sso`. +- The rapp ships its own pod-scoped NetworkPolicy: egress to the OpenBao + service, LLDAP, approval-engine and DNS only; no ingress. +- `sso` already hosts KeyCape, LLDAP and Authelia. An executor pod there shares + their namespace. It needs its own Pod Security settings (non-root, read-only + root filesystem, no privilege escalation, dropped capabilities) and + `automountServiceAccountToken` only if the Kubernetes Role below needs it. +- A separate namespace would isolate it more. The founder chose `sso`; record + that choice in the rapp's runtime contract. + +## ServiceAccount and RBAC ownership + +| Object | Owner | Note | +| --- | --- | --- | +| `ServiceAccount nk-bind-executor` (in `sso`) | rapp | per the ownership split | +| `Role` / `RoleBinding` in `sso` (the Kubernetes part of the ceiling) | rapp applies it; **content is set by `net-kingdom`** | the ceiling is NetKingdom canon (BEA standard). The rapp must not widen it. Default: no Kubernetes permissions at all unless a pinned step needs them. | +| OpenBao Kubernetes-auth role bound to `sso/nk-bind-executor` (15-minute TTL) and its policy | `railiance-platform` / `secrets-engine` | answer pending from secrets-engine | +| LLDAP directory account for the executor | NetKingdom / secrets-engine lane | pending | +| Approval consume scope for `service:nk-bind-executor` | approval-engine | pending | +| PDP gate (later) | access-engine | pending | + +## Proposed skeleton (not created) + +```text +rapp-bounded-executor/ + .repo-classification.yaml + README.md INTENT.md SCOPE.md AGENTS.md CLAUDE.md LICENSE + Makefile # check, render, server-dry-run, run ADMISSION_ID=, verify-live, rollback DIGEST= + declarations/rapp.yaml + manifests/ + serviceaccount.yaml # nk-bind-executor + role.yaml # ceiling from net-kingdom; empty by default + networkpolicy.yaml # pod-scoped egress allowlist + job.template.yaml # digest placeholder, admission id parameter + docs/runtime-contract.md # identity table, namespace sso decision, smoke, rollback + docs/platform-handoffs.md # OpenBao role, LLDAP account, approval scope + tools/render.py tools/verify_live.sh + tests/ # render tests, no cluster + workplans/ workplans/archived/ +``` + +And in `net-kingdom` (app repo): `Containerfile` for the executor and +`.forgejo/workflows/image.yaml` (fleet workflow) that publishes +`forgejo.coulomb.social/coulomb/bounded-executor@sha256:...`. + +## Checklist + +1. [ ] Founder: decide the repo should exist (F1) and where the executor source lives (F2). +2. [ ] Founder or NetKingdom agent: `rmgr rapp wrap --path ../rapp-bounded-executor --app bounded-executor --ownership-repo net-kingdom` (local files only). +3. [ ] Founder: create the Forgejo remote `coulomb/rapp-bounded-executor` and push. +4. [ ] NetKingdom: Containerfile and fleet `image.yaml` in the app repo; CI prints a digest. +5. [ ] `rmgr rapp pin-image --digest sha256:...`; `rmgr rapp validate --family-root ..`. +6. [ ] Rapp: ServiceAccount, Role (ceiling from NetKingdom), NetworkPolicy, Job template, Makefile, runtime contract. +7. [ ] Platform owners: OpenBao role and policy, LLDAP account, approval consume scope. +8. [ ] `make server-dry-run` against the cluster (read-only); `check-oci-image.sh` pull check. +9. [ ] Founder: `rmgr rapp place --reef reef-railiance` and a reef binding when the pilot is admitted. +10. [ ] Founder-run first `make run ADMISSION_ID=...` for the T06 pilot; evidence into the rapp. + +## Items for the founder + +- **F1** Admit creation of `rapp-bounded-executor` (local wrap plus Forgejo remote). +- **F2** Executor source home: inside `net-kingdom` (current decision) or a + separate app repo. If it stays in `net-kingdom`, that repo gains a + Containerfile and an image workflow. That is the fleet pattern, but it is new + for a canon repo. +- **F3** Job (recommended) or Deployment. +- **F4** Confirm `sso` over a dedicated namespace, accepting co-tenancy with KeyCape, LLDAP and Authelia. From dc927d775164c40a2e0f8d8ebb8a47d22182b86e Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 19:42:01 +0100 Subject: [PATCH 11/16] Prepare verified combined Coulomb attachment and content release Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a --- ...attachment-menu-validation-2026-10-31.json | 32 ++ ...llation-independent-return-2026-10-31.json | 371 ++++++++++++++++++ ...p-route-independent-return-2026-10-31.json | 29 ++ .../content-acceptance-build-2026-10-31.json | 45 +++ ...tent-acceptance-provenance-2026-10-31.json | 11 + ...ontent-acceptance-recovery-2026-10-31.json | 67 ++++ .../content-acceptance-target-2026-10-31.json | 21 + .../content-acceptance-tests-2026-10-31.json | 13 + ...t-lifecycle-baseline-tests-2026-10-31.json | 25 ++ ...ifecycle-release-row-drift-2026-10-31.json | 61 +++ ...ntent-lifecycle-validation-2026-10-31.json | 58 +++ .../ordered-list-fidelity-2026-10-31.json | 42 ++ release/content-acceptance-snapshot.py | 175 +++++++++ release/content-acceptance-values.yaml | 50 +++ .../coulomb-content-acceptance-1139a6d.json | 79 ++++ ...P-0015-coulomb-personal-content-release.md | 30 +- 16 files changed, 1105 insertions(+), 4 deletions(-) create mode 100644 release/content-acceptance-evidence/attachment-menu-validation-2026-10-31.json create mode 100644 release/content-acceptance-evidence/backup-installation-independent-return-2026-10-31.json create mode 100644 release/content-acceptance-evidence/backup-route-independent-return-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-acceptance-build-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-acceptance-provenance-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-acceptance-recovery-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-acceptance-target-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-acceptance-tests-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-lifecycle-baseline-tests-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-lifecycle-release-row-drift-2026-10-31.json create mode 100644 release/content-acceptance-evidence/content-lifecycle-validation-2026-10-31.json create mode 100644 release/content-acceptance-evidence/ordered-list-fidelity-2026-10-31.json create mode 100644 release/content-acceptance-snapshot.py create mode 100644 release/content-acceptance-values.yaml create mode 100644 release/coulomb-content-acceptance-1139a6d.json diff --git a/release/content-acceptance-evidence/attachment-menu-validation-2026-10-31.json b/release/content-acceptance-evidence/attachment-menu-validation-2026-10-31.json new file mode 100644 index 0000000..82c4af5 --- /dev/null +++ b/release/content-acceptance-evidence/attachment-menu-validation-2026-10-31.json @@ -0,0 +1,32 @@ +{ + "schema": "coulomb.attachment-menu-source-validation.v1", + "product_decision": "78175c34-58e1-4ad6-944d-0662c0462ae6", + "full_main_gate": { + "command": "TMPDIR=/var/tmp make check", + "passed": 670, + "failures": 0, + "errors": 0, + "skipped": 0, + "log_sha256": "357547d34f7d73f750721338cff3e07439e8c710d0f1cf838541c1b175268335" + }, + "isolated_release_gate": { + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "command": "make check", + "passed": 568, + "failures": 0, + "errors": 0, + "skipped": 0, + "log_sha256": "d9a9c8883f0d4021918c8600f9b5055cac24ca6b9943ee89db067f55d65ccdb1" + }, + "checks": [ + "Author upload/remove; authorized reader download; foreign-tenant, outsider, revoked, archived and anonymous denial", + "Stale page and CSRF refusal; immutable file names; 16 MiB limit; traversal and symlink denial", + "Retained body/source dates and associations through edit/rename; removal keeps historical revision bytes", + "Independent copy/move associations and managed files through 14 abrupt process deaths", + "Upload recovery at partial bytes, complete bytes, stored revision and published page", + "Real loopback browser upload/download/copy/move/remove and permission checks; no external requests" + ], + "synthetic_fixtures_only": true, + "production_deployed": false, + "productive_user_acceptance": false +} diff --git a/release/content-acceptance-evidence/backup-installation-independent-return-2026-10-31.json b/release/content-acceptance-evidence/backup-installation-independent-return-2026-10-31.json new file mode 100644 index 0000000..8417e00 --- /dev/null +++ b/release/content-acceptance-evidence/backup-installation-independent-return-2026-10-31.json @@ -0,0 +1,371 @@ +{ + "schema": "coulomb.backup-installation-independent-postflight.v1", + "verified_at": "2026-10-31T17:30:18.219708+00:00", + "status": "installed_files_and_inactive_units_verified", + "proposal_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "receipt_sha256": "96329afbd5662f71bf6d6b05c003f0810be12c10036f9cadb41cc373d3a1108b", + "receipt": { + "schema": "coulomb.backup-source-supervised-installation.v1", + "proposal_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "status": "sources_installed_units_inactive_verified", + "preflight": { + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + } + }, + "capacity": { + "state_bytes": 11259904, + "state_reserve_bytes": 7532969984, + "disk_reserve_bytes": 8606711808, + "free_bytes": 115579949056, + "free_inodes": 24523113, + "filesystem_blocks_reserved": false + }, + "sdk": { + "boto3": "1.40.18", + "botocore": "1.40.76" + }, + "route": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + } + }, + "timers_activated": false, + "capture_executed": false, + "email_sent": false, + "retained_data_deleted": false, + "started_at": "2026-10-31T17:22:48.757530+00:00", + "capacity_profile": { + "maximum_new_file_bytes": 1073741824, + "state_budget_bytes": 8589934592, + "minimum_free_bytes": 8589934592, + "minimum_free_inodes": 1024, + "control_margin_bytes": 16777216 + }, + "founder_confirmed_profile_and_installation": true, + "route_postflight": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + }, + "verified_targets": 19, + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.service", + "UnitFileState": "static" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.service", + "UnitFileState": "static" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-alert@.service", + "UnitFileState": "static" + } + }, + "updated_at": "2026-10-31T17:22:54.560675+00:00" + }, + "nested_receipt_sha256": "795d9eb672b24495c77ad636a210c5d2022c8c3124d276920db7eda02257e519", + "verified_targets": 19, + "installed_fingerprints": { + "/opt/coulomb-social-backup/paired-backup.py": { + "exists": true, + "sha256": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 534009, + "device": 2049, + "mtime_ns": 1793467368826077309 + }, + "/opt/coulomb-social-backup/snapshot-content-and-db.py": { + "exists": true, + "sha256": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549255, + "device": 2049, + "mtime_ns": 1793430923413704430 + }, + "/opt/coulomb-social-backup/mirror-paired-backup.py": { + "exists": true, + "sha256": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549201, + "device": 2049, + "mtime_ns": 1793467368831077393 + }, + "/opt/coulomb-social-backup/check-paired-backup-health.py": { + "exists": true, + "sha256": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549309, + "device": 2049, + "mtime_ns": 1793467368836077477 + }, + "/opt/coulomb-social-backup/backup_capacity.py": { + "exists": true, + "sha256": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549354, + "device": 2049, + "mtime_ns": 1793467368843077594 + }, + "/opt/coulomb-social-backup/mirror-platform-runtime.json": { + "exists": true, + "sha256": "9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549355, + "device": 2049, + "mtime_ns": 1793467368847077662 + }, + "/opt/coulomb-social-backup/backup_alerts.py": { + "exists": true, + "sha256": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549359, + "device": 2049, + "mtime_ns": 1793467368887078332 + }, + "/opt/coulomb-social-backup/notify-backup-alert.py": { + "exists": true, + "sha256": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549360, + "device": 2049, + "mtime_ns": 1793467368891078399 + }, + "/opt/coulomb-social-backup/observe-backup-alerts.py": { + "exists": true, + "sha256": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549361, + "device": 2049, + "mtime_ns": 1793467368895078466 + }, + "/opt/railiance-platform/scripts/application_recovery_identities.py": { + "exists": true, + "sha256": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549356, + "device": 2049, + "mtime_ns": 1793467368852077745 + }, + "/opt/railiance-platform/scripts/application_recovery_mirror.py": { + "exists": true, + "sha256": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549357, + "device": 2049, + "mtime_ns": 1793467368857077829 + }, + "/opt/railiance-platform/scripts/application_recovery_resources.py": { + "exists": true, + "sha256": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549358, + "device": 2049, + "mtime_ns": 1793467368862077913 + }, + "/opt/railiance-platform/scripts/primary_application_archive.py": { + "exists": true, + "sha256": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 540610, + "device": 2049, + "mtime_ns": 1791537447437616251 + }, + "/etc/systemd/system/coulomb-social-backup-freshness.service": { + "exists": true, + "sha256": "8d6278c2cbca7f48b659c239c1d1e181fa4b9cf023fe7429850e294c6cd84d0e", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19366, + "device": 2049, + "mtime_ns": 1793467368868078014 + }, + "/etc/systemd/system/coulomb-social-paired-backup.service": { + "exists": true, + "sha256": "8b01b788bb08f453fdd4807104185c89411e0b0b4dedee9240f14f110ceb5513", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 14867, + "device": 2049, + "mtime_ns": 1793467368873078098 + }, + "/etc/systemd/system/coulomb-social-backup-freshness.timer": { + "exists": true, + "sha256": "75d1d0721c07fdd0703910c06bb8a678396440e7d95411a37ee7de4938f3ac31", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19367, + "device": 2049, + "mtime_ns": 1793467368878078181 + }, + "/etc/systemd/system/coulomb-social-paired-backup.timer": { + "exists": true, + "sha256": "5e87235e66f89a64a972562d6f163f62420628fc44fb7fd8d603a96c347dfbb9", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19368, + "device": 2049, + "mtime_ns": 1793467368882078248 + }, + "/etc/systemd/system/coulomb-social-backup-alert@.service": { + "exists": true, + "sha256": "4bfa019d29b1d0de278f03553602a5e824de50ed99ebcf3b5035cf4d5b06fe98", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19370, + "device": 2049, + "mtime_ns": 1793467368899078533 + }, + "/etc/coulomb-social-backup/alerting.env": { + "exists": true, + "sha256": "509844b5c8ef618b812d52f32e19c3c559623636dca62f7b16f437aa90cc0458", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 19369, + "device": 2049, + "mtime_ns": 1793467368904078617 + } + }, + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.service", + "UnitFileState": "static" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.service", + "UnitFileState": "static" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-alert@.service", + "UnitFileState": "static" + } + }, + "route": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + }, + "capacity": { + "state_bytes": 11259904, + "state_reserve_bytes": 7532969984, + "disk_reserve_bytes": 8606711808, + "free_bytes": 115555618816, + "free_inodes": 24523041, + "filesystem_blocks_reserved": false + }, + "sdk": { + "boto3": "1.40.18", + "botocore": "1.40.76" + }, + "runtime_modified_by_verification": false +} diff --git a/release/content-acceptance-evidence/backup-route-independent-return-2026-10-31.json b/release/content-acceptance-evidence/backup-route-independent-return-2026-10-31.json new file mode 100644 index 0000000..fb0a51f --- /dev/null +++ b/release/content-acceptance-evidence/backup-route-independent-return-2026-10-31.json @@ -0,0 +1,29 @@ +{ + "schema": "coulomb.backup-route-independent-postflight.v1", + "verified_at": "2026-10-31T17:19:36.926769+00:00", + "status": "durable_receipt_and_live_route_verified", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "receipt": { + "schema": "rapp-telemetry.backup-route-execution.v1", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "proposal_digest": "sha256:4bcad54f7b8e1e77b94112e452cbb58eaf8e882dc10305129c5f9ca69f014a66", + "status": "backup_email_route_deployed_verified", + "timers_activated": false, + "notification_sent_by_helper": false, + "production_data_restored": false, + "started_at": "2026-10-31T17:17:13.743344+00:00", + "helm_revision": 6, + "verified_resource_count": 87, + "after_resources_sha256": "600c4998e54b8e11845849c7d8e4c6b6699556c21f5df6ca79bb86b2e8d1683f", + "live_desired_fields_verified": true, + "runtime_config_verified": true, + "existing_image_and_pvc_preserved": true, + "completed_at": "2026-10-31T17:17:31.488654+00:00" + }, + "receipt_sha256": "c26d636f59109956f281ad46f4885c4db800a9553cf05cac324aa3155ef7ac40", + "original_execution_packet_matches": true, + "live_desired_fields_verified": true, + "runtime_config_verified": true, + "existing_image_and_pvc_preserved": true, + "runtime_modified_by_verification": false +} diff --git a/release/content-acceptance-evidence/content-acceptance-build-2026-10-31.json b/release/content-acceptance-evidence/content-acceptance-build-2026-10-31.json new file mode 100644 index 0000000..4a60dbe --- /dev/null +++ b/release/content-acceptance-evidence/content-acceptance-build-2026-10-31.json @@ -0,0 +1,45 @@ +{ + "schema": "coulomb.content-acceptance-build.v1", + "completed_at": "2026-10-31T18:38:08.525103+00:00", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "build_context": "clean restricted git archive", + "source_full_gate_tests": 568, + "runtime_packages_and_python_unchanged": true, + "runtime": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "django_checks": { + "check": "System check identified no issues (0 silenced).", + "makemigrations": "No changes detected" + }, + "new_database_migrations": false, + "production_deployed": false +} diff --git a/release/content-acceptance-evidence/content-acceptance-provenance-2026-10-31.json b/release/content-acceptance-evidence/content-acceptance-provenance-2026-10-31.json new file mode 100644 index 0000000..6c1e4c2 --- /dev/null +++ b/release/content-acceptance-evidence/content-acceptance-provenance-2026-10-31.json @@ -0,0 +1,11 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "packaged_file_count": 79, + "all_packaged_source_bytes_match": true, + "source_revision_label_verified": true, + "registry_digest_resolves": true, + "application_source_overlay": false, + "manifest_sha256": "2eca0618df5bdf35928869c3b848be36190065a4cca8f855bba372df6c516556" +} diff --git a/release/content-acceptance-evidence/content-acceptance-recovery-2026-10-31.json b/release/content-acceptance-evidence/content-acceptance-recovery-2026-10-31.json new file mode 100644 index 0000000..ddc5c3b --- /dev/null +++ b/release/content-acceptance-evidence/content-acceptance-recovery-2026-10-31.json @@ -0,0 +1,67 @@ +{ + "schema": "coulomb.packaged-content-acceptance-recovery.v1", + "status": "packaged_recovered_export_and_views_verified", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "restore_receipt_sha256": "3d6fe0b5d7426c9fb39ba22f18770319e0c89ba1981c61db171199d3b4e68524", + "retained_point_captured_at": "2026-10-31T07:13:52Z", + "candidate_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "previous_image_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "eb982edcdb94c976ab3d4e9797e4c34eb72a3ee3886a37e64508744ac172a8a8", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged": true, + "application_source_overlay": false, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "driver_sha256": "da85e5f7d5cf68f599a897a932d51399340bdd026a7cddf9603bef4aeb40909a" +} diff --git a/release/content-acceptance-evidence/content-acceptance-target-2026-10-31.json b/release/content-acceptance-evidence/content-acceptance-target-2026-10-31.json new file mode 100644 index 0000000..cd50ca6 --- /dev/null +++ b/release/content-acceptance-evidence/content-acceptance-target-2026-10-31.json @@ -0,0 +1,21 @@ +{ + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "8d79c64cff4bd2c62f03494e80847dc99b965ddabf35645da67e8d1020220798", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "image_only_server_preview": true +} diff --git a/release/content-acceptance-evidence/content-acceptance-tests-2026-10-31.json b/release/content-acceptance-evidence/content-acceptance-tests-2026-10-31.json new file mode 100644 index 0000000..971fb31 --- /dev/null +++ b/release/content-acceptance-evidence/content-acceptance-tests-2026-10-31.json @@ -0,0 +1,13 @@ +{ + "schema": "coulomb.packaged-content-acceptance-tests.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "tests": 110, + "failures": 0, + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "synthetic_fixtures_only": true +} diff --git a/release/content-acceptance-evidence/content-lifecycle-baseline-tests-2026-10-31.json b/release/content-acceptance-evidence/content-lifecycle-baseline-tests-2026-10-31.json new file mode 100644 index 0000000..6b3e676 --- /dev/null +++ b/release/content-acceptance-evidence/content-lifecycle-baseline-tests-2026-10-31.json @@ -0,0 +1,25 @@ +{ + "schema": "coulomb.packaged-content-lifecycle-tests.v1", + "source": "23ffe2cf4b214666754a2420558943438e54251a", + "image_id": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "tests": 29, + "failures": 4, + "expected_baseline_regressions": true, + "failed_nodes": [ + "test_process_death_during_copy_or_move_is_recoverable[copy_page-during_first_asset]", + "test_process_death_during_copy_or_move_is_recoverable[copy_page-during_second_asset]", + "test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_first_asset]", + "test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_second_asset]" + ], + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "test_tooling_injected": true, + "application_modules_from_image_verified": true, + "production_access": false, + "junit_sha256": "edc52bd3a321d028d255b3d478020b3383d94747957ffe2985583b1f0741cb85", + "driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36", + "test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" +} diff --git a/release/content-acceptance-evidence/content-lifecycle-release-row-drift-2026-10-31.json b/release/content-acceptance-evidence/content-lifecycle-release-row-drift-2026-10-31.json new file mode 100644 index 0000000..8c8aeec --- /dev/null +++ b/release/content-acceptance-evidence/content-lifecycle-release-row-drift-2026-10-31.json @@ -0,0 +1,61 @@ +{ + "changed_columns": { + "django_session": [ + "session_key", + "session_data", + "expire_date" + ], + "members_member": [ + "updated_at", + "last_login_at" + ], + "members_user": [ + "last_login" + ] + }, + "file_changes": [], + "retained_fingerprint": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 92, + "business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "schema": "coulomb.content-release-row-drift.v1", + "changed_tables": [ + { + "table": "django_session", + "retained_rows": 42, + "current_rows": 43, + "retained_sha256": "c958e33ec782f9f2fd1b3cceba4a64da0038fde51050f25905ae42de91ca6dde", + "current_sha256": "594e246ef6297d9be9b6e8d9eda076a5b2ab36afb9263606b719f64f51293ba6" + }, + { + "table": "members_member", + "retained_rows": 1, + "current_rows": 1, + "retained_sha256": "4d465d2b9d1e6311e505f861f2ed4b67763a545a965f9a7d0c586a3d07e3e9a3", + "current_sha256": "ab82b3f2c5e49f8634c598c9642894747f4b6fcff0f5230ac27f41939feb8640" + }, + { + "table": "members_user", + "retained_rows": 1, + "current_rows": 1, + "retained_sha256": "81ccb3a40191e910a68c101896df518a588c00231728934c4c900577f8ae4f12", + "current_sha256": "0fc7a71b4b80a71fbedce527bbd2cae3858cf6a062e5714fc231be13ff987256" + } + ], + "all_other_business_tables_equal": true, + "migration_history_equal": true, + "content_equal": true, + "retained_point": "2026-10-31T07:13:52Z", + "current_business_rows": 93, + "current_business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "private_row_values_printed": false, + "production_changes": false +} diff --git a/release/content-acceptance-evidence/content-lifecycle-validation-2026-10-31.json b/release/content-acceptance-evidence/content-lifecycle-validation-2026-10-31.json new file mode 100644 index 0000000..f426e59 --- /dev/null +++ b/release/content-acceptance-evidence/content-lifecycle-validation-2026-10-31.json @@ -0,0 +1,58 @@ +{ + "schema": "coulomb.content-lifecycle-validation.v1", + "recorded_at": "2026-10-31T11:54:24.215512+00:00", + "status": "source_fix_verified", + "workplan_task": "CSOC-WP-0013-T05", + "bug": "A process exit during attachment creation left bytes absent from the copy/move cleanup journal; the orphan blocked retry.", + "regression_observed_before_fix": { + "baseline": "f70708839cd5ca43310f6891c8d94aaff915139c", + "operations": [ + "copy_page", + "transfer_page" + ], + "interruption": "partial first attachment flushed, immediate os._exit(86)", + "result": "both failed because the partial destination survived recovery" + }, + "fix": [ + "Refuse existing attachment entries before recording cleanup authority.", + "Persist intended new attachment name before exclusive creation under the content writer lock.", + "Flush recovery and attachment directory entries before destination publication." + ], + "focused_cases": { + "total": 29, + "abrupt_child_process_exit": 14, + "existing_destination_collision": 8, + "prior_retention_and_recovery": 7 + }, + "full_gate": { + "command": "make check", + "tests": 546, + "failures": 0, + "errors": 0, + "skipped": 0, + "warnings": 69, + "junit_sha256": "dd3d0433c8109f00f6dddd119cfdf8281915f94da2da1f5e6009c31cfe76c65b", + "log_sha256": "eb59a394c95c5307c17069874640d1655ef61c5aa7bb426ef6d21db4929f748e" + }, + "files": [ + { + "path": "coulomb_social/apps/spaces/pageops.py", + "sha256": "055ef25b5efc75798cf17ffa68739de4d4099d2cab4d041e7c7eacdfaf43803e" + }, + { + "path": "tests/test_content_recovery.py", + "sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449" + } + ], + "policy": "User accepted retain-all default and separately reviewed manual cutoff; keep live page and all history of deleted pages.", + "live_purge_authorized": false, + "live_purge_executed": false, + "production_deployed": false, + "productive_acceptance": false, + "limits": [ + "Child process termination on synthetic local files is proved; physical power-loss durability is not experimentally proved.", + "Cooperating application/import writers must use the content writer lock.", + "Release delivery and productive copy/move review remain open." + ], + "policy_decision_id": "c3a191f2-ce06-483f-82d7-ef8c54ea0d5e" +} diff --git a/release/content-acceptance-evidence/ordered-list-fidelity-2026-10-31.json b/release/content-acceptance-evidence/ordered-list-fidelity-2026-10-31.json new file mode 100644 index 0000000..cbcbd89 --- /dev/null +++ b/release/content-acceptance-evidence/ordered-list-fidelity-2026-10-31.json @@ -0,0 +1,42 @@ +{ + "schema": "coulomb.ordered-list-fidelity.v1", + "verified_at": "2026-10-31T17:20:32.943192+00:00", + "source_articles_sha256": "f33967c79c0f8a60a7d504f5d5e4082f8bc9e3c34f6a3160cb200fd380831d62", + "prior_audit_sha256": "3926e24a3b903f5e1d1c7133dc5f9df1e958183432c5131949ce838eb60acf1e", + "current_audit_sha256": "d8f382e24d7adfe3c43dc71f32f8f6e647bf68239cbcdc6769a6c8fb85d666b9", + "records": 245, + "rendered_cards_verified": 242, + "source_bodies_unchanged": true, + "missing_bodies_retained": 3, + "ordered_list_starts_restored": 15, + "affected_records": 4, + "markup_change_cases_before": 6, + "markup_change_cases_after": 3, + "remaining_removed_tags": { + "topic": 3 + }, + "remaining_removed_attributes": [ + { + "tag": "topic", + "attribute": "name", + "count": 2 + } + ], + "private_review_directory": "~/.config/coulomb-social/auth/exports/rendering-review-20261031-numbering", + "source_hashes": { + "coulomb_social/apps/spaces/content.py": "5d6bb5c800b3e776fff385fa73284f4916403676dc2a8bc06b8fadc7d2387633", + "tests/test_rendering_audit.py": "ae092753f4d1d006bb88f1ce11581975c105c5231819f9409ddaaf2fc83d32f5" + }, + "validation": { + "focused_passed": 14, + "full_gate_passed": 650, + "full_gate_warnings": 69, + "failures": 0, + "errors": 0, + "skipped": 0, + "full_gate_seconds": 91.85 + }, + "production_deployed": false, + "original_bubble_render_compared": false, + "productive_fidelity_accepted": false +} diff --git a/release/content-acceptance-snapshot.py b/release/content-acceptance-snapshot.py new file mode 100644 index 0000000..52c92ec --- /dev/null +++ b/release/content-acceptance-snapshot.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Node-side quiesced coulomb-social snapshot. Binary output stays private. + +Run on railiance01 after reviewed live approval. Does not restore production. +Default is server dry-run; --execute briefly pauses the app and captures a +paired pg_dump/content archive. A separate isolated restore must validate it. +""" +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import signal +import stat +import subprocess +import sys +import time +from pathlib import Path + +NS = 'coulomb-social' +IMAGE = ('forgejo.coulomb.social/coulomb/coulomb-social@sha256:' + '7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5') +CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' +LOCK_PATH = Path('/var/lib/coulomb-social-backup/snapshot.lock') + + +class Refused(RuntimeError): + pass + + +def run(args: list[str], *, payload: bytes | None = None, timeout: int = 180) -> bytes: + p = subprocess.run(args, input=payload, capture_output=True, timeout=timeout) + if p.returncode: + raise Refused('command_failed') + return p.stdout + + +def k(*args: str, payload: bytes | None = None) -> bytes: + return run(['kubectl', *args], payload=payload) + + +def document(*args: str) -> dict: + return json.loads(k(*args, '-o', 'json')) + + +def private_write(path: Path, value: bytes) -> None: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, 'wb') as f: + f.write(value) + + +def snapshot() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--execute', action='store_true') + parser.add_argument('--destination', type=Path, required=True) + args = parser.parse_args() + os.environ['KUBECONFIG'] = '/etc/rancher/k3s/k3s.yaml' + started = time.monotonic() + if document('get', 'namespace', 'kube-system')['metadata']['uid'] != CLUSTER_UID: + raise Refused('wrong_cluster') + dep = document('get', 'deployment', 'coulomb-social', '-n', NS) + spec = dep['spec']['template']['spec'] + if dep['spec']['replicas'] != 1 or spec['containers'][0]['image'] != IMAGE: + raise Refused('unexpected_release') + if document('get', 'pvc', 'coulomb-social-content', '-n', NS)['status']['phase'] != 'Bound': + raise Refused('pvc_not_bound') + cluster = document('get', 'cluster', 'apps-pg', '-n', 'databases') + primary = cluster['status']['currentPrimary'] + name = 'csoc-paired-snapshot-' + time.strftime('%Y%m%d%H%M%S', time.gmtime()) + pod = { + 'apiVersion': 'v1', 'kind': 'Pod', + 'metadata': {'name': name, 'namespace': NS, 'labels': {'csoc-recovery': name}}, + 'spec': { + 'restartPolicy': 'Never', 'automountServiceAccountToken': False, + 'securityContext': {'runAsNonRoot': True, 'runAsUser': 10001, + 'runAsGroup': 10001, 'fsGroup': 10001, + 'fsGroupChangePolicy': 'OnRootMismatch'}, + 'containers': [{'name': 'snapshot', 'image': IMAGE, + 'command': ['python', '-c', 'import time;time.sleep(1800)'], + 'resources': {'requests': {'cpu': '10m', 'memory': '64Mi'}, + 'limits': {'cpu': '100m', 'memory': '256Mi'}}, + 'securityContext': {'allowPrivilegeEscalation': False, + 'readOnlyRootFilesystem': True, + 'capabilities': {'drop': ['ALL']}}, + 'volumeMounts': [{'name': 'content', 'mountPath': '/snapshot', 'readOnly': True}]}], + 'volumes': [{'name': 'content', 'persistentVolumeClaim': + {'claimName': 'coulomb-social-content', 'readOnly': True}}], + }, + } + body = json.dumps(pod).encode() + k('create', '--dry-run=server', '-f', '-', payload=body) + if not args.execute: + print(json.dumps({'status': 'reviewed', 'server_dry_run': True, + 'image_pinned': True, 'pvc_read_only': True, + 'db_scope': 'coulomb_social_db', 'replicas_restored_to': 1})) + return 0 + dest = args.destination + if not dest.is_absolute() or dest.exists(): + raise Refused('destination_must_be_new_absolute_directory') + dest.mkdir(mode=0o700) + paused = False + created = False + receipt = {'status': 'incomplete', 'app': 'coulomb-social', + 'database': 'coulomb_social_db', 'image': IMAGE, + 'started_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} + try: + paused = True + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=0') + k('wait', '--for=delete', 'pod', '-n', NS, + '-l', 'app.kubernetes.io/instance=coulomb-social', '--timeout=120s') + created = True + k('create', '-f', '-', payload=body) + k('wait', '--for=condition=Ready', 'pod/' + name, '-n', NS, '--timeout=120s') + dump = k('exec', '-n', 'databases', primary, '-c', 'postgres', '--', + 'pg_dump', '-U', 'postgres', '-d', 'coulomb_social_db', '-Fc') + if not dump.startswith(b'PGDMP'): + raise Refused('invalid_pg_dump') + private_write(dest / 'database.dump', dump) + archive_code = ('import io,sys,tarfile; b=io.BytesIO(); ' + 't=tarfile.open(fileobj=b,mode="w:gz"); ' + 't.add("/snapshot",arcname="content"); ' + 't.close();sys.stdout.buffer.write(b.getvalue())') + content = k('exec', '-n', NS, name, '--', 'python', '-c', archive_code) + private_write(dest / 'content.tar.gz', content) + receipt.update(status='captured', writers_quiesced=True, + database_sha256=hashlib.sha256(dump).hexdigest(), + content_sha256=hashlib.sha256(content).hexdigest(), + database_bytes=len(dump), content_bytes=len(content)) + finally: + try: + if created: + k('delete', 'pod', name, '-n', NS, '--wait=true', '--timeout=120s') + finally: + if paused: + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=1') + k('rollout', 'status', 'deployment/coulomb-social', '-n', NS, '--timeout=180s') + receipt['app_resumed'] = True + receipt['elapsed_seconds'] = round(time.monotonic() - started, 2) + private_write(dest / 'receipt.json', json.dumps(receipt, indent=2).encode()) + print(json.dumps(receipt)) + return 0 + + +def main() -> int: + if '--execute' not in sys.argv: + return snapshot() + LOCK_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + parent = LOCK_PATH.parent.lstat() + if (not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.geteuid() + or parent.st_mode & 0o077): + raise Refused('lock_directory_not_private') + fd = os.open(LOCK_PATH, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'w') as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused('snapshot_already_running') from None + def terminate(signum, frame): + raise Refused('snapshot_interrupted') + previous = signal.signal(signal.SIGTERM, terminate) + try: + return snapshot() + finally: + signal.signal(signal.SIGTERM, previous) + + +if __name__ == '__main__': + try: + sys.exit(main()) + except Exception as exc: + # Never expose child output, kubectl objects, credentials or raw exception messages. + print(json.dumps({'status': 'failed', 'failure_type': type(exc).__name__})) + sys.exit(1) diff --git a/release/content-acceptance-values.yaml b/release/content-acceptance-values.yaml new file mode 100644 index 0000000..25232f7 --- /dev/null +++ b/release/content-acceptance-values.yaml @@ -0,0 +1,50 @@ +# Production overrides for coulomb-social. +# Non-secret values only. Secrets live in K8s Secret coulomb-social-env. + +image: + repository: forgejo.coulomb.social/coulomb/coulomb-social + tag: "1139a6d30eee" + digest: "sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5" + +# Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). +# Apex cutover is deferred until data + self-registration are ready. +env: + DJANGO_SETTINGS_MODULE: coulomb_social.settings.prod + ALLOWED_HOSTS: app.coulomb.social,localhost + CSRF_TRUSTED_ORIGINS: https://app.coulomb.social + CONTENT_ROOT: /app/var/content + DEFAULT_TENANT_ID: "tenant:coulomb" + OIDC_ENABLED: "true" + OIDC_ISSUER: https://kc.coulomb.social + OIDC_CLIENT_ID: coulomb-social + OIDC_REDIRECT_URI: https://app.coulomb.social/auth/callback/ + OIDC_SCOPES: "openid profile email groups" + USER_ENGINE_BASE_URL: https://users.coulomb.social + USER_ENGINE_APPLICATION_ID: coulomb-social + USER_ENGINE_EXPECTED_AUDIENCE: user-engine-portal + NETKINGDOM_REGISTRATION_URL: https://users.92-205-62-239.nip.io/register + +probes: + hostHeader: app.coulomb.social + path: /healthz + +# One writable local-path volume; Recreate prevents concurrent old/new writers. +replicaCount: 1 +deploymentStrategy: Recreate +persistence: + content: + enabled: true + storageClass: local-path + size: 2Gi + mountPath: /app/var/content + retain: true + +# Wave-2 right-sizing (CSOC-WP-0017-T01, 2026-10-21 node budget): 7d p95 +# 0.4m CPU / 146Mi. Limits stay at the chart defaults (1000m/1Gi). +resources: + requests: + cpu: 15m + memory: 176Mi + limits: + cpu: 1000m + memory: 1Gi diff --git a/release/coulomb-content-acceptance-1139a6d.json b/release/coulomb-content-acceptance-1139a6d.json new file mode 100644 index 0000000..a256679 --- /dev/null +++ b/release/coulomb-content-acceptance-1139a6d.json @@ -0,0 +1,79 @@ +{ + "schema": "coulomb.reviewed-image-release.v1", + "proposal_id": "CSOC-CONTENT-ACCEPTANCE-20261031", + "scope": "ADMINISTER @ realm:kubernetes/railiance01; supervised founder execution only", + "operation": "image_only_existing_migration_0003", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "baseline_revision": 19, + "baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499", + "candidate_render_sha256": "680debbf4f35d9571916e2016c83327d1212665be82164c40d2d0a49081fc4a8", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea", + "candidate_spec_sha256": "8d79c64cff4bd2c62f03494e80847dc99b965ddabf35645da67e8d1020220798", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "product_source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "public_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "installed_helper_hashes": { + "/opt/coulomb-social-backup/paired-backup.py": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "/opt/coulomb-social-backup/mirror-paired-backup.py": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "/opt/coulomb-social-backup/check-paired-backup-health.py": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "/opt/coulomb-social-backup/backup_capacity.py": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "/opt/railiance-platform/scripts/application_recovery_identities.py": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "/opt/railiance-platform/scripts/application_recovery_mirror.py": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "/opt/railiance-platform/scripts/application_recovery_resources.py": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "/opt/coulomb-social-backup/backup_alerts.py": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "/opt/coulomb-social-backup/notify-backup-alert.py": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "/opt/coulomb-social-backup/observe-backup-alerts.py": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664" + }, + "clipboard_static_sha256": "347eabe0c8d93bf439df02b5a08a7ea71004600e6ff4e8a42cecbee52a9f0413", + "fresh_backup_before_helm": true, + "requires_dataset_equality_before_and_after": true, + "node_request_percent_limit": 90, + "helm_atomic_timeout_seconds": 300, + "rollback": "Restore Helm revision19/image522bcd7f and verified snapshot pin; preserve all post-capture data. No production data restore or schema downgrade.", + "approval_status": "prepared_not_admitted", + "recovery_rehearsal_scope": "Candidate and previous image verified read-only against retained independently recovered 2026-10-31T07:13:52Z point: all13 tables/86 files unchanged,40 owner views/40 outsider denials,85 selected-client export files verified. Live baseline remains93 business rows versus92 at the retained point; previously verified session/login-only drift remains unchanged. Fresh paired capture/readback before Helm and independent recovery of that fresh point remain required.", + "rehearsal_evidence_sha256": { + "content-acceptance-build-2026-10-31.json": "39c5c78174e0dfc7fd93c4b718c2b15aa68cea8298be517e28ee2bd206912dd9", + "content-acceptance-provenance-2026-10-31.json": "7d24b3eecf0c2c9c923812a318d448233f5bfc9ae4242046dae1c4d20e7e664a", + "content-acceptance-tests-2026-10-31.json": "3da4d155d84553d7f2949d7e14e5c9cc6c819a6be6af04db5819b7f43ed18e47", + "content-acceptance-recovery-2026-10-31.json": "4ec1b46d8076c42ac0e5bc106b7ff7debf25a65e6c02d907d7148c200a811ede", + "content-acceptance-target-2026-10-31.json": "6651cbf0e2df889315464638158c3aaad885c3cb39fd5b51f06a5037434efebb", + "attachment-menu-validation-2026-10-31.json": "7e491219c701d2e67bb2c3bc849af1fb1a58f787fd17e0527eefcf6345c62f2d", + "ordered-list-fidelity-2026-10-31.json": "4dc3091876d355648d7482187d54f6a0501355274c263d0009d5d5ff6f42f64f", + "content-lifecycle-validation-2026-10-31.json": "cbb093af470588ca6e8eb3ec5bc1ccee0be537ab37a46625b6ac267c17343d99", + "content-lifecycle-baseline-tests-2026-10-31.json": "de14e4e10337990ccec1c483f7d943bdc7e11b03aa813aff47d00a9b38a07b65", + "content-lifecycle-release-row-drift-2026-10-31.json": "b10f65bf55e900199ae6037767f32e8901aaccb6f7f3a86461f13dd02ec8efce", + "backup-route-independent-return-2026-10-31.json": "3a3feb56c1af7e3db7b0f2380ec355bb465663073c0a0160034a35984ef23f6e", + "backup-installation-independent-return-2026-10-31.json": "2fe1972f4db0ba2468027f4926cd8111d6aa4b61d404592eda794b47733ce0df" + }, + "new_resources_committed": 0, + "autopilot_or_eur_budget_claimed": false, + "values_file": "release/content-acceptance-values.yaml", + "snapshot_file": "release/content-acceptance-snapshot.py", + "evidence_directory": "release/content-acceptance-evidence", + "prerequisite_installation_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "requires_inactive_backup_units": true, + "product_changes": [ + "Preserve ordered-list start numbers", + "Durable interrupted copy/move asset recovery", + "Plain attachment menu: author upload/remove and authenticated reader downloads; revisioned associations and independent copies/moves" + ], + "product_decision": "78175c34-58e1-4ad6-944d-0662c0462ae6" +} diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 472fc6f..b64d3bf 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -424,15 +424,15 @@ coulomb-social/docs/release/backup-producer-custody-validation-2026-10-31.json. The historical eight-entry repair and recurring service acceptance remain product T04/T09. No cluster operation occurred; future release proposals bind the new source. -## T07 — Deliver the attachment crash-recovery correction +## T07 — Deliver the combined content and author attachment release ```task id: RAPPS-WP-0015-T07 status: wait priority: high -needs_human: false -wait_kind: external -blocking_reason: "Prepared image packet c9c32316 waits for CSOC-WP-0012-T08 inactive installation8f442bf0; native preflight confirms its receipt is absent." +needs_human: true +wait_kind: human +blocking_reason: "Combined packet e96752ec passes full native preflight after independently verified inactive installation8f442bf0; founder-terminal execution and independent return remain required." depends_on: - CSOC-WP-0012-T08 state_hub_task_id: "8f448934-bdfe-539f-8cab-a739ec752491" @@ -482,3 +482,25 @@ coulomb-social/docs/usecases/sharing-portability-review.md, lifecycle release se Evidence: content-lifecycle-release-{target,row-drift,preflight,proposal,preparation} JSON files in the consumer docs/release directory. T07 is now external wait for T08; product policy is settled, and productive lifecycle acceptance remains open. + + +2026-10-31 combined candidate: the route and inactive installation now have +founder-executed independent returns. User requested plain attachment actions, +including author add/remove (product decision78175c34), and accepted the three +custom-topic rendering cases for now. The new source1139a6d combines the original +crash-recovery correction, ordered-list starts and revisioned attachment actions. +Image7906f461 has exact packaged provenance and unchanged runtime dependencies; +no schema migration. Main gate670, isolated source gate568 and packaged110 pass. +The retained independently recovered pair remains unchanged across candidate/old +40owner/40outsider views and85file selected-client export, all13tables/86files. + +New immutable plan: release/coulomb-content-acceptance-1139a6d.json. +Exact proposal: sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3. +Full native read-only preflight passes at revision19 with unchanged93business +rows/86files, complete desired specification, Helm/server previews, installed +source prerequisite and inactive units. Historical c9c32316 remains unchanged; +use the new combined proposal only. Founder-terminal execution remains required. +The reviewed action captures a fresh pair before atomic Helm rollout and refreshes +the snapshot image pin after unchanged-data/view checks. No timer activation, +production restore, purge or mail. Productive acceptance remains with WP-0013. +Review/command: coulomb-social/docs/release/content-acceptance-review-2026-10-31.md. From a15cc888d263221c9082daf1e9ecd9ff38769f1e Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 19:44:16 +0100 Subject: [PATCH 12/16] Route content release wait to its exact execution decision Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a --- workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index b64d3bf..4e98503 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -432,9 +432,8 @@ status: wait priority: high needs_human: true wait_kind: human +decision_id: "d9b70cc5-e035-40ae-8d0d-020190581202" blocking_reason: "Combined packet e96752ec passes full native preflight after independently verified inactive installation8f442bf0; founder-terminal execution and independent return remain required." -depends_on: -- CSOC-WP-0012-T08 state_hub_task_id: "8f448934-bdfe-539f-8cab-a739ec752491" ``` From b39910e3306b03f80e52ee1158d6b95130e435e6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 21:29:42 +0100 Subject: [PATCH 13/16] Reconcile verified Coulomb revision 20 and close release delivery Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a --- WORK-RECORDS.md | 2 +- helm/coulomb-social-values.yaml | 4 ++-- ...P-0015-coulomb-personal-content-release.md | 23 +++++++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index af8ac76..ffb3b14 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -43,7 +43,7 @@ | task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | -| task | RAPPS-WP-0015-T07 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T07 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/helm/coulomb-social-values.yaml b/helm/coulomb-social-values.yaml index 56d4ded..25232f7 100644 --- a/helm/coulomb-social-values.yaml +++ b/helm/coulomb-social-values.yaml @@ -3,8 +3,8 @@ image: repository: forgejo.coulomb.social/coulomb/coulomb-social - tag: "23ffe2cf4b21" - digest: "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" + tag: "1139a6d30eee" + digest: "sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5" # Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). # Apex cutover is deferred until data + self-registration are ready. diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 4e98503..7682515 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -428,12 +428,11 @@ T04/T09. No cluster operation occurred; future release proposals bind the new so ```task id: RAPPS-WP-0015-T07 -status: wait +status: done priority: high -needs_human: true -wait_kind: human +needs_human: false decision_id: "d9b70cc5-e035-40ae-8d0d-020190581202" -blocking_reason: "Combined packet e96752ec passes full native preflight after independently verified inactive installation8f442bf0; founder-terminal execution and independent return remain required." +intervention_note: "Founder executed e96752ec; independent verification confirms revision20/image7906f461 with unchanged data. No execution rerun required. Source pins and fresh-point mirror/recovery/export are verified; productive verdicts remain CSOC-WP-0013." state_hub_task_id: "8f448934-bdfe-539f-8cab-a739ec752491" ``` @@ -503,3 +502,19 @@ The reviewed action captures a fresh pair before atomic Helm rollout and refresh the snapshot image pin after unchanged-data/view checks. No timer activation, production restore, purge or mail. Productive acceptance remains with WP-0013. Review/command: coulomb-social/docs/release/content-acceptance-review-2026-10-31.md. + +2026-10-31 verified combined release: founder executed exact proposal e96752ec, +completed20:22:30Z at Helm revision20/image7906f461. Independent read-only proof +verifies complete desired spec, identities, sole Ready writer,79packaged source +files and23capsule members. All93business rows/86files are unchanged;40owner views +and40outsider denials pass. Snapshot/helper hashes match. Fresh20:21paired point +was fetched from primary; timers remain inactive. Deployment approval decision +d9b70cc5 is resolved. Consumer recovery/export follow-through and live source-pin +reconciliation are in progress; productive verdicts remain CSOC-WP-0013. + +T07 complete: exact fresh20:21point independently mirrored/read back/decrypted +and restored (13tables/115rows,86files). Deployed and previous package views +pass40owner/40outsider checks; client export verifies85files with all source +SQL/files unchanged. Owner live values and consumer source/helper pins now match +revision20. Recovery remains isolated; timers are inactive/disabled. Remaining +user workflow verdicts are tracked by existing CSOC-WP-0013-T02/T03/T05/T06. From 979b7819df469a052021ccfa64ddbbc4eb640e12 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 22:16:46 +0100 Subject: [PATCH 14/16] Prepare attended Coulomb editor attachment visibility release Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a --- WORK-RECORDS.md | 1 + .../coulomb-editor-attachments-31eaa58.json | 77 ++++ ...llation-independent-return-2026-10-31.json | 371 ++++++++++++++++++ ...p-route-independent-return-2026-10-31.json | 29 ++ ...nce-independent-postflight-2026-10-31.json | 41 ++ ...ttachments-baseline-change-2026-10-31.json | 39 ++ .../editor-attachments-build-2026-10-31.json | 45 +++ ...tor-attachments-provenance-2026-10-31.json | 11 + ...ditor-attachments-recovery-2026-10-31.json | 67 ++++ .../editor-attachments-target-2026-10-31.json | 21 + .../editor-attachments-tests-2026-10-31.json | 13 + ...tor-attachments-validation-2026-10-31.json | 34 ++ release/editor-attachments-snapshot.py | 175 +++++++++ release/editor-attachments-values.yaml | 50 +++ ...P-0015-coulomb-personal-content-release.md | 28 ++ 15 files changed, 1002 insertions(+) create mode 100644 release/coulomb-editor-attachments-31eaa58.json create mode 100644 release/editor-attachments-evidence/backup-installation-independent-return-2026-10-31.json create mode 100644 release/editor-attachments-evidence/backup-route-independent-return-2026-10-31.json create mode 100644 release/editor-attachments-evidence/content-acceptance-independent-postflight-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-baseline-change-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-build-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-provenance-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-recovery-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-target-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-tests-2026-10-31.json create mode 100644 release/editor-attachments-evidence/editor-attachments-validation-2026-10-31.json create mode 100644 release/editor-attachments-snapshot.py create mode 100644 release/editor-attachments-values.yaml diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index ffb3b14..dd00903 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -44,6 +44,7 @@ | task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T07 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T08 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/release/coulomb-editor-attachments-31eaa58.json b/release/coulomb-editor-attachments-31eaa58.json new file mode 100644 index 0000000..bf8cddf --- /dev/null +++ b/release/coulomb-editor-attachments-31eaa58.json @@ -0,0 +1,77 @@ +{ + "schema": "coulomb.reviewed-image-release.v1", + "proposal_id": "CSOC-EDITOR-ATTACHMENTS-20261031", + "scope": "ADMINISTER @ realm:kubernetes/railiance01; supervised founder execution only", + "operation": "image_only_existing_migration_0003", + "cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786", + "baseline_revision": 20, + "baseline_helm_manifest_sha256": "e388f62c97e97d54d87671825bd0b05c854a70b0cca8a00faee452ae7841799e", + "candidate_render_sha256": "78ee8e0a73f87e1152b6f826029ed85385b6293c61a1cdab7171b6c6e40580bc", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3", + "baseline_spec_sha256": "8d79c64cff4bd2c62f03494e80847dc99b965ddabf35645da67e8d1020220798", + "candidate_spec_sha256": "4deab26fc15d63e09922259b3362cd4e5c6bfa10884de058f776c0de8b642b00", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "product_source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "public_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3", + "installed_helper_hashes": { + "/opt/coulomb-social-backup/paired-backup.py": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "/opt/coulomb-social-backup/snapshot-content-and-db.py": "23f92c96b840df61f92858004241e860e0ddb32ed6fd7448294d3c8278906e46", + "/opt/coulomb-social-backup/mirror-paired-backup.py": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "/opt/coulomb-social-backup/check-paired-backup-health.py": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "/opt/coulomb-social-backup/backup_capacity.py": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "/opt/railiance-platform/scripts/application_recovery_identities.py": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "/opt/railiance-platform/scripts/application_recovery_mirror.py": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "/opt/railiance-platform/scripts/application_recovery_resources.py": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "/opt/coulomb-social-backup/backup_alerts.py": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "/opt/coulomb-social-backup/notify-backup-alert.py": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "/opt/coulomb-social-backup/observe-backup-alerts.py": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664" + }, + "clipboard_static_sha256": "347eabe0c8d93bf439df02b5a08a7ea71004600e6ff4e8a42cecbee52a9f0413", + "fresh_backup_before_helm": true, + "requires_dataset_equality_before_and_after": true, + "node_request_percent_limit": 90, + "helm_atomic_timeout_seconds": 300, + "rollback": "Restore Helm revision20/image7906f461 and verified snapshot pin, preserving all data; no production restore or schema downgrade.", + "approval_status": "prepared_not_admitted", + "recovery_rehearsal_scope": "Candidate and previous image verified against independently recovered 2026-10-31T20:21:01Z point:13tables/86files unchanged,40owner views/40outsider denials,85export files verified. Current97-row/98-file baseline adds2spaces/2memberships/12files from productive acceptance; all prior rows and86files are unchanged. Fresh paired capture/readback before Helm and independent recovery afterward remain required.", + "rehearsal_evidence_sha256": { + "editor-attachments-build-2026-10-31.json": "bb58b2fe23d101ab82ee0b210cc8f552f3667c785bd0207b1e1656b0577c106d", + "editor-attachments-provenance-2026-10-31.json": "3d4ec97702c5165af2198da4c4563cba204355480f5036fd22c48b9e3c300ba3", + "editor-attachments-tests-2026-10-31.json": "59d2d121befa7a8c3853fc89172bad5ce2abad1c1615cc2f2bea550f4d68a62d", + "editor-attachments-recovery-2026-10-31.json": "af68f6e86c0b3291d4ad880717c36eb95f1592f78d3c65596dd9cecd10755052", + "editor-attachments-target-2026-10-31.json": "b6d767ecb93b4638ca77a5052dcf3aab8a95dee922d3927956ad578290f9e58f", + "editor-attachments-baseline-change-2026-10-31.json": "db79f2838b88281c562b36996e74694c2a98e113c962b46eab0ef2cf69dd3b8c", + "editor-attachments-validation-2026-10-31.json": "427e228a9f2763cd583496d65e21b7599ef5e0df074dc507277ce2051169f562", + "backup-route-independent-return-2026-10-31.json": "3a3feb56c1af7e3db7b0f2380ec355bb465663073c0a0160034a35984ef23f6e", + "backup-installation-independent-return-2026-10-31.json": "2fe1972f4db0ba2468027f4926cd8111d6aa4b61d404592eda794b47733ce0df", + "content-acceptance-independent-postflight-2026-10-31.json": "6a8a4056b143011c50af24dd31d6338d00892bfe626666f85e607d86562dfe7c" + }, + "new_resources_committed": 0, + "autopilot_or_eur_budget_claimed": false, + "values_file": "release/editor-attachments-values.yaml", + "snapshot_file": "release/editor-attachments-snapshot.py", + "evidence_directory": "release/editor-attachments-evidence", + "prerequisite_installation_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "requires_inactive_backup_units": true, + "product_changes": [ + "Show saved attachments and downloads in editor tabs", + "Keep attachment list and submitted text after stale edit refusal", + "Open attachment management in a separate tab to preserve unsaved editor text" + ], + "product_decision": "Productive review feedback: attachments present on original but absent from editing tabs." +} diff --git a/release/editor-attachments-evidence/backup-installation-independent-return-2026-10-31.json b/release/editor-attachments-evidence/backup-installation-independent-return-2026-10-31.json new file mode 100644 index 0000000..8417e00 --- /dev/null +++ b/release/editor-attachments-evidence/backup-installation-independent-return-2026-10-31.json @@ -0,0 +1,371 @@ +{ + "schema": "coulomb.backup-installation-independent-postflight.v1", + "verified_at": "2026-10-31T17:30:18.219708+00:00", + "status": "installed_files_and_inactive_units_verified", + "proposal_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "receipt_sha256": "96329afbd5662f71bf6d6b05c003f0810be12c10036f9cadb41cc373d3a1108b", + "receipt": { + "schema": "coulomb.backup-source-supervised-installation.v1", + "proposal_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42", + "status": "sources_installed_units_inactive_verified", + "preflight": { + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "not-found", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "", + "UnitFileState": "" + } + }, + "capacity": { + "state_bytes": 11259904, + "state_reserve_bytes": 7532969984, + "disk_reserve_bytes": 8606711808, + "free_bytes": 115579949056, + "free_inodes": 24523113, + "filesystem_blocks_reserved": false + }, + "sdk": { + "boto3": "1.40.18", + "botocore": "1.40.76" + }, + "route": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + } + }, + "timers_activated": false, + "capture_executed": false, + "email_sent": false, + "retained_data_deleted": false, + "started_at": "2026-10-31T17:22:48.757530+00:00", + "capacity_profile": { + "maximum_new_file_bytes": 1073741824, + "state_budget_bytes": 8589934592, + "minimum_free_bytes": 8589934592, + "minimum_free_inodes": 1024, + "control_margin_bytes": 16777216 + }, + "founder_confirmed_profile_and_installation": true, + "route_postflight": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + }, + "verified_targets": 19, + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.service", + "UnitFileState": "static" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.service", + "UnitFileState": "static" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-alert@.service", + "UnitFileState": "static" + } + }, + "updated_at": "2026-10-31T17:22:54.560675+00:00" + }, + "nested_receipt_sha256": "795d9eb672b24495c77ad636a210c5d2022c8c3124d276920db7eda02257e519", + "verified_targets": 19, + "installed_fingerprints": { + "/opt/coulomb-social-backup/paired-backup.py": { + "exists": true, + "sha256": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 534009, + "device": 2049, + "mtime_ns": 1793467368826077309 + }, + "/opt/coulomb-social-backup/snapshot-content-and-db.py": { + "exists": true, + "sha256": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549255, + "device": 2049, + "mtime_ns": 1793430923413704430 + }, + "/opt/coulomb-social-backup/mirror-paired-backup.py": { + "exists": true, + "sha256": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549201, + "device": 2049, + "mtime_ns": 1793467368831077393 + }, + "/opt/coulomb-social-backup/check-paired-backup-health.py": { + "exists": true, + "sha256": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549309, + "device": 2049, + "mtime_ns": 1793467368836077477 + }, + "/opt/coulomb-social-backup/backup_capacity.py": { + "exists": true, + "sha256": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549354, + "device": 2049, + "mtime_ns": 1793467368843077594 + }, + "/opt/coulomb-social-backup/mirror-platform-runtime.json": { + "exists": true, + "sha256": "9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549355, + "device": 2049, + "mtime_ns": 1793467368847077662 + }, + "/opt/coulomb-social-backup/backup_alerts.py": { + "exists": true, + "sha256": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549359, + "device": 2049, + "mtime_ns": 1793467368887078332 + }, + "/opt/coulomb-social-backup/notify-backup-alert.py": { + "exists": true, + "sha256": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549360, + "device": 2049, + "mtime_ns": 1793467368891078399 + }, + "/opt/coulomb-social-backup/observe-backup-alerts.py": { + "exists": true, + "sha256": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549361, + "device": 2049, + "mtime_ns": 1793467368895078466 + }, + "/opt/railiance-platform/scripts/application_recovery_identities.py": { + "exists": true, + "sha256": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549356, + "device": 2049, + "mtime_ns": 1793467368852077745 + }, + "/opt/railiance-platform/scripts/application_recovery_mirror.py": { + "exists": true, + "sha256": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549357, + "device": 2049, + "mtime_ns": 1793467368857077829 + }, + "/opt/railiance-platform/scripts/application_recovery_resources.py": { + "exists": true, + "sha256": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 549358, + "device": 2049, + "mtime_ns": 1793467368862077913 + }, + "/opt/railiance-platform/scripts/primary_application_archive.py": { + "exists": true, + "sha256": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 540610, + "device": 2049, + "mtime_ns": 1791537447437616251 + }, + "/etc/systemd/system/coulomb-social-backup-freshness.service": { + "exists": true, + "sha256": "8d6278c2cbca7f48b659c239c1d1e181fa4b9cf023fe7429850e294c6cd84d0e", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19366, + "device": 2049, + "mtime_ns": 1793467368868078014 + }, + "/etc/systemd/system/coulomb-social-paired-backup.service": { + "exists": true, + "sha256": "8b01b788bb08f453fdd4807104185c89411e0b0b4dedee9240f14f110ceb5513", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 14867, + "device": 2049, + "mtime_ns": 1793467368873078098 + }, + "/etc/systemd/system/coulomb-social-backup-freshness.timer": { + "exists": true, + "sha256": "75d1d0721c07fdd0703910c06bb8a678396440e7d95411a37ee7de4938f3ac31", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19367, + "device": 2049, + "mtime_ns": 1793467368878078181 + }, + "/etc/systemd/system/coulomb-social-paired-backup.timer": { + "exists": true, + "sha256": "5e87235e66f89a64a972562d6f163f62420628fc44fb7fd8d603a96c347dfbb9", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19368, + "device": 2049, + "mtime_ns": 1793467368882078248 + }, + "/etc/systemd/system/coulomb-social-backup-alert@.service": { + "exists": true, + "sha256": "4bfa019d29b1d0de278f03553602a5e824de50ed99ebcf3b5035cf4d5b06fe98", + "mode": 420, + "uid": 0, + "gid": 0, + "inode": 19370, + "device": 2049, + "mtime_ns": 1793467368899078533 + }, + "/etc/coulomb-social-backup/alerting.env": { + "exists": true, + "sha256": "509844b5c8ef618b812d52f32e19c3c559623636dca62f7b16f437aa90cc0458", + "mode": 384, + "uid": 0, + "gid": 0, + "inode": 19369, + "device": 2049, + "mtime_ns": 1793467368904078617 + } + }, + "units": { + "coulomb-social-paired-backup.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.service", + "UnitFileState": "static" + }, + "coulomb-social-paired-backup.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-paired-backup.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-freshness.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.service", + "UnitFileState": "static" + }, + "coulomb-social-backup-freshness.timer": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-freshness.timer", + "UnitFileState": "disabled" + }, + "coulomb-social-backup-alert@coulomb-social-paired-backup.service.service": { + "LoadState": "loaded", + "ActiveState": "inactive", + "SubState": "dead", + "FragmentPath": "/etc/systemd/system/coulomb-social-backup-alert@.service", + "UnitFileState": "static" + } + }, + "route": { + "helm_revision": 6, + "runtime_and_complete_resources_verified": true + }, + "capacity": { + "state_bytes": 11259904, + "state_reserve_bytes": 7532969984, + "disk_reserve_bytes": 8606711808, + "free_bytes": 115555618816, + "free_inodes": 24523041, + "filesystem_blocks_reserved": false + }, + "sdk": { + "boto3": "1.40.18", + "botocore": "1.40.76" + }, + "runtime_modified_by_verification": false +} diff --git a/release/editor-attachments-evidence/backup-route-independent-return-2026-10-31.json b/release/editor-attachments-evidence/backup-route-independent-return-2026-10-31.json new file mode 100644 index 0000000..fb0a51f --- /dev/null +++ b/release/editor-attachments-evidence/backup-route-independent-return-2026-10-31.json @@ -0,0 +1,29 @@ +{ + "schema": "coulomb.backup-route-independent-postflight.v1", + "verified_at": "2026-10-31T17:19:36.926769+00:00", + "status": "durable_receipt_and_live_route_verified", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "receipt": { + "schema": "rapp-telemetry.backup-route-execution.v1", + "execution_digest": "sha256:4d8481061dc9ce6835169defc96887945052332a0db8d4234ae84032d63cc328", + "proposal_digest": "sha256:4bcad54f7b8e1e77b94112e452cbb58eaf8e882dc10305129c5f9ca69f014a66", + "status": "backup_email_route_deployed_verified", + "timers_activated": false, + "notification_sent_by_helper": false, + "production_data_restored": false, + "started_at": "2026-10-31T17:17:13.743344+00:00", + "helm_revision": 6, + "verified_resource_count": 87, + "after_resources_sha256": "600c4998e54b8e11845849c7d8e4c6b6699556c21f5df6ca79bb86b2e8d1683f", + "live_desired_fields_verified": true, + "runtime_config_verified": true, + "existing_image_and_pvc_preserved": true, + "completed_at": "2026-10-31T17:17:31.488654+00:00" + }, + "receipt_sha256": "c26d636f59109956f281ad46f4885c4db800a9553cf05cac324aa3155ef7ac40", + "original_execution_packet_matches": true, + "live_desired_fields_verified": true, + "runtime_config_verified": true, + "existing_image_and_pvc_preserved": true, + "runtime_modified_by_verification": false +} diff --git a/release/editor-attachments-evidence/content-acceptance-independent-postflight-2026-10-31.json b/release/editor-attachments-evidence/content-acceptance-independent-postflight-2026-10-31.json new file mode 100644 index 0000000..cdbc019 --- /dev/null +++ b/release/editor-attachments-evidence/content-acceptance-independent-postflight-2026-10-31.json @@ -0,0 +1,41 @@ +{ + "schema": "coulomb.content-acceptance-independent-postflight.v1", + "status": "deployed_image_and_complete_native_state_verified", + "proposal_digest": "sha256:e96752ecfa7adef2fd1d9da48902c8e63de124ccfd272fe20ba79b758b3ca4a3", + "receipt_sha256": "074ec03d62d43f35bd6c276213a28de4a7b0161132379221bec0e402b5334233", + "helm_revision": 20, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "source": "1139a6d30eee4f75317b46bd45120a15d818455f", + "packaged_source_files_verified": 79, + "sole_ready_content_writer": true, + "all_frozen_capsule_files_verified": 23, + "installed_helper_hashes_verified": true, + "snapshot_pin_refreshed": true, + "public_health_landing_static_verified": true, + "postflight": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "fresh_paired_point": { + "captured_at": "2026-10-31T20:21:01Z", + "destination": "s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T202101Z-2ad4faf215939d67e82f67a4.tar.age", + "version_id": "1793478121590106", + "encrypted_sha256": "91b4ffe4874a53e3422d604b7c3fa85f6551a481411ef82b2209993552486bfb", + "encrypted_bytes": 2181848, + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed" + }, + "timers_activated": false, + "production_data_restored": false, + "productive_acceptance_inferred": false +} diff --git a/release/editor-attachments-evidence/editor-attachments-baseline-change-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-baseline-change-2026-10-31.json new file mode 100644 index 0000000..71dc2f7 --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-baseline-change-2026-10-31.json @@ -0,0 +1,39 @@ +{ + "schema": "coulomb.acceptance-baseline-change.v1", + "retained_point": "2026-10-31T20:21:01Z", + "old_business_rows": 93, + "new_business_rows": 97, + "table_changes": { + "spaces_space": { + "old": 2, + "new": 4, + "removed": 0, + "added": 2 + }, + "spaces_spacemembership": { + "old": 1, + "new": 3, + "removed": 0, + "added": 2 + } + }, + "old_content_files": 86, + "new_content_files": 98, + "old_files_missing": 0, + "old_files_changed": 0, + "new_files_added": 12, + "old_rows_preserved": true, + "current_baseline": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "interpretation": "Changes observed during the user-requested productive acceptance session; no user verdict inferred." +} diff --git a/release/editor-attachments-evidence/editor-attachments-build-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-build-2026-10-31.json new file mode 100644 index 0000000..30efe22 --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-build-2026-10-31.json @@ -0,0 +1,45 @@ +{ + "schema": "coulomb.content-acceptance-build.v1", + "completed_at": "2026-10-31T21:13:04.039838+00:00", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "build_context": "clean restricted git archive", + "source_full_gate_tests": 568, + "runtime_packages_and_python_unchanged": true, + "runtime": { + "python": "3.12.16", + "distributions": { + "h11": "0.16.0", + "httpcore": "1.0.9", + "psycopg": "3.3.4", + "joserfc": "1.7.4", + "anyio": "4.14.2", + "cryptography": "50.0.0", + "idna": "3.18", + "certifi": "2026.7.22", + "packaging": "26.3", + "python-decouple": "3.8", + "psycopg-binary": "3.3.4", + "sqlparse": "0.5.5", + "whitenoise": "6.12.0", + "cffi": "2.1.1", + "markdown": "3.10.3", + "webencodings": "0.5.1", + "pycparser": "3.0", + "bleach": "6.4.0", + "authlib": "1.7.2", + "django": "6.1", + "typing_extensions": "4.16.0", + "gunicorn": "26.0.0", + "asgiref": "3.12.1", + "httpx": "0.28.1", + "dj-database-url": "3.1.2" + } + }, + "django_checks": { + "check": "System check identified no issues (0 silenced).", + "makemigrations": "No changes detected" + }, + "new_database_migrations": false, + "production_deployed": false +} diff --git a/release/editor-attachments-evidence/editor-attachments-provenance-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-provenance-2026-10-31.json new file mode 100644 index 0000000..df09c4b --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-provenance-2026-10-31.json @@ -0,0 +1,11 @@ +{ + "schema": "coulomb.packaged-source-provenance.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "packaged_file_count": 79, + "all_packaged_source_bytes_match": true, + "source_revision_label_verified": true, + "registry_digest_resolves": true, + "application_source_overlay": false, + "manifest_sha256": "99401a60fdd9a95aff1168858e81fd7d8802cd7603f15c0ea35eb6917981f9fe" +} diff --git a/release/editor-attachments-evidence/editor-attachments-recovery-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-recovery-2026-10-31.json new file mode 100644 index 0000000..1623bcb --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-recovery-2026-10-31.json @@ -0,0 +1,67 @@ +{ + "schema": "coulomb.packaged-content-acceptance-recovery.v1", + "status": "packaged_recovered_export_and_views_verified", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "restore_receipt_sha256": "9dacbd44544f93abf99354158cf610b4fb1f4e59611b22056b3086e0033231ab", + "retained_point_captured_at": "2026-10-31T20:21:01Z", + "candidate_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "previous_image_application": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 93, + "business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca", + "content_files": 86, + "content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0, + "actual_owner_pages_rendered": 40, + "unsaved_outsider_page_denials": 40, + "production_identity_or_grant_created": false + }, + "client_export": { + "status": "recovered_client_export_verified", + "schema": "coulomb.client-dataset-export.v1", + "content_files": 85, + "records": { + "members.user": 1, + "members.member": 1, + "spaces.space": 1, + "spaces.spacemembership": 1 + }, + "admin_events": 0, + "archive_sha256": "ecf5dba7ce891475cd5df15eb30205606f805850af7339ecd46e1479b45d6c5f", + "offboarding_complete": false, + "all_source_tables_compared": 13, + "all_source_files_compared": 86, + "archive_member_hashes_verified": true, + "selected_client_content_complete_locally": true, + "source_sql_and_content_unchanged": true, + "external_bindings_require_review": 0, + "sessions_and_credentials_excluded": true + }, + "source_content_files_unchanged": 86, + "all_tables_unchanged": true, + "application_source_overlay": false, + "network_internal": true, + "published_ports": 0, + "production_access": false, + "production_restore": false, + "driver_sha256": "29c53d551eec2b4f4d60b047f123c727eef76ee7677c054bbfb0ab008355cff9" +} diff --git a/release/editor-attachments-evidence/editor-attachments-target-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-target-2026-10-31.json new file mode 100644 index 0000000..023ddde --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-target-2026-10-31.json @@ -0,0 +1,21 @@ +{ + "baseline_revision": 20, + "baseline_helm_manifest_sha256": "e388f62c97e97d54d87671825bd0b05c854a70b0cca8a00faee452ae7841799e", + "deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe", + "baseline_spec_sha256": "8d79c64cff4bd2c62f03494e80847dc99b965ddabf35645da67e8d1020220798", + "candidate_spec_sha256": "4deab26fc15d63e09922259b3362cd4e5c6bfa10884de058f776c0de8b642b00", + "old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5", + "expected_data": { + "business_tables": 12, + "migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af", + "business_rows": 97, + "business_rows_sha256": "35b0ae7d9dd1ff21c2ceaa04649162545ec36a2c68629548238352d9cd0fb303", + "content_files": 98, + "content_files_sha256": "b657c0e470399b6c7500da500cbbec061bea12ab9633bd74e2f142d797b2d26a", + "global_slug_constraint": true, + "tenant_slug_constraint": true, + "migration_0003_applied": true, + "duplicate_slug_groups": 0 + }, + "image_only_server_preview": true +} diff --git a/release/editor-attachments-evidence/editor-attachments-tests-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-tests-2026-10-31.json new file mode 100644 index 0000000..3570551 --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-tests-2026-10-31.json @@ -0,0 +1,13 @@ +{ + "schema": "coulomb.packaged-content-acceptance-tests.v1", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "tests": 110, + "failures": 0, + "errors": 0, + "skipped": 0, + "network": "none", + "read_only_root": true, + "application_source_overlay": false, + "synthetic_fixtures_only": true +} diff --git a/release/editor-attachments-evidence/editor-attachments-validation-2026-10-31.json b/release/editor-attachments-evidence/editor-attachments-validation-2026-10-31.json new file mode 100644 index 0000000..23e9b42 --- /dev/null +++ b/release/editor-attachments-evidence/editor-attachments-validation-2026-10-31.json @@ -0,0 +1,34 @@ +{ + "schema": "coulomb.editor-attachments-validation.v1", + "reported_gap": "Attachments visible on the original page were absent from editor tabs.", + "main_source": "8b38afddf3f3648f209c60b1bafe3dd25501d857", + "release_source": "31eaa581a65e454b6ef9979326c5bdfe10e13806", + "image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e", + "main_full_gate": { + "tests": 670, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + "release_full_gate": { + "tests": 568, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + "packaged_tests": 110, + "browser_verifies": [ + "saved file list and exact download bytes in both editor tabs", + "management opens separately and keeps unsaved text", + "stale refusal keeps submitted text and attachments", + "save keeps attachments and rejects stale overwrite", + "existing copy/move/download/detach permissions journey" + ], + "runtime_changes": [ + "coulomb_social/apps/spaces/views.py", + "coulomb_social/templates/spaces/page_form.html" + ], + "storage_schema_or_authority_changed": false, + "productive_acceptance_inferred": false, + "test_environment": "Main uses /var/tmp; isolated branch native observer requires /tmp. Final complete gates pass without deselection." +} diff --git a/release/editor-attachments-snapshot.py b/release/editor-attachments-snapshot.py new file mode 100644 index 0000000..342510b --- /dev/null +++ b/release/editor-attachments-snapshot.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Node-side quiesced coulomb-social snapshot. Binary output stays private. + +Run on railiance01 after reviewed live approval. Does not restore production. +Default is server dry-run; --execute briefly pauses the app and captures a +paired pg_dump/content archive. A separate isolated restore must validate it. +""" +from __future__ import annotations + +import argparse +import fcntl +import hashlib +import json +import os +import signal +import stat +import subprocess +import sys +import time +from pathlib import Path + +NS = 'coulomb-social' +IMAGE = ('forgejo.coulomb.social/coulomb/coulomb-social@sha256:' + 'b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e') +CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786' +LOCK_PATH = Path('/var/lib/coulomb-social-backup/snapshot.lock') + + +class Refused(RuntimeError): + pass + + +def run(args: list[str], *, payload: bytes | None = None, timeout: int = 180) -> bytes: + p = subprocess.run(args, input=payload, capture_output=True, timeout=timeout) + if p.returncode: + raise Refused('command_failed') + return p.stdout + + +def k(*args: str, payload: bytes | None = None) -> bytes: + return run(['kubectl', *args], payload=payload) + + +def document(*args: str) -> dict: + return json.loads(k(*args, '-o', 'json')) + + +def private_write(path: Path, value: bytes) -> None: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, 'wb') as f: + f.write(value) + + +def snapshot() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--execute', action='store_true') + parser.add_argument('--destination', type=Path, required=True) + args = parser.parse_args() + os.environ['KUBECONFIG'] = '/etc/rancher/k3s/k3s.yaml' + started = time.monotonic() + if document('get', 'namespace', 'kube-system')['metadata']['uid'] != CLUSTER_UID: + raise Refused('wrong_cluster') + dep = document('get', 'deployment', 'coulomb-social', '-n', NS) + spec = dep['spec']['template']['spec'] + if dep['spec']['replicas'] != 1 or spec['containers'][0]['image'] != IMAGE: + raise Refused('unexpected_release') + if document('get', 'pvc', 'coulomb-social-content', '-n', NS)['status']['phase'] != 'Bound': + raise Refused('pvc_not_bound') + cluster = document('get', 'cluster', 'apps-pg', '-n', 'databases') + primary = cluster['status']['currentPrimary'] + name = 'csoc-paired-snapshot-' + time.strftime('%Y%m%d%H%M%S', time.gmtime()) + pod = { + 'apiVersion': 'v1', 'kind': 'Pod', + 'metadata': {'name': name, 'namespace': NS, 'labels': {'csoc-recovery': name}}, + 'spec': { + 'restartPolicy': 'Never', 'automountServiceAccountToken': False, + 'securityContext': {'runAsNonRoot': True, 'runAsUser': 10001, + 'runAsGroup': 10001, 'fsGroup': 10001, + 'fsGroupChangePolicy': 'OnRootMismatch'}, + 'containers': [{'name': 'snapshot', 'image': IMAGE, + 'command': ['python', '-c', 'import time;time.sleep(1800)'], + 'resources': {'requests': {'cpu': '10m', 'memory': '64Mi'}, + 'limits': {'cpu': '100m', 'memory': '256Mi'}}, + 'securityContext': {'allowPrivilegeEscalation': False, + 'readOnlyRootFilesystem': True, + 'capabilities': {'drop': ['ALL']}}, + 'volumeMounts': [{'name': 'content', 'mountPath': '/snapshot', 'readOnly': True}]}], + 'volumes': [{'name': 'content', 'persistentVolumeClaim': + {'claimName': 'coulomb-social-content', 'readOnly': True}}], + }, + } + body = json.dumps(pod).encode() + k('create', '--dry-run=server', '-f', '-', payload=body) + if not args.execute: + print(json.dumps({'status': 'reviewed', 'server_dry_run': True, + 'image_pinned': True, 'pvc_read_only': True, + 'db_scope': 'coulomb_social_db', 'replicas_restored_to': 1})) + return 0 + dest = args.destination + if not dest.is_absolute() or dest.exists(): + raise Refused('destination_must_be_new_absolute_directory') + dest.mkdir(mode=0o700) + paused = False + created = False + receipt = {'status': 'incomplete', 'app': 'coulomb-social', + 'database': 'coulomb_social_db', 'image': IMAGE, + 'started_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} + try: + paused = True + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=0') + k('wait', '--for=delete', 'pod', '-n', NS, + '-l', 'app.kubernetes.io/instance=coulomb-social', '--timeout=120s') + created = True + k('create', '-f', '-', payload=body) + k('wait', '--for=condition=Ready', 'pod/' + name, '-n', NS, '--timeout=120s') + dump = k('exec', '-n', 'databases', primary, '-c', 'postgres', '--', + 'pg_dump', '-U', 'postgres', '-d', 'coulomb_social_db', '-Fc') + if not dump.startswith(b'PGDMP'): + raise Refused('invalid_pg_dump') + private_write(dest / 'database.dump', dump) + archive_code = ('import io,sys,tarfile; b=io.BytesIO(); ' + 't=tarfile.open(fileobj=b,mode="w:gz"); ' + 't.add("/snapshot",arcname="content"); ' + 't.close();sys.stdout.buffer.write(b.getvalue())') + content = k('exec', '-n', NS, name, '--', 'python', '-c', archive_code) + private_write(dest / 'content.tar.gz', content) + receipt.update(status='captured', writers_quiesced=True, + database_sha256=hashlib.sha256(dump).hexdigest(), + content_sha256=hashlib.sha256(content).hexdigest(), + database_bytes=len(dump), content_bytes=len(content)) + finally: + try: + if created: + k('delete', 'pod', name, '-n', NS, '--wait=true', '--timeout=120s') + finally: + if paused: + k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=1') + k('rollout', 'status', 'deployment/coulomb-social', '-n', NS, '--timeout=180s') + receipt['app_resumed'] = True + receipt['elapsed_seconds'] = round(time.monotonic() - started, 2) + private_write(dest / 'receipt.json', json.dumps(receipt, indent=2).encode()) + print(json.dumps(receipt)) + return 0 + + +def main() -> int: + if '--execute' not in sys.argv: + return snapshot() + LOCK_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + parent = LOCK_PATH.parent.lstat() + if (not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.geteuid() + or parent.st_mode & 0o077): + raise Refused('lock_directory_not_private') + fd = os.open(LOCK_PATH, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'w') as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + raise Refused('snapshot_already_running') from None + def terminate(signum, frame): + raise Refused('snapshot_interrupted') + previous = signal.signal(signal.SIGTERM, terminate) + try: + return snapshot() + finally: + signal.signal(signal.SIGTERM, previous) + + +if __name__ == '__main__': + try: + sys.exit(main()) + except Exception as exc: + # Never expose child output, kubectl objects, credentials or raw exception messages. + print(json.dumps({'status': 'failed', 'failure_type': type(exc).__name__})) + sys.exit(1) diff --git a/release/editor-attachments-values.yaml b/release/editor-attachments-values.yaml new file mode 100644 index 0000000..9e85e9b --- /dev/null +++ b/release/editor-attachments-values.yaml @@ -0,0 +1,50 @@ +# Production overrides for coulomb-social. +# Non-secret values only. Secrets live in K8s Secret coulomb-social-env. + +image: + repository: forgejo.coulomb.social/coulomb/coulomb-social + tag: "31eaa581a65e" + digest: "sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e" + +# Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). +# Apex cutover is deferred until data + self-registration are ready. +env: + DJANGO_SETTINGS_MODULE: coulomb_social.settings.prod + ALLOWED_HOSTS: app.coulomb.social,localhost + CSRF_TRUSTED_ORIGINS: https://app.coulomb.social + CONTENT_ROOT: /app/var/content + DEFAULT_TENANT_ID: "tenant:coulomb" + OIDC_ENABLED: "true" + OIDC_ISSUER: https://kc.coulomb.social + OIDC_CLIENT_ID: coulomb-social + OIDC_REDIRECT_URI: https://app.coulomb.social/auth/callback/ + OIDC_SCOPES: "openid profile email groups" + USER_ENGINE_BASE_URL: https://users.coulomb.social + USER_ENGINE_APPLICATION_ID: coulomb-social + USER_ENGINE_EXPECTED_AUDIENCE: user-engine-portal + NETKINGDOM_REGISTRATION_URL: https://users.92-205-62-239.nip.io/register + +probes: + hostHeader: app.coulomb.social + path: /healthz + +# One writable local-path volume; Recreate prevents concurrent old/new writers. +replicaCount: 1 +deploymentStrategy: Recreate +persistence: + content: + enabled: true + storageClass: local-path + size: 2Gi + mountPath: /app/var/content + retain: true + +# Wave-2 right-sizing (CSOC-WP-0017-T01, 2026-10-21 node budget): 7d p95 +# 0.4m CPU / 146Mi. Limits stay at the chart defaults (1000m/1Gi). +resources: + requests: + cpu: 15m + memory: 176Mi + limits: + cpu: 1000m + memory: 1Gi diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 7682515..3a151d4 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -518,3 +518,31 @@ pass40owner/40outsider checks; client export verifies85files with all source SQL/files unchanged. Owner live values and consumer source/helper pins now match revision20. Recovery remains isolated; timers are inactive/disabled. Remaining user workflow verdicts are tracked by existing CSOC-WP-0013-T02/T03/T05/T06. + +## T08 — Deliver the editor attachment visibility correction + +```task +id: RAPPS-WP-0015-T08 +status: wait +priority: high +needs_human: true +wait_kind: human +decision_id: "0d538348-aadd-41a2-88f6-191defcf22b2" +blocking_reason: "Exact editor attachment proposal2cddcf38 passes native preflight; founder-terminal execution and independent return are required." +``` + +Productive revision20 review identified that attachments are visible on the +original page but absent from editor tabs. CSOC-WP-0013-T06 owns the correction: +display saved attachments/downloads in the editor and preserve that list after +stale-edit refusal, with management opening separately to retain unsaved text. +Prepare the minimal follow-up image from the deployed source, preserve existing +schema/data/permissions, and reuse the exact attended image-release lane with a +fresh paired rollback point. Complete after founder execution and independent +postflight/recovery/source-pin reconciliation. Productive acceptance remains +CSOC-WP-0013-T03/T06; T07's verified revision20 delivery stays complete. + +Prepared source31eaa58/imageb58d858a; exact proposal2cddcf38b7052287c10dce467a200a74cfa9699c666ae9ac538fc82917129e7a. Main670/isolated568/packaged110 pass. +Recovered views/export pass. Full native preflight at revision20/97rows/98files +passes; existing93rows/86files are unchanged by the productive review additions. +Plan: release/coulomb-editor-attachments-31eaa58.json. Review/command: +coulomb-social/docs/release/content-acceptance-review-2026-10-31.md. From 0c72b3bec85a98faf9e9cc76631ad2f0ed3f4025 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 31 Oct 2026 22:17:56 +0100 Subject: [PATCH 15/16] Link editor release execution task to State Hub Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a --- workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 3a151d4..4ebe899 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -529,6 +529,7 @@ needs_human: true wait_kind: human decision_id: "0d538348-aadd-41a2-88f6-191defcf22b2" blocking_reason: "Exact editor attachment proposal2cddcf38 passes native preflight; founder-terminal execution and independent return are required." +state_hub_task_id: "e7b04735-e618-5551-9ecf-6d423c83d156" ``` Productive revision20 review identified that attachments are visible on the From 4e2809cb1ebb75fef17c18be44aa86d297c7f4cc Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 1 Nov 2026 00:41:57 +0100 Subject: [PATCH 16/16] Close verified Coulomb editor release delivery at revision 21 Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a --- WORK-RECORDS.md | 2 +- helm/coulomb-social-values.yaml | 4 +-- ...P-0015-coulomb-personal-content-release.md | 25 +++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index dd00903..50d329a 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -44,7 +44,7 @@ | task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0015-T07 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | -| task | RAPPS-WP-0015-T08 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | +| task | RAPPS-WP-0015-T08 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md | | task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | | task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md | diff --git a/helm/coulomb-social-values.yaml b/helm/coulomb-social-values.yaml index 25232f7..9e85e9b 100644 --- a/helm/coulomb-social-values.yaml +++ b/helm/coulomb-social-values.yaml @@ -3,8 +3,8 @@ image: repository: forgejo.coulomb.social/coulomb/coulomb-social - tag: "1139a6d30eee" - digest: "sha256:7906f461bc8d2d5204fd4fde67a251850f725b914452a66172919d6853672ed5" + tag: "31eaa581a65e" + digest: "sha256:b58d858abd5b4acbd6b893c3f622f7a9fdb5e606164ab9866eb3cfaa04df1a0e" # Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social). # Apex cutover is deferred until data + self-registration are ready. diff --git a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md index 4ebe899..4d609ed 100644 --- a/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md +++ b/workplans/RAPPS-WP-0015-coulomb-personal-content-release.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: railiance-apps created: "2026-10-06" -updated: "2026-10-31" +updated: "2026-11-01" related: - CSOC-WP-0008 state_hub_workstream_id: "79abacf8-361e-51ab-829d-40dec8e142d1" @@ -523,12 +523,11 @@ user workflow verdicts are tracked by existing CSOC-WP-0013-T02/T03/T05/T06. ```task id: RAPPS-WP-0015-T08 -status: wait +status: done priority: high -needs_human: true -wait_kind: human +needs_human: false decision_id: "0d538348-aadd-41a2-88f6-191defcf22b2" -blocking_reason: "Exact editor attachment proposal2cddcf38 passes native preflight; founder-terminal execution and independent return are required." +intervention_note: "Founder executed2cddcf38; revision21/imageb58d858a independently verified, source pins reconciled, fresh-point recovery/export passed. No rerun required. User confirmed editor visibility; remaining productive checks stay CSOC-WP-0013." state_hub_task_id: "e7b04735-e618-5551-9ecf-6d423c83d156" ``` @@ -547,3 +546,19 @@ Recovered views/export pass. Full native preflight at revision20/97rows/98files passes; existing93rows/86files are unchanged by the productive review additions. Plan: release/coulomb-editor-attachments-31eaa58.json. Review/command: coulomb-social/docs/release/content-acceptance-review-2026-10-31.md. + +2026-11-01 verified return: founder executed2cddcf38, completed at2026-10-31 +23:32:38Z, Helm21/imageb58d858a/source31eaa58. Independent complete-spec/source/ +helper checks agree with durable receipt:97business rows/98files unchanged, +43owner views/43outsider denials,79packaged files and21frozen capsule files. +Live GET-only check renders43editors and verifies42download links across41editors +with saved resources. Bernd confirms both reopened tabs show the attachment. + +The fresh23:29:54point independently mirrors/reads back/decrypts/restores all13 +SQL tables/119rows and98files. Deployed and previous packages read the recovered +store; exports verify its content tenant, with all source +SQL/files unchanged. Live values and consumer source/helper pins now match +revision21; snapshot server dry-run and9snapshot/restore regressions pass. +Recovery container is stopped and timers remain inactive/disabled. T08 is done; +remaining copy/move/sharing/portability outcomes stay CSOC-WP-0013-T03/T05/T06. +Evidence: coulomb-social/docs/release/editor-attachments-*-2026-11-01.json.