From d64f9809f90532d52c2992174e63c1a216a50211 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 11:44:20 +0700 Subject: [PATCH 01/13] research: add bounded recovery-closure experiments and lossless evidence Add separate v3 receiver policies, model enumeration, HTTP mutation controls, unchanged pinned CaPU/ATMAN adapters, ordinary FSM and operation-idempotency controls. Local 40-test suite passed; 560 bounded traces retained. Preserve negative conclusions and known prior art. Draft-only research; no production, novelty, hardware, performance, or superiority claim. Existing v1/v2 untouched. --- .github/workflows/recovery-closure-v3.yml | 40 +++ experiments/recovery_closure_v3/.gitignore | 3 + .../EVIDENCE_MANIFEST.json | 36 +++ .../recovery_closure_v3/INVESTOR_BRIEF.md | 43 ++++ experiments/recovery_closure_v3/PROTOCOL.md | 20 ++ experiments/recovery_closure_v3/README.md | 69 ++++++ experiments/recovery_closure_v3/VALIDATION.md | 13 + experiments/recovery_closure_v3/demo.py | 15 ++ .../evidence/archive-parts/000.b64 | 1 + .../evidence/archive-parts/001.b64 | 1 + .../evidence/archive-parts/002.b64 | 1 + .../evidence/archive-parts/003.b64 | 1 + .../evidence/archive-parts/004.b64 | 1 + .../evidence/archive-parts/005.b64 | 1 + .../evidence/archive-parts/006.b64 | 1 + .../evidence/archive-parts/007.b64 | 1 + .../evidence/archive-parts/008.b64 | 1 + .../evidence/archive-parts/009.b64 | 1 + .../evidence/observed-summary.json | 35 +++ .../recovery_closure_v3/finite_model.py | 91 +++++++ experiments/recovery_closure_v3/observer.py | 20 ++ experiments/recovery_closure_v3/receiver.py | 185 ++++++++++++++ .../recovery_closure_v3/requirements.txt | 1 + .../recovery_closure_v3/restore_evidence.py | 41 ++++ experiments/recovery_closure_v3/run.py | 56 +++++ .../recovery_closure_v3/source_pins.py | 44 ++++ experiments/recovery_closure_v3/test_http.py | 232 ++++++++++++++++++ .../recovery_closure_v3/validate_results.py | 39 +++ 28 files changed, 993 insertions(+) create mode 100644 .github/workflows/recovery-closure-v3.yml create mode 100644 experiments/recovery_closure_v3/.gitignore create mode 100644 experiments/recovery_closure_v3/EVIDENCE_MANIFEST.json create mode 100644 experiments/recovery_closure_v3/INVESTOR_BRIEF.md create mode 100644 experiments/recovery_closure_v3/PROTOCOL.md create mode 100644 experiments/recovery_closure_v3/README.md create mode 100644 experiments/recovery_closure_v3/VALIDATION.md create mode 100644 experiments/recovery_closure_v3/demo.py create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/000.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/001.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/002.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/003.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/004.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/005.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/006.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/007.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/008.b64 create mode 100644 experiments/recovery_closure_v3/evidence/archive-parts/009.b64 create mode 100644 experiments/recovery_closure_v3/evidence/observed-summary.json create mode 100644 experiments/recovery_closure_v3/finite_model.py create mode 100644 experiments/recovery_closure_v3/observer.py create mode 100644 experiments/recovery_closure_v3/receiver.py create mode 100644 experiments/recovery_closure_v3/requirements.txt create mode 100644 experiments/recovery_closure_v3/restore_evidence.py create mode 100644 experiments/recovery_closure_v3/run.py create mode 100644 experiments/recovery_closure_v3/source_pins.py create mode 100644 experiments/recovery_closure_v3/test_http.py create mode 100644 experiments/recovery_closure_v3/validate_results.py diff --git a/.github/workflows/recovery-closure-v3.yml b/.github/workflows/recovery-closure-v3.yml new file mode 100644 index 0000000..344b6f2 --- /dev/null +++ b/.github/workflows/recovery-closure-v3.yml @@ -0,0 +1,40 @@ +name: Recovery closure v3 +on: + pull_request: + paths: + - 'experiments/recovery_closure_v3/**' + - '.github/workflows/recovery-closure-v3.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + recovery: + runs-on: ubuntu-latest + timeout-minutes: 8 + strategy: + fail-fast: false + matrix: + python: ['3.11', '3.13'] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install pinned dependency + run: python -m pip install -r experiments/recovery_closure_v3/requirements.txt + - name: Verify pinned source dependencies + run: python experiments/recovery_closure_v3/source_pins.py + - name: Restore and validate recorded evidence + run: | + python experiments/recovery_closure_v3/restore_evidence.py --output recorded-evidence + python experiments/recovery_closure_v3/validate_results.py recorded-evidence + - name: Run new model and HTTP experiments + run: | + python experiments/recovery_closure_v3/run.py --output evidence-v3 + python experiments/recovery_closure_v3/validate_results.py evidence-v3 + - uses: actions/upload-artifact@v4 + if: always() + with: + name: recovery-closure-v3-python-${{ matrix.python }} + path: evidence-v3/ + if-no-files-found: error diff --git a/experiments/recovery_closure_v3/.gitignore b/experiments/recovery_closure_v3/.gitignore new file mode 100644 index 0000000..41f31dc --- /dev/null +++ b/experiments/recovery_closure_v3/.gitignore @@ -0,0 +1,3 @@ +.pinned/ +__pycache__/ +evidence-rerun/ diff --git a/experiments/recovery_closure_v3/EVIDENCE_MANIFEST.json b/experiments/recovery_closure_v3/EVIDENCE_MANIFEST.json new file mode 100644 index 0000000..d8a17ef --- /dev/null +++ b/experiments/recovery_closure_v3/EVIDENCE_MANIFEST.json @@ -0,0 +1,36 @@ +{ + "schema": "recovery-closure-evidence/1", + "codec": "bz2 of UTF-8 JSON object; *.json values re-serialize with indent=2, sort_keys=True and final newline; logs write verbatim", + "compressed_bytes": 5476, + "compressed_sha256": "af053ef76da16f703248aa65b0e2116ca2c5ab2d49ad91856f90af1f2d886b88", + "files": { + "http-tests.txt": { + "bytes": 4566, + "sha256": "ae9e39170a70dbffe7130a09f536adf73c1398484f5ff8a07141ccf7b4ca5101" + }, + "http-traces.json": { + "bytes": 62744, + "sha256": "b347cf7d47e8886b31341946f43555e0684260ec3421032ae822c95ca01eb69d" + }, + "model.json": { + "bytes": 1057600, + "sha256": "c9465a2bc29c40e817deda895ebddc8706d04fb9d8d3d72263f4bb972967a854" + }, + "summary.json": { + "bytes": 13736, + "sha256": "53f09e04095bf4eb975240e8ce619833e0797caf040c34bc7e86e305cc071129" + } + }, + "parts": [ + "evidence/archive-parts/000.b64", + "evidence/archive-parts/001.b64", + "evidence/archive-parts/002.b64", + "evidence/archive-parts/003.b64", + "evidence/archive-parts/004.b64", + "evidence/archive-parts/005.b64", + "evidence/archive-parts/006.b64", + "evidence/archive-parts/007.b64", + "evidence/archive-parts/008.b64", + "evidence/archive-parts/009.b64" + ] +} diff --git a/experiments/recovery_closure_v3/INVESTOR_BRIEF.md b/experiments/recovery_closure_v3/INVESTOR_BRIEF.md new file mode 100644 index 0000000..9427b52 --- /dev/null +++ b/experiments/recovery_closure_v3/INVESTOR_BRIEF.md @@ -0,0 +1,43 @@ +# Safe recovery for agent actions + +## Executive Summary + +**We built a reproducible test for a dangerous recovery boundary.** A missing result is not evidence that an old command can no longer execute. In a deliberately faulty receiver, a retry and a delayed original request both produce effects. Checking a durable closure at the actual effect transaction prevents that counterexample in this laboratory. + +**The result is a project-level research contribution, not a new distributed-systems invention.** The same contract works with a conventional state machine. A stable operation-idempotency key is also a successful conventional control. No superiority, customer demand, revenue, silicon performance or production safety is established. + +## The demonstration distinguishes safety from getting work done + +The unit measured is a durable SQLite row per logical operation. Two rows mean a duplicate effect; zero rows after a permanently lost request mean unfinished work. A separate read-only process observes the receiver ledger. All traffic is loopback HTTP on one trusted host. + +| Receiver policy | Delayed original, after release | Permanently lost original | +|---|---:|---:| +| Conservative HOLD | 1 effect, but 0 at the recovery checkpoint | 0 effects | +| Empty lookup treated as negative | 2 effects | 1 effect | +| Closure checked only on admission | 2 effects | 1 effect | +| Closure checked atomically with effect | 1 effect | 1 effect | +| Conventional operation-key idempotency | 1 effect | 1 effect | + +These are two deliberately controlled demonstrations, not measured customer incident rates. The weak rows are intentional mutation controls, not bugs attributed to the earlier conservative CaPU v2. + +Run `python demo.py --case delayed` and `python demo.py --case lost`. Do not omit the successful conventional control from a presentation. + +## What the evidence establishes + +The new HTTP suite passed 40 test methods without failures, errors or skips. The finite model enumerated 560 distinct bounded traces: 56 causal schedules, two initial-delivery conditions and five policies. All 16 native-CaPU/conventional-FSM matrix comparisons matched. Full new traces and logs are retained losslessly; `restore_evidence.py` verifies their original byte identities. + +The integration hypothesis is executable: a receiver's negative receipt must close the old attempt's future effect path, not merely report a past snapshot. It must not become a new execution grant. Tests also cover receiver restart, a tampered negative receipt, changed authority generation and a foreign operation's closure. + +## A defensible product hypothesis + +Build a recovery-conformance kit for teams whose agents invoke consequential tools. The proposed value is finding unsafe retries and explaining the receiver contract needed to repair them. The first test of this business hypothesis should be one real connector integration and an external team reproducing a failure and its correction. Measure integration effort, useful defects found and willingness to pay; do not infer those from this lab. + +A usable introduction: “We test whether an agent workflow can recover from an ambiguous tool result without duplicating the action or silently using stale authority. Our current reference makes the boundary reproducible, including negative controls and conventional alternatives.” + +## Further questions before an investment claim + +Can a real target service enforce a stable operation identity or close an old attempt? What happens when the receiver is unavailable, its storage is rolled back, or multiple regions disagree? Is the kit easier to integrate or more diagnostic than existing tests? Is there a paying owner for the failure mode? These questions remain unanswered. + +## Caveats and assumptions + +SQLite insertion is the effect. Closure and effect share one database transaction; no atomicity with an arbitrary external payment or device is proved. Public fixture keys and synthetic A7 tags are not production authentication. No Byzantine protection, physical power-loss proof, deployment bypass resistance, unbounded exactly-once guarantee, hardware acceleration or full four-repository integration is claimed. Independent code review and external reproduction remain separate gates. diff --git a/experiments/recovery_closure_v3/PROTOCOL.md b/experiments/recovery_closure_v3/PROTOCOL.md new file mode 100644 index 0000000..6cf6cda --- /dev/null +++ b/experiments/recovery_closure_v3/PROTOCOL.md @@ -0,0 +1,20 @@ +# Recovery closure experiment — protocol fixed before new trials + +Date: 2026-09-05. Decision: whether CaPU/ATMAN supports a defensible investor-facing recovery demonstration, not whether it replaces a CPU. + +Source: unchanged CaPU HTTP v2 at 8a2f2a37023a50aeac52cb8c8aed84b2eeceec88; CaPU A6/A7 and ATMAN authority pins from its bootstrap. Old HTTP behavior remains a safe HOLD control. New receiver/adapters below are experimental additions, NOT defects attributed to v2. + +H1: Reinterpreting an empty result lookup as NOT_COMMITTED admits a duplicate when the original request is delayed. +H2: A persistent attempt tombstone checked only at request admission is insufficient if an already admitted handler is paused before its effect. +H3: Checking that tombstone in the SAME receiver transaction as the effect prevents this counterexample and permits retry after a permanently dropped first request. +H4: A conventional FSM given the same receiver guarantee matches native CaPU. Operation-key idempotency is a strong conventional control; no architectural superiority is presumed. + +Arms: hold, snapshot_negative (deliberately unsafe), admission_fence (deliberately weak), atomic_fence (candidate), operation_idempotency (separate conventional control). + +Primary outcome: number of durable effect rows per logical operation; >1 is unsafe. Completion after a permanently dropped initial request is measured separately from safety. No timing, throughput, energy, revenue or investor-interest claims from these trials. + +Finite model: enumerate all interleavings preserving each local causal chain; two attempts, one closure query, one receipt application, one optional replay. Report exact bounded trace counts, NOT empirical incident probabilities or unbounded proof. + +HTTP: real loopback sockets and a receiver subprocess; test-controlled gates before/after admission; SQLite insertion itself is the effect. Native/FSM use unchanged upstream authority and lifecycle logic. Use a separate read-only observer; do not hide duplicates behind operation dedup in the candidate. Candidate only deduplicates the same attempt. Closure and effect share one transactional database; no cross-system atomicity is claimed. + +Non-claims: new distributed-systems theorem, patentability/priority, real external payment, production receipt cryptography, Byzantine protection, physical power loss, unavailable receiver recovery, complete ATMAN/Bardo/COSMIC integration, silicon acceleration, market validation. Public deterministic fixture keys only, no expenditure or deployment. diff --git a/experiments/recovery_closure_v3/README.md b/experiments/recovery_closure_v3/README.md new file mode 100644 index 0000000..96df44c --- /dev/null +++ b/experiments/recovery_closure_v3/README.md @@ -0,0 +1,69 @@ +# Recovery closure: where a safe retry becomes possible + +**Experimental reference — not production infrastructure.** This directory investigates a missing *integration contract*, not a newly discovered defect in CaPU HTTP v2. The original v2 correctly refuses to reinterpret a missing record as a negative outcome. + +## Main finding + +A receipt saying "not executed yet" is insufficient for a safe new attempt while an old request can still execute. Even a persistent cancellation tombstone fails if the receiver checks it only on admission and then pauses before the effect. In the candidate receiver, the closure check and effect insertion share a single SQLite transaction. This provides a bounded recovery demonstration without inventing a new authorization. + +This is a new reproducible experiment for this project, **not a claim to have invented fencing, idempotency, or exactly-once processing**. A normal FSM with the same receiver contract is deliberately retained as an equal-guarantee control. A conventional operation-key idempotency implementation is another strong control. + +## Run + +```sh +cd experiments/recovery_closure_v3 +python -m pip install -r requirements.txt +python source_pins.py +python run.py --output evidence-rerun +python validate_results.py evidence-rerun +python demo.py --case delayed +python demo.py --case lost +``` + +The downloadable source package also includes the pinned dependencies for offline reproduction after installing cryptography. A fresh Git checkout obtains exact source blobs through `source_pins.py`, verifies their Git blob IDs, and then runs the original bootstrap to verify CaPU A6/A7 and ATMAN authority pins. No unpinned branch is imported. + +## What is compared + +| Arm | Missing result | Where an old attempt is blocked | Purpose | +|---|---|---|---| +| `hold` | Remains UNKNOWN | Controller refuses retry | Existing conservative contract | +| `snapshot_negative` | Mislabelled NOT_COMMITTED | Nowhere | Deliberately unsafe mutation | +| `admission_fence` | Returns NOT_COMMITTED | Before the processing pause | Deliberately weak mutation | +| `atomic_fence` | Returns NOT_COMMITTED only after durable closure | Same receiver transaction as effect | Candidate receiver contract | +| `operation_idempotency` | No negative-outcome inference required | One result per stable operation key | Conventional service-side control | + +The first four arms use both unmodified native CaPU and the existing ordinary FSM. They share the original ATMAN authority verifier, fixture receipt authentication, transport and persistence. Only lifecycle transition logic differs. The fifth arm retries directly against receiver-side operation deduplication and does **not** claim to use CaPU authority or attempt-bound negative receipts. + +The candidate has a uniqueness constraint only per **attempt**, not per logical operation. Thus the duplicate across attempts 0 and 1 is genuinely observable in the weak controls. The conventional idempotency control intentionally deduplicates the entire logical operation. + +## Measurement boundary + +One logical operation, two attempts, one closure query, receipt application and optional replay. The finite model enumerates 56 causal orderings for each initial-delivery condition and each of five policies: 560 traces total. These counts are **not probabilities of production incidents** and do not establish unbounded correctness. + +Live tests use a receiver subprocess, loopback HTTP and a separate read-only observer process. Controller method calls run in the test process, with persisted controller state; the receiver restart case kills and restarts the actual receiver process. Earlier v1/v2 crash tests are rerun separately and are not re-labelled as new tests. + +SQLite row insertion **is** the effect. Closure and that effect are in the same database. This does not make a SQLite record atomic with a payment provider, robot, email delivery or other remote side effect. + +## Evidence and review + +`evidence/observed-summary.json` is the observed summary, while `evidence/archive-parts/` retains all new trace files and the test log without losing bytes. Run `python restore_evidence.py` and `python validate_results.py evidence-restored` to verify the original observations. The downloadable ZIP additionally preserves the complete v1/v2 regression outputs. `EVIDENCE_MANIFEST.json` records uncompressed and compressed hashes. `PROTOCOL.md` records the hypotheses written before the new trials. `VALIDATION.md` records recomputation and limitations. Test PASS is not independent code review. Keep this work in a draft PR without merging until the agreed review gate is satisfied. + +## Why this is useful + +The kit distinguishes three outcomes that must not be conflated: unsafe retry, safe but blocked recovery, and safe recoverable execution under an explicit receiver contract. An integration can be tested against that contract rather than branded "safe" just because its logs or receipts are signed. + +The commercial hypothesis is a repeatable recovery-conformance test and integration service for agent tool execution. Customer demand, integration effort, willingness to pay and superiority over existing solutions are **unmeasured**. + +## Boundaries that remain open + +A7 receipt tags and authority keys are public deterministic lab fixtures, not production credentials. There is no Byzantine-device defense, bypass-resistant deployment, storage rollback protection, physical power-loss proof, multi-region failover or finite-time progress during receiver unavailability. Authorization remains linearized at controller admission. Reconciliation does not renew an expired grant. No Bardo/COSMIC code is exercised; no full ATMAN runtime, hardware, speed or energy claim is made. + +## Prior art and source context + +AWS, "Making retries safe with idempotent APIs": https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ + +Martin Kleppmann, "How to do distributed locking" (2016): https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html + +gRPC, "Cancellation": https://grpc.io/docs/guides/cancellation/ + +These sources already establish relevant retry, receiver-enforcement and cancellation limitations. This experiment makes those boundaries executable in the pinned CaPU/ATMAN composition; it does not establish scientific priority. diff --git a/experiments/recovery_closure_v3/VALIDATION.md b/experiments/recovery_closure_v3/VALIDATION.md new file mode 100644 index 0000000..4e45709 --- /dev/null +++ b/experiments/recovery_closure_v3/VALIDATION.md @@ -0,0 +1,13 @@ +# Validation record + +Assessment: share as a bounded research demonstration with the limitations below; not a production or investment-readiness certificate. + +- Source: unchanged CaPU HTTP v2 at 8a2f2a37023a50aeac52cb8c8aed84b2eeceec88, with its pinned CaPU A6/A7 and ATMAN authority modules. New receiver policies and the negative-receipt adapter are separate experimental additions. +- Regressions: original v1 55/55 and v2 28/28 passed again. Their result digests matched the historical files. These are not new independent guarantees. +- New live suite: 40 test methods, zero failures/errors/skips; 36 matrix records and 7 boundary records. Three test methods contain two engine subcases. Native/FSM shared authority, persistence, transport and synthetic authentication; only lifecycle logic differs. +- Model: 560 distinct traces. An independent permutation filter confirmed all 56 linear extensions of the two event chains. `validate_results.py` recomputed the aggregates from full histories and compared all 16 native/FSM pairs. +- Evidence: archive reconstruction reproduced four original output files byte-for-byte. Sizes and SHA-256 values are in EVIDENCE_MANIFEST.json. Hashes are integrity checks, not independent timestamps or completeness attestations. +- Scope: intentionally selected fault schedules and mutation controls. No probability model, incident-rate estimate, speed benchmark or claim that existing conservative v2 is defective. +- Code review: local checks are not independent review. Keep the PR draft; record the native Codex response separately and never substitute another approval. + +The standard analytical HTML report packager could not be loaded in this runtime. This delivery uses a repository research note and executable evidence, not a claimed rendered/validated HTML report. diff --git a/experiments/recovery_closure_v3/demo.py b/experiments/recovery_closure_v3/demo.py new file mode 100644 index 0000000..b0f3c85 --- /dev/null +++ b/experiments/recovery_closure_v3/demo.py @@ -0,0 +1,15 @@ +"""Live, bounded demonstration. These are synthetic effects, never real payments.""" +import argparse +import json +from test_http import scenario + +p = argparse.ArgumentParser() +p.add_argument('--case', choices=('delayed', 'lost'), default='delayed') +a = p.parse_args() +gate = 'after_check' if a.case == 'delayed' else 'drop_before_effect' +print('LIVE LOOPBACK LAB — SQLite rows are the effects; no real money or devices.', flush=True) +print('Policy Effects at recovery Final effects', flush=True) +for policy in ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence', 'operation_idempotency'): + r = scenario(policy, gate, None if policy == 'operation_idempotency' else 'native') + print(f"{policy:30} {r['effects_at_recovery']:19} {r['final_effects']:15}", flush=True) +print('No CPU advantage or superiority over a correct conventional implementation is claimed.', flush=True) diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/000.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/000.b64 new file mode 100644 index 0000000..8bb77f2 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/000.b64 @@ -0,0 +1 @@ +QlpoOTFBWSZTWcyFDpsEhNEfgFBn//A//7+uv///6mAyL7wAAACQAKAA+QAAAAAAAABhrAQAEA2AWYEEG7AHVOdtG4XinAAPeyEaEqCvuaBD6CIUxIAAGsAACDYAAAAAAAZAB6GQSSmJoAAppshTJoaDQAG1Gh6gAajQEyYamigAAAAAAAAIm9VQaAAAGgBoAAGjQAASeqUlTSDTT1NAAAAAAAAABNSlVP0UAeoNGgyAADE0ANAAApKUJoEJtAiajNNImnqbUxNGNJmSBhK+X8ZvcAuI75uAnARo6NPcND8WgIvqECwEegnE4nE9U4mVZOEXCro1XqWTRiyYxSWTRiyYxVJZMYsmMSSSRRJJJSUQGITSUkiRAYhJJJSUAMYCSSUlJJJDGAMCIYAEQxAIYgGChgDAiAAiUkpFFJKUlGExCaTSSmMJEK1rKSyaMWTGKSyaMWTGbVvVkqSxRUlFN6kwSDJMEgyTBIMkwSDyksmjFkxm15ZKksUVJRTeSYJB6kpKIpInlpLTNGi0y110pKIpIiSkoikiJKSiKSJ1UlSWKLJooyUlJJFiSkpJKTYxElGISyWSkyJGIgoxCSUlKSUpKSESRRREElJSklKSkklJSmKKIkiiJCRAoiAiEhEKMREMijERDCIYMQoiSKIkJECICG2Ztmz+gv0kss9O3hucl3YswzBmDnB4GLuOGLhjtMavKaorq6lV5c3XNzcrzXEA2tJSZKTJSZKQlJKU1qSkJSSmjJZKUkptkpMlISklKZKTJSZKQlJKUiIgQYiCCW0REEkRFsRF diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/001.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/001.b64 new file mode 100644 index 0000000..b235b57 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/001.b64 @@ -0,0 +1 @@ +BMSEqIJIiCXYjCUeXCSd0LnXdrJaSslSRpKSSSkqSkkkMJkskpSSkpJJZJSkqSySlJKSkmNkslJQyzJmWYzGMcLGGMLpZIpJSkskoSSUpLJKUkslKSzVJZNJZIpJSkskrtkqXaSyaSy7ZKl2ksujSUkklJUlJJLopJSkskrtEYgDCRhIRDFGIl0QhISMiF1OxnU7OjEDohdsUWIogDCRRhIRDGjESdCWIrmMaIwhIYkZqMRhdTsZ1OzoogdGF2ZWZZmWamVmWZlmplmZZZmTTM0zNTNMzTKzDNTNMzTKzTM00zNNMzTqWsrbKtRJEkSRJEk1tJIkiSJNkm1STZNSTWSRJskSRJEmybJeRCEhIyIVUxlreU8AAAB5lvKeA9RPNOiIiJ0YgDCRhIRHmpTWMtt6iG3kYgeaW9CC8xRiJcMRBBBiIIL1eeq3d29XLeVd5W7u3lyy6tt1W83pjUYxojCEhiRkYVbZjKr1T0AAADeZXlPAeonmnRERE6ixFEAYSKMJCI8ANaAQA1gMjDYQBAAANWgABEANYNgMjEAQA1gwjDYQBG9RhryKIF4iPRIS8xoxEuiItiKiIoJiQhJEQSREWxFEExISogkiIJcUQmKITEJCQhRCY0UQmISEheohCQkZEJjOp2dRGIAwkYSER0QujEBrRosURRAGEijCQiNmNGIl1xliK5jRiMISGJGajEYXU7GdTs6KIHRhcUQmKITEJCQhRCY0UQmISEhfO9+vc9sRi53U6konLq651y4dLudK66iNzvc92tW9kkwSDJMEgyTBIMkw diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/002.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/002.b64 new file mode 100644 index 0000000..a7ff757 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/002.b64 @@ -0,0 +1 @@ +SCk0gREGWtpJMEgyTBIFZJAkGDJESURYLBEYxiLJpAiINJMEgyTBIMkwSCk0gREGe7XSUiiklKSjCYhNJpJTGEiFbykpJJIsUlJJJSWKICjEJZNkpJExRAYxDXk0gREG9ShiUkxJCZMpEgrypLSkmk0lSWlJLSaSjSJooxBaZUlilJmko0hooxJ7mtbeVtryUlEUkRJSURSRFslJEUkRKREpSQkJSSkJCSxCJJZKESEwkYQwhoSExGShiUkxJCZMpEgklJRFJESUlEUkRJSURSREoYlJMSQmTKRIL4dfEszaFiDFMirE0ahWAxYKwsJZTSGJYpiZImLKissIxJqlMLUpgUshlMpliWkyVrWmpoNBksTSZGEYRlMpotFm1tbYxjGMYxjGMajGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxjGMYxj91vdjBENPhU1o0Ws7cHGdGZx4/v/k8VV4qOePD9Zf8y/qjiyONHBwttGj+6pX516e2fD25wj5o6lD4qOiR/HqR1KFWRhKR1VXV2SHFMkNFcCJZTAociHIrQqw4FDj9/AUOJDKMXaOCsh/7E3z6JHUq6BXKKrqk45EYWiXEZF36HGJvp/8Ch0SVUOvgFD/bpI43nUVXkFH3qrrKkjisgdy6wOoYV1FRxR1oqpxTKku3fw8HRLjKTG93CLz7e+VEj/GpX4D6mWZmMZZmYxmlLJKTBGjRSaEqUsklKWSSlJKWSVKWSSk0aKSkjRSUklJCQzRSaEpNDNCQ diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/003.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/003.b64 new file mode 100644 index 0000000..e5e812d --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/003.b64 @@ -0,0 +1 @@ +jNRSY0MI0UUmNDCTGhhEMJjE0UmCNFJgkwSEwpNCUmhmhIYvLWq/0xVO7aLHNDqiVqWlMVZME6BQxSjBQxFxSMi4FDhFxFkpiZMlOkpJHRTEI4SnAoYVLBQyiKnIaAkbnBCYQ5ygSnKG4RYi4ptCrBQwxFzQkuBQ0qprEDBQ0kVgoYqCMIYTm5alNqR2vW807KjELbs1zNm3C5y45mOpQtdzNllbrZQzjMdSha7mbLK3VruZrLFkxOUHNbNrZz8onOar3c5HydPbpXPd8fu/4j8wKHukSh3FZI1SlloHsD/ksk8ZHYR+3jozGYnS44zGYnLDp3S9DA22zLWFsniAodhHhPAV3Dx4bGzhzhsbGzhzlFPKdYooosVtV5ptlKyyqylRbwyjqUstuMqRS1hlFHUqLcZR1KWW3GVIpW6lLLLLDNrjKkU1SKSBWvXs9mq0P2OyO4oZMKGPRjrJXgAobw3TuVeInbuFDoOhYssXVZZmBnUOp2CuE4cCuE4cCuFw4FcDhwK5RKF10nOcuhHg5IuvXgqedI6/lumpGkXSqdBHkCOqI6xRQhIBBV1mjX1cv2dl/WAI5gEWdmUu178CcT9Xu+z9Xt27dNE/D0/Qq4J4kWE4lMmVZOJk4qxMVLIuJJmZwNTGpwnBymLUxlyZcNatcXHLWOGa1wajilqnDVcocHKsTIbTismjHJo1pyrLhjiccNNHKVZtcIzRnHFxoxiNWNXI4OViyORpY0bVylacoxmTMs1Rpo0y4ldaV1tRs7rmKpNTnY2dxzWNKtOVcHGrOXHGswYa diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/004.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/004.b64 new file mode 100644 index 0000000..802e8f0 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/004.b64 @@ -0,0 +1 @@ +l2XZNl266ZapqbrN2ZrXFxy1rkTGjHJpxWjhpyrFwxxOOGmjkTgnCZGpchqXIaKNDQ5Q0OQ0PM6dJSbu1D0LUdmUvG56vtSSIeO7DfTPvfQcIy5xqjsrECSEHeSQFj7OXU+bc97fQn0TJTIspUwstWjLLCyhomRWWU0ZVjCarC1BYaZDKzZsmmRYJkmSwmqwtA93CLikf9cqk60QvUVdvi+Pl7+1ISuzll3TDTkMcUkJZxykww5V1SZc1AFzJIYTLUpatLZbQiIiIiIRERERERERERERG2rQiIhERERERERERa2lVu+D5nuPg95zXbOqrEEkKlJCVl+WDUyNbRfg6FxTLllje1MkySCEQUm0bLcqUslzAARjHGqn2NsL4mFg1BU1RkKOwgsUhKyhBLyiaavdOOBE61U1uew5KoSMDFL3sIPfShHQliAAQqtaMvm9c4OOFJiKRrJEsDuuI/fJUUIQEqoyz222xkGvjcTyUWgjGfmSQlekhK8ojVXW60rMZVQJYo8sYquURkm683tyXCIwldR1Fz2CFHhIxebHTOTYwK02vW2JHYIQEIS5RMaRk8m5uTsXYFfTDJntnp69e8kRTmRe2IJB+4j+L+8j9xH6FH4/if2f3dVeQyMMsyCYWLRNMqwRhYVFlhWjCRloQyjwKiHd/Aj+sj+BH/YlQY/vIxR7iOhHH8SMo8iPIjP8CP5/B5sR5v3keBHkexH+ZKg9H5dBUo/h6ke4jPkxHl4kqDsSoPmR/oKlHyEeoqUeAUq/iKlH9vmSoPgR/B8XwClXUjCpR8BP diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/005.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/005.b64 new file mode 100644 index 0000000..61319cb --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/005.b64 @@ -0,0 +1 @@ +sR6GKO73kerOwjz+4jijo+ZHzJUHcju8koh2ClX7g/ssTf6CewqUeT0ceBUqPsR6PvA/c2MF8lAF1iNVB6+qVJO36N3Pp8ObV+LbW32b4llLYAAA1obbbZUxYgxi0ymmShpkWaatU2ENGQYxttLUttTaytYiIiIiIiIiIiIiIiIiIiIiIm22a1EREREREREREREMVhMTEZUYqyqjCLCliSyqjFMWomBqGSpqosCMTLRTVMrAsSmoH32Ktg1ZpZZmGtZsqv6jEbTZSlml0qM1lC0WGjCMhhGQ1CMq1UjGKYsW5jlHKmA5OLQ5OLQ5Mi1UaKaLLlMq5DkCuLkaLQxVo4TQxVocmmolkwliYKZTE00xlFhZThDnCyplTUaTVNFrmNrZs1TSOWYRaGjTUjQ0aakaGjTUj98KHXjamltrNRhspce4KGdl2qrG0W1rLWOSP6fyf+iP4PkFKvmR+kj8yOzj7FXcvovDmzZs7VSukr1NSmaRdUiLChMpRTKgLj6B0QBAEAQBAEAQGkaSCrFViqxVdREZP3syEAhJJCSSQAAAAA9l8JXfz61tVuzO7jvra2tuhCMJw8TCsNlZW/9e7/AAiBgQMkgAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIgAAABVEIAAAAG28HcR3zkV1KHr1em9Xby45zjnOOccc5xznHOcc82FGZ4uzpra2N4d12dhh2f464ReDjgjxIR7jzdoOoTEmkbEXZg8eCP/vfqKKh1MoCLH8ePcdvTrDicC+HbPHp4ugSxPZXSW17BJOvoIeAdefs5 diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/006.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/006.b64 new file mode 100644 index 0000000..d5fc073 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/006.b64 @@ -0,0 +1 @@ +5f59a7ZTQ93sL8FV/uGUcjCMP9SEdUEq/A/Pr3fh3FH6hH49u891csdhv40HiI/lFPYhHn8lQ7h1YGJrErBi2X3CHgjy/yI4JShSAgYeTAAkBr0GWGzUc26D5Nt81IBSkAwBs1pZ49hIYQlq9wGn9xKq+fdttg6GOG+yzhRGE0jYCEgloncCAQSwyurdoAICdkBrOpA5zenw0oo6JO/t8wvmBQwP1yeXcIR1BQ6tJVQ3+mOCOv4kXRwhHbqFdJULkpUq6E+JUTCr8SdJQmIS2Z56kMb875ctdZtISUUEaweAAJAMAwJGPe4pXX67oXu8fX7CO32PYodAoevv+7oPv9RR859FNJI9/wJ0Iau/8AAAAB8+rVm4AAAAAIAAQAOttVt8ha9t6REVlkUtNswx/IRfgp4UMU1DKYYFD1c6tuc7UqdM2bNmZqy00lam2WrJMQ4AAAAD2bbW297VaWtqiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiJJCSQkkJJCSQkkJJCSQkkJJCSSEkJJCSQkkhISSSEgEgEgEgEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkhJISSEkCESSkkllmEDAQ1rN3O66TTCq3SGvOaz5tGYwYZhmGwJCRoiPbVpSXIidVovk+XiLM9m4Z1EdRPb3/bz93y9zxlFH5hCPXkR70ClfkIwpf4OUI9ishGKevz9/iAofAR5HFK8yd/u+vjz9Av0ARLDSJ diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/007.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/007.b64 new file mode 100644 index 0000000..0cdb328 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/007.b64 @@ -0,0 +1 @@ +MNJWCMI+9E+pUeklel4SijyPD0oPToUPASlD3vuCjz6FD9bxVH2Cqh+LnqCh2I9CLoHSKk8wofqLtWIu7gmPe9UR0Iu/j2iPeh5ZeJM/SbyCt4kfyxUmkngZ+0TdyEdEkKeYVUO72cEfr6CL1R5DLFmRiMDGmLVkypGExGKsIsStRMoGimJLKGFhMhkMkymCsphZYpimlGtNU0hpGBZBYmRlIyUwjCMKspgrKYimiwjRYqrBQqmyIoooooooooooooomKKKKKEUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUWq2YBJunHZv723R+csODm274bnXesQAhJUyt0TxoqoEpFXyCj8vueaUir1iMidXPt9ekR8ap9Owp4FR1dFR9fzusleP2fokRHiFDYQ8Q6zqKOjpEzoQj8P0pRgVHUod709f7eeBF9wj5jv5kI8gvyVGo69upCPy0lf0+494oxQ/b8BFH5+r7veKP2BQ30/oxCMFdYq8gV3eM6SP430VP55K6cQUOCYHu+ACh9P5tSH0LycKia+UFDxFHUK7E6EI+uIR1KidqDqXArykSU7oJVvv9PTv/r0ifL5Xz81F8/LiUeBF94o8Pddfj45z3mIR6kqqHPLo/331AUPHl169MdXs7NiI7vApPuFGHmHnQwbqCPPQ6yEYoceZd+sIzwKjs+oVx3EXbqqNIR269VML7AKHTr2Cqh3lQs5JXrUdQr9hdOvj93wPuOklfiJ8hR9SongFfB9SLJRlCPN29riiOnV269ArgnxTyKrDhCOCofjJPE31E3XCP diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/008.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/008.b64 new file mode 100644 index 0000000..4107be3 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/008.b64 @@ -0,0 +1 @@ +EKH3yJKdbsKMehQ/mr/wb0lQvW9CLkR4AKHq9UlelJ8hN9N7ftEe6QjsqHQ79d5/MFDqQjxIR0KicKHt3IR7/Aj74o7dgUNUWEsAUP5F37IBeqplQ8ruI8kqHGEzCZI7AoeYltmiN2OtySUdCEddOAqk2EdXbt+iStd5HTiTqRe7qFen2KUfDzSV4d/Gd6qKPN19HYR1godq6iOgUOOkBQ6cnBGjdRHfg60MFZhYWKpU/GFZSimJMqAtI9COnx+UK7dOnIn68I93V72eHuk9wKH2qQmfDSVUPT4UHQi+XakhOOoo7ijv9+EOof8Xp0vRFcFGUousR0I8nUKHWk69dnoqOPj5/bEpxgocUxVTv8QqofbKpHQqyQwmUu3bp04BQ7+5DwIughQ6Ozq6KpGoNUMi41VUcHQqODqRfAR1krsRdQzPN5uSj5kXhUI9RVWgYV68DiMZ4iPSSvYSlD1KHiR91U9CLt9QAAAAOcAAA+pVrzzwAAAD6Pv/X+N7/5PbXVqR0cYpTgV2cSj1RQ98p09NsJvHtI9Ar9NVFHxSQp4/EI9gChxJwR4CUofplPpwR8ffFX+wd34eECukVJlUqv+PxJ7y8Lq++JF8uxCOXVUeOdRHQi9D3PH0Qr4BVQ3njwiOvR1YR8fYK7FKI8Pn14R4Ujzwi3eSq8hilGL2mENhDlhDpJWKrjCGKqqMFbmegKGOiwjo45HX/F+/H00RThVqMJoYM1Y1MSE1DWU0ZYrEWarSzaxUmSmoxMMWs1lqsJpYsy1mpizMZYoVyStVcwWMMDKOGVQ5 diff --git a/experiments/recovery_closure_v3/evidence/archive-parts/009.b64 b/experiments/recovery_closure_v3/evidence/archive-parts/009.b64 new file mode 100644 index 0000000..9ddb90b --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/archive-parts/009.b64 @@ -0,0 +1 @@ +pEZlNo4/wyjfVuvw5fFrVvb+V+5byq1rbcAAAADu4AAAAA2226iOlUh/SjVdIUOvTpt1kf1ncxlTE2xtg1ZvD/+LuSKcKEhmQodNgA== diff --git a/experiments/recovery_closure_v3/evidence/observed-summary.json b/experiments/recovery_closure_v3/evidence/observed-summary.json new file mode 100644 index 0000000..a3f4bcc --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/observed-summary.json @@ -0,0 +1,35 @@ +{ + "schema": "recovery-closure-observation/1", + "observed_on": "2026-09-05", + "source_commit": "8a2f2a37023a50aeac52cb8c8aed84b2eeceec88", + "http_tests": {"errors": 0, "failures": 0, "run": 40, "skipped": 0}, + "model_trace_count": 560, + "model_summary": [ + {"policy":"hold","initial_request_dropped":false,"schedules":56,"duplicate_traces":0,"incomplete_traces":0}, + {"policy":"hold","initial_request_dropped":true,"schedules":56,"duplicate_traces":0,"incomplete_traces":56}, + {"policy":"snapshot_negative","initial_request_dropped":false,"schedules":56,"duplicate_traces":50,"incomplete_traces":0}, + {"policy":"snapshot_negative","initial_request_dropped":true,"schedules":56,"duplicate_traces":0,"incomplete_traces":0}, + {"policy":"admission_fence","initial_request_dropped":false,"schedules":56,"duplicate_traces":15,"incomplete_traces":0}, + {"policy":"admission_fence","initial_request_dropped":true,"schedules":56,"duplicate_traces":0,"incomplete_traces":0}, + {"policy":"atomic_fence","initial_request_dropped":false,"schedules":56,"duplicate_traces":0,"incomplete_traces":0}, + {"policy":"atomic_fence","initial_request_dropped":true,"schedules":56,"duplicate_traces":0,"incomplete_traces":0}, + {"policy":"operation_idempotency","initial_request_dropped":false,"schedules":56,"duplicate_traces":0,"incomplete_traces":0}, + {"policy":"operation_idempotency","initial_request_dropped":true,"schedules":56,"duplicate_traces":0,"incomplete_traces":0} + ], + "native_fsm_pairs":16, + "native_fsm_all_equal":true, + "source_sha256": { + "finite_model.py":"1f9fd3ec837d710e99a1bc78605fe9d8cde7e4414f5ed2c5520b816cab190316", + "observer.py":"ae1a51df68ae1212528daf0f179294404c752a61e4e89df4ca0dfdc0ccd441fe", + "receiver.py":"326d762c8588da616b8139d237ab769b4d5e7a48d927e7c5b9a8acada8e88380", + "run.py":"64ce50e2fcefc522fb335b17a1a8f6174af4bfa83e27e28c8772a8d8e8961d6c", + "source_pins.py":"a71be4bd193a4b5d6a7079418f6d50054f24095f2a8cc4109eb78c17138c4d98", + "test_http.py":"9a6ff9b58d2e2e0e9d245eb4537998081205826dd06f0d44143093ff0ec01ef8" + }, + "prior_regressions": { + "v1_tests":55,"v2_tests":28, + "v1_digest":"7e0d4e80d71bbfb27dfda5939809992d39a993e70097779db8a911568f6ce38f", + "v2_digest":"1eb8e50a35df8cb9312616c0c21f6182c10592db10e0ba77e96943b9d22128d5" + }, + "non_claims":["Not a defect in conservative upstream v2","Not a new fencing or idempotency algorithm","Not a production risk probability","Not superiority over equal-guarantee FSM","Not external payment or hardware validation","Not independent review"] +} diff --git a/experiments/recovery_closure_v3/finite_model.py b/experiments/recovery_closure_v3/finite_model.py new file mode 100644 index 0000000..1f3e774 --- /dev/null +++ b/experiments/recovery_closure_v3/finite_model.py @@ -0,0 +1,91 @@ +"""Exhaustive finite interleavings, not a model of arbitrary distributed executions.""" +from __future__ import annotations +import hashlib +import itertools +import json + +POLICIES = ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence', 'operation_idempotency') +CHAINS = (('check0', 'effect0', 'replay0'), ('close', 'receipt', 'retry', 'check1', 'effect1')) + + +def schedules(): + # Recursive linear extensions avoid assuming a probability distribution. + def extend(a, b, prefix): + if a == len(CHAINS[0]) and b == len(CHAINS[1]): + yield tuple(prefix) + if a < len(CHAINS[0]): + yield from extend(a + 1, b, prefix + [CHAINS[0][a]]) + if b < len(CHAINS[1]): + yield from extend(a, b + 1, prefix + [CHAINS[1][b]]) + return list(extend(0, 0, [])) + + +def simulate(policy, order, dropped=False): + effects, closed, admitted = set(), set(), set() + response, known, retry = 'UNKNOWN', 'UNKNOWN', False + history = [] + + def effect(attempt): + if policy == 'atomic_fence' and attempt in closed: + return + if policy == 'operation_idempotency' and effects: + return + effects.add(attempt) # Common per-attempt idempotency, NOT per-operation. + + for event in order: + if event == 'check0' and not dropped and 0 not in closed: + admitted.add(0) + elif event == 'effect0' and 0 in admitted: + effect(0) + elif event == 'replay0' and not dropped and 0 not in closed: + effect(0) # A redelivered request performs a new admission check. + elif event == 'close': + if 0 in effects: + response = 'COMMITTED' + elif policy in ('hold', 'operation_idempotency'): + response = 'UNKNOWN' + else: + response = 'NOT_COMMITTED' + if policy in ('admission_fence', 'atomic_fence'): + closed.add(0) + elif event == 'receipt': + known = response + elif event == 'retry': + retry = known == 'NOT_COMMITTED' or policy == 'operation_idempotency' + elif event == 'check1' and retry: + admitted.add(1) + elif event == 'effect1' and 1 in admitted: + effect(1) + history.append({'event': event, 'effect_attempts': sorted(effects), + 'closed_attempts': sorted(closed), 'known': known, 'retry': retry}) + return {'policy': policy, 'initial_request_dropped': dropped, + 'order': list(order), 'effect_count': len(effects), + 'duplicate': len(effects) > 1, 'incomplete': not effects, 'history': history} + + +def run(): + orders = schedules() + # Independent combinatorial check: all permutations filtered by both chains. + brute = {p for p in itertools.permutations(sum(CHAINS, ())) + if all(all(p.index(a) < p.index(b) for a, b in zip(c, c[1:])) for c in CHAINS)} + assert set(orders) == brute and len(orders) == 56 + traces, summary = [], [] + for policy in POLICIES: + for dropped in (False, True): + batch = [simulate(policy, o, dropped) for o in orders] + traces += batch + summary.append({'policy': policy, 'initial_request_dropped': dropped, + 'schedules': len(batch), 'duplicate_traces': sum(r['duplicate'] for r in batch), + 'incomplete_traces': sum(r['incomplete'] for r in batch)}) + for policy in ('hold', 'atomic_fence', 'operation_idempotency'): + assert not any(r['duplicate'] for r in traces if r['policy'] == policy) + for policy in ('snapshot_negative', 'admission_fence'): + assert any(r['duplicate'] for r in traces if r['policy'] == policy) + assert all(r['effect_count'] == 1 for r in traces if r['policy'] in ('atomic_fence', 'operation_idempotency')) + assert all(r['incomplete'] for r in traces if r['policy'] == 'hold' and r['initial_request_dropped']) + encoded = json.dumps(traces, sort_keys=True, separators=(',', ':')).encode() + return {'summary': summary, 'trace_count': len(traces), + 'trace_sha256': hashlib.sha256(encoded).hexdigest(), 'traces': traces} + +if __name__ == '__main__': + print(json.dumps(run(), indent=2)) diff --git a/experiments/recovery_closure_v3/observer.py b/experiments/recovery_closure_v3/observer.py new file mode 100644 index 0000000..d28c60f --- /dev/null +++ b/experiments/recovery_closure_v3/observer.py @@ -0,0 +1,20 @@ +"""Read-only effect observer; does not import controller or receipt code.""" +import json +from pathlib import Path +import sqlite3 +import sys + + +def observe(path): + db = sqlite3.connect(Path(path).resolve().as_uri() + '?mode=ro', uri=True) + try: + rows = db.execute('SELECT operation,attempt FROM effects ORDER BY id').fetchall() + closed = db.execute('SELECT operation,attempt FROM closed ORDER BY operation,attempt').fetchall() + return {'integrity': db.execute('PRAGMA integrity_check').fetchone()[0], + 'effect_count': len(rows), 'effect_rows': rows, 'closed_rows': closed, + 'request_count': db.execute('SELECT COUNT(*) FROM calls').fetchone()[0]} + finally: + db.close() + +if __name__ == '__main__': + print(json.dumps(observe(sys.argv[1]), sort_keys=True)) diff --git a/experiments/recovery_closure_v3/receiver.py b/experiments/recovery_closure_v3/receiver.py new file mode 100644 index 0000000..70adc5d --- /dev/null +++ b/experiments/recovery_closure_v3/receiver.py @@ -0,0 +1,185 @@ +"""Experimental receiver policies. SQLite INSERT is the effect, not a remote API. + +snapshot_negative and admission_fence deliberately violate the stronger closure +contract. They are mutation controls, not behaviors attributed to upstream v2. +No production crypto, device authentication, or access-control boundary here. +""" +from __future__ import annotations +import argparse +from contextlib import contextmanager +from dataclasses import asdict +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +from pathlib import Path +import socket +import sqlite3 +import threading +from source_pins import prepare + +HTTP = prepare() +from proof import A6, encoded, fixture_receipt # noqa: E402 + +POLICIES = ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence', 'operation_idempotency') +GATES = ('normal', 'before_check', 'after_check', 'drop_before_effect') + + +class Server(ThreadingHTTPServer): + daemon_threads = True + def __init__(self, root, policy, gate): + if policy not in POLICIES or gate not in GATES: + raise ValueError('invalid experiment policy/gate') + root.mkdir(parents=True, exist_ok=True) + self.path, self.policy, self.gate = root / 'receiver.sqlite', policy, gate + self.arrived, self.release, self.finished = (threading.Event() for _ in range(3)) + with self.tx() as db: + db.execute('CREATE TABLE IF NOT EXISTS calls(id INTEGER PRIMARY KEY, token TEXT NOT NULL)') + db.execute('CREATE TABLE IF NOT EXISTS effects(id INTEGER PRIMARY KEY, operation TEXT NOT NULL, attempt INTEGER NOT NULL, UNIQUE(operation,attempt))') + db.execute('CREATE TABLE IF NOT EXISTS closed(operation TEXT NOT NULL, attempt INTEGER NOT NULL, PRIMARY KEY(operation,attempt))') + db.execute('CREATE TABLE IF NOT EXISTS receipts(token TEXT PRIMARY KEY, value TEXT NOT NULL)') + db.execute('CREATE TABLE IF NOT EXISTS meta(id INTEGER PRIMARY KEY CHECK(id=1), seq INTEGER NOT NULL)') + db.execute('INSERT OR IGNORE INTO meta VALUES(1,0)') + super().__init__(('127.0.0.1', 0), Handler) + + @contextmanager + def tx(self): + db = sqlite3.connect(self.path, timeout=10, isolation_level=None) + db.execute('PRAGMA synchronous=FULL') + try: + db.execute('BEGIN IMMEDIATE') + yield db + db.commit() + except BaseException: + db.rollback() + raise + finally: + db.close() + + @staticmethod + def key(t): + return encoded(list(t.lineage())), t.attempt_id + + @staticmethod + def blocked(db, key): + return db.execute('SELECT 1 FROM closed WHERE operation=? AND attempt=?', key).fetchone() is not None + + def resolve(self, t): + key = self.key(t) + identity = encoded(asdict(t)) + with self.tx() as db: + existing = db.execute('SELECT value FROM receipts WHERE token=?', (identity,)).fetchone() + if existing: + receipt = json.loads(existing[0]) + result = {'status': receipt['outcome'], 'receipt': receipt} + else: + exists = db.execute('SELECT 1 FROM effects WHERE operation=? AND attempt=?', key).fetchone() + if exists: + status = 'COMMITTED' + elif self.policy in ('hold', 'operation_idempotency'): + status = 'UNKNOWN' + else: + status = 'NOT_COMMITTED' + if self.policy in ('admission_fence', 'atomic_fence'): + db.execute('INSERT OR IGNORE INTO closed VALUES(?,?)', key) + result = {'status': status} + if status != 'UNKNOWN': + seq = db.execute('SELECT seq FROM meta WHERE id=1').fetchone()[0] + receipt = fixture_receipt(t, seq=seq, outcome=status) + db.execute('UPDATE meta SET seq=seq+1 WHERE id=1') + db.execute('INSERT INTO receipts VALUES(?,?)', (identity, encoded(receipt))) + result['receipt'] = receipt + # The receipt leaves this function ONLY AFTER the database commit. + return result + + def apply(self, t): + key = self.key(t) + is_initial = t.attempt_id == 0 + with self.tx() as db: + db.execute('INSERT INTO calls(token) VALUES(?)', (encoded(asdict(t)),)) + if is_initial and self.gate == 'drop_before_effect': + self.arrived.set() + self.finished.set() + return {'status': 'DROPPED'} + if is_initial and self.gate == 'before_check': + self.arrived.set() + if not self.release.wait(15): + self.finished.set() + return {'status': 'HARNESS_GATE_TIMEOUT'} + with self.tx() as db: + allowed = not self.blocked(db, key) + if is_initial and self.gate == 'after_check': + self.arrived.set() + if not self.release.wait(15): + self.finished.set() + return {'status': 'HARNESS_GATE_TIMEOUT'} + try: + with self.tx() as db: + # Deliberate mutation: admission_fence uses only the earlier check. + if not allowed or (self.policy == 'atomic_fence' and self.blocked(db, key)): + result = {'status': 'CLOSED_REJECTED'} + elif (self.policy == 'operation_idempotency' and + db.execute('SELECT 1 FROM effects WHERE operation=?', (key[0],)).fetchone()): + result = {'status': 'OPERATION_REPLAY'} + else: + before = db.total_changes + db.execute('INSERT OR IGNORE INTO effects(operation,attempt) VALUES(?,?)', key) + result = {'status': 'EFFECT' if db.total_changes > before else 'ATTEMPT_REPLAY'} + return result + finally: + if is_initial: + self.finished.set() + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def answer(self, result, status=200): + payload = encoded(result).encode() + try: + self.send_response(status) + self.send_header('Content-Length', str(len(payload))) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(payload) + except (BrokenPipeError, ConnectionResetError): + pass + + def do_POST(self): + try: + size = int(self.headers.get('Content-Length', 0)) + if not 0 < size <= 65536: + raise ValueError('body limit') + body = json.loads(self.rfile.read(size)) + if self.path == '/gate': + return self.answer({'arrived': self.server.arrived.is_set(), 'finished': self.server.finished.is_set()}) + if self.path == '/release': + self.server.release.set() + return self.answer({'released': True}) + t = A6.AuthorityToken(**body['token']) + if self.path in ('/resolve', '/receipt'): + return self.answer(self.server.resolve(t)) + if self.path == '/effect': + result = self.server.apply(t) + if result['status'] == 'DROPPED': + self.close_connection = True + try: + self.connection.shutdown(socket.SHUT_RDWR) + except OSError: + pass + self.connection.close() + return + return self.answer(result) + return self.answer({'error': 'route'}, 404) + except (KeyError, TypeError, ValueError): + return self.answer({'error': 'invalid input'}, 400) + + +if __name__ == '__main__': + p = argparse.ArgumentParser() + p.add_argument('--root', type=Path, required=True) + p.add_argument('--policy', choices=POLICIES, required=True) + p.add_argument('--gate', choices=GATES, required=True) + a = p.parse_args() + s = Server(a.root, a.policy, a.gate) + print(encoded({'url': f'http://127.0.0.1:{s.server_port}'}), flush=True) + s.serve_forever(poll_interval=0.01) diff --git a/experiments/recovery_closure_v3/requirements.txt b/experiments/recovery_closure_v3/requirements.txt new file mode 100644 index 0000000..c011dd5 --- /dev/null +++ b/experiments/recovery_closure_v3/requirements.txt @@ -0,0 +1 @@ +cryptography==46.0.4 diff --git a/experiments/recovery_closure_v3/restore_evidence.py b/experiments/recovery_closure_v3/restore_evidence.py new file mode 100644 index 0000000..56fe0c0 --- /dev/null +++ b/experiments/recovery_closure_v3/restore_evidence.py @@ -0,0 +1,41 @@ +"""Restore recorded observations byte-for-byte; does NOT rerun experiments.""" +import argparse +import base64 +import bz2 +import hashlib +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--output', type=Path, default=ROOT / 'evidence-restored') + args = parser.parse_args() + manifest = json.loads((ROOT / 'EVIDENCE_MANIFEST.json').read_text()) + text = ''.join((ROOT / path).read_text().strip() for path in manifest['parts']) + compressed = base64.b64decode(text, validate=True) + if hashlib.sha256(compressed).hexdigest() != manifest['compressed_sha256']: + raise ValueError('compressed evidence hash mismatch') + data = json.loads(bz2.decompress(compressed)) + if set(data) != set(manifest['files']): + raise ValueError('unexpected evidence members') + restored = {} + for name, value in data.items(): + if Path(name).name != name: + raise ValueError('invalid evidence filename') + raw = (json.dumps(value, indent=2, sort_keys=True) + '\n').encode() if name.endswith('.json') else value.encode() + expected = manifest['files'][name] + if len(raw) != expected['bytes'] or hashlib.sha256(raw).hexdigest() != expected['sha256']: + raise ValueError('evidence mismatch: ' + name) + restored[name] = raw + args.output.mkdir(parents=True, exist_ok=True) + for name, raw in restored.items(): + target = args.output / name + if target.exists() and target.read_bytes() != raw: + raise FileExistsError('refusing to overwrite different evidence: ' + name) + target.write_bytes(raw) + print('RESTORED 4 original evidence files; all sizes and SHA-256 hashes match.') + +if __name__ == '__main__': + main() diff --git a/experiments/recovery_closure_v3/run.py b/experiments/recovery_closure_v3/run.py new file mode 100644 index 0000000..3f0b75f --- /dev/null +++ b/experiments/recovery_closure_v3/run.py @@ -0,0 +1,56 @@ +"""Run bounded model exploration and live HTTP cases; retain raw evidence.""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import platform +import sys +import time +import unittest +import finite_model +import test_http + +ROOT = Path(__file__).resolve().parent + + +def dump(path, value): + path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n') + + +def run(output): + output.mkdir(parents=True, exist_ok=True) + start = time.monotonic() + model = finite_model.run() + dump(output / 'model.json', model) + test_http.TRACES.clear() + with (output / 'http-tests.txt').open('w') as log: + result = unittest.TextTestRunner(stream=log, verbosity=2).run(unittest.defaultTestLoader.loadTestsFromModule(test_http)) + dump(output / 'http-traces.json', test_http.TRACES) + matrix = [r for r in test_http.TRACES if 'policy' in r] + paired = [] + for policy in test_http.POLICIES: + for gate in test_http.GATES: + pair = [r for r in matrix if r['policy'] == policy and r['gate'] == gate] + if len(pair) != 2: + paired.append({'policy': policy, 'gate': gate, 'equal': False, 'reason': 'missing pair'}) + continue + a, b = [{k: v for k, v in r.items() if k != 'engine'} for r in pair] + paired.append({'policy': policy, 'gate': gate, 'equal': a == b}) + summary = {'schema': 'recovery-closure-experiment/1', 'source_commit': '8a2f2a37023a50aeac52cb8c8aed84b2eeceec88', + 'environment': {'python': sys.version, 'platform': platform.platform()}, + 'model_trace_count': model['trace_count'], 'model_summary': model['summary'], + 'http_tests': {'run': result.testsRun, 'failures': len(result.failures), 'errors': len(result.errors), 'skipped': len(result.skipped)}, + 'http_matrix': [{k: v for k, v in r.items() if k != 'steps'} for r in matrix], + 'boundary_trace_count': len(test_http.TRACES) - len(matrix), 'native_fsm_comparisons': paired, + 'elapsed_seconds_including_subprocess_startup_not_a_performance_benchmark': round(time.monotonic()-start, 3), + 'source_sha256': {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(ROOT.glob('*.py'))}} + summary['success'] = result.wasSuccessful() and all(r['equal'] for r in paired) + dump(output / 'summary.json', summary) + print(json.dumps({k: summary[k] for k in ('success', 'http_tests', 'model_trace_count', 'boundary_trace_count')}, indent=2)) + return 0 if summary['success'] else 1 + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--output', type=Path, default=ROOT / 'evidence/new') + sys.exit(run(parser.parse_args().output)) diff --git a/experiments/recovery_closure_v3/source_pins.py b/experiments/recovery_closure_v3/source_pins.py new file mode 100644 index 0000000..d97bccf --- /dev/null +++ b/experiments/recovery_closure_v3/source_pins.py @@ -0,0 +1,44 @@ +"""Load unmodified, hash-checked CaPU/ATMAN source. No code is fetched implicitly.""" +from __future__ import annotations +import hashlib +import importlib +import json +from pathlib import Path +import sys +import urllib.request + +ROOT = Path(__file__).resolve().parent +REF = '8a2f2a37023a50aeac52cb8c8aed84b2eeceec88' +PINS = {'experiments/capu_atman_recovery_v1/bootstrap.py': 'f78bb3e3908a22188143df27f6079a0de6727205', 'experiments/capu_atman_recovery_v1/proof.py': '58101572a487a454bb4d60e296ab332c81c4b61b', 'experiments/capu_atman_http_recovery_v2/http_boundary.py': '14706efa877a804d9ce90e51b058c201f056b29e', 'LICENSE': '4235d7bdc5e4c3afaf1292abec24d10d88d28360'} + + +def git_blob(data): + return hashlib.sha1(b'blob ' + str(len(data)).encode() + b'\0' + data).hexdigest() + + +def prepare(fetch=False): + for path, expected in PINS.items(): + dst = ROOT / '.pinned' / path + if not dst.exists(): + if not fetch: + raise FileNotFoundError('Run python source_pins.py first: ' + path) + url = f'https://raw.githubusercontent.com/safal207/CaPU/{REF}/{path}' + with urllib.request.urlopen(url, timeout=30) as r: + data = r.read(200000) + if git_blob(data) != expected: + raise RuntimeError('Git blob mismatch: ' + path) + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_bytes(data) + if git_blob(dst.read_bytes()) != expected: + raise RuntimeError('Git blob mismatch: ' + path) + v1 = ROOT / '.pinned/experiments/capu_atman_recovery_v1' + v2 = ROOT / '.pinned/experiments/capu_atman_http_recovery_v2' + sys.path.insert(0, str(v1)) + old_bootstrap = importlib.import_module('bootstrap') + old_bootstrap.sources(fetch=fetch) + sys.path.insert(0, str(v2)) + return importlib.import_module('http_boundary') + +if __name__ == '__main__': + prepare(fetch=True) + print('Verified source pins:', json.dumps(PINS, sort_keys=True)) diff --git a/experiments/recovery_closure_v3/test_http.py b/experiments/recovery_closure_v3/test_http.py new file mode 100644 index 0000000..6576f3d --- /dev/null +++ b/experiments/recovery_closure_v3/test_http.py @@ -0,0 +1,232 @@ +"""Deterministic interleavings over real sockets and an external effect observer.""" +from __future__ import annotations +from dataclasses import asdict +import json +from pathlib import Path +import selectors +import subprocess +import sys +import tempfile +import time +import unittest +from source_pins import prepare + +HTTP = prepare() +from proof import fixture_bundle, token # noqa: E402 +ROOT = Path(__file__).resolve().parent +TRACES = [] +POLICIES = ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence') +GATES = ('before_check', 'after_check', 'drop_before_effect', 'normal') + + +class Harness: + def __init__(self, root, policy, gate, engine='native'): + self.root, self.policy, self.gate, self.engine = root, policy, gate, engine + self.server = None + self.start() + self.controller = None + if engine: + self.controller = HTTP.HTTPController(root / 'controller', engine, self.url) + self.controller.initialize() + + def start(self): + self.server = subprocess.Popen( + [sys.executable, str(ROOT / 'receiver.py'), '--root', str(self.root / 'device'), + '--policy', self.policy, '--gate', self.gate], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + selector = selectors.DefaultSelector() + selector.register(self.server.stdout, selectors.EVENT_READ) + try: + if not selector.select(10): + self.close() + raise TimeoutError('receiver startup') + line = self.server.stdout.readline() + if not line: + _, err = self.server.communicate(timeout=3) + raise RuntimeError('receiver failed: ' + err) + self.url = json.loads(line)['url'] + finally: + selector.close() + + def restart(self): + self.close() + self.gate = 'normal' + self.start() + if self.engine: + self.controller = HTTP.HTTPController(self.root / 'controller', self.engine, self.url) + + def close(self): + if self.server: + if self.server.poll() is None: + self.server.kill() + self.server.communicate(timeout=5) + + def request(self, path, value=None, timeout=2): + result = HTTP.exchange(self.url, path, value or {'probe': True}, timeout) + if result['transport'] != 'RESPONSE_RECEIVED': + raise AssertionError(result) + return result['body'] + + def wait(self, key): + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + if self.request('/gate').get(key): + return + time.sleep(0.005) + raise TimeoutError(key) + + def dispatch(self, attempt, **kwargs): + if self.controller: + return self.controller.dispatch(fixture_bundle(token(attempt), **kwargs), timeout=.12) + return {'forwarded': True, 'http': HTTP.exchange(self.url, '/effect', {'token': asdict(token(attempt))}, .12)} + + def resolve(self, attempt): + reply = self.request('/resolve', {'token': asdict(token(attempt))}) + # EXPLICIT NEW EXPERIMENTAL ADAPTER: v2's recover accepts only positives. + # No state is forced and no native lifecycle implementation is modified. + if self.controller and 'receipt' in reply: + reply['application'] = self.controller.reconcile(reply['receipt']) + return reply + + def witness(self): + result = subprocess.run([sys.executable, str(ROOT / 'observer.py'), + str(self.root / 'device/receiver.sqlite')], + capture_output=True, text=True, check=True, timeout=8) + return json.loads(result.stdout) + + def snapshot(self, event): + return {'event': event, 'witness': self.witness(), + 'controller': self.controller.observation() if self.controller else None} + + +def scenario(policy, gate, engine): + with tempfile.TemporaryDirectory() as temp: + h = Harness(Path(temp), policy, gate, engine) + steps = [] + try: + first = h.dispatch(0) + assert first['forwarded'], first + if gate != 'normal': + h.wait('arrived') + steps.append(h.snapshot('INITIAL_REQUEST')) + closure = h.resolve(0) + if engine and 'receipt' in closure: + assert closure['application']['applied'], closure + retry = h.dispatch(1) + steps.append(h.snapshot('AFTER_RECOVERY_BEFORE_RELEASE')) + h.request('/release') + if gate in ('before_check', 'after_check'): + h.wait('finished') + if engine: + if retry['forwarded']: + positive = h.resolve(1) + assert positive['status'] == 'COMMITTED' and positive['application']['applied'], positive + elif gate != 'drop_before_effect' and closure['status'] != 'COMMITTED': + positive = h.resolve(0) + assert positive['status'] == 'COMMITTED' and positive['application']['applied'], positive + steps.append(h.snapshot('FINAL')) + if policy == 'operation_idempotency': + expected = 1 + elif gate == 'drop_before_effect': + expected = 0 if policy == 'hold' else 1 + elif gate == 'normal': + expected = 1 + elif policy == 'snapshot_negative' or (policy == 'admission_fence' and gate == 'after_check'): + expected = 2 + else: + expected = 1 + actual = steps[-1]['witness']['effect_count'] + assert actual == expected, (policy, gate, actual, expected) + assert all(s['witness']['integrity'] == 'ok' for s in steps) + assert not (Path(temp) / 'controller/device.sqlite').exists() + return {'policy': policy, 'gate': gate, 'engine': engine or 'ordinary_operation_idempotency', + 'closure_status': closure['status'], 'retry_forwarded': retry['forwarded'], + 'effects_at_recovery': steps[1]['witness']['effect_count'], + 'final_effects': actual, 'expected_effects': expected, 'steps': steps} + finally: + h.close() + + +class MatrixTests(unittest.TestCase): + pass + +for _policy in POLICIES: + for _gate in GATES: + for _engine in ('native', 'baseline'): + def test(self, policy=_policy, gate=_gate, engine=_engine): + TRACES.append(scenario(policy, gate, engine)) + setattr(MatrixTests, f'test_{_policy}_{_gate}_{_engine}', test) +for _gate in GATES: + def test(self, gate=_gate): + TRACES.append(scenario('operation_idempotency', gate, None)) + setattr(MatrixTests, f'test_operation_idempotency_{_gate}', test) + + +class BoundaryTests(unittest.TestCase): + def test_atomic_tombstone_and_receipt_survive_receiver_restart(self): + for engine in ('native', 'baseline'): + with self.subTest(engine=engine), tempfile.TemporaryDirectory() as temp: + h = Harness(Path(temp), 'atomic_fence', 'drop_before_effect', engine) + try: + h.dispatch(0) + reply = h.resolve(0) + self.assertTrue(reply['application']['applied']) + before = h.witness() + h.restart() + replay = h.request('/effect', {'token': asdict(token(0))}) + self.assertEqual(replay['status'], 'CLOSED_REJECTED') + self.assertTrue(h.dispatch(1)['forwarded']) + self.assertTrue(h.resolve(1)['application']['applied']) + after = h.witness() + self.assertEqual(after['effect_count'], 1) + self.assertEqual(before['closed_rows'], after['closed_rows']) + TRACES.append({'boundary': 'receiver_restart', 'engine': engine, 'before': before, 'after': after}) + finally: + h.close() + + def test_tampered_negative_receipt_does_not_open_retry(self): + for engine in ('native', 'baseline'): + with self.subTest(engine=engine), tempfile.TemporaryDirectory() as temp: + h = Harness(Path(temp), 'atomic_fence', 'drop_before_effect', engine) + try: + h.dispatch(0) + reply = h.request('/resolve', {'token': asdict(token(0))}) + receipt = dict(reply['receipt']) + receipt['auth_tag'] ^= 1 + self.assertFalse(h.controller.reconcile(receipt)['authenticated']) + self.assertFalse(h.dispatch(1)['forwarded']) + self.assertTrue(h.controller.reconcile(reply['receipt'])['applied']) + self.assertTrue(h.dispatch(1)['forwarded']) + self.assertEqual(h.witness()['effect_count'], 1) + TRACES.append({'boundary': 'tampered_negative', 'engine': engine, 'after': h.snapshot('VALID_CLOSURE_REQUIRED')}) + finally: + h.close() + + def test_closure_is_not_new_execution_authority(self): + for engine in ('native', 'baseline'): + with self.subTest(engine=engine), tempfile.TemporaryDirectory() as temp: + h = Harness(Path(temp), 'atomic_fence', 'drop_before_effect', engine) + try: + h.dispatch(0) + self.assertTrue(h.resolve(0)['application']['applied']) + h.controller.context(generation=2, state_version='state/2') + self.assertFalse(h.dispatch(1)['forwarded']) + self.assertTrue(h.dispatch(1, generation=2, state_version='state/2')['forwarded']) + self.assertEqual(h.witness()['effect_count'], 1) + TRACES.append({'boundary': 'authority_generation', 'engine': engine, 'after': h.snapshot('FRESH_AUTHORITY_REQUIRED')}) + finally: + h.close() + + def test_foreign_attempt_closure_does_not_close_own_attempt(self): + with tempfile.TemporaryDirectory() as temp: + h = Harness(Path(temp), 'atomic_fence', 'normal', None) + try: + foreign = token(0, command_id=10) + reply = h.request('/resolve', {'token': asdict(foreign)}) + self.assertEqual(reply['status'], 'NOT_COMMITTED') + own = h.request('/effect', {'token': asdict(token())}) + self.assertEqual(own['status'], 'EFFECT') + self.assertEqual(h.witness()['effect_count'], 1) + TRACES.append({'boundary': 'foreign_closure', 'after': h.witness()}) + finally: + h.close() diff --git a/experiments/recovery_closure_v3/validate_results.py b/experiments/recovery_closure_v3/validate_results.py new file mode 100644 index 0000000..61e9004 --- /dev/null +++ b/experiments/recovery_closure_v3/validate_results.py @@ -0,0 +1,39 @@ +"""Independent aggregation of saved evidence; does not rerun the HTTP harness.""" +import collections +import hashlib +import json +from pathlib import Path +import sys + +root = Path(sys.argv[1]) +s = json.loads((root / 'summary.json').read_text()) +m = json.loads((root / 'model.json').read_text()) +h = json.loads((root / 'http-traces.json').read_text()) +assert s['success'] and s['http_tests'] == {'run': 40, 'failures': 0, 'errors': 0, 'skipped': 0} +assert len(m['traces']) == m['trace_count'] == s['model_trace_count'] == 560 +assert len({(x['policy'], x['initial_request_dropped'], tuple(x['order'])) for x in m['traces']}) == 560 +aggregate = collections.defaultdict(lambda: [0, 0, 0]) +for t in m['traces']: + a = aggregate[(t['policy'], t['initial_request_dropped'])] + a[0] += 1 + a[1] += t['effect_count'] > 1 + a[2] += t['effect_count'] == 0 +for row in m['summary']: + assert aggregate[(row['policy'], row['initial_request_dropped'])] == [row['schedules'], row['duplicate_traces'], row['incomplete_traces']] +raw = json.dumps(m['traces'], sort_keys=True, separators=(',', ':')).encode() +assert hashlib.sha256(raw).hexdigest() == m['trace_sha256'] +matrix = [r for r in h if 'policy' in r] +assert len(matrix) == 36 and len(h) - len(matrix) == 7 +pairs = collections.defaultdict(list) +for r in matrix: + assert r['final_effects'] == r['steps'][-1]['witness']['effect_count'] + assert r['final_effects'] == len(r['steps'][-1]['witness']['effect_rows']) + if r['engine'] in ('native', 'baseline'): + pairs[(r['policy'], r['gate'])].append({k: v for k, v in r.items() if k != 'engine'}) +assert len(pairs) == 16 +assert all(len(p) == 2 and p[0] == p[1] for p in pairs.values()) +for path, expected in s['source_sha256'].items(): + # Source pins refer to the files as executed, not a later unrecorded version. + actual = hashlib.sha256((Path(__file__).resolve().parent / path).read_bytes()).hexdigest() + assert actual == expected, path +print('VALIDATED: 560 distinct bounded traces, 40 tests, 36 HTTP matrix rows, 7 boundary traces, 16 native/FSM pairs.') From c3960204c6a63c9e7f804a65abf0269b5daf726c Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:11:38 +0700 Subject: [PATCH 02/13] fix(recovery-lab): harden evidence validation and test cryptography 50.0.1 Require independent expected outcomes, complete source identities, raw/summary consistency, explicit optimized-Python checks, and POSIX no-follow restoration. Preserve original archived observations and explain the historical dependency commit-label error instead of rewriting measurement history. Add 32 defensive validator/restoration tests and retain earlier local patch logs losslessly. Pin cryptography 50.0.1, record its actual runtime version, disable checkout credential persistence, and add Python 3.11/3.13 CI with resolved dependency audit. Local 32 normal + 32 optimized checks and 40 HTTP methods/560 model traces passed using the existing 46.0.4 environment; new-version compatibility must be established by CI. Local model/HTTP JSON match original bytes. No receiver/lifecycle change, no main merge, no external endorsement. Owner requested assistant self-review and CodeRabbit without Codex; keep draft. --- .github/workflows/recovery-closure-v3.yml | 48 ++- .../recovery_closure_v3/PUBLICATION.md | 48 +++ experiments/recovery_closure_v3/README.md | 12 +- .../recovery_closure_v3/REVIEW_FIXES.md | 94 ++++++ experiments/recovery_closure_v3/VALIDATION.md | 12 +- .../review-fix-2026-09-05/MANIFEST.json | 40 +++ .../evidence/review-fix-2026-09-05/README.md | 33 ++ .../review-fix-2026-09-05/files.json.bz2.b64 | 1 + .../evidence_contract.json | 22 ++ .../recovery_closure_v3/finite_model.py | 15 +- experiments/recovery_closure_v3/provenance.py | 49 +++ .../recovery_closure_v3/requirements.txt | 2 +- .../recovery_closure_v3/restore_evidence.py | 55 +++- experiments/recovery_closure_v3/run.py | 18 +- experiments/recovery_closure_v3/test_http.py | 21 +- .../recovery_closure_v3/test_validation.py | 307 ++++++++++++++++++ .../recovery_closure_v3/validate_results.py | 226 +++++++++++-- 17 files changed, 937 insertions(+), 66 deletions(-) create mode 100644 experiments/recovery_closure_v3/PUBLICATION.md create mode 100644 experiments/recovery_closure_v3/REVIEW_FIXES.md create mode 100644 experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/MANIFEST.json create mode 100644 experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/README.md create mode 100644 experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/files.json.bz2.b64 create mode 100644 experiments/recovery_closure_v3/evidence_contract.json create mode 100644 experiments/recovery_closure_v3/provenance.py create mode 100644 experiments/recovery_closure_v3/test_validation.py diff --git a/.github/workflows/recovery-closure-v3.yml b/.github/workflows/recovery-closure-v3.yml index 344b6f2..5037c94 100644 --- a/.github/workflows/recovery-closure-v3.yml +++ b/.github/workflows/recovery-closure-v3.yml @@ -10,31 +10,65 @@ permissions: jobs: recovery: runs-on: ubuntu-latest - timeout-minutes: 8 + timeout-minutes: 12 strategy: fail-fast: false matrix: python: ['3.11', '3.13'] steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - name: Install pinned dependency - run: python -m pip install -r experiments/recovery_closure_v3/requirements.txt + - name: Install and record pinned dependency + shell: bash + run: | + mkdir -p ci-evidence + python -m pip install -r experiments/recovery_closure_v3/requirements.txt 2>&1 | tee ci-evidence/install.txt + python -m pip check | tee ci-evidence/pip-check.txt + python -m pip freeze > ci-evidence/packages.txt + python - <<'PY' + import json, platform + from importlib.metadata import version + from pathlib import Path + from cryptography.hazmat.backends.openssl.backend import backend + data = {'python': platform.python_version(), 'cryptography': version('cryptography'), 'openssl': backend.openssl_version_text()} + Path('ci-evidence/runtime.json').write_text(json.dumps(data, indent=2) + '\n') + if data['cryptography'] != '50.0.1': + raise SystemExit('Wrong cryptography version') + PY - name: Verify pinned source dependencies run: python experiments/recovery_closure_v3/source_pins.py - name: Restore and validate recorded evidence + shell: bash run: | python experiments/recovery_closure_v3/restore_evidence.py --output recorded-evidence - python experiments/recovery_closure_v3/validate_results.py recorded-evidence + python experiments/recovery_closure_v3/validate_results.py recorded-evidence --historical | tee ci-evidence/validation-history.json + - name: Validator and restoration regression tests + shell: bash + run: | + cd experiments/recovery_closure_v3 + python -m unittest -v test_validation 2>&1 | tee ../../ci-evidence/validator-tests.txt + python -O -m unittest -v test_validation 2>&1 | tee ../../ci-evidence/validator-tests-optimized.txt - name: Run new model and HTTP experiments + shell: bash + run: | + python experiments/recovery_closure_v3/run.py --output evidence-v3 | tee ci-evidence/run.txt + python experiments/recovery_closure_v3/validate_results.py evidence-v3 | tee ci-evidence/validation-fresh.json + python -O experiments/recovery_closure_v3/validate_results.py evidence-v3 | tee ci-evidence/validation-fresh-optimized.json + - name: Audit resolved runtime requirements + shell: bash run: | - python experiments/recovery_closure_v3/run.py --output evidence-v3 - python experiments/recovery_closure_v3/validate_results.py evidence-v3 + python -m venv "$RUNNER_TEMP/capu-audit" + "$RUNNER_TEMP/capu-audit/bin/python" -m pip install 'pip-audit==2.10.1' + "$RUNNER_TEMP/capu-audit/bin/python" -m pip_audit -r experiments/recovery_closure_v3/requirements.txt --strict --progress-spinner off --format json --output ci-evidence/dependency-audit.json - uses: actions/upload-artifact@v4 if: always() with: name: recovery-closure-v3-python-${{ matrix.python }} - path: evidence-v3/ + path: | + evidence-v3/ + ci-evidence/ if-no-files-found: error diff --git a/experiments/recovery_closure_v3/PUBLICATION.md b/experiments/recovery_closure_v3/PUBLICATION.md new file mode 100644 index 0000000..cf0017c --- /dev/null +++ b/experiments/recovery_closure_v3/PUBLICATION.md @@ -0,0 +1,48 @@ +# PR #106: validator fixes and dependency follow-up + +This follow-up applies the locally reviewed validator/restoration patch to the +existing `research/recovery-closure-v3` branch. The parent is +`d64f9809f90532d52c2992174e63c1a216a50211`; main is not changed. Keep the PR +draft and unmerged pending review. The owner requested assistant self-review +and optional CodeRabbit review, explicitly without Codex. + +## Scope and evidence + +The receiver, observer, lifecycle policies and pinned upstream source are +unchanged. This is evidence-verifier hardening, not a new recovery result or +new algorithm. `REVIEW_FIXES.md` and `evidence/review-fix-2026-09-05/` preserve +the earlier local patch results on Python 3.13.5 / cryptography 46.0.4. +They are not CI evidence for the dependency update. + +`requirements.txt` now pins cryptography 50.0.1. Upstream lists that release +on 2026-08-25 and security fixes since 46.0.4: +https://cryptography.io/en/latest/changelog/ +https://pypi.org/project/cryptography/50.0.1/ + +The local environment could not download the new wheel (network/DNS failure). +Consequently compatibility of 50.0.1 must be established by the actual new CI +runs, not by relabeling the earlier local results. The workflow installs and +checks 50.0.1, records the Python/OpenSSL/package environment, reruns the 32 +validator/restoration methods in normal and optimized mode, reruns all 40 HTTP +methods and 560 bounded model traces, then validates fresh and historical +artifacts. It also scans resolved requirements using pip-audit 2.10.1 in a +separate environment; scan failure is not suppressed. + +Inspect the real run conclusion and artifacts. A configured workflow is not +an executed check. A clean dependency scan only means no known findings in +that scan; it is not a security or production certification. + +## Historical provenance, deliberately not rewritten + +The original `source_commit` field labels the HTTP-v2 dependency, not the v3 +producer. Original archive chunks, original observed summary and checksums +remain untouched. `evidence_contract.json` documents the distinction. Historical +validation is opt-in (`--historical`) and byte-pinned. Fresh runs record actual +Git revision/dirty status or explicit unavailability, plus installed cryptography. + +## External handoff + +No external email or endorsement is part of this change. After CI and review, +a separate bounded reproduction invitation can point to a pinned commit. +SQLite insertion remains the effect; no atomicity for real payments, arbitrary +external services, hostile hosts, production cryptography or hardware is claimed. diff --git a/experiments/recovery_closure_v3/README.md b/experiments/recovery_closure_v3/README.md index 96df44c..f8822f2 100644 --- a/experiments/recovery_closure_v3/README.md +++ b/experiments/recovery_closure_v3/README.md @@ -1,3 +1,8 @@ +> **2026-09-05 review follow-up:** see [PUBLICATION.md](PUBLICATION.md). +> Historical evidence requires `--historical`; fresh evidence uses schema `/2`. +> The dependency is pinned to 50.0.1; inspect the actual CI run for compatibility +> and audit results. No production approval or external endorsement is implied. + # Recovery closure: where a safe retry becomes possible **Experimental reference — not production infrastructure.** This directory investigates a missing *integration contract*, not a newly discovered defect in CaPU HTTP v2. The original v2 correctly refuses to reinterpret a missing record as a negative outcome. @@ -46,7 +51,7 @@ SQLite row insertion **is** the effect. Closure and that effect are in the same ## Evidence and review -`evidence/observed-summary.json` is the observed summary, while `evidence/archive-parts/` retains all new trace files and the test log without losing bytes. Run `python restore_evidence.py` and `python validate_results.py evidence-restored` to verify the original observations. The downloadable ZIP additionally preserves the complete v1/v2 regression outputs. `EVIDENCE_MANIFEST.json` records uncompressed and compressed hashes. `PROTOCOL.md` records the hypotheses written before the new trials. `VALIDATION.md` records recomputation and limitations. Test PASS is not independent code review. Keep this work in a draft PR without merging until the agreed review gate is satisfied. +`evidence/observed-summary.json` is the observed summary, while `evidence/archive-parts/` retains all new trace files and the test log without losing bytes. Run `python restore_evidence.py` and `python validate_results.py evidence-restored --historical` to verify the original observations. The downloadable ZIP additionally preserves the complete v1/v2 regression outputs. `EVIDENCE_MANIFEST.json` records uncompressed and compressed hashes. `PROTOCOL.md` records the hypotheses written before the new trials. `VALIDATION.md` records recomputation and limitations. Test PASS is not independent code review. Keep this work in a draft PR without merging until the agreed review gate is satisfied. ## Why this is useful @@ -67,3 +72,8 @@ Martin Kleppmann, "How to do distributed locking" (2016): https://martin.kleppma gRPC, "Cancellation": https://grpc.io/docs/guides/cancellation/ These sources already establish relevant retry, receiver-enforcement and cancellation limitations. This experiment makes those boundaries executable in the pinned CaPU/ATMAN composition; it does not establish scientific priority. + +## Publication follow-up + +See [PUBLICATION.md](PUBLICATION.md) for the dependency update, CI contract, +historical provenance correction, and current review boundary. diff --git a/experiments/recovery_closure_v3/REVIEW_FIXES.md b/experiments/recovery_closure_v3/REVIEW_FIXES.md new file mode 100644 index 0000000..4847310 --- /dev/null +++ b/experiments/recovery_closure_v3/REVIEW_FIXES.md @@ -0,0 +1,94 @@ +# Evidence-verifier hardening — 2026-09-05 + +> Historical local-patch record. See [PUBLICATION.md](PUBLICATION.md) for the +> publication/dependency follow-up. The sections below describe the earlier +> local run, not the current PR head. + +Status: locally tested patch based on CaPU PR #106 at +`d64f9809f90532d52c2992174e63c1a216a50211`. Not published, not merged, +not externally approved. This changes evidence checking and restoration, not +the experimental receiver's state-transition or effect semantics. + +## Findings addressed in this patch + +1. Equal native/FSM output is no longer sufficient. Each of the 36 HTTP + records must have the expected identity and independently calculated effect + count. Expected values supplied by the report are checked, not trusted. + Both summary tables are reconstructed from the raw records. The 16 pair + records and all seven boundary identities are mandatory. +2. The producer input inventory is explicit and complete for this lab version. + Empty, missing, extra and changed entries fail. It includes the runner, + validator, test source, restoration tool, contract and requirements. +3. Validation uses explicit exceptions and nonzero CLI exit codes; `-O`, `-OO` + and `PYTHONOPTIMIZE` do not bypass it. Model and HTTP scenario assertions + are also explicit exceptions. Running the full harness under optimization + is refused because upstream pinned code is outside this hardening patch. +4. New evidence uses schema `/2`, records the actual Git revision when + available, records dirty status, and separates the pinned v2 dependency. + Exported source without Git records `null` rather than inventing a commit. +5. The exact historical archive is accepted only with `--historical` and fixed + artifact checksums. All retained fields of `evidence/observed-summary.json` + are protected by exact checksums for both original archive and repository formatting and its numeric claims are reconciled. +6. Archive restoration uses POSIX directory descriptors, no-follow and exclusive + opens. Existing matching regular files are not rewritten. Symlinked paths, + non-regular files and different existing contents are rejected. Unsupported + platforms fail closed. This does not establish arbitrary hostile-host or + storage-rollback protection. +7. The proposed workflow disables checkout credential persistence and adds + regression checks for the validator in ordinary and optimized Python. + +## Historical commit-label correction (without rewriting measurements) + +The original summary's `source_commit` value +`8a2f2a37023a50aeac52cb8c8aed84b2eeceec88` names the v2 DEPENDENCY. +It is not the producing v3 revision. The six recorded experiment-source hashes +match source files contained in candidate `d64f9809...`. That byte identity is +not a retroactive measurement of the original runtime's Git HEAD. + +Original summary, archive chunks and hashes are retained unchanged. The Git +observed-summary remains exactly as published. The ZIP contains a differently +formatted but value-identical copy; both exact byte identities are supported. `evidence_contract.json` records the candidate containing those +bytes; fresh `run.py` records its own provenance. Historical restoration must +not be described as a new run or as execution of the current patched producer. + +## Fixed-contract validation, not general verification + +The v3 transcript has a fixed, deterministic contract. Alongside semantic checks, +canonical model and HTTP transcript hashes pin all less prominent raw fields. +A legitimate scenario or ordering change therefore also requires explicit +review of the contract; this validator is not a generic verifier for later +experiments. Trusted verifier code and contract inputs remain assumptions. +Checksums do not authenticate an author, independently witness execution, or +prove completeness of external events. Two matching wrong implementations are +rejected for the stated cases; this is not an unbounded correctness proof. + +## Local verification + +Python 3.13.5, Linux, already installed cryptography 46.0.4. No dependency was +installed or upgraded for this run. + +- 32 validator/restoration test methods pass in normal mode. +- The same 32 pass under `python -O`; internal subcases also check `-OO` and + `PYTHONOPTIMIZE=1`. These are the same methods, not 64 new safety guarantees. +- A full new HTTP run passes all 40 methods with no failures/errors/skips. +- 560 bounded model traces and full HTTP transcripts match the original JSON + files byte-for-byte. Fresh summary/provenance and elapsed times are different + by design. +- Fresh and historical evidence pass normal and optimized CLI validation. +- Original v1/v2 suites were not rerun in this patch step. +- One earlier combined tool invocation hit its execution timeout. Its partial + HTTP log is retained separately and is not counted as a completed run. + +## Still open before external execution or merge + +The requirements file still pins `cryptography==46.0.4`. CodeRabbit's statement +that the experiment does not import it directly is incomplete: pinned +`proof.py` and ATMAN `authority.py` import it transitively. Official upstream +advisories/changelog identify fixes after that version. A reviewed update, +compatibility run and current dependency scan are still required. Do not treat +this patch's local synthetic run as approval of that dependency for production. + +No corrected code has been pushed from this session; no new GitHub CI or +CodeRabbit review has run on this patch. CodeRabbit's seven threads remain +review data, not approval. No Codex request was made. A draft invitation to +João is held separately, not sent and not evidence of his participation. diff --git a/experiments/recovery_closure_v3/VALIDATION.md b/experiments/recovery_closure_v3/VALIDATION.md index 4e45709..b0b9bf1 100644 --- a/experiments/recovery_closure_v3/VALIDATION.md +++ b/experiments/recovery_closure_v3/VALIDATION.md @@ -1,3 +1,8 @@ +> **2026-09-05 review follow-up:** see [PUBLICATION.md](PUBLICATION.md). +> Historical evidence requires `--historical`; fresh evidence uses schema `/2`. +> The dependency is pinned to 50.0.1; inspect the actual CI run for compatibility +> and audit results. No production approval or external endorsement is implied. + # Validation record Assessment: share as a bounded research demonstration with the limitations below; not a production or investment-readiness certificate. @@ -8,6 +13,11 @@ Assessment: share as a bounded research demonstration with the limitations below - Model: 560 distinct traces. An independent permutation filter confirmed all 56 linear extensions of the two event chains. `validate_results.py` recomputed the aggregates from full histories and compared all 16 native/FSM pairs. - Evidence: archive reconstruction reproduced four original output files byte-for-byte. Sizes and SHA-256 values are in EVIDENCE_MANIFEST.json. Hashes are integrity checks, not independent timestamps or completeness attestations. - Scope: intentionally selected fault schedules and mutation controls. No probability model, incident-rate estimate, speed benchmark or claim that existing conservative v2 is defective. -- Code review: local checks are not independent review. Keep the PR draft; record the native Codex response separately and never substitute another approval. +- Code review: local checks are not independent review. Keep the PR draft; the owner requested assistant self-review and optional CodeRabbit review without Codex. Record the actual review, not a substituted approval. The standard analytical HTML report packager could not be loaded in this runtime. This delivery uses a repository research note and executable evidence, not a claimed rendered/validated HTML report. + +## Publication follow-up + +See [PUBLICATION.md](PUBLICATION.md) for the dependency update, CI contract, +historical provenance correction, and current review boundary. diff --git a/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/MANIFEST.json b/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/MANIFEST.json new file mode 100644 index 0000000..70359cc --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/MANIFEST.json @@ -0,0 +1,40 @@ +{ + "schema": 1, + "kind": "historical-local-patch-run-not-50.0.1-ci", + "source_patch_sha256": "1a9c00ca9c67b0368a1acd503764aabf0790dbc91570f17a50f74f6ac49be044", + "compressed_sha256": "2f0328511e4d29c0b18aa73942ed419a8fa51b50a9a243e11209012708157b63", + "files": { + "http-tests.txt": { + "bytes": 4566, + "sha256": "ebfe0a1d433be47dac527589457dca850cdc974a4f5771d5b06b036c551c33e6" + }, + "semantic-replay-comparison.json": { + "bytes": 455, + "sha256": "4188916ffa453943b80d50f47e8d7f2faf2adfaa93d69ea6f1f16114f8f8fafc" + }, + "summary.json": { + "bytes": 14528, + "sha256": "595498b8b7dbd1da325d22234df5b1c597b988ed96565795fb9986f96f249b63" + }, + "validation-fresh.json": { + "bytes": 266, + "sha256": "12079e3c675a03ac698d305e8e817552afe7fde2267424239cd98954d223f960" + }, + "validation-history.json": { + "bytes": 300, + "sha256": "bb06156411077d937614e73a8cbfb41fed9819736cfb49ea4062998e273a5733" + }, + "validator-tests-optimized.txt": { + "bytes": 5405, + "sha256": "46dd1e1b9c68509b611b41895a934e90e5a8e43db6c2070b378cb1e4055c3fa3" + }, + "validator-tests.txt": { + "bytes": 5405, + "sha256": "1e973b431acc389e8c9d3c5717d9d10aa8014b2d96f1ee9f14b867fcf9f671db" + }, + "verification.json": { + "bytes": 3250, + "sha256": "7b1658b7a376105da50dca0e27c22ae634147c5a02180949ca5e05cc6f1c4350" + } + } +} diff --git a/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/README.md b/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/README.md new file mode 100644 index 0000000..a4261a8 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/README.md @@ -0,0 +1,33 @@ +# Earlier local validator-fix evidence + +Lossless archive of all eight evidence files from the previously delivered +validator-fix patch. This is historical Python 3.13.5 / cryptography 46.0.4 +local evidence, NOT CI evidence for 50.0.1 and not independent reproduction. + +`files.json.bz2.b64` is base64 of a bzip2 JSON object mapping filenames to exact +UTF-8 texts. `MANIFEST.json` records compressed and per-file SHA-256 values. +The `source_patch_sha256` field identifies the original patch, not a new run. + +From this directory, inspect without writing files: + +```sh +python - <<'PY' +import base64, bz2, hashlib, json +from pathlib import Path +manifest = json.loads(Path('MANIFEST.json').read_text()) +packed = base64.b64decode(Path('files.json.bz2.b64').read_text().strip(), validate=True) +if hashlib.sha256(packed).hexdigest() != manifest['compressed_sha256']: + raise SystemExit('Archive mismatch') +files = json.loads(bz2.decompress(packed)) +if set(files) != set(manifest['files']): + raise SystemExit('Unexpected members') +for name, text in files.items(): + raw = text.encode('utf-8'); expected = manifest['files'][name] + if len(raw) != expected['bytes'] or hashlib.sha256(raw).hexdigest() != expected['sha256']: + raise SystemExit('File mismatch: ' + name) +print(files['verification.json']) +PY +``` + +For new dependency results, inspect the new PR CI artifacts (`ci-evidence` and +`evidence-v3`) and their actual commit/environment records. diff --git a/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/files.json.bz2.b64 b/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/files.json.bz2.b64 new file mode 100644 index 0000000..e536e8d --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/review-fix-2026-09-05/files.json.bz2.b64 @@ -0,0 +1 @@ +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 diff --git a/experiments/recovery_closure_v3/evidence_contract.json b/experiments/recovery_closure_v3/evidence_contract.json new file mode 100644 index 0000000..a69edc1 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence_contract.json @@ -0,0 +1,22 @@ +{ + "artifact_sha256": { + "http-tests.txt": "ae9e39170a70dbffe7130a09f536adf73c1398484f5ff8a07141ccf7b4ca5101", + "http-traces.json": "b347cf7d47e8886b31341946f43555e0684260ec3421032ae822c95ca01eb69d", + "model.json": "c9465a2bc29c40e817deda895ebddc8706d04fb9d8d3d72263f4bb972967a854", + "summary.json": "53f09e04095bf4eb975240e8ce619833e0797caf040c34bc7e86e305cc071129" + }, + "http_canonical_sha256": "39a58e931eb522bd305b28e5b254af5a25620e2c108f8281dee9763baf807410", + "legacy_dependency_label": "8a2f2a37023a50aeac52cb8c8aed84b2eeceec88", + "model_trace_sha256": "8ec4fe5a66067ab23c77bf6ff589136d1a51a915191ed6c46b71178dee9b6a40", + "observation_sha256": "abe52266a298a237a25c67ba506050f554a9fd1e76a9fb81b4e4f451de2f4ca7", + "producer_candidate": "d64f9809f90532d52c2992174e63c1a216a50211", + "repository_observation_sha256": "0248c7daaa86ddca9af6f3ef52a4fc936c967e036a16f2d11a4948c3a0bcfe82", + "source_sha256": { + "finite_model.py": "1f9fd3ec837d710e99a1bc78605fe9d8cde7e4414f5ed2c5520b816cab190316", + "observer.py": "ae1a51df68ae1212528daf0f179294404c752a61e4e89df4ca0dfdc0ccd441fe", + "receiver.py": "326d762c8588da616b8139d237ab769b4d5e7a48d927e7c5b9a8acada8e88380", + "run.py": "64ce50e2fcefc522fb335b17a1a8f6174af4bfa83e27e28c8772a8d8e8961d6c", + "source_pins.py": "a71be4bd193a4b5d6a7079418f6d50054f24095f2a8cc4109eb78c17138c4d98", + "test_http.py": "9a6ff9b58d2e2e0e9d245eb4537998081205826dd06f0d44143093ff0ec01ef8" + } +} diff --git a/experiments/recovery_closure_v3/finite_model.py b/experiments/recovery_closure_v3/finite_model.py index 1f3e774..989ca67 100644 --- a/experiments/recovery_closure_v3/finite_model.py +++ b/experiments/recovery_closure_v3/finite_model.py @@ -68,7 +68,8 @@ def run(): # Independent combinatorial check: all permutations filtered by both chains. brute = {p for p in itertools.permutations(sum(CHAINS, ())) if all(all(p.index(a) < p.index(b) for a, b in zip(c, c[1:])) for c in CHAINS)} - assert set(orders) == brute and len(orders) == 56 + if not (set(orders) == brute and len(orders) == 56): + raise AssertionError('finite_model.py invariant at original line 71') traces, summary = [], [] for policy in POLICIES: for dropped in (False, True): @@ -78,11 +79,15 @@ def run(): 'schedules': len(batch), 'duplicate_traces': sum(r['duplicate'] for r in batch), 'incomplete_traces': sum(r['incomplete'] for r in batch)}) for policy in ('hold', 'atomic_fence', 'operation_idempotency'): - assert not any(r['duplicate'] for r in traces if r['policy'] == policy) + if not (not any(r['duplicate'] for r in traces if r['policy'] == policy)): + raise AssertionError('finite_model.py invariant at original line 81') for policy in ('snapshot_negative', 'admission_fence'): - assert any(r['duplicate'] for r in traces if r['policy'] == policy) - assert all(r['effect_count'] == 1 for r in traces if r['policy'] in ('atomic_fence', 'operation_idempotency')) - assert all(r['incomplete'] for r in traces if r['policy'] == 'hold' and r['initial_request_dropped']) + if not (any(r['duplicate'] for r in traces if r['policy'] == policy)): + raise AssertionError('finite_model.py invariant at original line 83') + if not (all(r['effect_count'] == 1 for r in traces if r['policy'] in ('atomic_fence', 'operation_idempotency'))): + raise AssertionError('finite_model.py invariant at original line 84') + if not (all(r['incomplete'] for r in traces if r['policy'] == 'hold' and r['initial_request_dropped'])): + raise AssertionError('finite_model.py invariant at original line 85') encoded = json.dumps(traces, sort_keys=True, separators=(',', ':')).encode() return {'summary': summary, 'trace_count': len(traces), 'trace_sha256': hashlib.sha256(encoded).hexdigest(), 'traces': traces} diff --git a/experiments/recovery_closure_v3/provenance.py b/experiments/recovery_closure_v3/provenance.py new file mode 100644 index 0000000..84dce4e --- /dev/null +++ b/experiments/recovery_closure_v3/provenance.py @@ -0,0 +1,49 @@ +"""Explicit producer-file inventory and honest checkout/archive provenance.""" +from __future__ import annotations + +import hashlib +from pathlib import Path +import subprocess + +ROOT = Path(__file__).resolve().parent +DEPENDENCY_COMMIT = '8a2f2a37023a50aeac52cb8c8aed84b2eeceec88' +SOURCE_FILES = ( + 'demo.py', 'finite_model.py', 'observer.py', 'provenance.py', + 'receiver.py', 'restore_evidence.py', 'run.py', 'source_pins.py', + 'test_http.py', 'test_validation.py', 'validate_results.py', + 'requirements.txt', 'evidence_contract.json', +) + + +def source_hashes(root: Path = ROOT) -> dict[str, str]: + """Hash the complete declared inventory, rejecting missing or linked sources.""" + hashes = {} + for name in SOURCE_FILES: + path = root / name + if path.is_symlink() or not path.is_file(): + raise ValueError('Missing/non-regular producer input: ' + name) + hashes[name] = hashlib.sha256(path.read_bytes()).hexdigest() + return hashes + + +def revision(root: Path = ROOT) -> dict: + """Identify the real checkout, or explicitly record that Git is unavailable.""" + def git(*args: str) -> str: + return subprocess.run( + ['git', '-C', str(root), *args], capture_output=True, text=True, + check=True, timeout=10, + ).stdout.strip() + try: + prefix = git('rev-parse', '--show-prefix') + commit = git('rev-parse', 'HEAD') + dirty = bool(git('status', '--porcelain', '--untracked-files=all', '--', '.')) + # A surrounding unrelated Git repository must not label an exported lab. + tracked = set(git('ls-files', '--', '.').splitlines()) + if not {'run.py', 'receiver.py', 'test_http.py'} <= tracked: + raise ValueError('Experiment is not tracked in this checkout') + return {'source_commit': commit, 'source_tree_dirty': dirty, + 'source_revision_status': 'git', 'source_path_prefix': prefix} + except (OSError, ValueError, subprocess.SubprocessError): + return {'source_commit': None, 'source_tree_dirty': None, + 'source_revision_status': 'archive_or_git_unavailable', + 'source_path_prefix': None} diff --git a/experiments/recovery_closure_v3/requirements.txt b/experiments/recovery_closure_v3/requirements.txt index c011dd5..1943c4d 100644 --- a/experiments/recovery_closure_v3/requirements.txt +++ b/experiments/recovery_closure_v3/requirements.txt @@ -1 +1 @@ -cryptography==46.0.4 +cryptography==50.0.1 diff --git a/experiments/recovery_closure_v3/restore_evidence.py b/experiments/recovery_closure_v3/restore_evidence.py index 56fe0c0..a927424 100644 --- a/experiments/recovery_closure_v3/restore_evidence.py +++ b/experiments/recovery_closure_v3/restore_evidence.py @@ -4,11 +4,57 @@ import bz2 import hashlib import json +import os +import stat from pathlib import Path ROOT = Path(__file__).resolve().parent + +def write_restored(output: Path, restored: dict[str, bytes]) -> None: + """Use directory descriptors and exclusive no-follow writes (POSIX only). + + Existing matching regular files may be read but are never rewritten. + Unsupported platforms fail closed rather than falling back to unsafe opens. + """ + if os.name != 'posix' or not hasattr(os, 'O_NOFOLLOW') or os.open not in os.supports_dir_fd: + raise RuntimeError('Safe restoration requires POSIX dir_fd and O_NOFOLLOW support') + flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW + absolute = Path(os.path.abspath(output)) + directory = os.open(absolute.anchor, flags) + try: + for part in absolute.parts[1:]: + try: + os.mkdir(part, 0o700, dir_fd=directory) + except FileExistsError: + pass + child = os.open(part, flags, dir_fd=directory) + os.close(directory) + directory = child + for name, raw in restored.items(): + if Path(name).name != name or name in ('', '.', '..') or '\\' in name: + raise ValueError('Invalid restored filename') + try: + fd = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, dir_fd=directory) + except FileExistsError: + fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, + dir_fd=directory) + with os.fdopen(fd, 'rb') as existing: + if not stat.S_ISREG(os.fstat(existing.fileno()).st_mode): + raise ValueError('Existing evidence is not a regular file: ' + name) + if existing.read(len(raw) + 1) != raw: + raise FileExistsError('Refusing to overwrite different evidence: ' + name) + continue + with os.fdopen(fd, 'wb') as target: + target.write(raw) + target.flush() + os.fsync(target.fileno()) + finally: + os.close(directory) + def main(): + """Restore the fixed historical archive without relabeling it as a new run.""" parser = argparse.ArgumentParser() parser.add_argument('--output', type=Path, default=ROOT / 'evidence-restored') args = parser.parse_args() @@ -22,19 +68,14 @@ def main(): raise ValueError('unexpected evidence members') restored = {} for name, value in data.items(): - if Path(name).name != name: + if Path(name).name != name or name in ('.', '..', '') or '\\' in name: raise ValueError('invalid evidence filename') raw = (json.dumps(value, indent=2, sort_keys=True) + '\n').encode() if name.endswith('.json') else value.encode() expected = manifest['files'][name] if len(raw) != expected['bytes'] or hashlib.sha256(raw).hexdigest() != expected['sha256']: raise ValueError('evidence mismatch: ' + name) restored[name] = raw - args.output.mkdir(parents=True, exist_ok=True) - for name, raw in restored.items(): - target = args.output / name - if target.exists() and target.read_bytes() != raw: - raise FileExistsError('refusing to overwrite different evidence: ' + name) - target.write_bytes(raw) + write_restored(args.output, restored) print('RESTORED 4 original evidence files; all sizes and SHA-256 hashes match.') if __name__ == '__main__': diff --git a/experiments/recovery_closure_v3/run.py b/experiments/recovery_closure_v3/run.py index 3f0b75f..56ef286 100644 --- a/experiments/recovery_closure_v3/run.py +++ b/experiments/recovery_closure_v3/run.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse import hashlib +import importlib.metadata import json from pathlib import Path import platform @@ -10,15 +11,21 @@ import unittest import finite_model import test_http +from provenance import DEPENDENCY_COMMIT, revision, source_hashes ROOT = Path(__file__).resolve().parent def dump(path, value): + """Save a complete JSON artifact with stable formatting.""" path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n') def run(output): + """Execute the unchanged scenario matrix and validate its generated evidence.""" + if sys.flags.optimize: + raise RuntimeError("Run the HTTP harness without optimization; the validator supports -O separately") + before = source_hashes() output.mkdir(parents=True, exist_ok=True) start = time.monotonic() model = finite_model.run() @@ -37,17 +44,20 @@ def run(output): continue a, b = [{k: v for k, v in r.items() if k != 'engine'} for r in pair] paired.append({'policy': policy, 'gate': gate, 'equal': a == b}) - summary = {'schema': 'recovery-closure-experiment/1', 'source_commit': '8a2f2a37023a50aeac52cb8c8aed84b2eeceec88', - 'environment': {'python': sys.version, 'platform': platform.platform()}, + summary = {'schema': 'recovery-closure-experiment/2', **revision(), 'dependency_commit': DEPENDENCY_COMMIT, + 'environment': {'python': sys.version, 'platform': platform.platform(), 'cryptography': importlib.metadata.version('cryptography')}, 'model_trace_count': model['trace_count'], 'model_summary': model['summary'], 'http_tests': {'run': result.testsRun, 'failures': len(result.failures), 'errors': len(result.errors), 'skipped': len(result.skipped)}, 'http_matrix': [{k: v for k, v in r.items() if k != 'steps'} for r in matrix], 'boundary_trace_count': len(test_http.TRACES) - len(matrix), 'native_fsm_comparisons': paired, 'elapsed_seconds_including_subprocess_startup_not_a_performance_benchmark': round(time.monotonic()-start, 3), - 'source_sha256': {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(ROOT.glob('*.py'))}} - summary['success'] = result.wasSuccessful() and all(r['equal'] for r in paired) + 'source_sha256': source_hashes()} + summary['success'] = result.wasSuccessful() and all(r['equal'] for r in paired) and before == summary['source_sha256'] dump(output / 'summary.json', summary) print(json.dumps({k: summary[k] for k in ('success', 'http_tests', 'model_trace_count', 'boundary_trace_count')}, indent=2)) + if summary['success']: + from validate_results import validate + validate(output) return 0 if summary['success'] else 1 if __name__ == '__main__': diff --git a/experiments/recovery_closure_v3/test_http.py b/experiments/recovery_closure_v3/test_http.py index 6576f3d..336f5de 100644 --- a/experiments/recovery_closure_v3/test_http.py +++ b/experiments/recovery_closure_v3/test_http.py @@ -105,13 +105,15 @@ def scenario(policy, gate, engine): steps = [] try: first = h.dispatch(0) - assert first['forwarded'], first + if not (first['forwarded']): + raise AssertionError(first) if gate != 'normal': h.wait('arrived') steps.append(h.snapshot('INITIAL_REQUEST')) closure = h.resolve(0) if engine and 'receipt' in closure: - assert closure['application']['applied'], closure + if not (closure['application']['applied']): + raise AssertionError(closure) retry = h.dispatch(1) steps.append(h.snapshot('AFTER_RECOVERY_BEFORE_RELEASE')) h.request('/release') @@ -120,10 +122,12 @@ def scenario(policy, gate, engine): if engine: if retry['forwarded']: positive = h.resolve(1) - assert positive['status'] == 'COMMITTED' and positive['application']['applied'], positive + if not (positive['status'] == 'COMMITTED' and positive['application']['applied']): + raise AssertionError(positive) elif gate != 'drop_before_effect' and closure['status'] != 'COMMITTED': positive = h.resolve(0) - assert positive['status'] == 'COMMITTED' and positive['application']['applied'], positive + if not (positive['status'] == 'COMMITTED' and positive['application']['applied']): + raise AssertionError(positive) steps.append(h.snapshot('FINAL')) if policy == 'operation_idempotency': expected = 1 @@ -136,9 +140,12 @@ def scenario(policy, gate, engine): else: expected = 1 actual = steps[-1]['witness']['effect_count'] - assert actual == expected, (policy, gate, actual, expected) - assert all(s['witness']['integrity'] == 'ok' for s in steps) - assert not (Path(temp) / 'controller/device.sqlite').exists() + if not (actual == expected): + raise AssertionError((policy, gate, actual, expected)) + if not (all(s['witness']['integrity'] == 'ok' for s in steps)): + raise AssertionError('test_http.py invariant at original line 140') + if not (not (Path(temp) / 'controller/device.sqlite').exists()): + raise AssertionError('test_http.py invariant at original line 141') return {'policy': policy, 'gate': gate, 'engine': engine or 'ordinary_operation_idempotency', 'closure_status': closure['status'], 'retry_forwarded': retry['forwarded'], 'effects_at_recovery': steps[1]['witness']['effect_count'], diff --git a/experiments/recovery_closure_v3/test_validation.py b/experiments/recovery_closure_v3/test_validation.py new file mode 100644 index 0000000..4a34d00 --- /dev/null +++ b/experiments/recovery_closure_v3/test_validation.py @@ -0,0 +1,307 @@ +"""Negative tests for local evidence verification, not new recovery guarantees.""" +from __future__ import annotations + +import base64 +import bz2 +import copy +import hashlib +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock + +import finite_model +import provenance +import restore_evidence +import validate_results as vr + +ROOT = Path(__file__).resolve().parent + + +class ValidationTests(unittest.TestCase): + """Mutate isolated fixture copies; never rewrite retained historical evidence.""" + + @classmethod + def setUpClass(cls): + """Decode exact retained artifacts once without executing the HTTP suite.""" + manifest = json.loads((ROOT / 'EVIDENCE_MANIFEST.json').read_text()) + packed = ''.join((ROOT / p).read_text().strip() for p in manifest['parts']) + cls.original = json.loads(bz2.decompress(base64.b64decode(packed))) + + def setUp(self): + """Give each mutation its own fresh directory and schema-/2 unit fixture.""" + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) + for name, value in self.original.items(): + raw = (json.dumps(value, indent=2, sort_keys=True) + '\n') if name.endswith('.json') else value + (self.path / name).write_text(raw) + self.s = copy.deepcopy(self.original['summary.json']) + self.s.update(schema='recovery-closure-experiment/2', **provenance.revision()) + self.s['dependency_commit'] = provenance.DEPENDENCY_COMMIT + self.s['source_sha256'] = provenance.source_hashes() + self.m = copy.deepcopy(self.original['model.json']) + self.h = copy.deepcopy(self.original['http-traces.json']) + + def save(self): + """Write deliberately altered unit-fixture data, not measured results.""" + for name, value in [('summary.json', self.s), ('model.json', self.m), ('http-traces.json', self.h)]: + (self.path / name).write_text(json.dumps(value, indent=2, sort_keys=True) + '\n') + + def rejected(self, message=None): + """Require a real validation exception rather than a printed warning.""" + self.save() + with self.assertRaises(vr.ValidationError) as error: + vr.validate(self.path) + if message: + self.assertIn(message, str(error.exception)) + + def test_exact_historical_archive(self): + """Exact historical bytes are accepted only as historical evidence.""" + self.assertEqual(vr.validate(self.path, historical=True)['status'], 'VALIDATED') + + def test_historical_requires_explicit_mode(self): + """Old schema is not silently labeled a fresh run.""" + with self.assertRaises(vr.ValidationError): + vr.validate(self.path) + + def test_fresh_unit_fixture(self): + """The complete schema-/2 test fixture is accepted.""" + self.save() + self.assertEqual(vr.validate(self.path)['status'], 'VALIDATED') + + def test_empty_missing_extra_or_changed_source_inventory(self): + """Source checks cannot be disabled by editing their input inventory.""" + expected = dict(self.s['source_sha256']) + variants = [{}, {k:v for k,v in expected.items() if k != 'run.py'}, + {**expected, '../unrelated': '0'*64}, {**expected, 'run.py': '0'*64}] + for variant in variants: + with self.subTest(keys=sorted(variant)): + self.s['source_sha256'] = variant + self.rejected() + + def test_source_file_absence(self): + """Missing local producer inputs are errors, not optional entries.""" + with self.assertRaises(ValueError): + provenance.source_hashes(self.path) + + def test_source_commit_misattribution(self): + """A plausible but different 40-hex commit is rejected.""" + self.s['source_commit'] = 'f' * 40 + self.rejected('revision mismatch') + + def test_dependency_commit_misattribution(self): + """Dependency and experiment revisions cannot be interchanged.""" + self.s['dependency_commit'] = 'f' * 40 + self.rejected('Dependency revision') + + def test_dirty_status_misattribution(self): + """A changed checkout status cannot be silently presented as clean.""" + self.s['source_tree_dirty'] = not self.s['source_tree_dirty'] + self.rejected('revision mismatch') + + def test_two_equally_wrong_engines_are_rejected_by_oracle(self): + """Agreement is insufficient even when the forged expected value agrees.""" + for row in self.h: + if row.get('policy') == 'atomic_fence' and row['gate'] == 'after_check': + row['final_effects'] = row['expected_effects'] = 2 + row['steps'][-1]['witness']['effect_count'] = 2 + row['steps'][-1]['witness']['effect_rows'].append(['[167,2,7,1,9,12]', 0]) + self.s['http_matrix'] = [{k:v for k,v in r.items() if k != 'steps'} for r in self.h if 'policy' in r] + self.rejected('Wrong actual effect count') + + def test_forged_declared_expectation(self): + """The report does not supply its own success oracle.""" + row = next(r for r in self.h if 'policy' in r) + row['expected_effects'] = 99 + self.rejected('Wrong declared oracle') + + def test_missing_or_duplicate_http_identity(self): + """One engine cannot stand in for a missing second engine.""" + row = next(r for r in self.h if r.get('engine') == 'native' and 'policy' in r) + row['engine'] = 'baseline' + self.rejected('HTTP scenario') + + def test_missing_http_record(self): + """A shortened transcript does not keep the success verdict.""" + self.h.pop() + self.rejected() + + def test_wrong_http_aggregate(self): + """Saved presentation figures must match the raw records.""" + self.s['http_matrix'][0]['final_effects'] = 99 + self.rejected('HTTP summary') + + def test_wrong_model_aggregate(self): + """Model totals in the summary are not trusted independently.""" + self.s['model_summary'][0]['duplicate_traces'] = 99 + self.rejected('Summary/model') + + def test_forged_pair_summary(self): + """A manually set or omitted equality flag is rejected.""" + self.s['native_fsm_comparisons'][0]['equal'] = False + self.rejected('Comparison summary') + + def test_rehashed_wrong_model(self): + """Rehashing a modified trace does not make its wrong outcome correct.""" + trace = next(t for t in self.m['traces'] if t['policy'] == 'atomic_fence') + trace['effect_count'] = 2 + trace['duplicate'] = True + trace['incomplete'] = False + trace['history'][-1]['effect_attempts'] = [0, 1] + self.m['trace_sha256'] = hashlib.sha256(vr.canonical(self.m['traces'])).hexdigest() + self.rejected('Wrong bounded-model outcome') + + def test_boundary_identity(self): + """All seven intended boundary records are required.""" + next(r for r in self.h if 'boundary' in r)['boundary'] = 'unknown' + self.rejected('Boundary identities') + + def test_boundary_raw_witness_change(self): + """A less prominent negative-case witness is covered by the contract pin.""" + next(r for r in self.h if r.get('boundary') == 'receiver_restart')['after']['effect_count'] = 2 + self.rejected('HTTP transcript') + + def test_failure_skip_and_bool_counts(self): + """False success, skipped tests and boolean counts must fail closed.""" + for key, value in [('errors', 1), ('failures', 40), ('skipped', 1), ('errors', False)]: + with self.subTest(key=key, value=value): + self.s['http_tests'] = dict(vr.EXPECTED_TESTS) + self.s['http_tests'][key] = value + self.rejected('HTTP test counts') + self.s['http_tests'] = dict(vr.EXPECTED_TESTS) + self.s['success'] = False + self.rejected('did not succeed') + + def test_modified_observed_summary(self): + """Retained human-facing figures cannot drift from their historical bytes.""" + self.save() + obs = json.loads((ROOT / 'evidence/observed-summary.json').read_text()) + obs['native_fsm_all_equal'] = False + path = self.path / 'observation.json' + path.write_text(json.dumps(obs)) + with self.assertRaises(vr.ValidationError): + vr.validate(self.path, observation=path) + + def test_duplicate_json_keys(self): + """Ambiguous duplicated fields are rejected before interpretation.""" + self.save() + (self.path / 'summary.json').write_text('{"success": false, "success": true}') + with self.assertRaises(vr.ValidationError): + vr.validate(self.path) + + def test_nonfinite_json(self): + """Non-standard JSON numbers are not silently accepted.""" + path = self.path / 'bad.json' + path.write_text('{"n": NaN}') + with self.assertRaises(vr.ValidationError): + vr.load_json(path) + + def test_cli_rejects_missing_and_malformed_files(self): + """CLI returns a nonzero exit status, including under -O.""" + (self.path / 'summary.json').write_text('not json') + result = subprocess.run([sys.executable, '-O', str(ROOT / 'validate_results.py'), str(self.path)], + capture_output=True, text=True, timeout=15) + self.assertNotEqual(result.returncode, 0) + self.assertIn('REJECTED', result.stderr) + + def test_cli_optimized_valid_and_invalid_evidence(self): + """-O, -OO and PYTHONOPTIMIZE do not disable evidence checks.""" + for flags, optimize in [(['-O'], None), (['-OO'], None), ([], '1')]: + with self.subTest(flags=flags, env=optimize): + self.s['success'] = True + self.save() + env = dict(os.environ) + if optimize: + env['PYTHONOPTIMIZE'] = optimize + command = [sys.executable, *flags, str(ROOT / 'validate_results.py'), str(self.path)] + result = subprocess.run(command, capture_output=True, text=True, env=env, timeout=15) + self.assertEqual(result.returncode, 0, result.stderr) + self.s['success'] = False + self.save() + result = subprocess.run(command, capture_output=True, text=True, env=env, timeout=15) + self.assertNotEqual(result.returncode, 0) + + def test_finite_model_invariants_survive_optimization(self): + """A broken schedule enumerator cannot pass the model's invariant check.""" + script = 'import finite_model; finite_model.schedules=lambda: []; finite_model.run()' + result = subprocess.run([sys.executable, '-O', '-c', script], cwd=ROOT, + capture_output=True, text=True, timeout=15) + self.assertNotEqual(result.returncode, 0) + self.assertIn('AssertionError', result.stderr) + + +class RestorationTests(unittest.TestCase): + """Exercise defensive restoration only in isolated temporary directories.""" + + def setUp(self): + """Create an isolated directory for each no-follow regression check.""" + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.output = self.root / 'out' + self.data = {'sample.json': b'{"ok": true}\n'} + + def test_create_and_matching_existing_file(self): + """Matching files are retained byte-for-byte without being rewritten.""" + restore_evidence.write_restored(self.output, self.data) + p = self.output / 'sample.json' + stamp = p.stat().st_mtime_ns + restore_evidence.write_restored(self.output, self.data) + self.assertEqual(p.read_bytes(), self.data['sample.json']) + self.assertEqual(p.stat().st_mtime_ns, stamp) + + def test_different_existing_file(self): + """Never overwrite a different existing artifact.""" + self.output.mkdir() + (self.output / 'sample.json').write_bytes(b'preserve') + with self.assertRaises(FileExistsError): + restore_evidence.write_restored(self.output, self.data) + self.assertEqual((self.output / 'sample.json').read_bytes(), b'preserve') + + def test_symlink_file(self): + """A symlink at the output filename is not followed.""" + self.output.mkdir() + other = self.root / 'other' + other.write_bytes(b'preserve') + (self.output / 'sample.json').symlink_to(other) + with self.assertRaises(OSError): + restore_evidence.write_restored(self.output, self.data) + self.assertEqual(other.read_bytes(), b'preserve') + + def test_symlink_directory(self): + """A symlink at an output-directory component is rejected.""" + other = self.root / 'other' + other.mkdir() + self.output.symlink_to(other, target_is_directory=True) + with self.assertRaises(OSError): + restore_evidence.write_restored(self.output, self.data) + self.assertEqual(list(other.iterdir()), []) + + def test_non_regular_existing_file(self): + """Reject a FIFO without blocking on its read side.""" + self.output.mkdir() + os.mkfifo(self.output / 'sample.json') + with self.assertRaises(ValueError): + restore_evidence.write_restored(self.output, self.data) + + def test_invalid_member_name(self): + """Archive member names must be simple non-special basenames.""" + for name in ('../outside', '.', '..', '/absolute', 'a\\b'): + with self.subTest(name=name), self.assertRaises((ValueError, OSError)): + restore_evidence.write_restored(self.output, {name: b'no'}) + + def test_unsupported_platform_fails_closed(self): + """Unsupported no-follow semantics must not fall back to Path.write_bytes.""" + with mock.patch.object(restore_evidence.os, 'name', 'unsupported'): + with self.assertRaises(RuntimeError): + restore_evidence.write_restored(self.output, self.data) + self.assertFalse(self.output.exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/experiments/recovery_closure_v3/validate_results.py b/experiments/recovery_closure_v3/validate_results.py index 61e9004..abdc6ca 100644 --- a/experiments/recovery_closure_v3/validate_results.py +++ b/experiments/recovery_closure_v3/validate_results.py @@ -1,39 +1,199 @@ -"""Independent aggregation of saved evidence; does not rerun the HTTP harness.""" +"""Fail-closed checks for the fixed v3 contract; not an authenticity attestation. + +Fresh runs must identify the complete current producer inputs. Historical mode +accepts only the exact original archive and explicitly corrects its dependency +commit label, without rewriting any historical file. +""" +from __future__ import annotations + +import argparse import collections import hashlib import json from pathlib import Path import sys -root = Path(sys.argv[1]) -s = json.loads((root / 'summary.json').read_text()) -m = json.loads((root / 'model.json').read_text()) -h = json.loads((root / 'http-traces.json').read_text()) -assert s['success'] and s['http_tests'] == {'run': 40, 'failures': 0, 'errors': 0, 'skipped': 0} -assert len(m['traces']) == m['trace_count'] == s['model_trace_count'] == 560 -assert len({(x['policy'], x['initial_request_dropped'], tuple(x['order'])) for x in m['traces']}) == 560 -aggregate = collections.defaultdict(lambda: [0, 0, 0]) -for t in m['traces']: - a = aggregate[(t['policy'], t['initial_request_dropped'])] - a[0] += 1 - a[1] += t['effect_count'] > 1 - a[2] += t['effect_count'] == 0 -for row in m['summary']: - assert aggregate[(row['policy'], row['initial_request_dropped'])] == [row['schedules'], row['duplicate_traces'], row['incomplete_traces']] -raw = json.dumps(m['traces'], sort_keys=True, separators=(',', ':')).encode() -assert hashlib.sha256(raw).hexdigest() == m['trace_sha256'] -matrix = [r for r in h if 'policy' in r] -assert len(matrix) == 36 and len(h) - len(matrix) == 7 -pairs = collections.defaultdict(list) -for r in matrix: - assert r['final_effects'] == r['steps'][-1]['witness']['effect_count'] - assert r['final_effects'] == len(r['steps'][-1]['witness']['effect_rows']) - if r['engine'] in ('native', 'baseline'): - pairs[(r['policy'], r['gate'])].append({k: v for k, v in r.items() if k != 'engine'}) -assert len(pairs) == 16 -assert all(len(p) == 2 and p[0] == p[1] for p in pairs.values()) -for path, expected in s['source_sha256'].items(): - # Source pins refer to the files as executed, not a later unrecorded version. - actual = hashlib.sha256((Path(__file__).resolve().parent / path).read_bytes()).hexdigest() - assert actual == expected, path -print('VALIDATED: 560 distinct bounded traces, 40 tests, 36 HTTP matrix rows, 7 boundary traces, 16 native/FSM pairs.') +from provenance import DEPENDENCY_COMMIT, ROOT, SOURCE_FILES, revision, source_hashes + +POLICIES = ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence') +GATES = ('before_check', 'after_check', 'drop_before_effect', 'normal') +ENGINES = ('native', 'baseline') +EXPECTED_TESTS = {'run': 40, 'failures': 0, 'errors': 0, 'skipped': 0} + + +class ValidationError(ValueError): + """The supplied evidence does not satisfy the declared experiment contract.""" + + +def require(condition: bool, message: str) -> None: + """Enforce a check even with Python -O or PYTHONOPTIMIZE.""" + if not condition: + raise ValidationError(message) + + +def load_json(path: Path): + """Reject duplicate keys and non-standard non-finite JSON numbers.""" + def pairs(items): + value = {} + for key, item in items: + require(key not in value, 'Duplicate JSON key: ' + key) + value[key] = item + return value + def constant(value): + raise ValidationError('Non-finite JSON constant: ' + value) + return json.loads(path.read_text(encoding='utf-8'), object_pairs_hook=pairs, + parse_constant=constant) + + +def canonical(value) -> bytes: + """Canonical JSON also distinguishes booleans from integer counts.""" + return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode() + + +def equal(actual, expected, label: str) -> None: + """Compare complete JSON values, not Python's bool/int-coercing equality.""" + require(canonical(actual) == canonical(expected), label) + + +def expected_effects(policy: str, gate: str) -> int: + """Outcome oracle independent of the result's declared expected_effects.""" + if policy == 'operation_idempotency': + return 1 + if gate == 'drop_before_effect': + return 0 if policy == 'hold' else 1 + if gate == 'normal': + return 1 + if policy == 'snapshot_negative' or (policy == 'admission_fence' and gate == 'after_check'): + return 2 + return 1 + + +def validate(root: Path, historical: bool = False, source_root: Path = ROOT, + observation: Path | None = None) -> dict: + """Check outcomes, raw-to-summary consistency and complete source identity.""" + contract = load_json(source_root / 'evidence_contract.json') + s, m, h = [load_json(root / name) for name in + ('summary.json', 'model.json', 'http-traces.json')] + require(s['success'] is True, 'Run did not succeed') + equal(s['http_tests'], EXPECTED_TESTS, 'HTTP test counts/failures/skips') + require(type(m['trace_count']) is int and m['trace_count'] == 560, 'Model trace count') + require(len(m['traces']) == 560 and s['model_trace_count'] == 560, 'Incomplete model coverage') + keys = {(t['policy'], t['initial_request_dropped'], tuple(t['order'])) for t in m['traces']} + require(len(keys) == 560, 'Duplicate model scenario identity') + aggregate = collections.defaultdict(lambda: [0, 0, 0]) + for t in m['traces']: + n = t['effect_count'] + require(type(n) is int and 0 <= n <= 2, 'Invalid effect count') + require(type(t['initial_request_dropped']) is bool, 'Delivery condition must be boolean') + require(t['duplicate'] is (n > 1) and t['incomplete'] is (n == 0), 'Trace outcome flags') + require(n == len(t['history'][-1]['effect_attempts']), 'Trace/history contradiction') + a = aggregate[(t['policy'], t['initial_request_dropped'])] + a[0] += 1 + a[1] += int(n > 1) + a[2] += int(n == 0) + expected_summary = [] + for policy in (*POLICIES, 'operation_idempotency'): + for dropped in (False, True): + duplicates = 0 if dropped else {'snapshot_negative': 50, 'admission_fence': 15}.get(policy, 0) + incomplete = 56 if dropped and policy == 'hold' else 0 + equal(aggregate[(policy, dropped)], [56, duplicates, incomplete], 'Wrong bounded-model outcome') + expected_summary.append({'policy': policy, 'initial_request_dropped': dropped, + 'schedules': 56, 'duplicate_traces': duplicates, + 'incomplete_traces': incomplete}) + equal(m['summary'], expected_summary, 'Model aggregate is not the contract aggregate') + equal(s['model_summary'], expected_summary, 'Summary/model aggregate mismatch') + digest = hashlib.sha256(canonical(m['traces'])).hexdigest() + require(digest == m['trace_sha256'] == contract['model_trace_sha256'], + 'Model histories differ from the frozen v3 contract') + + matrix = [r for r in h if 'policy' in r] + boundary = [r for r in h if 'policy' not in r] + expected_ids = {(p, g, e) for p in POLICIES for g in GATES for e in ENGINES} + expected_ids |= {('operation_idempotency', g, 'ordinary_operation_idempotency') for g in GATES} + ids = [(r['policy'], r['gate'], r['engine']) for r in matrix] + require(len(ids) == 36 and len(set(ids)) == 36 and set(ids) == expected_ids, + 'Missing, duplicate or unexpected HTTP scenario') + pairs = collections.defaultdict(dict) + for r in matrix: + label = f"{r['policy']}/{r['gate']}/{r['engine']}" + expected = expected_effects(r['policy'], r['gate']) + equal(r['final_effects'], expected, 'Wrong actual effect count: ' + label) + equal(r['expected_effects'], expected, 'Wrong declared oracle: ' + label) + equal([step['event'] for step in r['steps']], + ['INITIAL_REQUEST', 'AFTER_RECOVERY_BEFORE_RELEASE', 'FINAL'], 'Missing HTTP phases') + for step in r['steps']: + w = step['witness'] + require(w['integrity'] == 'ok', 'Observer integrity failure') + equal(w['effect_count'], len(w['effect_rows']), 'Observer row/count mismatch') + equal(r['final_effects'], r['steps'][-1]['witness']['effect_count'], 'Final/observer mismatch') + equal(r['effects_at_recovery'], r['steps'][1]['witness']['effect_count'], 'Recovery/observer mismatch') + if r['engine'] in ENGINES: + pairs[(r['policy'], r['gate'])][r['engine']] = {k: v for k, v in r.items() if k != 'engine'} + comparisons = [] + for p in POLICIES: + for g in GATES: + pair = pairs[(p, g)] + equal(pair['native'], pair['baseline'], 'Native/baseline evidence differs') + comparisons.append({'policy': p, 'gate': g, 'equal': True}) + equal(s['native_fsm_comparisons'], comparisons, 'Comparison summary mismatch') + equal(s['http_matrix'], [{k: v for k, v in r.items() if k != 'steps'} for r in matrix], + 'HTTP summary does not match raw records') + require(len(boundary) == s['boundary_trace_count'] == 7, 'Boundary trace coverage') + boundary_ids = [(r['boundary'], r.get('engine')) for r in boundary] + expected_boundary = {(b, e) for b in ('receiver_restart', 'tampered_negative', 'authority_generation') for e in ENGINES} + expected_boundary.add(('foreign_closure', None)) + require(len(set(boundary_ids)) == 7 and set(boundary_ids) == expected_boundary, 'Boundary identities') + # Fixed deterministic transcript pins protect less prominent fields, including + # controller state, closure identity and negative-receipt boundary witnesses. + require(hashlib.sha256(canonical(h)).hexdigest() == contract['http_canonical_sha256'], + 'HTTP transcript differs from the frozen v3 contract') + + observed = observation or source_root / 'evidence/observed-summary.json' + require(hashlib.sha256(observed.read_bytes()).hexdigest() in {contract['observation_sha256'], contract['repository_observation_sha256']}, + 'Historical observed-summary was edited') + obs = load_json(observed) + for field in ('http_tests', 'model_trace_count', 'model_summary'): + equal(obs[field], s[field], 'Observed summary mismatch: ' + field) + equal(obs['native_fsm_pairs'], len(comparisons), 'Observed pair count') + require(obs['native_fsm_all_equal'] is True, 'Observed pair verdict') + equal(obs['source_sha256'], contract['source_sha256'], 'Historical source inventory mismatch') + equal(obs['source_commit'], contract['legacy_dependency_label'], 'Historical dependency label changed') + + if historical: + require(s['schema'] == 'recovery-closure-experiment/1', 'Not the historical schema') + for name, expected_hash in contract['artifact_sha256'].items(): + require(hashlib.sha256((root / name).read_bytes()).hexdigest() == expected_hash, + 'Historical artifact changed: ' + name) + equal(s['source_sha256'], contract['source_sha256'], 'Historical sources missing/changed') + equal(s['source_commit'], contract['legacy_dependency_label'], 'Historical commit label changed') + mode = 'exact historical archive; legacy source_commit labels the v2 dependency, not the v3 producer' + else: + require(s['schema'] == 'recovery-closure-experiment/2', 'Fresh evidence requires schema /2; use --historical for archive') + require(set(s['source_sha256']) == set(SOURCE_FILES), 'Incomplete/unexpected source inventory') + equal(s['source_sha256'], source_hashes(source_root), 'Producer files changed') + for name, expected in revision(source_root).items(): + equal(s[name], expected, 'Producer revision mismatch: ' + name) + equal(s['dependency_commit'], DEPENDENCY_COMMIT, 'Dependency revision mismatch') + mode = 'fresh local producer identities and fixed outcome contract' + return {'status': 'VALIDATED', 'mode': mode, 'tests': 40, 'model_traces': 560, + 'matrix_rows': 36, 'boundary_rows': 7, 'native_fsm_pairs': 16, + 'not_an_independent_or_cryptographic_attestation': True} + + +def main() -> int: + """Return nonzero on malformed, partial or inconsistent evidence.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('evidence', type=Path) + parser.add_argument('--historical', action='store_true') + args = parser.parse_args() + try: + result = validate(args.evidence, args.historical) + except (ValidationError, KeyError, TypeError, ValueError, IndexError, OSError) as exc: + print('REJECTED: ' + str(exc), file=sys.stderr) + return 1 + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) From 1c13a2e7a4e03f1e1c92b31a2c6f993b815e3b1b Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:19:26 +0700 Subject: [PATCH 03/13] fix(recovery): gate execution on actual pinned cryptography runtime --- .../dependency_preflight.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 experiments/recovery_closure_v3/dependency_preflight.py diff --git a/experiments/recovery_closure_v3/dependency_preflight.py b/experiments/recovery_closure_v3/dependency_preflight.py new file mode 100644 index 0000000..46b1ecb --- /dev/null +++ b/experiments/recovery_closure_v3/dependency_preflight.py @@ -0,0 +1,108 @@ +"""Check the actual installed dependency before running the recovery experiment. + +This is a version/smoke-test gate, not a package authenticity attestation or a +vulnerability scan. Historical evidence never goes through this runtime gate. +""" +from __future__ import annotations + +import argparse +from importlib import metadata +import json +from pathlib import Path +import platform +import sys + +ROOT = Path(__file__).resolve().parent +EXPECTED_VERSION = "50.0.1" + + +class DependencyError(RuntimeError): + """The runtime cannot support a claim to have tested the candidate pin.""" + + +def check_version(value: str, label: str) -> None: + """Reject old, missing, prerelease, or merely claimed compatible versions.""" + if value != EXPECTED_VERSION: + raise DependencyError(f"{label}: expected {EXPECTED_VERSION}, observed {value!r}") + + +def check_requirement(path: Path = ROOT / "requirements.txt") -> None: + """Cross-check the candidate pin against its checked-in requirements file.""" + lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.lstrip().startswith("#")] + if lines != [f"cryptography=={EXPECTED_VERSION}"]: + raise DependencyError("Unexpected dependency requirement; revise the gate explicitly") + + +def validate_report(report: dict) -> None: + """Check recorded version and smoke-test claims; this does not attest a run.""" + keys = {"schema", "package", "installed_version", "module_version", "openssl", + "python", "ed25519_roundtrip", "changed_message_rejected"} + if not isinstance(report, dict) or set(report) != keys: + raise DependencyError("Incomplete or unexpected dependency report") + if report["schema"] != "capu-dependency-preflight/1" or report["package"] != "cryptography": + raise DependencyError("Unexpected dependency report identity") + check_version(report["installed_version"], "Recorded distribution") + check_version(report["module_version"], "Recorded import") + if report["ed25519_roundtrip"] is not True or report["changed_message_rejected"] is not True: + raise DependencyError("Dependency smoke test did not pass") + for field in ("openssl", "python"): + if not isinstance(report[field], str) or not report[field].strip(): + raise DependencyError("Missing runtime detail: " + field) + + +def verify_runtime() -> dict: + """Read the real installed package and exercise the Ed25519 API it provides.""" + check_requirement() + try: + installed = metadata.version("cryptography") + except metadata.PackageNotFoundError as exc: + raise DependencyError("cryptography is not installed") from exc + # Reject before loading fixtures or invoking any HTTP receiver. + check_version(installed, "Installed cryptography") + import cryptography + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.backends.openssl.backend import backend + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + check_version(cryptography.__version__, "Imported cryptography") + # This is deliberately a public, synthetic test key, never a user credential. + private = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) + public = private.public_key() + message = b"CaPU PR106 dependency compatibility smoke test" + signature = private.sign(message) + public.verify(signature, message) + try: + public.verify(signature, message + b" changed") + except InvalidSignature: + rejected = True + else: + raise DependencyError("Ed25519 accepted a changed message") + report = {"schema": "capu-dependency-preflight/1", "package": "cryptography", + "installed_version": installed, "module_version": cryptography.__version__, + "openssl": backend.openssl_version_text(), "python": platform.python_version(), + "ed25519_roundtrip": True, "changed_message_rejected": rejected} + validate_report(report) + return report + + +def main() -> int: + """Emit a success report only after all real runtime checks pass.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + try: + report = verify_runtime() + text = json.dumps(report, indent=2, sort_keys=True) + "\n" + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("x", encoding="utf-8") as stream: + stream.write(text) + print(text, end="") + return 0 + except (DependencyError, ImportError, OSError, ValueError) as exc: + print("DEPENDENCY_BLOCKED: " + str(exc), file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) From 0c8819501d663ee7b2e62e478898b1170256bea9 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:19:46 +0700 Subject: [PATCH 04/13] test(recovery): cover dependency gate and optimized interpreter rejection --- .../test_dependency_preflight.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 experiments/recovery_closure_v3/test_dependency_preflight.py diff --git a/experiments/recovery_closure_v3/test_dependency_preflight.py b/experiments/recovery_closure_v3/test_dependency_preflight.py new file mode 100644 index 0000000..269b467 --- /dev/null +++ b/experiments/recovery_closure_v3/test_dependency_preflight.py @@ -0,0 +1,108 @@ +"""Unit checks of upgrade gating; synthetic positives are NOT compatibility runs.""" +from __future__ import annotations +import copy +from importlib import metadata +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest import mock +import dependency_preflight as gate + +ROOT = Path(__file__).resolve().parent + + +class DependencyGateTests(unittest.TestCase): + """Exercise pin enforcement without installing or mocking an upgraded binary.""" + + def fixture(self): + """Return explicitly synthetic metadata for report-validation unit tests.""" + return {"schema": "capu-dependency-preflight/1", "package": "cryptography", + "installed_version": gate.EXPECTED_VERSION, "module_version": gate.EXPECTED_VERSION, + "openssl": "synthetic unit fixture", "python": "synthetic unit fixture", + "ed25519_roundtrip": True, "changed_message_rejected": True} + + def test_candidate_requirement_matches(self): + """Require an exact checked-in pin, not an open version range.""" + gate.check_requirement() + + def test_old_and_prerelease_versions_rejected(self): + """The check never silently accepts an old or unrelated version.""" + for value in ("46.0.4", "48.0.1", "50.0.0", "50.0.1rc1", "51.0.0", "", None): + with self.subTest(version=value), self.assertRaises(gate.DependencyError): + gate.check_version(value, "unit test") + + def test_exact_version_check(self): + """Check a version string only; no binary compatibility is inferred.""" + gate.check_version(gate.EXPECTED_VERSION, "unit test") + + def test_changed_requirement_rejected(self): + """A changed requirements file needs a deliberate gate update.""" + with tempfile.TemporaryDirectory() as td: + p = Path(td) / "requirements.txt" + for text in ("cryptography>=50", "cryptography==46.0.4", "", "cryptography==50.0.1\ncffi==2"): + p.write_text(text) + with self.subTest(text=text), self.assertRaises(gate.DependencyError): + gate.check_requirement(p) + + def test_missing_package_rejected(self): + """Absent metadata cannot create a success report.""" + with mock.patch.object(gate.metadata, "version", side_effect=metadata.PackageNotFoundError): + with self.assertRaises(gate.DependencyError): + gate.verify_runtime() + + def test_old_install_rejected_before_import(self): + """Stop on installed version before executing crypto or recovery work.""" + with mock.patch.object(gate.metadata, "version", return_value="46.0.4"): + with self.assertRaisesRegex(gate.DependencyError, "Installed cryptography"): + gate.verify_runtime() + + def test_complete_synthetic_report_shape(self): + """A fabricated unit fixture tests shape, not an external success claim.""" + gate.validate_report(self.fixture()) + + def test_missing_or_additional_report_field(self): + """Missing and unexpected claims are rejected.""" + for key in self.fixture(): + data = self.fixture(); data.pop(key) + with self.subTest(key=key), self.assertRaises(gate.DependencyError): + gate.validate_report(data) + data = self.fixture(); data["unverified"] = True + with self.assertRaises(gate.DependencyError): + gate.validate_report(data) + + def test_smoke_claims_are_real_booleans(self): + """Integer truthiness does not count as an executed successful check.""" + for key in ("ed25519_roundtrip", "changed_message_rejected"): + for value in (False, 1, "true", None): + data = self.fixture(); data[key] = value + with self.subTest(key=key, value=value), self.assertRaises(gate.DependencyError): + gate.validate_report(data) + + def test_module_and_distribution_both_match(self): + """Imported-module and installed-distribution versions must agree.""" + for key in ("installed_version", "module_version"): + data = self.fixture(); data[key] = "46.0.4" + with self.subTest(key=key), self.assertRaises(gate.DependencyError): + gate.validate_report(data) + + def test_optimized_wrong_version_still_fails(self): + """-O and -OO cannot disable the dependency gate.""" + for opt in ("-O", "-OO"): + result = subprocess.run([sys.executable, opt, "-c", + 'import dependency_preflight as d; d.check_version("46.0.4", "unit")'], + cwd=ROOT, capture_output=True, text=True, timeout=10) + self.assertNotEqual(result.returncode, 0) + self.assertIn("DependencyError", result.stderr) + + def test_empty_runtime_details_rejected(self): + """Retain interpreter and backend observations rather than blank labels.""" + for key in ("openssl", "python"): + data = self.fixture(); data[key] = "" + with self.subTest(key=key), self.assertRaises(gate.DependencyError): + gate.validate_report(data) + + +if __name__ == "__main__": + unittest.main() From 813c42ee0db965820ebc2ddf35415007861d0112 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:20:02 +0700 Subject: [PATCH 05/13] fix(evidence): include dependency gate in complete producer inventory --- experiments/recovery_closure_v3/provenance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/experiments/recovery_closure_v3/provenance.py b/experiments/recovery_closure_v3/provenance.py index 84dce4e..c718778 100644 --- a/experiments/recovery_closure_v3/provenance.py +++ b/experiments/recovery_closure_v3/provenance.py @@ -8,6 +8,7 @@ ROOT = Path(__file__).resolve().parent DEPENDENCY_COMMIT = '8a2f2a37023a50aeac52cb8c8aed84b2eeceec88' SOURCE_FILES = ( + 'dependency_preflight.py', 'test_dependency_preflight.py', 'demo.py', 'finite_model.py', 'observer.py', 'provenance.py', 'receiver.py', 'restore_evidence.py', 'run.py', 'source_pins.py', 'test_http.py', 'test_validation.py', 'validate_results.py', From f720465132c2e81e0daa0753ce5ce368101dda22 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:20:21 +0700 Subject: [PATCH 06/13] fix(recovery): check installed runtime before loading HTTP fixtures --- experiments/recovery_closure_v3/run.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/experiments/recovery_closure_v3/run.py b/experiments/recovery_closure_v3/run.py index 56ef286..2367171 100644 --- a/experiments/recovery_closure_v3/run.py +++ b/experiments/recovery_closure_v3/run.py @@ -2,7 +2,6 @@ from __future__ import annotations import argparse import hashlib -import importlib.metadata import json from pathlib import Path import platform @@ -10,7 +9,7 @@ import time import unittest import finite_model -import test_http +from dependency_preflight import verify_runtime from provenance import DEPENDENCY_COMMIT, revision, source_hashes ROOT = Path(__file__).resolve().parent @@ -25,6 +24,8 @@ def run(output): """Execute the unchanged scenario matrix and validate its generated evidence.""" if sys.flags.optimize: raise RuntimeError("Run the HTTP harness without optimization; the validator supports -O separately") + dependency_report = verify_runtime() + import test_http before = source_hashes() output.mkdir(parents=True, exist_ok=True) start = time.monotonic() @@ -45,7 +46,7 @@ def run(output): a, b = [{k: v for k, v in r.items() if k != 'engine'} for r in pair] paired.append({'policy': policy, 'gate': gate, 'equal': a == b}) summary = {'schema': 'recovery-closure-experiment/2', **revision(), 'dependency_commit': DEPENDENCY_COMMIT, - 'environment': {'python': sys.version, 'platform': platform.platform(), 'cryptography': importlib.metadata.version('cryptography')}, + 'environment': {'python': sys.version, 'platform': platform.platform(), 'dependencies': dependency_report}, 'model_trace_count': model['trace_count'], 'model_summary': model['summary'], 'http_tests': {'run': result.testsRun, 'failures': len(result.failures), 'errors': len(result.errors), 'skipped': len(result.skipped)}, 'http_matrix': [{k: v for k, v in r.items() if k != 'steps'} for r in matrix], From d073b5956b01b4bd37f9a7cb93029b0dc30527ec Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:20:58 +0700 Subject: [PATCH 07/13] fix(demo): require actual dependency preflight before HTTP scenarios --- experiments/recovery_closure_v3/demo.py | 36 +++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/experiments/recovery_closure_v3/demo.py b/experiments/recovery_closure_v3/demo.py index b0f3c85..b02845f 100644 --- a/experiments/recovery_closure_v3/demo.py +++ b/experiments/recovery_closure_v3/demo.py @@ -1,15 +1,23 @@ -"""Live, bounded demonstration. These are synthetic effects, never real payments.""" +"""Live bounded demonstration: synthetic effects, not payments or devices.""" import argparse -import json -from test_http import scenario - -p = argparse.ArgumentParser() -p.add_argument('--case', choices=('delayed', 'lost'), default='delayed') -a = p.parse_args() -gate = 'after_check' if a.case == 'delayed' else 'drop_before_effect' -print('LIVE LOOPBACK LAB — SQLite rows are the effects; no real money or devices.', flush=True) -print('Policy Effects at recovery Final effects', flush=True) -for policy in ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence', 'operation_idempotency'): - r = scenario(policy, gate, None if policy == 'operation_idempotency' else 'native') - print(f"{policy:30} {r['effects_at_recovery']:19} {r['final_effects']:15}", flush=True) -print('No CPU advantage or superiority over a correct conventional implementation is claimed.', flush=True) +from dependency_preflight import verify_runtime + + +def main() -> None: + """Require the actual candidate dependency before loading any HTTP scenarios.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--case', choices=('delayed', 'lost'), default='delayed') + args = parser.parse_args() + verify_runtime() + from test_http import scenario + gate = 'after_check' if args.case == 'delayed' else 'drop_before_effect' + print('LIVE LOOPBACK LAB — SQLite rows are the effects; no real money or devices.', flush=True) + print('Policy Effects at recovery Final effects', flush=True) + for policy in ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence', 'operation_idempotency'): + result = scenario(policy, gate, None if policy == 'operation_idempotency' else 'native') + print(f"{policy:30} {result['effects_at_recovery']:19} {result['final_effects']:15}", flush=True) + print('No CPU advantage or superiority over a correct conventional implementation is claimed.', flush=True) + + +if __name__ == '__main__': + main() From a7af617403ff9a880b54f34346e72c5946ed1a4f Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:21:36 +0700 Subject: [PATCH 08/13] fix(evidence): reject fresh results without matching dependency smoke report --- experiments/recovery_closure_v3/validate_results.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/experiments/recovery_closure_v3/validate_results.py b/experiments/recovery_closure_v3/validate_results.py index abdc6ca..15b36fb 100644 --- a/experiments/recovery_closure_v3/validate_results.py +++ b/experiments/recovery_closure_v3/validate_results.py @@ -13,6 +13,7 @@ from pathlib import Path import sys +from dependency_preflight import DependencyError, validate_report from provenance import DEPENDENCY_COMMIT, ROOT, SOURCE_FILES, revision, source_hashes POLICIES = ('hold', 'snapshot_negative', 'admission_fence', 'atomic_fence') @@ -169,6 +170,10 @@ def validate(root: Path, historical: bool = False, source_root: Path = ROOT, mode = 'exact historical archive; legacy source_commit labels the v2 dependency, not the v3 producer' else: require(s['schema'] == 'recovery-closure-experiment/2', 'Fresh evidence requires schema /2; use --historical for archive') + try: + validate_report(s.get('environment', {}).get('dependencies')) + except DependencyError as exc: + raise ValidationError(str(exc)) from exc require(set(s['source_sha256']) == set(SOURCE_FILES), 'Incomplete/unexpected source inventory') equal(s['source_sha256'], source_hashes(source_root), 'Producer files changed') for name, expected in revision(source_root).items(): From ed0bf96aba0f8acfc5d026369df84d8fc857ef89 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:22:35 +0700 Subject: [PATCH 09/13] test(evidence): reject missing or incorrectly labeled dependency results --- .../recovery_closure_v3/test_validation.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/experiments/recovery_closure_v3/test_validation.py b/experiments/recovery_closure_v3/test_validation.py index 4a34d00..c10297d 100644 --- a/experiments/recovery_closure_v3/test_validation.py +++ b/experiments/recovery_closure_v3/test_validation.py @@ -15,6 +15,7 @@ from unittest import mock import finite_model +from dependency_preflight import EXPECTED_VERSION import provenance import restore_evidence import validate_results as vr @@ -42,6 +43,12 @@ def setUp(self): (self.path / name).write_text(raw) self.s = copy.deepcopy(self.original['summary.json']) self.s.update(schema='recovery-closure-experiment/2', **provenance.revision()) + # Synthetic metadata for validator tests, NOT a measured dependency run. + self.s['environment'] = {'dependencies': { + 'schema': 'capu-dependency-preflight/1', 'package': 'cryptography', + 'installed_version': EXPECTED_VERSION, 'module_version': EXPECTED_VERSION, + 'openssl': 'synthetic unit fixture', 'python': 'synthetic unit fixture', + 'ed25519_roundtrip': True, 'changed_message_rejected': True}} self.s['dependency_commit'] = provenance.DEPENDENCY_COMMIT self.s['source_sha256'] = provenance.source_hashes() self.m = copy.deepcopy(self.original['model.json']) @@ -84,6 +91,21 @@ def test_empty_missing_extra_or_changed_source_inventory(self): self.s['source_sha256'] = variant self.rejected() + def test_dependency_version_mislabel_is_rejected(self): + """A fresh result made with the old dependency is not an upgrade result.""" + self.s['environment']['dependencies']['installed_version'] = '46.0.4' + self.rejected('Recorded distribution') + + def test_missing_dependency_report_is_rejected(self): + """Fresh evidence must include actual runtime compatibility metadata.""" + self.s['environment'].pop('dependencies') + self.rejected('dependency report') + + def test_failed_dependency_smoke_is_rejected(self): + """A version string alone does not count as a successful runtime check.""" + self.s['environment']['dependencies']['changed_message_rejected'] = False + self.rejected('smoke test') + def test_source_file_absence(self): """Missing local producer inputs are errors, not optional entries.""" with self.assertRaises(ValueError): From 4c5da994634316e3ade5dbb60ebcd6f27f4a167d Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:23:22 +0700 Subject: [PATCH 10/13] ci(recovery): verify actual dependency API and demo on exact PR head --- .github/workflows/recovery-closure-v3.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/recovery-closure-v3.yml b/.github/workflows/recovery-closure-v3.yml index 5037c94..02bb509 100644 --- a/.github/workflows/recovery-closure-v3.yml +++ b/.github/workflows/recovery-closure-v3.yml @@ -19,6 +19,7 @@ jobs: - uses: actions/checkout@v4 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -39,6 +40,11 @@ jobs: if data['cryptography'] != '50.0.1': raise SystemExit('Wrong cryptography version') PY + - name: Check actual dependency API before execution + shell: bash + run: | + python experiments/recovery_closure_v3/dependency_preflight.py --output ci-evidence/dependency-preflight.json + python -O experiments/recovery_closure_v3/dependency_preflight.py --output ci-evidence/dependency-preflight-optimized.json - name: Verify pinned source dependencies run: python experiments/recovery_closure_v3/source_pins.py - name: Restore and validate recorded evidence @@ -46,18 +52,23 @@ jobs: run: | python experiments/recovery_closure_v3/restore_evidence.py --output recorded-evidence python experiments/recovery_closure_v3/validate_results.py recorded-evidence --historical | tee ci-evidence/validation-history.json - - name: Validator and restoration regression tests + - name: Validator restoration and dependency regression tests shell: bash run: | cd experiments/recovery_closure_v3 - python -m unittest -v test_validation 2>&1 | tee ../../ci-evidence/validator-tests.txt - python -O -m unittest -v test_validation 2>&1 | tee ../../ci-evidence/validator-tests-optimized.txt + python -m unittest -v test_validation test_dependency_preflight 2>&1 | tee ../../ci-evidence/validator-tests.txt + python -O -m unittest -v test_validation test_dependency_preflight 2>&1 | tee ../../ci-evidence/validator-tests-optimized.txt - name: Run new model and HTTP experiments shell: bash run: | python experiments/recovery_closure_v3/run.py --output evidence-v3 | tee ci-evidence/run.txt python experiments/recovery_closure_v3/validate_results.py evidence-v3 | tee ci-evidence/validation-fresh.json python -O experiments/recovery_closure_v3/validate_results.py evidence-v3 | tee ci-evidence/validation-fresh-optimized.json + - name: Run bounded demonstrations with the installed pin + shell: bash + run: | + python experiments/recovery_closure_v3/demo.py --case delayed | tee ci-evidence/demo-delayed.txt + python experiments/recovery_closure_v3/demo.py --case lost | tee ci-evidence/demo-lost.txt - name: Audit resolved runtime requirements shell: bash run: | From 5da424a191975c6f3fde017d1b4326295b61f493 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:24:21 +0700 Subject: [PATCH 11/13] docs(recovery): document runtime gate and separate local from CI evidence --- .../recovery_closure_v3/DEPENDENCY_UPGRADE.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 experiments/recovery_closure_v3/DEPENDENCY_UPGRADE.md diff --git a/experiments/recovery_closure_v3/DEPENDENCY_UPGRADE.md b/experiments/recovery_closure_v3/DEPENDENCY_UPGRADE.md new file mode 100644 index 0000000..36e4ede --- /dev/null +++ b/experiments/recovery_closure_v3/DEPENDENCY_UPGRADE.md @@ -0,0 +1,88 @@ +# Actual dependency gate — 2026-09-05 + +This follow-up completes the remaining runtime-preflight portion of the prepared +publication candidate. It builds on `c3960204c6a63c9e7f804a65abf0269b5daf726c`; +it does not replace that commit with the old all-in-one patch. + +## What changed + +`dependency_preflight.py` checks the exact checked-in requirement, installed +distribution version, imported module version, a real Ed25519 sign/verify round +trip, and rejection of a changed message. Expected version: `cryptography==50.0.1`. +The key is a public synthetic fixture, not a credential. + +`run.py` and `demo.py` call this gate before importing HTTP fixtures. Fresh +summary evidence records the report under `environment.dependencies`; the +validator requires its complete shape, matching versions, and successful smoke +checks. Both new Python files are part of the explicit producer inventory. + +Historical archive bytes and their original metadata remain untouched. Use +`--historical` only for the exact retained original archive. A result made at an +earlier code revision should be validated with that pinned revision, not relabeled +as a fresh result of the new producer. + +## Reproduce + +From `experiments/recovery_closure_v3` in a checkout of the desired exact commit: + +```sh +python -m pip install -r requirements.txt +python -m pip check +python dependency_preflight.py +python -O dependency_preflight.py +python source_pins.py +python -m unittest -v test_validation test_dependency_preflight +python -O -m unittest -v test_validation test_dependency_preflight +python run.py --output ../../evidence-v3 +python validate_results.py ../../evidence-v3 +python -O validate_results.py ../../evidence-v3 +python demo.py --case delayed +python demo.py --case lost +``` + +There are 47 validator/restoration/dependency test methods, executed in two +interpreter modes; this is not 94 independent safety guarantees. The HTTP suite +remains 40 methods and the finite model remains 560 bounded traces. Synthetic +positive metadata in unit tests tests the validator; it is not a measured +compatibility run. + +CI checks out the exact PR head (or the dispatched SHA), installs the actual +pin, records the normal and optimized preflight reports, runs the tests, full +experiment and both demonstrations, and audits runtime requirements in a +separate environment. It preserves the existing contents-read-only permission +and disabled checkout credential persistence. Inspect the final candidate's +actual CI jobs and artifacts; an earlier commit's green badge is not this run. + +## Local checks and limitations + +The supplied publication archive had all 68 manifest entries verified. Thirteen +base code/config files matched the Git blob identities at `c3960204...` before +applying only the remaining delta. The updated 47-method suite passed normally +and under `-O` on local Python 3.13.5. + +Local cryptography remained 46.0.4: both actual preflight invocations rejected it +with exit status 1. A local full HTTP run with 50.0.1 is NOT claimed. The new +version's real execution must be established from the exact-head CI. Complete +local test logs and input hashes are retained in +`evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json`. + +To decode those historical local logs without executing their contents: + +```python +import base64, bz2, hashlib, json +from pathlib import Path +p = Path('evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json') +entry = json.loads(p.read_text())['logs'] +raw = bz2.decompress(base64.b64decode(entry['data'], validate=True)) +if len(raw) != entry['bytes'] or hashlib.sha256(raw).hexdigest() != entry['sha256']: + raise SystemExit('Local log archive mismatch') +logs = json.loads(raw) +print('\n'.join(logs)) +``` + +A version/smoke report is neither a package-authenticity attestation nor a +vulnerability scan, and report-shape validation cannot prove an independent +execution. The fixed source/outcome contract remains trusted. No change to the +recovery algorithm, real-payment guarantees, production readiness, architectural +superiority or investment claims is made. No merge or external email is included. +Assistant self-review and CodeRabbit are requested without Codex; keep the PR draft. From b949165e2702a9d66aa86997d5334d7a1551fd14 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:25:12 +0700 Subject: [PATCH 12/13] chore(evidence): preserve dependency-gate local checks with lossless logs --- .../LOCAL_CHECKS.json | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json diff --git a/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json b/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json new file mode 100644 index 0000000..ba4f946 --- /dev/null +++ b/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json @@ -0,0 +1,31 @@ +{ + "schema": "capu-pr106-dependency-gate-local-checks/1", + "date": "2026-09-05", + "base_commit": "c3960204c6a63c9e7f804a65abf0269b5daf726c", + "candidate_archive_sha256": "948bde09a5e0221f4e7b3f88daa80bb64dc69217b170abe648016945689a9574", + "base_files_matched_to_remote": 13, + "local_python": "3.13.5", + "local_cryptography": "46.0.4", + "unit_methods_per_mode": 47, + "unit_normal": "PASS", + "unit_optimized": "PASS", + "actual_runtime_gate": "EXPECTED_REJECTION_OF_46.0.4", + "local_full_http_with_50_0_1": "NOT_RUN", + "ci_claim": "Inspect the exact final commit CI separately; this file is local evidence only.", + "tested_inputs": { + "dependency_preflight.py": "b5948ca80d588def6cd5cefa23609962bce4fabd775ba00370e77854e756b686", + "test_dependency_preflight.py": "b4672f2d9752db8a7812ce05346189a07bdc3d061e278a65206119cad6a83c63", + "provenance.py": "a2041d007755b0cde6bf2d3b64ce7b501ea799bbefea44430991b773895cdff0", + "run.py": "58964f0e8b304ee8ec2c5d2a1cc01fca82ba43192e0a0d46b43604aa7a6c7606", + "demo.py": "04296248f00a794bc670e5cef3ca8f66d4de8470dd315798e4421bfde5968c66", + "validate_results.py": "5f2f2bd05ee5c926e11d2157c244b74f2ec08bc0684325b06b2ff91060fec228", + "test_validation.py": "f15995f9b916e64f1c7dc522be057b6b130c59edb20908a9be03b56e90c7e7e9", + "requirements.txt": "4026896996503d662ae0d9b5db045d1d5ff511c681dbfc09f5d5a11ee06b28c5" + }, + "logs": { + "encoding": "base64+bzip2+json", + "bytes": 17390, + "sha256": "8db57e0485019245fe0b8f65f10f0363d3e46e03a1f42b7d643807af5726edf8", + "data": "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" + } +} From f78b1a343df056ec047d5c21b221b3892231bdeb Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:30:41 +0700 Subject: [PATCH 13/13] fix(evidence): restore exact local-log archive bytes after publication check --- .../evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json b/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json index ba4f946..0cafe7e 100644 --- a/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json +++ b/experiments/recovery_closure_v3/evidence/dependency-gate-2026-09-05/LOCAL_CHECKS.json @@ -26,6 +26,6 @@ "encoding": "base64+bzip2+json", "bytes": 17390, "sha256": "8db57e0485019245fe0b8f65f10f0363d3e46e03a1f42b7d643807af5726edf8", - "data": "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" + "data": "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" } }