From fa729d255081ad09e377e1e115fd899962207498 Mon Sep 17 00:00:00 2001 From: DavidHLP Date: Sun, 11 Oct 2026 10:01:30 -0700 Subject: [PATCH] fix: preserve sealed recovery acceptance accounting --- docs/DEVELOPMENT.md | 7 ++ .../data/repository_corpus_manifest.json | 4 +- .../agent/src/authorized_budget_period.py | 14 ++++ services/agent/src/revalidation_history.py | 27 ++++++- .../tests/test_v16_confirmation_history.py | 80 ++++++++++++++++++- 5 files changed, 125 insertions(+), 7 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 58f00eb15..1d5ea5666 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -500,6 +500,7 @@ The same explicitly authorized rollover contract applies to the later supported | `acceptance-revalidation-v16` | Settled V15, including the stale-confirmation U03 failure | | `acceptance-revalidation-v17` | Settled V16, including the cancellation-state U03 failure | | `acceptance-revalidation-v18` | Settled V17, including the equivalent-array-definition boundary failure | +| `acceptance-revalidation-v19` | Settled V18, including the pre-dispatch save-intent recovery failure | Before preparation, retain the predecessor ledger, binding and guard at their pinned fingerprints and preserve the entire earlier history chain. The selected policy's @@ -556,6 +557,12 @@ settled costs; local grammar regressions do not replace genuine model acceptance The same cumulative USD200 ceiling, three unknown liabilities and purpose/token caps apply. Establish fresh acceptance for the changed candidate. +V19 retains the permanently halted V18 ledger, binding and guard for save-intent +restart recovery. Preserve the original incomplete U03 result and its settled costs; +the earlier U02 gate remains evidence for its original candidate only. The same +cumulative USD200 ceiling, three unknown liabilities and purpose/token caps apply. +Establish fresh acceptance for the changed candidate. + Issue the evidence-bound U02 gate only after those inputs validate. Gate artifact references are relative to the private directory containing the gate; place the referenced artifacts there: diff --git a/services/agent/data/repository_corpus_manifest.json b/services/agent/data/repository_corpus_manifest.json index d5076af48..5f856ebb0 100644 --- a/services/agent/data/repository_corpus_manifest.json +++ b/services/agent/data/repository_corpus_manifest.json @@ -29,8 +29,8 @@ }, { "doc_id": "repository-development", - "version": "sha256-afbb26c451fbaeb42cc30c0b897c4c6115eeb33a9216a1020a0410c258c89185", - "chunk_id": "repository-development:sha256-afbb26c451fbaeb42cc30c0b897c4c6115eeb33a9216a1020a0410c258c89185:1", + "version": "sha256-fde1dceb193829a479b2980bde4f77a264937ad9b819905cd4235be22af9f565", + "chunk_id": "repository-development:sha256-fde1dceb193829a479b2980bde4f77a264937ad9b819905cd4235be22af9f565:1", "source_path": "docs/DEVELOPMENT.md", "access_scope": "repository-public", "sample_kind": "real", diff --git a/services/agent/src/authorized_budget_period.py b/services/agent/src/authorized_budget_period.py index 3c4daaed9..b73b1f781 100644 --- a/services/agent/src/authorized_budget_period.py +++ b/services/agent/src/authorized_budget_period.py @@ -78,6 +78,7 @@ REVALIDATION_V16_POLICY_ID = "acceptance-revalidation-v16" REVALIDATION_V17_POLICY_ID = "acceptance-revalidation-v17" REVALIDATION_V18_POLICY_ID = "acceptance-revalidation-v18" +REVALIDATION_V19_POLICY_ID = "acceptance-revalidation-v19" REVALIDATION_POLICY_IDS = frozenset({REVALIDATION_POLICY_ID, REVALIDATION_V2_POLICY_ID, REVALIDATION_V3_POLICY_ID, REVALIDATION_V4_POLICY_ID, REVALIDATION_V5_POLICY_ID, REVALIDATION_V6_POLICY_ID, REVALIDATION_V7_POLICY_ID, REVALIDATION_V8_POLICY_ID, REVALIDATION_V9_POLICY_ID, REVALIDATION_V10_POLICY_ID, REVALIDATION_V11_POLICY_ID, REVALIDATION_V12_POLICY_ID}) REVALIDATION_V2_HISTORY = MappingProxyType({ "attempts": 164, "known_actual_micro_usd": 42_307, @@ -275,6 +276,17 @@ }) REVALIDATION_POLICY_IDS = REVALIDATION_POLICY_IDS | {REVALIDATION_V18_POLICY_ID} +REVALIDATION_V19_HISTORY = MappingProxyType({ + "attempts": 1590, "known_actual_micro_usd": 471_411, + "known_committed_micro_usd": 14_092_514, + "unknown_attempts": 3, "unknown_encumbrance_micro_usd": 2_359_296, + "cumulative_attempt_limit": 1831, "cumulative_limit_micro_usd": 200_000_000, +}) +REVALIDATION_V19_POLICY = MappingProxyType({ + **REVALIDATION_V18_POLICY, "history": REVALIDATION_V19_HISTORY, +}) +REVALIDATION_POLICY_IDS = REVALIDATION_POLICY_IDS | {REVALIDATION_V19_POLICY_ID} + def policy_for(policy_id: str): if policy_id == POLICY_ID: @@ -315,6 +327,8 @@ def policy_for(policy_id: str): return REVALIDATION_V17_POLICY if policy_id == REVALIDATION_V18_POLICY_ID: return REVALIDATION_V18_POLICY + if policy_id == REVALIDATION_V19_POLICY_ID: + return REVALIDATION_V19_POLICY raise PeriodError("unsupported policy") diff --git a/services/agent/src/revalidation_history.py b/services/agent/src/revalidation_history.py index 772242f3c..3407cc8c3 100644 --- a/services/agent/src/revalidation_history.py +++ b/services/agent/src/revalidation_history.py @@ -320,13 +320,30 @@ def _check_unknown_rollover(snapshot, guard, *, predecessor="v2"): } +V18_ROLLOVER_IDENTITY = { + "period_id": "revalidation-v18-20261011", + "identity": "22bc94738c1045fb808d6dfff07c902f", + "config_sha256": "e4a0d82bee049228bfc54d97df3b5326d7c28156ef8a60fc10c2c30af90be017", + "policy_id": "acceptance-revalidation-v18", +} +V18_ROLLOVER_SHA256 = { + "budget.sqlite3": "3120884dac903caf59bbd55d5c28194440192dd55a45b08c06544fb842ac2034", + "binding.json": "774da3d801ff55bb46211ca8d8258d3f7028c91aa5c0fc0371efff26fe0e9bca", + "dav58-increment-22bc94738c1045fb808d6dfff07c902f.json": "2f7f35a997d5bf355cb92674ce47b7c3837b7a44f67464e162cf56d127b3b260", +} + + def _validate_rollover_history(sources, *, settled_policy="v1"): - from authorized_budget_period import REVALIDATION_V14_HISTORY, REVALIDATION_V15_HISTORY, REVALIDATION_V16_HISTORY, REVALIDATION_V17_HISTORY, REVALIDATION_V18_HISTORY + from authorized_budget_period import REVALIDATION_V14_HISTORY, REVALIDATION_V15_HISTORY, REVALIDATION_V16_HISTORY, REVALIDATION_V17_HISTORY, REVALIDATION_V18_HISTORY, REVALIDATION_V19_HISTORY from authorized_budget_period import PeriodError, PeriodIdentity, REVALIDATION_V2_HISTORY, REVALIDATION_V4_HISTORY, REVALIDATION_V5_HISTORY, REVALIDATION_V6_HISTORY, REVALIDATION_V7_HISTORY, REVALIDATION_V8_HISTORY, REVALIDATION_V9_HISTORY, REVALIDATION_V10_HISTORY, REVALIDATION_V11_HISTORY, REVALIDATION_V12_HISTORY, _parent, _file from model_budget import ModelBudget, authorization_slot from dav58_live_guard import IncrementalGuard - if settled_policy == "v17": + if settled_policy == "v18": + original = _validate_rollover_history(sources, settled_policy="v17") + identity, fingerprints = V18_ROLLOVER_IDENTITY, V18_ROLLOVER_SHA256 + totals, history = (104, 35_994, 932_400), REVALIDATION_V19_HISTORY + elif settled_policy == "v17": original = _validate_rollover_history(sources, settled_policy="v16") identity, fingerprints = V17_ROLLOVER_IDENTITY, V17_ROLLOVER_SHA256 totals, history = (96, 33_723, 864_000), REVALIDATION_V18_HISTORY @@ -418,7 +435,7 @@ def _validate_rollover_history(sources, *, settled_policy="v1"): try: # Validate sealed receipts without making the permanently halted journal resumable. receipt_view = guard - if settled_policy in {"v13", "v14", "v15", "v16", "v17"}: + if settled_policy in {"v13", "v14", "v15", "v16", "v17", "v18"}: if guard.get("halted") is not True: raise PeriodError("previous guard must be permanently halted") receipt_view = {**guard, "halted": False} @@ -431,10 +448,12 @@ def _validate_rollover_history(sources, *, settled_policy="v1"): def validate_history(sources, *, policy_id="acceptance-revalidation-v1"): - from authorized_budget_period import REVALIDATION_V13_POLICY_ID, REVALIDATION_V14_POLICY_ID, REVALIDATION_V15_POLICY_ID, REVALIDATION_V16_POLICY_ID, REVALIDATION_V17_POLICY_ID, REVALIDATION_V18_POLICY_ID + from authorized_budget_period import REVALIDATION_V13_POLICY_ID, REVALIDATION_V14_POLICY_ID, REVALIDATION_V15_POLICY_ID, REVALIDATION_V16_POLICY_ID, REVALIDATION_V17_POLICY_ID, REVALIDATION_V18_POLICY_ID, REVALIDATION_V19_POLICY_ID from authorized_budget_period import PeriodError, REVALIDATION_HISTORY, _parent, _file from authorized_budget_period import REVALIDATION_POLICY_ID, REVALIDATION_V2_POLICY_ID, REVALIDATION_V3_POLICY_ID, REVALIDATION_V4_POLICY_ID, REVALIDATION_V5_POLICY_ID, REVALIDATION_V6_POLICY_ID, REVALIDATION_V7_POLICY_ID, REVALIDATION_V8_POLICY_ID, REVALIDATION_V9_POLICY_ID, REVALIDATION_V10_POLICY_ID, REVALIDATION_V11_POLICY_ID, REVALIDATION_V12_POLICY_ID + if policy_id == REVALIDATION_V19_POLICY_ID: + return _validate_rollover_history(sources, settled_policy="v18") if policy_id == REVALIDATION_V18_POLICY_ID: return _validate_rollover_history(sources, settled_policy="v17") if policy_id == REVALIDATION_V17_POLICY_ID: diff --git a/services/agent/tests/test_v16_confirmation_history.py b/services/agent/tests/test_v16_confirmation_history.py index 6c04ad417..0d762a57d 100644 --- a/services/agent/tests/test_v16_confirmation_history.py +++ b/services/agent/tests/test_v16_confirmation_history.py @@ -1,4 +1,82 @@ -from authorized_budget_period import policy_for +import hashlib +import json +from types import SimpleNamespace + +import pytest + +from authorized_budget_period import PeriodError, policy_for + + +@pytest.mark.parametrize("mutation", [None, "fingerprint", "state", "sql_gate", "halted", + "unknown_usage_attempts", "unsettled_attempts", "attempts", + "actual_micro_usd", "committed_micro_usd", "guard", "receipts"]) +def test_v19_binding_requires_intact_settled_and_halted_v18(monkeypatch, tmp_path, mutation): + import model_budget + import revalidation_history as history + from dav58_live_guard import IncrementalGuard + + accounting = tmp_path / "accounting" + accounting.mkdir(mode=0o700) + guard_name = f'dav58-increment-{history.V18_ROLLOVER_IDENTITY["identity"]}.json' + guard = {"halted": mutation != "guard", "receipts": []} + pins = {} + for name, raw in {"budget.sqlite3": b"sealed fixture", "binding.json": b"{}", + guard_name: json.dumps(guard).encode()}.items(): + path = accounting / name + path.write_bytes(raw) + path.chmod(0o600) + pins[name] = hashlib.sha256(raw).hexdigest() + monkeypatch.setattr(history, "V18_ROLLOVER_SHA256", pins) + real_validate = history._validate_rollover_history + predecessor = {"sha256": {"older": "retained"}} + def previous(sources, *, settled_policy="v1"): + if settled_policy == "v17": + return predecessor + return real_validate(sources, settled_policy=settled_policy) + monkeypatch.setattr(history, "_validate_rollover_history", previous) + monkeypatch.setattr(model_budget, "authorization_slot", lambda expected: tmp_path) + snapshot = {"state": "halted", "sql_gate": "halted", "halted": 1, "attempts": 104, + "actual_micro_usd": 35994, "committed_micro_usd": 932400, + "unknown_usage_attempts": 0, "unsettled_attempts": 0} + if mutation == "fingerprint": + (accounting / "budget.sqlite3").write_bytes(b"changed") + elif mutation in snapshot: + snapshot[mutation] = "active" if mutation in {"state", "sql_gate"} else 0 if mutation == "halted" else snapshot[mutation] + 1 + def sealed_view(expected, retained): + assert expected.policy_id == "acceptance-revalidation-v18" + assert retained is predecessor + return SimpleNamespace(snapshot=lambda: snapshot, policy={}) + monkeypatch.setattr(model_budget.ModelBudget, "_sealed_history_view", sealed_view) + def check_receipts(receipt_view, *args): + assert receipt_view["halted"] is False + if mutation == "receipts": + raise ValueError("dispatch mismatch") + monkeypatch.setattr(IncrementalGuard, "_validate_resume", check_receipts) + if mutation: + with pytest.raises(PeriodError): + history.validate_history({}, policy_id="acceptance-revalidation-v19") + else: + audit = history.validate_history({}, policy_id="acceptance-revalidation-v19") + assert audit["sha256"]["older"] == "retained" + assert audit["baseline"] == dict(policy_for("acceptance-revalidation-v19")["history"]) + assert audit["acceptance_evidence"] is audit["unknown_released"] is False + + +def test_recovery_revalidation_retains_settled_v18_and_unknown_liabilities(): + old = policy_for("acceptance-revalidation-v18") + policy = policy_for("acceptance-revalidation-v19") + history = policy["history"] + assert history["attempts"] == old["history"]["attempts"] + 104 == 1590 + assert history["known_actual_micro_usd"] == old["history"]["known_actual_micro_usd"] + 35994 == 471411 + assert history["known_committed_micro_usd"] == old["history"]["known_committed_micro_usd"] + 932400 == 14092514 + assert history["unknown_attempts"] == old["history"]["unknown_attempts"] == 3 + assert history["unknown_encumbrance_micro_usd"] == old["history"]["unknown_encumbrance_micro_usd"] == 2359296 + assert history["cumulative_limit_micro_usd"] == old["history"]["cumulative_limit_micro_usd"] == 200000000 + assert history["cumulative_attempt_limit"] == history["attempts"] + policy["attempts"] == 1831 + assert policy["lanes"] == old["lanes"] + assert policy["limit_micro_usd"] == old["limit_micro_usd"] + assert (history["known_committed_micro_usd"] + history["unknown_encumbrance_micro_usd"] + + policy["limit_micro_usd"] <= history["cumulative_limit_micro_usd"]) def test_array_range_revalidation_retains_settled_v17_and_unknown_liabilities():