Verify latest backup mirror and refresh native generation retention
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m47s
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:
parent
29c9db5b65
commit
51a527128a
8 changed files with 910 additions and 0 deletions
165
docs/release/generation-retention-review-2026-10-31.json
Normal file
165
docs/release/generation-retention-review-2026-10-31.json
Normal 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
|
||||
}
|
||||
}
|
||||
83
docs/release/generation-retention-review-2026-10-31.md
Normal file
83
docs/release/generation-retention-review-2026-10-31.md
Normal 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.
|
||||
|
|
@ -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
|
||||
72
docs/release/mirror-date-point-native-2026-10-31.json
Normal file
72
docs/release/mirror-date-point-native-2026-10-31.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
335
scripts/observe-backup-retention.py
Normal file
335
scripts/observe-backup-retention.py
Normal 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)
|
||||
217
tests/test_backup_retention_observation.py
Normal file
217
tests/test_backup_retention_observation.py
Normal 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 == []
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue