Prepare supervised Coulomb identity and export image release
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
tegwick 2026-10-31 04:47:32 +01:00
parent 8434134391
commit 84a5aa0c92
15 changed files with 1616 additions and 1 deletions

View file

@ -41,6 +41,7 @@
| task | RAPPS-WP-0015-T02 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T03 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T05 | progress | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md |
| task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md |
| task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md |

View file

@ -0,0 +1,38 @@
# Prepared Coulomb identity/export image release
The exact supervised proposal is
`sha256:9f1daf6c5fb56b2b4fc9817f28f5ad090c884b0295f7d6f4469e42d48fa8d59f`.
Open decision: `75b94bb6-4709-495f-91fe-43e53e2867ed`.
The full founder packet is
[coulomb-social release review](/home/worsch/coulomb-social/docs/release/identity-export-release-review-2026-10-31.md).
`tools/coulomb-image-release.py` prepares the capsule by default. `--preflight`
checks the live target without capture or deployment. `--execute --expect-digest`
requires the founder's own interactive terminal and the exact typed proposal.
The node controller freezes verified source bytes into its private durable
journal before execution. An existing attempt refuses blind replay.
This distinct controller adapts the previously reviewed storage controller
from owner source 45d6178. It requires migration 0003 and the global constraint
already present, adds exact migration-history equality, verifies installed
helpers during preflight, verifies the desired complete spec through an image-only
server-dry-run patch and verifies rollback readiness/specification. Unknown
helper changes refuse overwrite. Historical storage commands/capsules remain
unchanged and must not be replayed.
Candidate values live in `release/identity-export-values.yaml`; the current
`helm/coulomb-social-values.yaml` remains pinned to the actual revision 18 image.
The new snapshot-helper bytes are separate review material until execution.
No unit/timer, sender, grant, apex or data-restore operation is included.
Validation: 19 release safety cases pass; Ruff, Python compile and Helm lint pass.
The sealed native preflight is retained in
`docs/evidence/2026-10-31-coulomb-image-release-preflight.json`.
Four hash-bound candidate evidence files travel inside the capsule. The product
review names source 441 tests, actual packaged 25 tests, all 80 packaged source matches
and the retained isolated recovery/export/view checks with their limits.
After founder execution, inspect the durable receipt and independent current
state before reconciling the live values and snapshot pin. The product then
mirrors/retrieves the newly captured point and verifies packaged export on that
coherent post-correction data. Source publication alone does not finish T05.

View file

@ -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
}

View 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
}

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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"
}

View file

@ -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"
}

View 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)

View 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

View file

@ -0,0 +1,194 @@
"""Synthetic release safety cases; no cluster, credentials or real content."""
import copy
import importlib.util
import json
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
SOURCE = Path(__file__).resolve().parents[1] / "tools/coulomb_image_release_node.py"
spec = importlib.util.spec_from_file_location("storage_release", SOURCE)
release = importlib.util.module_from_spec(spec)
spec.loader.exec_module(release)
launcher_spec = importlib.util.spec_from_file_location("storage_launcher", SOURCE.with_name("coulomb-image-release.py"))
launcher = importlib.util.module_from_spec(launcher_spec)
launcher_spec.loader.exec_module(launcher)
def capsule(tmp_path):
root = tmp_path / "capsule"
root.mkdir()
(root / "probe.py").write_bytes(b"synthetic read-only probe")
plan = {"files": {"probe.py": release.sha((root / "probe.py").read_bytes())}}
(root / "manifest.json").write_text(json.dumps(plan))
return root, "sha256:" + release.sha(release.canonical(plan))
def test_capsule_tampering_refuses_before_privileged_work(tmp_path, monkeypatch):
root, digest = capsule(tmp_path)
monkeypatch.setattr(release.os, "geteuid", lambda: 0)
monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal")
(root / "probe.py").write_bytes(b"changed")
with pytest.raises(release.Refused, match="capsule_file_differs"):
release.Release(root, digest)
assert not (tmp_path / "journal").exists()
def test_preflight_never_creates_an_execution_journal(tmp_path, monkeypatch):
root, digest = capsule(tmp_path)
monkeypatch.setattr(release.os, "geteuid", lambda: 0)
monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal")
instance = release.Release(root, digest, read_only=True)
instance.save()
assert not (tmp_path / "journal").exists()
def test_prior_attempt_is_not_blindly_replayed(tmp_path, monkeypatch):
root, digest = capsule(tmp_path)
journal = tmp_path / "journal"
(journal / digest.removeprefix("sha256:")).mkdir(parents=True)
monkeypatch.setattr(release.os, "geteuid", lambda: 0)
monkeypatch.setattr(release, "JOURNAL_ROOT", journal)
with pytest.raises(release.Refused, match="prior_execution_requires_review"):
release.Release(root, digest)
def test_mutable_staging_is_frozen_before_live_work(tmp_path, monkeypatch):
root, digest = capsule(tmp_path)
monkeypatch.setattr(release.os, "geteuid", lambda: 0)
monkeypatch.setattr(release, "JOURNAL_ROOT", tmp_path / "journal")
instance = release.Release(root, digest)
(root / "probe.py").write_bytes(b"changed after verification")
assert (instance.root / "probe.py").read_bytes() == b"synthetic read-only probe"
assert (instance.journal / "receipt.json").is_file()
@pytest.mark.parametrize("field", ["business_rows_sha256", "content_files_sha256", "migration_rows_sha256"])
def test_dataset_drift_is_not_accepted(field):
instance = object.__new__(release.Release)
baseline = {"business_tables": 2, "business_rows": 3, "business_rows_sha256": "synthetic-a",
"content_files": 4, "content_files_sha256": "synthetic-b",
"migration_rows_sha256": "synthetic-migrations"}
instance.plan = {"expected_data": baseline}
changed = dict(baseline, duplicate_slug_groups=0, tenant_slug_constraint=True)
changed[field] = "different"
with pytest.raises(release.Refused, match="dataset_drifted"):
instance.same_data(changed)
def test_failed_preflight_never_captures_or_upgrades():
instance = object.__new__(release.Release)
instance.preflight = Mock(side_effect=release.Refused("guard stopped"))
instance.capture = Mock()
instance.command = Mock()
with pytest.raises(release.Refused):
instance.execute()
instance.capture.assert_not_called()
instance.command.assert_not_called()
@pytest.mark.parametrize("rollback_spec,rollback_ready,verified", [
({"synthetic": "old"}, 1, True),
({"synthetic": "candidate"}, 1, False),
({"synthetic": "old"}, 0, False),
])
def test_postflight_failure_rolls_back_image_without_restoring_data(
tmp_path, monkeypatch, rollback_spec, rollback_ready, verified):
pin = tmp_path / "snapshot.py"
pin.write_bytes(b"old pin")
monkeypatch.setattr(release, "SNAPSHOT", pin)
instance = object.__new__(release.Release)
instance.receipt = {}
instance.root = tmp_path
spec = {"synthetic": "candidate"}
baseline = {"business_tables": 2, "business_rows": 3, "business_rows_sha256": "synthetic-a",
"content_files": 4, "content_files_sha256": "synthetic-b",
"migration_rows_sha256": "synthetic-migrations"}
instance.plan = {"candidate_spec_sha256": release.sha(release.canonical(spec)), "baseline_revision": 18,
"baseline_spec_sha256": release.sha(release.canonical({"synthetic": "old"})),
"expected_data": baseline}
instance.preflight = Mock()
instance.capture = Mock()
instance.save = Mock()
instance.deployment = Mock(side_effect=[
{"spec": copy.deepcopy(spec), "status": {"readyReplicas": 1}},
{"spec": rollback_spec, "status": {"readyReplicas": rollback_ready}},
])
instance.probe = lambda mode: dict(baseline, business_rows_sha256="changed")
instance.command = Mock(return_value=SimpleNamespace(stdout=b"", returncode=0))
instance.restore_pin = Mock()
with pytest.raises(release.Refused, match="dataset_drifted"):
instance.execute()
commands = [call.args[0] for call in instance.command.call_args_list]
assert commands[0][:2] == ["helm", "upgrade"] and "--atomic" in commands[0]
assert commands[1][:4] == ["helm", "rollback", "coulomb-social", "18"]
assert not any("pg_restore" in command for command in commands)
instance.restore_pin.assert_called_once_with(b"old pin")
assert instance.receipt["image_rollback_completed"] is verified
def test_live_launcher_rejects_unattended_execution_before_ssh(monkeypatch):
_, digest, _ = launcher.prepare()
monkeypatch.setattr(launcher.sys, "argv", ["release", "--execute", "--expect-digest", digest])
monkeypatch.setattr(launcher.sys, "stdin", SimpleNamespace(isatty=lambda: False))
command = Mock()
monkeypatch.setattr(launcher.subprocess, "run", command)
with pytest.raises(SystemExit, match="own terminal"):
launcher.main()
command.assert_not_called()
def test_timeout_gives_the_existing_capture_resume_window(monkeypatch):
instance = object.__new__(release.Release)
instance.env = {}
process = Mock()
process.communicate.side_effect = release.subprocess.TimeoutExpired(["synthetic"], 1)
monkeypatch.setattr(release.subprocess, "Popen", Mock(return_value=process))
with pytest.raises(release.Refused, match="cleanup_window"):
instance.command(["synthetic"], timeout=1)
process.terminate.assert_called_once()
process.wait.assert_called_once_with(timeout=450)
process.kill.assert_not_called()
@pytest.mark.parametrize("field", ["global_slug_constraint", "migration_0003_applied"])
@pytest.mark.parametrize("value", [False, None])
def test_image_only_release_requires_existing_migration_and_constraint(field, value):
instance = object.__new__(release.Release)
baseline = {"business_tables": 2, "business_rows": 3,
"business_rows_sha256": "rows", "content_files": 4,
"content_files_sha256": "files", "migration_rows_sha256": "migrations"}
instance.plan = {"expected_data": baseline}
observed = dict(baseline, duplicate_slug_groups=0, tenant_slug_constraint=True,
global_slug_constraint=True, migration_0003_applied=True)
instance.same_data(observed)
observed[field] = value
with pytest.raises(release.Refused, match="already_migrated_baseline_required"):
instance.same_data(observed)
def test_rollback_does_not_overwrite_unrecognized_snapshot_helper(tmp_path, monkeypatch):
pin = tmp_path / "snapshot.py"
pin.write_bytes(b"concurrent owner change")
monkeypatch.setattr(release, "SNAPSHOT", pin)
instance = object.__new__(release.Release)
instance.plan = {"files": {"release/identity-export-snapshot.py": release.sha(b"new pin")}}
with pytest.raises(release.Refused, match="snapshot_pin_changed_during_rollback"):
instance.restore_pin(b"old pin")
assert pin.read_bytes() == b"concurrent owner change"
def test_known_candidate_helper_can_be_rolled_back(tmp_path, monkeypatch):
pin = tmp_path / "snapshot.py"
pin.write_bytes(b"new pin")
monkeypatch.setattr(release, "SNAPSHOT", pin)
instance = object.__new__(release.Release)
instance.plan = {"files": {"release/identity-export-snapshot.py": release.sha(b"new pin")}}
instance.restore_pin(b"old pin")
assert pin.read_bytes() == b"old pin"
# A completed inverse can be checked again without changing it.
instance.restore_pin(b"old pin")
assert pin.read_bytes() == b"old pin"

View file

@ -0,0 +1,185 @@
"""Prepare/read-only-check a sealed release; live mode is attended founder work.
This launcher does not mint an approval or bypass gated-executor. The existing
gated-executor profile covers Kubernetes, not the root-owned backup helper refresh.
Only the founder runs --execute in their own terminal after reviewing the packet.
"""
from __future__ import annotations
import argparse
import hashlib
import io
import json
import os
import shlex
import subprocess
import sys
import tarfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PLAN = ROOT / "release/coulomb-identity-export-23ffe2c.json"
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
def prepare():
plan = json.loads(PLAN.read_text())
paths = [
ROOT / name
for name in (
"tools/coulomb-image-release.py",
"tools/coulomb_image_release_node.py",
"tools/coulomb_image_probe.py",
"release/identity-export-values.yaml",
"release/identity-export-snapshot.py",
)
]
paths += sorted((ROOT / "charts/coulomb-social").rglob("*"))
paths += sorted((ROOT / "release/identity-export-evidence").glob("*.json"))
paths = [p for p in paths if p.is_file()]
if any(p.is_symlink() or not p.resolve().is_relative_to(ROOT) for p in paths):
raise RuntimeError("regular_reviewed_files_required")
content = {str(p.relative_to(ROOT)): p.read_bytes() for p in paths}
plan["files"] = {
name: hashlib.sha256(data).hexdigest() for name, data in content.items()
}
for name, expected in plan["rehearsal_evidence_sha256"].items():
if plan["files"].get("release/identity-export-evidence/" + name) != expected:
raise RuntimeError("reviewed_candidate_evidence_changed")
digest = "sha256:" + hashlib.sha256(canonical(plan)).hexdigest()
content["manifest.json"] = canonical(plan)
return plan, digest, content
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--preflight", action="store_true")
parser.add_argument("--execute", action="store_true")
parser.add_argument("--expect-digest")
parser.add_argument("--output", type=Path)
args = parser.parse_args()
if args.preflight and args.execute:
raise SystemExit("choose read-only preflight or attended execution")
plan, digest, content = prepare()
if args.expect_digest and args.expect_digest != digest:
raise SystemExit("reviewed proposal changed; do not execute")
if not args.preflight and not args.execute:
print(
json.dumps(
{
"status": "prepared",
"proposal_digest": digest,
"file_count": len(content),
"candidate": plan["candidate_image"],
"live_changes": False,
}
)
)
return
if args.execute:
if not args.expect_digest or not sys.stdin.isatty():
raise SystemExit(
"founder must review and run the exact digest in their own terminal"
)
print("Reviewed proposal: " + digest)
print(
"Coulomb-only image update; existing database schema retained, fresh paired backup, postflight and backup image-pin refresh."
)
if (
input("Type DEPLOY followed by the full digest to proceed: ").strip()
!= "DEPLOY " + digest
):
raise SystemExit("not approved; no live change")
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w:") as archive:
for name, data in content.items():
entry = tarfile.TarInfo(name)
entry.size, entry.mode = len(data), 0o600
archive.addfile(entry, io.BytesIO(data))
remote = "/tmp/csoc-reviewed-image-" + digest.removeprefix("sha256:")
receiver = r"""
import hashlib,io,json,os,pathlib,sys,tarfile
root=pathlib.Path(sys.argv[1]);expected=sys.argv[2]
raw=sys.stdin.buffer.read(2*1024**2+1);assert len(raw)<=2*1024**2
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive:
entries=archive.getmembers();assert len({e.name for e in entries})==len(entries)
assert all(e.isfile() and not pathlib.Path(e.name).is_absolute() and '..' not in pathlib.Path(e.name).parts for e in entries)
data={e.name:archive.extractfile(e).read() for e in entries}
plan=json.loads(data['manifest.json'])
canonical=json.dumps(plan,sort_keys=True,separators=(',',':')).encode()
assert 'sha256:'+hashlib.sha256(canonical).hexdigest()==expected
assert set(data)==set(plan['files'])|{'manifest.json'}
for name,digest in plan['files'].items():assert hashlib.sha256(data[name]).hexdigest()==digest
root.mkdir(mode=0o700,exist_ok=True)
assert root.lstat().st_uid==os.getuid() and root.lstat().st_mode&0o077==0
for name,value in data.items():
target=root/name;target.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
if target.exists():assert not target.is_symlink() and target.read_bytes()==value;continue
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as stream:stream.write(value)
"""
staged = subprocess.run(
[
"ssh",
"railiance01",
"python3 -c "
+ shlex.quote(receiver)
+ " "
+ shlex.quote(remote)
+ " "
+ shlex.quote(digest),
],
input=buffer.getvalue(),
capture_output=True,
timeout=60,
check=False,
)
if staged.returncode:
raise SystemExit("reviewed capsule staging refused; no live change")
node_hash = plan["files"]["tools/coulomb_image_release_node.py"]
guard = (
"import hashlib,pathlib,sys;p=pathlib.Path("
+ repr(remote + "/tools/coulomb_image_release_node.py")
+ ");"
"b=p.read_bytes();assert hashlib.sha256(b).hexdigest()=="
+ repr(node_hash)
+ ";"
"sys.argv=['reviewed-release','--capsule',"
+ repr(remote)
+ ",'--expect-digest',"
+ repr(digest)
+ ","
+ repr("--execute" if args.execute else "--preflight")
+ "];exec(compile(b,str(p),'exec'),{'__name__':'__main__'})"
)
result = subprocess.run(
["ssh", "railiance01", "sudo -n python3 -c " + shlex.quote(guard)],
capture_output=True,
timeout=3600,
check=False,
)
try:
receipt = json.loads(result.stdout)
except (ValueError, TypeError):
receipt = {
"status": "remote_execution_unconfirmed",
"proposal_digest": digest,
"journal": "/var/lib/coulomb-social-backup/releases/"
+ digest.removeprefix("sha256:")
+ "/receipt.json",
}
if args.output:
fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(receipt, stream, indent=2)
print(json.dumps(receipt))
if result.returncode:
raise SystemExit(1)
if __name__ == "__main__":
main()

View 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))

View file

@ -0,0 +1,525 @@
"""Attended founder-only node release; argv/data are sealed by the reviewed capsule.
This is a supervisor command, not a new agent credential or approval boundary.
No Secret object/output is fetched. Existing approved backup custody stays local.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import re
import stat
import subprocess
import tempfile
import time
import urllib.request
from datetime import UTC, datetime
from pathlib import Path
JOURNAL_ROOT = Path("/var/lib/coulomb-social-backup/releases")
SNAPSHOT = Path("/opt/coulomb-social-backup/snapshot-content-and-db.py")
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
def sha(data):
return hashlib.sha256(data).hexdigest()
class Refused(RuntimeError):
pass
class Release:
def __init__(self, capsule, expected, read_only=False):
self.root = capsule
self.plan = json.loads((capsule / "manifest.json").read_text())
frozen = {}
if "sha256:" + sha(canonical(self.plan)) != expected:
raise Refused("proposal_digest_differs")
for name, digest in self.plan["files"].items():
path = capsule / name
if (
not path.resolve().is_relative_to(capsule.resolve())
or Path(name).is_absolute()
or ".." in Path(name).parts
or path.is_symlink()
or not path.is_file()
or sha(path.read_bytes()) != digest
):
raise Refused("capsule_file_differs")
frozen[name] = path.read_bytes()
if sha(frozen[name]) != digest:
raise Refused("capsule_changed_during_verification")
if os.geteuid() != 0:
raise Refused("attended_founder_root_execution_required")
self.env = dict(os.environ, KUBECONFIG="/etc/rancher/k3s/k3s.yaml")
self.read_only = read_only
self.receipt = {
"schema": "coulomb.guarded-image-release.v1",
"proposal_digest": expected,
"started_at": datetime.now(UTC).isoformat(),
"status": "preflight",
"production_data_restored": False,
"timers_activated": False,
}
self.journal = JOURNAL_ROOT / expected.removeprefix("sha256:")
if read_only:
return
if self.journal.exists():
raise Refused("prior_execution_requires_review")
self.journal.mkdir(parents=True, mode=0o700)
self.root = self.journal / "capsule"
self.root.mkdir(mode=0o700)
for name, data in frozen.items():
target = self.root / name
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "wb") as stream:
stream.write(data)
self.receipt["pid"] = os.getpid()
self.save()
def save(self):
if self.read_only:
return
fd, name = tempfile.mkstemp(prefix=".receipt-", dir=self.journal)
with os.fdopen(fd, "w") as stream:
json.dump(self.receipt, stream, sort_keys=True, indent=2)
stream.flush()
os.fsync(stream.fileno())
os.replace(name, self.journal / "receipt.json")
directory = os.open(self.journal, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
def command(self, arguments, payload=None, env=None, timeout=180, allowed=(0,)):
# In particular, give the paired capture its existing app-resume window
# on timeout; subprocess.run would SIGKILL it before that cleanup.
with tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr:
process = subprocess.Popen(
arguments,
env=env or self.env,
stdin=subprocess.PIPE if payload is not None else subprocess.DEVNULL,
stdout=stdout,
stderr=stderr,
)
try:
process.communicate(payload, timeout=timeout)
except subprocess.TimeoutExpired:
process.terminate()
try:
process.wait(timeout=450)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
raise Refused("private_child_timed_out_after_cleanup_window") from None
stdout.seek(0)
body = stdout.read(8 * 1024**2 + 1)
if len(body) > 8 * 1024**2:
raise Refused("bounded_child_output_required")
result = subprocess.CompletedProcess(arguments, process.returncode, body)
if result.returncode not in allowed:
raise Refused("private_child_refused")
return result
def kdoc(self, *arguments):
return json.loads(self.command(["kubectl", *arguments, "-o", "json"]).stdout)
def history(self):
return json.loads(
self.command(
[
"helm",
"history",
"coulomb-social",
"-n",
"coulomb-social",
"-o",
"json",
]
).stdout
)
def deployment(self):
return self.kdoc("get", "deployment", "coulomb-social", "-n", "coulomb-social")
def probe(self, mode="data"):
source = (self.root / "tools/coulomb_image_probe.py").read_text()
result = self.command(
[
"kubectl",
"exec",
"-n",
"coulomb-social",
"deploy/coulomb-social",
"--",
"python",
"-c",
source,
mode,
]
)
return json.loads(result.stdout)
def same_data(self, actual):
for name in (
"business_tables",
"business_rows",
"business_rows_sha256",
"content_files",
"content_files_sha256",
"migration_rows_sha256",
):
if actual[name] != self.plan["expected_data"][name]:
raise Refused("approved_recovered_dataset_drifted")
if actual["duplicate_slug_groups"] != 0 or actual["tenant_slug_constraint"] is not True:
raise Refused("storage_invariant_refused")
if (actual.get("global_slug_constraint") is not True
or actual.get("migration_0003_applied") is not True):
raise Refused("already_migrated_baseline_required")
def preflight(self):
self.verify_installed_helpers()
if (
self.kdoc("get", "namespace", "kube-system")["metadata"]["uid"]
!= self.plan["cluster_uid"]
):
raise Refused("wrong_cluster")
node = self.kdoc("get", "nodes")["items"]
if len(node) != 1 or not any(
a["address"] == "92.205.62.239" for a in node[0]["status"]["addresses"]
):
raise Refused("wrong_node")
conditions = {v["type"]: v["status"] for v in node[0]["status"]["conditions"]}
if conditions.get("Ready") != "True" or any(
conditions.get(k) != "False"
for k in ("MemoryPressure", "DiskPressure", "PIDPressure")
):
raise Refused("node_pressure")
allocated = self.command(
["kubectl", "describe", "node", node[0]["metadata"]["name"]]
).stdout.decode()
requests = {}
for resource in ("cpu", "memory"):
found = re.search(
r"(?m)^\s+" + resource + r"\s+\S+\s+\((\d+)%\)", allocated
)
if not found or int(found[1]) > 90:
raise Refused("node_request_headroom_refused")
requests[resource] = int(found[1])
self.receipt["node_request_percent"] = requests
history = self.history()
if (
history[-1]["revision"] != self.plan["baseline_revision"]
or history[-1]["status"] != "deployed"
):
raise Refused("helm_baseline_changed")
manifest = self.command(
["helm", "get", "manifest", "coulomb-social", "-n", "coulomb-social"]
).stdout
if sha(manifest) != self.plan["baseline_helm_manifest_sha256"]:
raise Refused("helm_manifest_changed")
deployment = self.deployment()
if (
deployment["metadata"]["uid"] != self.plan["deployment_uid"]
or sha(canonical(deployment["spec"])) != self.plan["baseline_spec_sha256"]
):
raise Refused("live_spec_changed")
if (
deployment["status"].get("readyReplicas") != 1
or deployment["spec"]["strategy"]["type"] != "Recreate"
):
raise Refused("single_ready_writer_required")
# Helm's three-way merge preserves unmanaged pod metadata. Bind the
# complete desired live spec rather than treating kubectl apply's
# last-applied projection as Helm's result.
image_patch = [
{"op": "test", "path": "/spec", "value": deployment["spec"]},
{"op": "replace", "path": "/spec/template/spec/containers/0/image",
"value": self.plan["candidate_image"]},
]
preview = self.kdoc(
"patch", "deployment", "coulomb-social", "-n", "coulomb-social",
"--type=json", "--dry-run=server", "-p", json.dumps(image_patch),
)
if sha(canonical(preview["spec"])) != self.plan["candidate_spec_sha256"]:
raise Refused("image_only_server_preview_differs")
pvc = self.kdoc("get", "pvc", "coulomb-social-content", "-n", "coulomb-social")
if (
pvc["metadata"]["uid"] != self.plan["pvc_uid"]
or pvc["status"]["phase"] != "Bound"
):
raise Refused("pvc_changed")
apps = self.kdoc("get", "applications.argoproj.io", "-A")["items"]
if any(
a.get("spec", {}).get("destination", {}).get("namespace")
== "coulomb-social"
for a in apps
):
raise Refused("gitops_adoption_requires_owner_path")
rendered = self.command(
[
"helm",
"template",
"coulomb-social",
str(self.root / "charts/coulomb-social"),
"-f",
str(self.root / "release/identity-export-values.yaml"),
"-n",
"coulomb-social",
]
).stdout
if sha(rendered) != self.plan["candidate_render_sha256"]:
raise Refused("candidate_render_changed")
self.command(["kubectl", "diff", "-f", "-"], payload=rendered, allowed=(0, 1))
self.command(
["kubectl", "apply", "--dry-run=server", "-f", "-"], payload=rendered
)
self.command(
[
"helm",
"upgrade",
"coulomb-social",
str(self.root / "charts/coulomb-social"),
"-f",
str(self.root / "release/identity-export-values.yaml"),
"-n",
"coulomb-social",
"--dry-run=server",
]
)
before = self.probe()
self.same_data(before)
self.receipt["preflight"] = before
self.save()
def verify_installed_helpers(self):
for path, expected in self.plan["installed_helper_hashes"].items():
metadata = Path(path).lstat()
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != 0
or metadata.st_mode & 0o022
or sha(Path(path).read_bytes()) != expected
):
raise Refused("installed_backup_helper_changed")
def capture(self):
self.verify_installed_helpers()
environment = dict(self.env)
for name in ("primary.env", "recipient.env"):
path = Path("/etc/coulomb-social-backup") / name
metadata = path.lstat()
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != 0
or metadata.st_mode & 0o077
):
raise Refused("private_own_backup_configuration_required")
environment.update(
dict(
line.split("=", 1)
for line in path.read_text().splitlines()
if line and not line.startswith("#")
)
)
output = self.command(
["python3", "/opt/coulomb-social-backup/paired-backup.py", "--execute"],
env=environment,
timeout=1200,
)
point = json.loads(output.stdout)
if (
point.get("status") != "offsite_fetched_verified"
or not point.get("remote_retrieval_verified")
or not point.get("version_id")
or point.get("version_id") == "null"
or point.get("image") != self.plan["old_image"]
or point.get("age_recipient") != self.plan["public_recipient"]
or not point.get("immutable_create")
or not point.get("destination", "").startswith(
"s3://railiance-platform-pg-backup/platform-pg/application-archives/coulomb-social/"
)
):
raise Refused("fresh_paired_primary_readback_required")
self.receipt["fresh_paired_point"] = point
self.save()
self.same_data(self.probe())
def restore_pin(self, old):
path = SNAPSHOT
if (
sha(path.read_bytes())
== self.plan["files"]["release/identity-export-snapshot.py"]
):
self.replace_pin(path, old)
elif sha(path.read_bytes()) != sha(old):
raise Refused("snapshot_pin_changed_during_rollback")
def replace_pin(self, path, value):
fd, name = tempfile.mkstemp(prefix=".reviewed-snapshot-", dir=path.parent)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(value)
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
try:
os.fsync(directory)
finally:
os.close(directory)
finally:
if os.path.exists(name):
os.unlink(name)
def execute(self):
began = time.monotonic()
self.preflight()
self.capture()
old_pin = SNAPSHOT.read_bytes()
self.receipt["status"] = "upgrade_started"
self.save()
try:
self.command(
[
"helm",
"upgrade",
"coulomb-social",
str(self.root / "charts/coulomb-social"),
"-f",
str(self.root / "release/identity-export-values.yaml"),
"-n",
"coulomb-social",
"--atomic",
"--wait",
"--timeout",
"5m",
],
timeout=660,
)
after = self.deployment()
if (
sha(canonical(after["spec"])) != self.plan["candidate_spec_sha256"]
or after["status"].get("readyReplicas") != 1
):
raise Refused("candidate_spec_or_readiness_differs")
probe = self.probe("views")
self.same_data(probe)
for suffix in ("/healthz", "/", "/static/spaces/copy-content.js"):
with urllib.request.urlopen(
"https://app.coulomb.social" + suffix, timeout=20
) as response:
body = response.read(1024**2)
if response.status != 200:
raise Refused("public_http_postflight_refused")
if (
suffix.endswith("copy-content.js")
and sha(body) != self.plan["clipboard_static_sha256"]
):
raise Refused("served_static_differs")
path = SNAPSHOT
if (
sha(path.read_bytes())
!= self.plan["installed_helper_hashes"][str(path)]
):
raise Refused("snapshot_pin_drifted")
self.replace_pin(
path, (self.root / "release/identity-export-snapshot.py").read_bytes()
)
self.command(
[
"python3",
str(path),
"--destination",
"/tmp/csoc-post-image-server-review",
]
)
self.receipt.update(
status="image_candidate_deployed_verified",
postflight=probe,
helm_revision=self.history()[-1]["revision"],
snapshot_pin_refreshed=True,
public_health_landing_static_verified=True,
existing_global_constraint_preserved=True,
)
except Exception:
self.receipt["rollback_requested"] = True
try:
self.command(
[
"helm",
"rollback",
"coulomb-social",
str(self.plan["baseline_revision"]),
"-n",
"coulomb-social",
"--wait",
"--timeout",
"5m",
],
timeout=360,
)
self.restore_pin(old_pin)
restored = self.deployment()
if (sha(canonical(restored["spec"])) != self.plan["baseline_spec_sha256"]
or restored["status"].get("readyReplicas") != 1):
raise Refused("rollback_spec_or_readiness_differs")
self.receipt["image_rollback_completed"] = True
except Exception: # noqa: BLE001 -- retain explicit unconfirmed rollback outcome
self.receipt["image_rollback_completed"] = False
raise
self.receipt.update(
elapsed_seconds=round(time.monotonic() - began, 3),
completed_at=datetime.now(UTC).isoformat(),
private_values_printed=False,
)
self.save()
return self.receipt
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--capsule", type=Path, required=True)
parser.add_argument("--expect-digest", required=True)
parser.add_argument("--execute", action="store_true")
parser.add_argument("--preflight", action="store_true")
args = parser.parse_args()
if not args.execute and not args.preflight:
print(json.dumps({"status": "prepared", "live_changes": False}))
return
if args.execute and args.preflight:
raise SystemExit("choose read-only preflight or attended execution")
release = Release(args.capsule, args.expect_digest, read_only=args.preflight)
try:
if args.preflight:
release.preflight()
print(
json.dumps(
dict(
release.receipt,
status="live_release_preflight_verified",
live_changes=False,
)
)
)
return
print(json.dumps(release.execute()))
except Exception as error: # noqa: BLE001 -- private child diagnostics are suppressed
release.receipt.update(
status="release_refused_or_failed", failure_type=type(error).__name__
)
release.save()
print(json.dumps(release.receipt))
raise SystemExit(1) from None
if __name__ == "__main__":
main()

View file

@ -8,7 +8,7 @@ status: active
owner: codex
topic_slug: railiance-apps
created: "2026-10-06"
updated: "2026-10-08"
updated: "2026-10-31"
related:
- CSOC-WP-0008
state_hub_workstream_id: "79abacf8-361e-51ab-829d-40dec8e142d1"
@ -340,3 +340,45 @@ server dry-run pass. No Secret, issuer, mail, caller, PVC, offsite schedule or
Bubble/apex change. T04 remains wait for productive complete user acceptance;
product WP-0008/WP-0010 retain their broader open tasks. Product receipt:
docs/release/sharing-portability-review-2026-10-08.md.
## T05 — Deliver the identity-binding and operator-export image
```task
id: RAPPS-WP-0015-T05
status: progress
priority: high
needs_human: true
decision_id: "75b94bb6-4709-495f-91fe-43e53e2867ed"
intervention_note: "Exact image-only proposal9f1daf6c passes native read-only preflight. Founder terminal execution remains required; then verify durable receipt, current source/values/helper pins and consumer recovery/export follow-through."
```
Owner execution dependency for CSOC-WP-0012-T03 and CSOC-WP-0015-T04. This is
separate from completed storage release 5fd7ccd8 and date correction 47829c7a.
Candidate source 23ffe2c/image 522bcd7f is published and packaged-source verified;
441 source tests and 25 packaged identity/export tests pass. The retained isolated
recovery verifies 85 client files and 40 owner/40 outsider views without app overlays.
Current production remains revision 18/image 51281946; default live Helm values are
unchanged until the verified execution return.
Prepared separate image-only launcher/node/probe, candidate values and snapshot
pin. The controller requires both existing slug constraints and migration 0003,
compares complete business/migration-history/content fingerprints, makes a fresh
admitted primary backup/readback, then performs atomic Helm upgrade and postflight.
Rollback verifies the previous live specification, Ready replica and recognized
helper; no data restore/schema downgrade or automatic replay. The old storage
launcher is not reused. 19 synthetic safety tests, Ruff, compile and Helm lint pass.
Sealed read-only preflight 9f1daf6c passes at 2026-10-31T03:41:35Z: revision 18,
12 business tables/92 rows/86 files, exact migration history, unchanged helper pins,
CPU 68%/memory 76%, image-only chart/server-preview checks and no Argo adoption.
The expected live spec preserves the unmanaged restart annotation; raw kubectl
apply remains dry-run only. Receipt: docs/evidence/2026-10-31-coulomb-image-release-preflight.json.
Founder packet: coulomb-social/docs/release/identity-export-release-review-2026-10-31.md.
The new execution has not occurred. No node helper, Helm release, timer or data
changed. Existing recovery rehearsal predates the date correction; independent
retrieval/restore/export of the new execution point remains consumer follow-through.
Done when founder execution and independent postflight are verified, the native
receipt and actual image/helper pins are reconciled into owner/product source,
and the product records reference that exact release. This does not accept the
remaining productive journeys or service/offboarding obligations for the consumer.