Verify latest backup mirror and refresh native generation retention
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m47s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
tegwick 2026-10-31 03:50:31 +01:00
parent 29c9db5b65
commit 51a527128a
8 changed files with 910 additions and 0 deletions

View file

@ -0,0 +1,165 @@
{
"age_window_counterexample": {
"fixed_age_window_preserves_generation_policy": false,
"input_generations": 15,
"oldest_selected_age_days": 48,
"selected_generations_older_than_30_days": 14,
"synthetic_source_gap": true
},
"archive_bodies_refetched_by_observer": false,
"blockers": [],
"bucket_bytes": {
"railiance-coulomb-social-archives": 8669342,
"railiance-coulomb-social-recovery": 832889296,
"railiance-platform-pg-backup": 8665936
},
"bucket_version_counts": {
"railiance-coulomb-social-archives": 8,
"railiance-coulomb-social-recovery": 6,
"railiance-platform-pg-backup": 4
},
"candidate_versions": [
{
"associated_receipt": {
"receipt_bytes": 1664,
"receipt_object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T002756Z-c31b84b48b3874bdaa87cbf5.tar.age.cbbf4c0b9cab6829293e00435f225e9cc3c0167930307c289428c1f9ded2a6f7.receipt.json",
"receipt_sha256": "cbbf4c0b9cab6829293e00435f225e9cc3c0167930307c289428c1f9ded2a6f7",
"receipt_version_id": "1793408541632331"
},
"bucket": "railiance-coulomb-social-archives",
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "f7be502c7e122283e70b10d2fd8a2cbf9550d2261192ba11f94d945a9f39af51",
"logical_primary_identity": {
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "f7be502c7e122283e70b10d2fd8a2cbf9550d2261192ba11f94d945a9f39af51",
"object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T002756Z-c31b84b48b3874bdaa87cbf5.tar.age",
"version_id": "1793406532815253"
},
"object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T002756Z-c31b84b48b3874bdaa87cbf5.tar.age",
"version_id": "1793408525635522"
}
],
"credential_values_printed": false,
"delete_marker_counts": {
"railiance-coulomb-social-archives": 0,
"railiance-coulomb-social-recovery": 0,
"railiance-platform-pg-backup": 0
},
"deletion_capability": false,
"exact_version_heads": 12,
"expiration_candidates": 1,
"generation_selection_valid": true,
"head_observed_at": "2026-10-31T02:47:11.870754+00:00",
"independent_public_receipts_fetched_verified": 2,
"keep": [
{
"bucket": "railiance-coulomb-social-archives",
"ciphertext_bytes": 2120392,
"ciphertext_sha256": "acbf92eda5074761bb6d00170e6e459e787359afcd377f2f18324ba17496510a",
"logical_primary_identity": {
"ciphertext_bytes": 2120392,
"ciphertext_sha256": "acbf92eda5074761bb6d00170e6e459e787359afcd377f2f18324ba17496510a",
"object_key": "platform-pg/application-archives/coulomb-social/2026/10/29/20261029T212246Z-fd128164c7bb22a3a6c42acf.tar.age",
"version_id": "1793309020705518"
},
"object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/29/20261029T212246Z-fd128164c7bb22a3a6c42acf.tar.age",
"version_id": "1793309156692541"
},
{
"bucket": "railiance-coulomb-social-archives",
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "59c2724399f067fabaa53550ffe52205e4752c91a5064e173b59859f9dea4744",
"logical_primary_identity": {
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "59c2724399f067fabaa53550ffe52205e4752c91a5064e173b59859f9dea4744",
"object_key": "platform-pg/application-archives/coulomb-social/2026/10/30/20261030T235040Z-7f5732d82580c7ff97e3f13e.tar.age",
"version_id": "1793404296527269"
},
"object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/30/20261030T235040Z-7f5732d82580c7ff97e3f13e.tar.age",
"version_id": "1793404632402018"
},
{
"bucket": "railiance-coulomb-social-archives",
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "ac9fe6711c967a1c79caeca961c24ebf0ce795fb98e018852da00a1795341709",
"logical_primary_identity": {
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "ac9fe6711c967a1c79caeca961c24ebf0ce795fb98e018852da00a1795341709",
"object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T020721Z-4aa7a5d4146abf20f0b23e66.tar.age",
"version_id": "1793412500034233"
},
"object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T020721Z-4aa7a5d4146abf20f0b23e66.tar.age",
"version_id": "1793414655337141"
}
],
"keep_versions": 3,
"known_logical_points": 4,
"native_lifecycle_rules": {
"railiance-coulomb-social-archives": [],
"railiance-coulomb-social-recovery": [],
"railiance-platform-pg-backup": [
{
"Expiration": {
"Days": 30
},
"Filter": {
"Prefix": ""
},
"ID": "retain-30-days",
"NoncurrentVersionExpiration": {
"NoncurrentDays": 30
},
"Status": "Enabled"
}
]
},
"observed_at": "2026-10-31T02:47:09.500259+00:00",
"owner_source_sha256": {
"application_archive_retention_plan.py": "08bc57308f53bbfd0d58fdbb9b5671dd4285e31c5b5137e201524df606aa3f9f",
"primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d"
},
"personal_session_used": false,
"private_age_identity_read": false,
"private_review_sha256": "7c9a7a2035e16c60833c04fcd5d10e8a075338241bc12e26728792ad86c4fd87",
"protected_kit": {
"automatic_expiry": false,
"bucket": "railiance-coulomb-social-recovery",
"bytes": 277647360,
"object_key": "kits/91d8cec1b6b4409aa4e38e7840482b6bb9ca05b26452eac265beabe27048d464/recovery-kit.tar",
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
"schema": "platform.coulomb-protected-recovery-kit.v1",
"sha256": "91d8cec1b6b4409aa4e38e7840482b6bb9ca05b26452eac265beabe27048d464",
"version_id": "1793405649619843"
},
"protected_kit_and_proof_versions_verified": true,
"protected_kit_body_refetched": false,
"protected_recovery_proof": {
"automatic_expiry": false,
"bucket": "railiance-coulomb-social-recovery",
"bytes": 4372,
"independent_readback_verified": true,
"object_key": "kits/receipts/368b7872d2c0186858d93c3bb9db2943c576942e1db131caaf0e643c433db2a5/recovery-proof.json",
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
"sha256": "368b7872d2c0186858d93c3bb9db2943c576942e1db131caaf0e643c433db2a5",
"version_id": "1793405751488728"
},
"provider_mutations": false,
"provider_observation_sha256": "b1c4248a9b525d57213068fe31bdd571733462468a7d654ff0866715b79fe42e",
"ready_to_execute": false,
"routine_api_credentials_used": true,
"schema": "coulomb.native-generation-retention-review.v1",
"source_sha256": {
"inventory": "1ebf6ee3eecf4adc32400c45df2500b876d5747c0e0327aeb3c180655c9d955d",
"observation": "b1c4248a9b525d57213068fe31bdd571733462468a7d654ff0866715b79fe42e",
"recovery": "25c12e2f682940d6fb7887773efa4d042a909d02305a480dcc846425cf66a205"
},
"sources_manifest_sha256": "6038db92589c9547a930c21bd50c3c00d3d2ec2801d283c6a88b0768e231f551",
"status": "selection_reviewed_execution_not_admitted",
"timers_activated": false,
"unknown_objects_preserved": true,
"validation": {
"browser_journeys_included": true,
"make_check_passed": 415,
"skipped": 0
}
}

View file

@ -0,0 +1,83 @@
# Current generation retention and mirror coverage
The existing backup taken at 02:07:21 UTC before the founder's date correction is
now independently mirrored and retrieved. The unchanged mirror worker ran twice
against a private copy of that exact node archive. Both runs retained the same
ciphertext and filtered public receipt versions; the separate reader verified
their complete bytes, hashes and sizes. This is a pre-correction recovery point,
not a newly captured post-correction point or an additional application restore.
Evidence: [native mirror proof](mirror-date-point-native-2026-10-31.json) and its
[source manifest](mirror-date-point-native-2026-10-31.sources.txt). No installed
node worker, node mirror state, unit or timer was changed. No decryption key was
read and no retained object was deleted.
## Repeatable provider observation
`scripts/observe-backup-retention.py` replaces the one-off observation procedure
with a read-only command. Its default preview contacts no provider. Explicit
execution uses the existing publisher only for primary metadata reads and the
separate reader for mirror/protected-kit reads. It loads the four hash-pinned
platform helpers, never uses the custodian or a personal Console session, and
writes only a fresh private observation outside Git.
The observer requires enabled versioning, explicit bounded pagination completion,
unique in-prefix versions and exact immutable-version HEAD identity/hash/size.
Only the provider's specific `NoSuchLifecycleConfiguration` response establishes
absent lifecycle rules; an unavailable or missing bucket is not accepted as
nonexpiring custody. Retained public receipts are independently fetched and
compared byte-for-byte with the worker's explicit public-field filter. Archive
and protected-kit bodies are not fetched again merely to refresh metadata;
their previous independent readback and recovery proofs remain separate evidence.
```sh
/path/to/locked-sdk/python scripts/observe-backup-retention.py --execute \
--inventory /private/mirrored-inventory.json \
--recovery docs/release/provider-kit-recovery-2026-10-31.json \
--credentials-directory /private/existing-coulomb-recovery-custody \
--platform-scripts /private/reviewed-platform/scripts \
--runtime-manifest deploy/backup/mirror-platform-runtime.json \
--output /private/new-observation.json
uv run python scripts/review-backup-retention.py \
--inventory /private/mirrored-inventory.json \
--recovery docs/release/provider-kit-recovery-2026-10-31.json \
--observation /private/new-observation.json \
--platform-scripts /private/reviewed-platform/scripts \
--output /private/new-generation-review.json
```
## Native result
Four logical points are represented in the current reviewed inventory. Complete
provider listings contain four primary versions, eight mirror versions and six
protected-kit/proof/fixture versions, with no delete markers. Twelve exact HEADs
match the known point, receipt and protected-custody bindings. Both retained
worker public receipts were independently fetched and verified. Mirror and kit
buckets still have no lifecycle rules; the primary's existing global 30-day
current/noncurrent expiry remains unchanged.
The pinned 14-daily/four-weekly selector keeps three points: October 29, the
protected October 30 recovered point, and the newest October 31 point at 02:07.
It identifies the earlier October 31 00:27 point as one expiration candidate,
including its exact independently retained receipt. The candidate is still
retained. Unknown objects and admission fixtures remain preserved. These three
days of available history do not establish a full 14-day/four-week history.
The [current review proof](generation-retention-review-2026-10-31.json) records
counts, exact selected versions, source/input hashes and the native observation.
The private inputs and full observation remain under
`~/.config/railiance-platform/coulomb-social-recovery/retention-review-20261031/`.
## Remaining operating requirements
This closes fresh observation and current mirror coverage, not retention
execution. The separately admitted mirror-only expiry caller/executor, native
deletion boundaries, partial-outcome recovery and immediate keep-set revalidation
are still required. Existing publisher/reader deletion denials must remain intact.
No primary or protected-kit deletion belongs in that executor. Historical local
copies, failed-drill kits and boundary fixtures still require owner disposition.
Node operating delivery, capacity admission, received failure/stale alerts and
recurring activation remain open, as do WP-0012's service-contract requirements.
The already approved daily schedule and independent custody are not reopened.

View file

@ -0,0 +1,7 @@
/home/worsch/coulomb-social/deploy/backup/mirror-platform-runtime.json: 9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1
/home/worsch/coulomb-social/scripts/check-paired-backup-health.py: c003871fe91c5c31788fd9bfc50aef3b2a275c06c5a42c2c3165b92d5ea80db9
/home/worsch/coulomb-social/scripts/mirror-paired-backup.py: 9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c
/home/worsch/coulomb-social/scripts/observe-backup-retention.py: 9dc7e862ef2888998744d47cb458f0401b870e96cf283df5dd375b627b3f176f
/home/worsch/coulomb-social/scripts/review-backup-retention.py: 929c0416add6bfddee5bce9e6aaeb436d2d9de8084db48d061f66186f7127794
/home/worsch/coulomb-social/tests/test_backup_retention_observation.py: 65292156128f914336e05a00c72fbb23c53587d79a8df93a9a3e0976b38b5a6b
/home/worsch/railiance-platform/scripts/application_archive_retention_plan.py: 08bc57308f53bbfd0d58fdbb9b5671dd4285e31c5b5137e201524df606aa3f9f

View file

@ -0,0 +1,72 @@
{
"schema": "coulomb.native-mirror-worker-proof.v1",
"status": "native_worker_and_independent_receipt_verified",
"completed_at": "2026-10-31T02:44:16.363553+00:00",
"primary": {
"application": "coulomb-social",
"captured_at": "2026-10-31T02:07:21Z",
"object_key": "platform-pg/application-archives/coulomb-social/2026/10/31/20261031T020721Z-4aa7a5d4146abf20f0b23e66.tar.age",
"version_id": "1793412500034233",
"ciphertext_sha256": "ac9fe6711c967a1c79caeca961c24ebf0ce795fb98e018852da00a1795341709",
"ciphertext_bytes": 2181848,
"age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3",
"image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:512819463a1902b43a615beee14be7bf97e6a05e016385082b9380692ad1a8cd",
"database_sha256": "6968b759e476b65c1911107315fec269d11dd489af995d6a9d4b4da8b18b25f2",
"content_sha256": "631e2ebc3e7b114f83945d889b133fef2491030509dfb11024a24c485fe80ec8"
},
"recovery_mirror": {
"bucket": "railiance-coulomb-social-archives",
"ciphertext_bytes": 2181848,
"ciphertext_sha256": "ac9fe6711c967a1c79caeca961c24ebf0ce795fb98e018852da00a1795341709",
"hash_readback_verified": true,
"object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T020721Z-4aa7a5d4146abf20f0b23e66.tar.age",
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
"receipt_bytes": 1664,
"receipt_object_key": "archives/platform-pg/application-archives/coulomb-social/2026/10/31/20261031T020721Z-4aa7a5d4146abf20f0b23e66.tar.age.85383a8d70268aab71902e8dbb1e103749b43f017a965319dbd1c9152db64210.receipt.json",
"receipt_readback_verified": true,
"receipt_sha256": "85383a8d70268aab71902e8dbb1e103749b43f017a965319dbd1c9152db64210",
"receipt_version_id": "1793414656525890",
"version_id": "1793414655337141"
},
"worker_results": [
{
"status": "mirrored_with_retained_receipt",
"primary_recaptured": false,
"ciphertext_readback_verified": true,
"receipt_readback_verified": true,
"recovery_key_read": false,
"independent_reader_used": false,
"retained_objects_deleted": false,
"application_restore_verified": false
},
{
"status": "mirrored_with_retained_receipt",
"primary_recaptured": false,
"ciphertext_readback_verified": true,
"receipt_readback_verified": true,
"recovery_key_read": false,
"independent_reader_used": false,
"retained_objects_deleted": false,
"application_restore_verified": false
}
],
"native_versions_before": 0,
"native_versions_after": 2,
"one_ciphertext_and_one_receipt_version_after_retry": true,
"independent_reader_ciphertext_and_receipt_verified": true,
"retained_receipt_equals_exact_public_field_filter": true,
"local_primary_receipt_and_ciphertext_unchanged": true,
"retry_last_mirror_bytes_unchanged": true,
"runtime_source_commit": "09f4fd33cbeefecb833998246cf53848bb1e5d8d",
"private_state_copy_used": true,
"production_application_contact": false,
"node_helpers_or_state_modified": false,
"node_timers_changed": false,
"native_capture_performed": false,
"decryption_identity_read": false,
"retained_objects_deleted": false,
"operating_capacity_profile_admitted": false,
"alert_reception_verified": false,
"scheduled_service_execution_verified": false,
"sources_manifest_sha256": "11486b9080e29d0a37e7e6f8af8fe14602ab8d87db1871e500ce492721ef9eef"
}

View file

@ -0,0 +1,9 @@
/home/worsch/coulomb-social/deploy/backup/mirror-platform-runtime.json: 9253c5f6e956b67f499d3e12d2851be7fabd3bc56b00496b8810bbd0be8209f1
/home/worsch/coulomb-social/scripts/backup_capacity.py: cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893
/home/worsch/coulomb-social/scripts/check-paired-backup-health.py: c003871fe91c5c31788fd9bfc50aef3b2a275c06c5a42c2c3165b92d5ea80db9
/home/worsch/coulomb-social/scripts/mirror-paired-backup.py: 9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c
/home/worsch/railiance-platform/scripts/application_recovery_identities.py: 20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d
/home/worsch/railiance-platform/scripts/application_recovery_mirror.py: d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178
/home/worsch/railiance-platform/scripts/application_recovery_resources.py: ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498
/home/worsch/railiance-platform/scripts/primary_application_archive.py: a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d
/tmp/csoc-native-mirror-date-point-20261031.py: 282cd40b470554294afa2bf98c60455480a02e4e14e409207e6fb029db306a18

View file

@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""Read current version/custody evidence for the offline generation reviewer.
Preview is offline. --execute performs only scoped provider reads and publishes
one new private observation. It never captures, decrypts, deletes or changes IAM.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import os
from datetime import UTC, datetime
from pathlib import Path
spec = importlib.util.spec_from_file_location(
"retention_mirror", Path(__file__).with_name("mirror-paired-backup.py")
)
worker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(worker)
health = worker.health
Refused = health.Refused
PRIMARY = "railiance-platform-pg-backup"
KIT = "railiance-coulomb-social-recovery"
SCOPES = {PRIMARY: health.PREFIX, health.MIRROR_BUCKET: "archives/", KIT: "kits/"}
def status(error):
return getattr(error, "response", {}).get("ResponseMetadata", {}).get("HTTPStatusCode")
def version_id(value):
if (
not isinstance(value, str)
or not value
or value == "null"
or any(c in value for c in "\r\n")
):
raise Refused("immutable_version_required")
return value
def observe_bucket(client, bucket):
prefix = SCOPES[bucket]
versioning = client.get_bucket_versioning(Bucket=bucket)
if versioning.get("Status") != "Enabled":
raise Refused("enabled_versioning_required")
try:
lifecycle = client.get_bucket_lifecycle_configuration(Bucket=bucket)
rules = lifecycle["Rules"]
except Exception as error:
# A bucket itself being missing or inaccessible is not "no lifecycle".
code = getattr(error, "response", {}).get("Error", {}).get("Code")
if status(error) != 404 or code != "NoSuchLifecycleConfiguration":
raise
rules = []
if not isinstance(rules, list):
raise Refused("lifecycle_rules_required")
versions, markers, seen, identities, arguments = [], [], set(), set(), {}
for _ in range(100):
page = client.list_object_versions(Bucket=bucket, Prefix=prefix, MaxKeys=1000, **arguments)
for field, destination in [("Versions", versions), ("DeleteMarkers", markers)]:
for row in page.get(field, []):
key, version = row["Key"], version_id(row["VersionId"])
if not isinstance(key, str) or not key.startswith(prefix):
raise Refused("version_outside_application_prefix")
if (key, version) in identities:
raise Refused("duplicate_or_changing_version_inventory")
identities.add((key, version))
item = {"Key": key, "VersionId": version}
if field == "Versions":
if type(row.get("Size")) is not int or row["Size"] < 0:
raise Refused("version_size_required")
item["Size"] = row["Size"]
if "IsLatest" in row:
item["IsLatest"] = row["IsLatest"]
destination.append(item)
if page.get("IsTruncated") is False:
return {
"bucket": bucket,
"prefix": prefix,
"inventory_complete": True,
"versioning": {"Status": "Enabled"},
"lifecycle": {"status": "observed", "Rules": rules},
"versions": versions,
"delete_markers": markers,
}
if page.get("IsTruncated") is not True:
raise Refused("explicit_pagination_completion_required")
cursor = page.get("NextKeyMarker"), page.get("NextVersionIdMarker")
if not isinstance(cursor[0], str) or not cursor[0].startswith(prefix) or cursor in seen:
raise Refused("invalid_or_repeated_version_cursor")
seen.add(cursor)
arguments = {"KeyMarker": cursor[0]}
if cursor[1] is not None:
arguments["VersionIdMarker"] = version_id(cursor[1])
raise Refused("version_inventory_limit_exceeded")
def observe(primary_client, reader, inventory, recovery, now=None):
now = now or datetime.now(UTC)
if not isinstance(inventory, list) or not inventory:
raise Refused("nonempty_verified_inventory_required")
expected = {}
def add(bucket, key, version, digest, size):
version_id(version)
if (
bucket not in SCOPES
or not isinstance(key, str)
or not key.startswith(SCOPES[bucket])
or not isinstance(digest, str)
or len(digest) != 64
or any(c not in "0123456789abcdef" for c in digest)
or type(size) is not int
or size <= 0
):
raise Refused("exact_version_binding_required")
identity = bucket, key, version
if identity in expected and expected[identity] != (digest, size):
raise Refused("conflicting_version_binding")
expected[identity] = digest, size
for receipt in inventory:
health.validate_mirror(receipt, receipt)
add(
PRIMARY,
receipt["object_key"],
receipt["version_id"],
receipt["ciphertext_sha256"],
receipt["ciphertext_bytes"],
)
ref = receipt["recovery_mirror"]
add(
health.MIRROR_BUCKET,
ref["object_key"],
ref["version_id"],
ref["ciphertext_sha256"],
ref["ciphertext_bytes"],
)
if "receipt_object_key" in ref:
health.validate_mirror(receipt, receipt, require_retained_receipt=True)
add(
health.MIRROR_BUCKET,
ref["receipt_object_key"],
ref["receipt_version_id"],
ref["receipt_sha256"],
ref["receipt_bytes"],
)
if (
recovery.get("status") != "application_restored_verified"
or recovery.get("application") != "coulomb-social"
or recovery.get("application_restore_verified") is not True
):
raise Refused("verified_application_recovery_required")
for name in ["protected_kit", "protected_recovery_proof"]:
ref = recovery[name]
suffix = "/recovery-kit.tar" if name == "protected_kit" else "/recovery-proof.json"
prefix = "kits/" if name == "protected_kit" else "kits/receipts/"
if (
ref.get("bucket") != KIT
or ref.get("project_id") != health.PROJECT
or ref.get("automatic_expiry") is not False
or ref.get("object_key") != prefix + ref["sha256"] + suffix
):
raise Refused("protected_custody_binding_required")
add(KIT, ref["object_key"], ref["version_id"], ref["sha256"], ref["bytes"])
clients = {PRIMARY: primary_client, health.MIRROR_BUCKET: reader, KIT: reader}
observations = [observe_bucket(clients[bucket], bucket) for bucket in SCOPES]
versions = {
(b["bucket"], v["Key"], v["VersionId"]): v for b in observations for v in b["versions"]
}
heads = []
for (bucket, key, version), (digest, size) in sorted(expected.items()):
listed = versions.get((bucket, key, version))
if listed is None and bucket == PRIMARY:
continue # Primary age expiry never removes an independent mirror generation.
if listed is None or listed["Size"] != size:
raise Refused("retained_version_missing_or_changed")
value = clients[bucket].head_object(Bucket=bucket, Key=key, VersionId=version)
if (
value.get("VersionId") != version
or value.get("ContentLength") != size
or value.get("Metadata", {}).get("sha256") != digest
or value.get("DeleteMarker")
):
raise Refused("exact_version_head_differs")
heads.append(
{
"bucket": bucket,
"object_key": key,
"version_id": version,
"sha256": digest,
"bytes": size,
}
)
sidecars = 0
for receipt in inventory:
ref = receipt["recovery_mirror"]
if "receipt_object_key" not in ref:
continue
response = reader.get_object(
Bucket=health.MIRROR_BUCKET,
Key=ref["receipt_object_key"],
VersionId=ref["receipt_version_id"],
)
with response["Body"] as stream:
payload = stream.read(ref["receipt_bytes"] + 1)
if (
response.get("VersionId") != ref["receipt_version_id"]
or response.get("ContentLength") != ref["receipt_bytes"]
or len(payload) != ref["receipt_bytes"]
or hashlib.sha256(payload).hexdigest() != ref["receipt_sha256"]
or payload != worker.canonical(worker.public_receipt(receipt, receipt))
):
raise Refused("independent_public_receipt_readback_differs")
sidecars += 1
return {
"schema": "coulomb.provider-retention-observation.v1",
"observed_at": now.isoformat(),
"head_observed_at": datetime.now(UTC).isoformat(),
"observations": observations,
"version_heads": heads,
"independent_public_receipts_fetched_verified": sidecars,
"archive_bodies_refetched": False,
"protected_kit_body_refetched": False,
"provider_mutations": False,
"credentials_printed": False,
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--execute", action="store_true", help="Read provider metadata and receipts"
)
for name in [
"inventory",
"recovery",
"credentials-directory",
"platform-scripts",
"runtime-manifest",
"output",
]:
parser.add_argument("--" + name, type=Path)
args = parser.parse_args()
if not args.execute:
print(json.dumps({"status": "preview", "provider_contacted": False}))
return
if any(
getattr(args, name) is None
for name in [
"inventory",
"recovery",
"credentials_directory",
"platform_scripts",
"runtime_manifest",
"output",
]
):
raise Refused("explicit_observation_inputs_required")
worker.load_runtime(args.platform_scripts, args.runtime_manifest)
from application_recovery_identities import private_directory, private_json
from application_recovery_resources import s3
private_directory(args.credentials_directory)
private_directory(args.output.parent)
if args.output.exists() or args.output.is_symlink():
raise Refused("fresh_observation_output_required")
inputs = {name: getattr(args, name).read_bytes() for name in ["inventory", "recovery"]}
# Use explicit static credentials: no SDK ambient identity or metadata fallback.
import boto3
from botocore.config import Config
pair = private_json(args.credentials_directory / "publisher-pair.json")
if not all(
isinstance(pair.get(k), str) and pair[k].strip()
for k in ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
):
raise Refused("own_publisher_pair_required")
primary = boto3.client(
"s3",
endpoint_url="https://s3.nl-ams.scw.cloud",
region_name="nl-ams",
aws_access_key_id=pair["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=pair["AWS_SECRET_ACCESS_KEY"],
config=Config(
signature_version="s3v4",
connect_timeout=15,
read_timeout=30,
retries={"max_attempts": 0},
s3={"addressing_style": "path"},
),
)
reader = None
try:
reader = s3(private_json(args.credentials_directory / "reader-pair.json"), health.PROJECT)
result = observe(
primary, reader, json.loads(inputs["inventory"]), json.loads(inputs["recovery"])
)
finally:
primary.close()
if reader is not None:
reader.close()
result["source_sha256"] = {
name: hashlib.sha256(value).hexdigest() for name, value in inputs.items()
}
fd = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "w") as stream:
json.dump(result, stream, indent=2, sort_keys=True)
stream.write("\n")
print(
json.dumps(
{
"status": "provider_observation_completed",
"provider_mutations": False,
"exact_version_heads": len(result["version_heads"]),
"independent_receipts_verified": result[
"independent_public_receipts_fetched_verified"
],
}
)
)
if __name__ == "__main__":
try:
main()
except Exception as error:
print(json.dumps({"status": "observation_refused", "failure_type": type(error).__name__}))
raise SystemExit(1)

View file

@ -0,0 +1,217 @@
"""Provider observation must distinguish absence, uncertainty and exact versions."""
import copy
import hashlib
import importlib.util
import io
from pathlib import Path
import pytest
from tests.test_backup_health import NOW
from tests.test_backup_health import health_state as health_state
from tests.test_backup_health import monitor as monitor
from tests.test_backup_retention_review import evidence as evidence
from tests.test_backup_retention_review import reviewer as reviewer
@pytest.fixture
def observer(monkeypatch):
scripts = Path(__file__).resolve().parents[1] / "scripts"
monkeypatch.syspath_prepend(str(scripts))
spec = importlib.util.spec_from_file_location(
"retention_observer", scripts / "observe-backup-retention.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class ProviderError(Exception):
def __init__(self, status, code):
self.response = {"ResponseMetadata": {"HTTPStatusCode": status}, "Error": {"Code": code}}
class ReadOnlyStore:
def __init__(self, evidence):
self.buckets = {b["bucket"]: copy.deepcopy(b) for b in evidence[2]["observations"]}
self.heads = {
(h["bucket"], h["object_key"], h["version_id"]): {
"VersionId": h["version_id"],
"ContentLength": h["bytes"],
"Metadata": {"sha256": h["sha256"]},
}
for h in evidence[2]["version_heads"]
}
self.payloads = {}
self.calls = []
def get_bucket_versioning(self, Bucket):
self.calls.append(("versioning", Bucket))
return self.buckets[Bucket]["versioning"]
def get_bucket_lifecycle_configuration(self, Bucket):
self.calls.append(("lifecycle", Bucket))
return self.buckets[Bucket]["lifecycle"]
def list_object_versions(self, Bucket, Prefix, MaxKeys, **kwargs):
self.calls.append(("versions", Bucket, Prefix))
assert Prefix == self.buckets[Bucket]["prefix"] and MaxKeys == 1000
return {"Versions": copy.deepcopy(self.buckets[Bucket]["versions"]), "IsTruncated": False}
def head_object(self, Bucket, Key, VersionId):
self.calls.append(("head", Bucket, Key, VersionId))
return self.heads[Bucket, Key, VersionId]
def get_object(self, Bucket, Key, VersionId):
self.calls.append(("get", Bucket, Key, VersionId))
return {
"VersionId": VersionId,
"ContentLength": self.heads[Bucket, Key, VersionId]["ContentLength"],
"Body": io.BytesIO(self.payloads[Bucket, Key, VersionId]),
}
def without_sidecar(evidence):
ref = evidence[0][0]["recovery_mirror"]
for key in list(ref):
if key.startswith("receipt_"):
del ref[key]
def test_observation_rechecks_current_versions_and_passes_offline_review(
observer, reviewer, evidence
):
without_sidecar(evidence)
store = ReadOnlyStore(evidence)
before = copy.deepcopy(evidence)
result = observer.observe(store, store, evidence[0], evidence[1], NOW)
result["head_observed_at"] = NOW.isoformat()
plan = reviewer.review(evidence[0], evidence[1], result, evidence[3], NOW)
assert len(result["version_heads"]) == 4 and plan["generation_selection_valid"]
assert not result["provider_mutations"] and not result["archive_bodies_refetched"]
assert not result["protected_kit_body_refetched"]
assert evidence == before
def test_missing_primary_is_allowed_but_missing_mirror_is_not(observer, evidence):
without_sidecar(evidence)
store = ReadOnlyStore(evidence)
store.buckets[observer.PRIMARY]["versions"] = []
result = observer.observe(store, store, evidence[0], evidence[1], NOW)
assert len(result["version_heads"]) == 3
store.buckets[observer.health.MIRROR_BUCKET]["versions"] = []
with pytest.raises(ValueError, match="retained_version_missing"):
observer.observe(store, store, evidence[0], evidence[1], NOW)
@pytest.mark.parametrize("change", ["version", "hash", "size", "delete_marker"])
def test_changed_exact_head_refuses_observation(observer, evidence, change):
without_sidecar(evidence)
store = ReadOnlyStore(evidence)
head = next(iter(store.heads.values()))
if change == "version":
head["VersionId"] = "another-version"
elif change == "hash":
head["Metadata"]["sha256"] = "0" * 64
elif change == "size":
head["ContentLength"] += 1
else:
head["DeleteMarker"] = True
with pytest.raises(ValueError, match="exact_version_head_differs"):
observer.observe(store, store, evidence[0], evidence[1], NOW)
@pytest.mark.parametrize(
"status,code,accepted",
[
(404, "NoSuchLifecycleConfiguration", True),
(404, "NoSuchBucket", False),
(403, "AccessDenied", False),
(500, "InternalError", False),
],
)
def test_only_explicit_lifecycle_absence_is_accepted(observer, evidence, status, code, accepted):
store = ReadOnlyStore(evidence)
def fail(**kwargs):
raise ProviderError(status, code)
store.get_bucket_lifecycle_configuration = fail
if accepted:
assert observer.observe_bucket(store, observer.KIT)["lifecycle"]["Rules"] == []
else:
with pytest.raises(ProviderError):
observer.observe_bucket(store, observer.KIT)
@pytest.mark.parametrize(
"failure",
["foreign", "duplicate", "null_version", "boolean_size", "no_completion", "cursor_cycle"],
)
def test_ambiguous_or_incomplete_inventory_refuses(observer, evidence, failure):
store = ReadOnlyStore(evidence)
bucket = observer.health.MIRROR_BUCKET
rows = store.buckets[bucket]["versions"]
if failure == "foreign":
rows[0]["Key"] = "foreign/object"
elif failure == "duplicate":
rows.append(dict(rows[0]))
elif failure == "null_version":
rows[0]["VersionId"] = "null"
elif failure == "boolean_size":
rows[0]["Size"] = True
elif failure == "no_completion":
store.list_object_versions = lambda **kwargs: {"Versions": []}
else:
store.list_object_versions = lambda **kwargs: {
"Versions": [],
"IsTruncated": True,
"NextKeyMarker": "archives/synthetic",
"NextVersionIdMarker": "cursor",
}
with pytest.raises(ValueError):
observer.observe_bucket(store, bucket)
@pytest.mark.parametrize("corrupt", [False, True])
def test_retained_receipt_is_fetched_and_compared_with_exact_public_filter(
observer, evidence, corrupt
):
receipt = evidence[0][0]
ref = receipt["recovery_mirror"]
payload = observer.worker.canonical(observer.worker.public_receipt(receipt, receipt))
digest = hashlib.sha256(payload).hexdigest()
ref.update(
receipt_object_key=ref["object_key"] + "." + digest + ".receipt.json",
receipt_sha256=digest,
receipt_bytes=len(payload),
receipt_version_id="sidecar",
)
store = ReadOnlyStore(evidence)
bucket = observer.health.MIRROR_BUCKET
store.buckets[bucket]["versions"].append(
{"Key": ref["receipt_object_key"], "VersionId": "sidecar", "Size": len(payload)}
)
key = bucket, ref["receipt_object_key"], "sidecar"
store.heads[key] = {
"VersionId": "sidecar",
"ContentLength": len(payload),
"Metadata": {"sha256": digest},
}
store.payloads[key] = b"bad" if corrupt else payload
if corrupt:
with pytest.raises(ValueError, match="independent_public_receipt_readback_differs"):
observer.observe(store, store, evidence[0], evidence[1], NOW)
else:
result = observer.observe(store, store, evidence[0], evidence[1], NOW)
assert result["independent_public_receipts_fetched_verified"] == 1
def test_foreign_protected_reference_refuses_before_provider_contact(observer, evidence):
without_sidecar(evidence)
store = ReadOnlyStore(evidence)
evidence[1]["protected_kit"]["bucket"] = "foreign"
with pytest.raises(ValueError, match="protected_custody_binding"):
observer.observe(store, store, evidence[0], evidence[1], NOW)
assert store.calls == []

View file

@ -133,6 +133,18 @@ expiry executor remains needed, with publisher/reader deletion denials intact.
Review/proof: docs/release/generation-retention-review-2026-10-30.{md,json}.
This is not recurring retention enforcement or additional expiry admission.
2026-10-31 current-inventory return: the date-operation 02:07 point is now
mirrored with its immutable public receipt and independent reader verification.
A repeatable read-only observer refreshes complete prefix listings, lifecycle,
12 exact-version HEADs and two public-receipt GET comparisons. Four logical
points are mapped; the pinned selector keeps three and identifies the earlier
October 31 00:27 point as one candidate with its exact receipt. Nothing was
deleted. Protected kit/proof custody remains unchanged, all unknown versions and
fixtures are preserved, and no age-only mirror expiry was added. Source and native
proof: docs/release/generation-retention-review-2026-10-31.{md,json}. Separate
expiry executor/caller admission, native boundaries and historical-copy
disposition remain open; the fresh review is not recurring retention enforcement.
## T02 — Activate and prove the first encrypted paired recovery point
```task
@ -332,6 +344,16 @@ service-contract returns remain separate; no timer activation is inferred. The
new root date journal now exists and remains included in the already tracked
operator offboarding/expiry obligation. No journal or historical pair was deleted.
2026-10-31 latest-existing-point mirror: unchanged worker executed twice from a
private copy of the native 02:07 date-operation archive. Independent reader GETs
verify the single ciphertext and public receipt version; retry changes neither.
The node's durable receipt and encrypted bytes were checked before copying.
Proof: docs/release/mirror-date-point-native-2026-10-31.{json,sources.txt}.
This point predates the date correction and is not a new capture or app restore.
No node worker/state/unit/timer changed. Fresh generation observation/selection is
recorded under T01; node operating execution, received alerts and the full service
contract remain required under T03.
## Completion
Finish when every task above has its stated evidence and acceptance, or hand off any newly discovered residual to a live work record first. Cross-owner dependencies remain their existing canonical records.