diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a8bb42650..83e14fb49 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -498,6 +498,7 @@ The same explicitly authorized rollover contract applies to the later supported | `acceptance-revalidation-v14` | Settled V13 on the integrated U03 candidate | | `acceptance-revalidation-v15` | Settled V14, including its invalid-JSON failure | | `acceptance-revalidation-v16` | Settled V15, including the stale-confirmation U03 failure | +| `acceptance-revalidation-v17` | Settled V16, including the cancellation-state U03 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 @@ -542,6 +543,12 @@ costs; the earlier U02 gate remains evidence for its original candidate only. Th same cumulative USD200 ceiling, three unknown liabilities and purpose/token caps apply. Fresh acceptance must bind the changed service rather than relabel old proof. +V17 retains the permanently halted V16 ledger, binding and guard for cancellation +diagnostic revalidation. Preserve the original U02 gate and incomplete U03 result; +numeric HTTP/Result failure diagnostics do not establish a root cause or a pass. +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 43f824847..57f7d9eb7 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-df792c4eeb93390edb6fccb5896153f620d8dfd9b7dd1735080baeaf7edc5a32", - "chunk_id": "repository-development:sha256-df792c4eeb93390edb6fccb5896153f620d8dfd9b7dd1735080baeaf7edc5a32:1", + "version": "sha256-f34f2a60cef2672d30a8208f0e2ebcc3a897eccd936808f69c7aff6375802379", + "chunk_id": "repository-development:sha256-f34f2a60cef2672d30a8208f0e2ebcc3a897eccd936808f69c7aff6375802379:1", "source_path": "docs/DEVELOPMENT.md", "access_scope": "repository-public", "sample_kind": "real", diff --git a/services/agent/e2e_u03_workflow.py b/services/agent/e2e_u03_workflow.py index ee3b123b9..39346935a 100644 --- a/services/agent/e2e_u03_workflow.py +++ b/services/agent/e2e_u03_workflow.py @@ -1270,7 +1270,7 @@ async def _cancel_old_run_fence( current_code, current = _envelope(current_response) if current_response.status_code != 200 or current_code != 0: analysis_task.cancel() - raise ValueError("cancel_fence_state_unavailable") + raise ValueError(f"cancel_fence_state_unavailable:http={current_response.status_code}:code={current_code}") if current.get("status") == "analyzing": analysis_run_id = current.get("runId") if not analysis_run_id: diff --git a/services/agent/src/authorized_budget_period.py b/services/agent/src/authorized_budget_period.py index ff402fe4d..ee4f12c3d 100644 --- a/services/agent/src/authorized_budget_period.py +++ b/services/agent/src/authorized_budget_period.py @@ -76,6 +76,7 @@ REVALIDATION_V14_POLICY_ID = "acceptance-revalidation-v14" REVALIDATION_V15_POLICY_ID = "acceptance-revalidation-v15" REVALIDATION_V16_POLICY_ID = "acceptance-revalidation-v16" +REVALIDATION_V17_POLICY_ID = "acceptance-revalidation-v17" 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, @@ -251,6 +252,17 @@ }) REVALIDATION_POLICY_IDS = REVALIDATION_POLICY_IDS | {REVALIDATION_V16_POLICY_ID} +REVALIDATION_V17_HISTORY = MappingProxyType({ + "attempts": 1390, "known_actual_micro_usd": 401_694, + "known_committed_micro_usd": 12_296_114, + "unknown_attempts": 3, "unknown_encumbrance_micro_usd": 2_359_296, + "cumulative_attempt_limit": 1631, "cumulative_limit_micro_usd": 200_000_000, +}) +REVALIDATION_V17_POLICY = MappingProxyType({ + **REVALIDATION_V16_POLICY, "history": REVALIDATION_V17_HISTORY, +}) +REVALIDATION_POLICY_IDS = REVALIDATION_POLICY_IDS | {REVALIDATION_V17_POLICY_ID} + def policy_for(policy_id: str): if policy_id == POLICY_ID: @@ -287,6 +299,8 @@ def policy_for(policy_id: str): return REVALIDATION_V15_POLICY if policy_id == REVALIDATION_V16_POLICY_ID: return REVALIDATION_V16_POLICY + if policy_id == REVALIDATION_V17_POLICY_ID: + return REVALIDATION_V17_POLICY raise PeriodError("unsupported policy") diff --git a/services/agent/src/revalidation_history.py b/services/agent/src/revalidation_history.py index f4f117c5d..78b1566ab 100644 --- a/services/agent/src/revalidation_history.py +++ b/services/agent/src/revalidation_history.py @@ -294,13 +294,30 @@ def _check_unknown_rollover(snapshot, guard, *, predecessor="v2"): } +V16_ROLLOVER_IDENTITY = { + "period_id": "revalidation-v16-20261011", + "identity": "c1956ef57b684979a5550b173a7ecde4", + "config_sha256": "43a23d85bd6afd63246b277cc3076bfcc8fd51ce5d41e64fcc70aba71b8742bc", + "policy_id": "acceptance-revalidation-v16", +} +V16_ROLLOVER_SHA256 = { + "budget.sqlite3": "22f4236db84a84708c2519c672573ad970941e9934213b5462b2cb58ea9e51b9", + "binding.json": "1212a6787c87526ebd067404336e49e99dc6d72c563e5244658a20a393831d81", + "dav58-increment-c1956ef57b684979a5550b173a7ecde4.json": "530b8f4d354f22ebc3a17155ea3b9ccd6c9061c944a664e3b427559f8f431956", +} + + def _validate_rollover_history(sources, *, settled_policy="v1"): - from authorized_budget_period import REVALIDATION_V14_HISTORY, REVALIDATION_V15_HISTORY, REVALIDATION_V16_HISTORY + from authorized_budget_period import REVALIDATION_V14_HISTORY, REVALIDATION_V15_HISTORY, REVALIDATION_V16_HISTORY, REVALIDATION_V17_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 == "v15": + if settled_policy == "v16": + original = _validate_rollover_history(sources, settled_policy="v15") + identity, fingerprints = V16_ROLLOVER_IDENTITY, V16_ROLLOVER_SHA256 + totals, history = (104, 35_977, 932_400), REVALIDATION_V17_HISTORY + elif settled_policy == "v15": original = _validate_rollover_history(sources, settled_policy="v14") identity, fingerprints = V15_ROLLOVER_IDENTITY, V15_ROLLOVER_SHA256 totals, history = (103, 35_648, 922_800), REVALIDATION_V16_HISTORY @@ -384,7 +401,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"}: + if settled_policy in {"v13", "v14", "v15", "v16"}: if guard.get("halted") is not True: raise PeriodError("previous guard must be permanently halted") receipt_view = {**guard, "halted": False} @@ -397,10 +414,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 + 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 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_V17_POLICY_ID: + return _validate_rollover_history(sources, settled_policy="v16") if policy_id == REVALIDATION_V16_POLICY_ID: return _validate_rollover_history(sources, settled_policy="v15") if policy_id == REVALIDATION_V14_POLICY_ID: diff --git a/services/agent/tests/test_delivery_gates.py b/services/agent/tests/test_delivery_gates.py index b42f00c95..f01955558 100644 --- a/services/agent/tests/test_delivery_gates.py +++ b/services/agent/tests/test_delivery_gates.py @@ -17,6 +17,29 @@ u03 = importlib.util.module_from_spec(_SPEC) _SPEC.loader.exec_module(u03) +@pytest.mark.parametrize("status,code", [(502, 50000), (200, 40900)]) +def test_cancel_fence_preserves_safe_failed_read_diagnostics(status, code): + import asyncio + import httpx + + async def scenario(): + async def respond(request): + if request.method == "POST": + data = {"threadId": str(uuid.uuid4()), "draft": {}} + return httpx.Response(200, json={"code": 0, "message": "success", "data": data, + "traceId": "private-trace"}) + return httpx.Response(status, json={"code": code, "message": "private-message", + "data": {"reason": "private-reason"}, + "traceId": "private-trace"}) + + async with httpx.AsyncClient(transport=httpx.MockTransport(respond), + base_url="http://localhost") as session: + with pytest.raises(ValueError) as failure: + await u03._cancel_old_run_fence(session, str(uuid.uuid4())) + assert str(failure.value) == f"cancel_fence_state_unavailable:http={status}:code={code}" + + asyncio.run(scenario()) + def _prior_five_fixture(tmp_path, monkeypatch, *, unsupported_rejected=True, raw_records=None): import agent_service.gate as gate diff --git a/services/agent/tests/test_v16_confirmation_history.py b/services/agent/tests/test_v16_confirmation_history.py index 1e2e66424..863be5211 100644 --- a/services/agent/tests/test_v16_confirmation_history.py +++ b/services/agent/tests/test_v16_confirmation_history.py @@ -16,3 +16,20 @@ def test_confirmation_revalidation_retains_settled_v15_and_unknown_liabilities() 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_cancellation_revalidation_retains_settled_v16_and_unknown_liabilities(): + old = policy_for("acceptance-revalidation-v16") + policy = policy_for("acceptance-revalidation-v17") + history = policy["history"] + assert history["attempts"] == old["history"]["attempts"] + 104 == 1390 + assert history["known_actual_micro_usd"] == old["history"]["known_actual_micro_usd"] + 35977 == 401694 + assert history["known_committed_micro_usd"] == old["history"]["known_committed_micro_usd"] + 932400 == 12296114 + 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"] == 1631 + 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"])