Add immutable Coulomb recovery mirroring through scoped APIs
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
This commit is contained in:
parent
7e0b7d9062
commit
b16f8bab8c
4 changed files with 225 additions and 2 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
141
scripts/application_recovery_mirror.py
Normal file
141
scripts/application_recovery_mirror.py
Normal file
|
|
@ -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)
|
||||
82
tests/test_application_recovery_mirror.py
Normal file
82
tests/test_application_recovery_mirror.py
Normal file
|
|
@ -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 == []
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue