Compare commits
16 commits
csoc-stora
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e2809cb1e | |||
| 0c72b3bec8 | |||
| 979b7819df | |||
| b39910e330 | |||
| a15cc888d2 | |||
| dc927d7751 | |||
| c733e86b0f | |||
| 9c74884641 | |||
| 75343cfec1 | |||
| d3c46421b8 | |||
| 169dd3e074 | |||
| 904dcb3ce3 | |||
| d795120f98 | |||
| a70e3b2e9f | |||
| 84a5aa0c92 | |||
| 8434134391 |
60 changed files with 5198 additions and 3 deletions
|
|
@ -41,6 +41,10 @@
|
|||
| 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 | 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 | 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 |
|
||||
|
|
|
|||
39
docs/coulomb-image-release-2026-10-31.md
Normal file
39
docs/coulomb-image-release-2026-10-31.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Completed Coulomb identity/export image release
|
||||
|
||||
The exact supervised proposal is
|
||||
`sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f`.
|
||||
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).
|
||||
|
||||
`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` 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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
70
docs/evidence/2026-10-31-coulomb-image-release-receipt.json
Normal file
70
docs/evidence/2026-10-31-coulomb-image-release-receipt.json
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
68
docs/evidence/2026-10-31-coulomb-storage-release.json
Normal file
68
docs/evidence/2026-10-31-coulomb-storage-release.json
Normal file
|
|
@ -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
|
||||
}
|
||||
151
docs/rapp-bounded-executor-intake.md
Normal file
151
docs/rapp-bounded-executor-intake.md
Normal file
|
|
@ -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/<image>@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=<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.
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
image:
|
||||
repository: forgejo.coulomb.social/coulomb/coulomb-social
|
||||
tag: "dd83c90"
|
||||
digest: "sha256:9a646d8e01a61ba5316d3c2eb0273064e17f2d0300ad07af766ccedda65ecb18"
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
175
release/content-acceptance-snapshot.py
Normal file
175
release/content-acceptance-snapshot.py
Normal file
|
|
@ -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)
|
||||
50
release/content-acceptance-values.yaml
Normal file
50
release/content-acceptance-values.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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."
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
175
release/content-lifecycle-snapshot.py
Normal file
175
release/content-lifecycle-snapshot.py
Normal file
|
|
@ -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)
|
||||
50
release/content-lifecycle-values.yaml
Normal file
50
release/content-lifecycle-values.yaml
Normal file
|
|
@ -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
|
||||
79
release/coulomb-content-acceptance-1139a6d.json
Normal file
79
release/coulomb-content-acceptance-1139a6d.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
69
release/coulomb-content-lifecycle-f306dbf.json
Normal file
69
release/coulomb-content-lifecycle-f306dbf.json
Normal file
|
|
@ -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
|
||||
}
|
||||
77
release/coulomb-editor-attachments-31eaa58.json
Normal file
77
release/coulomb-editor-attachments-31eaa58.json
Normal file
|
|
@ -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."
|
||||
}
|
||||
51
release/coulomb-identity-export-23ffe2c.json
Normal file
51
release/coulomb-identity-export-23ffe2c.json
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
175
release/editor-attachments-snapshot.py
Normal file
175
release/editor-attachments-snapshot.py
Normal file
|
|
@ -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)
|
||||
50
release/editor-attachments-values.yaml
Normal file
50
release/editor-attachments-values.yaml
Normal file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
175
release/identity-export-snapshot.py
Normal file
175
release/identity-export-snapshot.py
Normal file
|
|
@ -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)
|
||||
50
release/identity-export-values.yaml
Normal file
50
release/identity-export-values.yaml
Normal file
|
|
@ -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
|
||||
364
tests/test_coulomb_image_release.py
Normal file
364
tests/test_coulomb_image_release.py
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
"""Synthetic release safety cases; no cluster, credentials or real content."""
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
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()
|
||||
|
||||
|
||||
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)
|
||||
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"
|
||||
|
||||
|
||||
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'
|
||||
198
tools/coulomb-image-release.py
Normal file
198
tools/coulomb-image-release.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"""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_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",
|
||||
values,
|
||||
snapshot,
|
||||
)
|
||||
]
|
||||
paths += sorted((ROOT / "charts/coulomb-social").rglob("*"))
|
||||
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")
|
||||
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(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)
|
||||
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(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:
|
||||
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()
|
||||
134
tools/coulomb_image_probe.py
Normal file
134
tools/coulomb_image_probe.py
Normal file
|
|
@ -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))
|
||||
614
tools/coulomb_image_release_node.py
Normal file
614
tools/coulomb_image_release_node.py
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
"""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 contextlib import ExitStack
|
||||
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")
|
||||
INSTALL_RECEIPT = Path(
|
||||
"/var/lib/coulomb-social-backup-installations/20261031bacc0001/receipt.json"
|
||||
)
|
||||
|
||||
|
||||
def canonical(value):
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
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,
|
||||
"started_at": datetime.now(UTC).isoformat(),
|
||||
"status": "preflight",
|
||||
"production_data_restored": False,
|
||||
"timers_activated": False,
|
||||
}
|
||||
self.journal = JOURNAL_ROOT / expected.removeprefix("sha256:")
|
||||
self.verify_installation_prerequisite()
|
||||
if read_only:
|
||||
return
|
||||
if self.journal.exists():
|
||||
raise Refused("prior_execution_requires_review")
|
||||
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
|
||||
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)
|
||||
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 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"]
|
||||
!= 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 / self.artifact_name("values_file")),
|
||||
"-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 / self.artifact_name("values_file")),
|
||||
"-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"][self.artifact_name("snapshot_file")]
|
||||
):
|
||||
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 / self.artifact_name("values_file")),
|
||||
"-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 / self.artifact_name("snapshot_file")).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 = None
|
||||
try:
|
||||
release = Release(args.capsule, args.expect_digest, read_only=args.preflight)
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -8,7 +8,7 @@ status: active
|
|||
owner: codex
|
||||
topic_slug: railiance-apps
|
||||
created: "2026-10-06"
|
||||
updated: "2026-10-08"
|
||||
updated: "2026-11-01"
|
||||
related:
|
||||
- CSOC-WP-0008
|
||||
state_hub_workstream_id: "79abacf8-361e-51ab-829d-40dec8e142d1"
|
||||
|
|
@ -340,3 +340,225 @@ 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: done
|
||||
priority: high
|
||||
needs_human: false
|
||||
decision_id: "75b94bb6-4709-495f-91fe-43e53e2867ed"
|
||||
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"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## T06 — Preserve private custody when freezing future release evidence
|
||||
|
||||
```task
|
||||
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
|
||||
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.
|
||||
|
||||
## T07 — Deliver the combined content and author attachment release
|
||||
|
||||
```task
|
||||
id: RAPPS-WP-0015-T07
|
||||
status: done
|
||||
priority: high
|
||||
needs_human: false
|
||||
decision_id: "d9b70cc5-e035-40ae-8d0d-020190581202"
|
||||
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"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## T08 — Deliver the editor attachment visibility correction
|
||||
|
||||
```task
|
||||
id: RAPPS-WP-0015-T08
|
||||
status: done
|
||||
priority: high
|
||||
needs_human: false
|
||||
decision_id: "0d538348-aadd-41a2-88f6-191defcf22b2"
|
||||
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"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue