From b16f8bab8cd9afadc1af038bf1f48061c93503a2 Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 29 Oct 2026 22:25:41 +0100 Subject: [PATCH] Add immutable Coulomb recovery mirroring through scoped APIs Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b --- WORK-RECORDS.md | 2 +- scripts/application_recovery_mirror.py | 141 ++++++++++++++++++ tests/test_application_recovery_mirror.py | 82 ++++++++++ ...WP-0053-coulomb-paired-backup-admission.md | 2 +- 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 scripts/application_recovery_mirror.py create mode 100644 tests/test_application_recovery_mirror.py diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 0f8e0b4..83b45fc 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -152,7 +152,7 @@ | task | RPF-WP-0052-T03 | done | — | workplans/RPF-WP-0052-authelia-platform-smtp-admission.md | | task | RPF-WP-0053-T01 | done | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | | task | RPF-WP-0053-T02 | progress | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | -| task | RPF-WP-0053-T03 | todo | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | +| task | RPF-WP-0053-T03 | progress | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | | task | RPF-WP-0053-T04 | todo | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | | task | RPF-WP-0053-T05 | progress | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | | task | RPF-WP-0053-T06 | done | — | workplans/RPF-WP-0053-coulomb-paired-backup-admission.md | diff --git a/scripts/application_recovery_mirror.py b/scripts/application_recovery_mirror.py new file mode 100644 index 0000000..168dbc2 --- /dev/null +++ b/scripts/application_recovery_mirror.py @@ -0,0 +1,141 @@ +"""Coulomb immutable recovery mirror using the proven primary multipart engine. + +The adapter pins every logical request to Coulomb and remaps only to its own +isolated recovery project/bucket/prefix. Independent fetch uses the reader. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +import primary_application_archive as primary +from application_recovery_identities import ( + Refused, + checkpoint, + private_directory, + private_json, +) +from application_recovery_resources import s3, status + +PROJECT = "06d107bc-9d84-4672-b2a0-95701cc0c07f" +BUCKET = "railiance-coulomb-social-archives" +OPERATIONS = {"head_object", "get_object", "create_multipart_upload", "upload_part", + "complete_multipart_upload", "abort_multipart_upload"} + + +class Mirror: + def __init__(self, client): + self.client = client + + def __getattr__(self, operation): + if operation not in OPERATIONS: + raise Refused("mirror_operation_outside_contract") + def invoke(**arguments): + if arguments.get("Bucket") != primary.BUCKET: + raise Refused("mirror_logical_bucket_refused") + key = arguments.get("Key", "") + primary.validate_key("coulomb-social", key) + return getattr(self.client, operation)(**dict(arguments, Bucket=BUCKET, Key="archives/" + key)) + return invoke + + +def validate_receipt(receipt): + primary.validate_key("coulomb-social", receipt.get("object_key", "")) + primary.require_version(receipt.get("version_id")) + if (receipt.get("application") != "coulomb-social" + or receipt.get("schema") != "platform.application-primary-archive.v1" + or receipt.get("status") != "offsite_fetched_verified" + or receipt.get("remote_retrieval_verified") is not True + or receipt.get("destination") != "s3://" + primary.BUCKET + "/" + receipt["object_key"] + or receipt.get("immutable_create") is not True + or not re.fullmatch(r"[a-f0-9]{64}", receipt.get("ciphertext_sha256", "")) + or type(receipt.get("ciphertext_bytes")) is not int + or not 0 < receipt["ciphertext_bytes"] <= primary.MAX_BYTES): + raise Refused("verified_coulomb_primary_receipt_required") + + +def mirror(client, source, readback, receipt, journal, save): + validate_receipt(receipt) + binding = {k: receipt[k] for k in ("object_key", "version_id", "ciphertext_sha256", "ciphertext_bytes", + "captured_at", "image", "age_recipient", "database_sha256", "content_sha256")} + if journal: + if journal.get("primary_binding") != binding or journal.get("recovery_project_id") != PROJECT: + raise Refused("mirror_resume_binding_differs") + else: + journal.update(schema="platform.coulomb-recovery-mirror.v1", primary_binding=binding, + recovery_project_id=PROJECT, object_key=receipt["object_key"], + ciphertext_sha256=receipt["ciphertext_sha256"]) + primary.regular_file(source) + if primary.digest(source) != receipt["ciphertext_sha256"] or source.stat().st_size != receipt["ciphertext_bytes"]: + raise Refused("mirror_source_differs") + primary.transfer(Mirror(client), "coulomb-social", source, readback, journal, save) + reference = {"project_id": PROJECT, "bucket": BUCKET, + "object_key": "archives/" + receipt["object_key"], + "version_id": journal["version_id"], "ciphertext_sha256": receipt["ciphertext_sha256"], + "ciphertext_bytes": receipt["ciphertext_bytes"], "hash_readback_verified": True} + journal["recovery_mirror"] = reference + save() + return dict(receipt, recovery_mirror=reference) + + +def retrieve(client, output, receipt): + validate_receipt(receipt) + ref = receipt.get("recovery_mirror", {}) + if (ref.get("project_id") != PROJECT or ref.get("bucket") != BUCKET + or ref.get("object_key") != "archives/" + receipt["object_key"] + or ref.get("ciphertext_sha256") != receipt["ciphertext_sha256"] + or ref.get("ciphertext_bytes") != receipt["ciphertext_bytes"] + or ref.get("hash_readback_verified") is not True): + raise Refused("exact_recovery_mirror_reference_required") + primary.require_version(ref.get("version_id")) + primary.download(Mirror(client), receipt["object_key"], ref["version_id"], + receipt["ciphertext_bytes"], receipt["ciphertext_sha256"], output) + return {"status": "independent_mirror_retrieved_verified", "provider_mutations": False, + "recovery_mirror": ref, "application_restore_verified": False} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("operation", choices=("publish", "retrieve")) + parser.add_argument("--execute", action="store_true") + parser.add_argument("--directory", type=Path, default=Path.home() / ".config/railiance-platform/coulomb-social-recovery") + parser.add_argument("--receipt", type=Path, required=True) + parser.add_argument("--source", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--journal", type=Path) + parser.add_argument("--result", type=Path) + args = parser.parse_args() + if not args.execute: + print(json.dumps({"status": "preview", "provider_contacted": False, "operation": args.operation})) + return + private_directory(args.directory) + private_directory(args.output.parent) + receipt = private_json(args.receipt) + if args.result: + private_directory(args.result.parent) + if args.result.exists() or args.result.is_symlink(): + raise Refused("fresh_result_required") + if args.operation == "retrieve": + result = retrieve(s3(private_json(args.directory / "reader-pair.json"), PROJECT), args.output, receipt) + else: + if not args.journal or not args.source or not args.result: + raise Refused("publish_source_journal_result_required") + private_directory(args.journal.parent) + journal = private_json(args.journal) if args.journal.exists() else {} + result = mirror(s3(private_json(args.directory / "publisher-pair.json"), PROJECT), args.source, + args.output, receipt, journal, lambda: checkpoint(args.journal, journal)) + if args.result: + checkpoint(args.result, result) + print(json.dumps({"status": "mirror_publish_verified" if args.operation == "publish" else result["status"], + "application_restore_verified": False, "private_values_printed": False})) + + +if __name__ == "__main__": + try: + main() + except Exception as error: # noqa: BLE001 -- suppress private provider diagnostics + print(json.dumps({"status": "refused", "failure_type": type(error).__name__, "native_status": status(error)})) + sys.exit(1) diff --git a/tests/test_application_recovery_mirror.py b/tests/test_application_recovery_mirror.py new file mode 100644 index 0000000..e7d0ba6 --- /dev/null +++ b/tests/test_application_recovery_mirror.py @@ -0,0 +1,82 @@ +"""Exercise the real multipart engine through the exact recovery remapping.""" +import copy +import hashlib +import importlib.util +import sys +from pathlib import Path + +import pytest + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" +sys.path.insert(0, str(SCRIPTS)) +import application_recovery_mirror as mirror + +spec = importlib.util.spec_from_file_location("primary_tests", Path(__file__).with_name("test_primary_application_archive.py")) +provider = importlib.util.module_from_spec(spec) +spec.loader.exec_module(provider) + + +def fixture(tmp_path, failure=None): + tmp_path.chmod(0o700) + source = tmp_path / "ciphertext.age" + data = b"age-encryption.org/v1\nsynthetic-encrypted-pair" + source.write_bytes(data) + source.chmod(0o600) + key = mirror.primary.key_for("coulomb-social", "2026-10-29T00:00:00Z") + receipt = {"schema": "platform.application-primary-archive.v1", "application": "coulomb-social", + "status": "offsite_fetched_verified", "object_key": key, "version_id": "primary-version", + "destination": "s3://" + mirror.primary.BUCKET + "/" + key, + "ciphertext_sha256": hashlib.sha256(data).hexdigest(), "ciphertext_bytes": len(data), + "remote_retrieval_verified": True, "immutable_create": True, + "captured_at": "2026-10-29T00:00:00Z", "image": "synthetic-image", + "age_recipient": "synthetic-recipient", "database_sha256": "a" * 64, "content_sha256": "b" * 64} + return source, receipt, provider.Provider(data, failure) + + +def test_multipart_mirror_and_independent_version_retrieval(tmp_path): + source, original, client = fixture(tmp_path) + before = copy.deepcopy(original) + journal = {} + receipt = mirror.mirror(client, source, tmp_path / "readback.age", original, journal, lambda: None) + assert original == before + assert receipt["version_id"] == "primary-version" + assert receipt["recovery_mirror"]["version_id"] == client.version + for _, args in client.calls: + assert args["Bucket"] == mirror.BUCKET + assert args["Key"] == "archives/" + original["object_key"] + client.calls = [] + result = mirror.retrieve(client, tmp_path / "independent.age", receipt) + assert result["provider_mutations"] is False + assert [name for name, _ in client.calls] == ["get"] + + +def test_uncertain_completion_reuses_same_mirror_without_reupload(tmp_path): + source, receipt, client = fixture(tmp_path, "lost-response") + journal = {} + with pytest.raises(TimeoutError): + mirror.mirror(client, source, tmp_path / "readback.age", receipt, journal, lambda: None) + client.failure = None + mirror.mirror(client, source, tmp_path / "retry.age", receipt, journal, lambda: None) + assert len([name for name, _ in client.calls if name == "start"]) == 1 + + +@pytest.mark.parametrize("field,value", [("bucket", "foreign"), ("project_id", "foreign"), + ("object_key", "archives/foreign"), ("ciphertext_sha256", "c" * 64)]) +def test_foreign_mirror_reference_refuses_before_contact(tmp_path, field, value): + source, receipt, client = fixture(tmp_path) + result = mirror.mirror(client, source, tmp_path / "readback.age", receipt, {}, lambda: None) + client.calls = [] + result["recovery_mirror"][field] = value + with pytest.raises(mirror.Refused): + mirror.retrieve(client, tmp_path / "independent.age", result) + assert client.calls == [] + + +def test_adapter_cannot_address_siblings_or_delete(tmp_path): + _, receipt, client = fixture(tmp_path) + adapter = mirror.Mirror(client) + with pytest.raises(mirror.Refused): + adapter.delete_object(Bucket=mirror.primary.BUCKET, Key=receipt["object_key"]) + with pytest.raises(ValueError): + adapter.get_object(Bucket=mirror.primary.BUCKET, Key="platform-pg/application-archives/email-connect/foreign") + assert client.calls == [] diff --git a/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md b/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md index 2f02e6c..65b58d6 100644 --- a/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md +++ b/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md @@ -89,7 +89,7 @@ activation. No retained-object/version-delete rights belong in the publisher. ```task id: RPF-WP-0053-T03 -status: todo +status: progress priority: high intervention_note: "The first coherent capture/publication and daily schedule are already approved. Complete capacity/retention preflight, independently retained exact-version retrieval and isolated application recovery before timer activation." state_hub_task_id: "871c14da-2940-5be6-8ce8-953973422064"