From eece796b8bbee19551f6aa07ad389cbf83cabcdb Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 16 Oct 2026 09:20:33 +0200 Subject: [PATCH] fix: confirm exact canonical consumed act binding Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786 --- approval_engine/execution_confirmation.py | 10 +++-- tests/test_execution_confirmation.py | 47 +++++++++++++++++++++++ workplans/ADHOC-2026-10-16.md | 44 +++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 workplans/ADHOC-2026-10-16.md diff --git a/approval_engine/execution_confirmation.py b/approval_engine/execution_confirmation.py index 2fef8b9..fa7daaa 100644 --- a/approval_engine/execution_confirmation.py +++ b/approval_engine/execution_confirmation.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Any from urllib.parse import quote -from .binding import DIGEST_RE, canonical_binding +from .binding import DIGEST_RE, binding_digest from .pep import ApprovalHTTPClient, ApprovalProtocolError, _ID_RE @@ -71,7 +71,7 @@ class ExecutionConfirmationClient(ApprovalHTTPClient): if not isinstance(expected_pdp_digest, str) or not DIGEST_RE.fullmatch(expected_pdp_digest): raise ApprovalProtocolError("exact approval-free PDP digest is required") try: - expected = canonical_binding(expected_binding) + expected = binding_digest(expected_binding) except (TypeError, ValueError, KeyError): raise ApprovalProtocolError("expected act binding is invalid") from None obj = self._request("GET", f"/v1/approvals/{quote(approval_id, safe='')}") @@ -81,7 +81,11 @@ class ExecutionConfirmationClient(ApprovalHTTPClient): raise ApprovalProtocolError("consumption object is not a decision") served = obj.get("binding") try: - same_act = isinstance(served, dict) and canonical_binding(served) == expected + same_act = ( + isinstance(served, dict) + and binding_digest(served) == expected + and served.get("digest") == expected + ) except (TypeError, ValueError, KeyError): same_act = False if not same_act or served.get("pdp_digest") != expected_pdp_digest: diff --git a/tests/test_execution_confirmation.py b/tests/test_execution_confirmation.py index fed3f83..9892ff3 100644 --- a/tests/test_execution_confirmation.py +++ b/tests/test_execution_confirmation.py @@ -127,3 +127,50 @@ def test_denied_read_never_represents_confirmation(engine, app, tmp_path): raise HTTPError(req.full_url, status, "refused", {}, None) with pytest.raises(ApprovalProtocolError, match="unauthorized"): confirmer(client(tmp_path, opener), obj) + + +@pytest.mark.parametrize("original,substitute", [(True, 1), (False, 0), (1, 1.0), (1, True)]) +@pytest.mark.parametrize("recompute_served_digest", [False, True]) +def test_confirmation_refuses_python_equal_json_distinct_target( + engine, tmp_path, original, substitute, recompute_served_digest +): + expected = binding(target={"nested": [{"value": original}]}) + obj = engine.create(expected, validity(), pdp_digest=FREE, pdp_path=True, + human_control=True) + engine.add_entry(obj.id, "human:founder", principal_type="human") + engine.consume(obj.id, FULL, decision_id="decision:original") + result = engine.get(obj.id).as_dict() + result["binding"]["target"] = {"nested": [{"value": substitute}]} + if recompute_served_digest: + from approval_engine.binding import binding_digest + result["binding"]["digest"] = binding_digest(result["binding"]) + assert result["binding"]["target"] == expected["target"] + with pytest.raises(ApprovalProtocolError, match="binding does not match"): + client(tmp_path, lambda *_a, **_k: Response(result)).confirm_consumption( + obj.id, FULL, "decision:original", expected_binding=expected, + expected_pdp_digest=FREE) + + +@pytest.mark.parametrize("digest", [None, "sha256:" + "00" * 32, True]) +def test_confirmation_requires_served_native_digest_correspondence(engine, tmp_path, digest): + obj = consumed(engine) + result = engine.get(obj.id).as_dict() + if digest is None: + result["binding"].pop("digest") + else: + result["binding"]["digest"] = digest + with pytest.raises(ApprovalProtocolError, match="binding does not match"): + confirmer(client(tmp_path, lambda *_a, **_k: Response(result)), obj) + + +def test_confirmation_accepts_nested_json_types_and_key_order(engine, tmp_path): + expected = binding(target={"b": [True, False, 1, 1.0], "a": {"unicode": "รค"}}) + obj = engine.create(expected, validity(), pdp_digest=FREE, pdp_path=True, + human_control=True) + engine.add_entry(obj.id, "human:founder", principal_type="human") + engine.consume(obj.id, FULL, decision_id="decision:original") + result = engine.get(obj.id).as_dict() + result["binding"]["target"] = dict(reversed(list(expected["target"].items()))) + assert client(tmp_path, lambda *_a, **_k: Response(result)).confirm_consumption( + obj.id, FULL, "decision:original", expected_binding=expected, + expected_pdp_digest=FREE)["approval_id"] == obj.id diff --git a/workplans/ADHOC-2026-10-16.md b/workplans/ADHOC-2026-10-16.md new file mode 100644 index 0000000..cd19170 --- /dev/null +++ b/workplans/ADHOC-2026-10-16.md @@ -0,0 +1,44 @@ +--- +id: APPROVAL-WP-ADHOC-2026-10-16 +type: workplan +title: "Repair exact consume-confirmation native binding comparison" +domain: infotech +repo: approval-engine +status: finished +flavor: implementation +owner: codex +topic_slug: netkingdom +created: "2026-10-16" +updated: "2026-10-16" +--- + +## Refuse Python-equal JSON-distinct consumed act bindings + +```task +id: APPROVAL-WP-ADHOC-2026-10-16-T01 +status: done +priority: high +``` + +Bounded source repair demanded by the CUST-WP-0080 strict execution-seam proof. +INTENT basis: exact binding and authenticated consumption evidence; no authority +or native grant is added. Existing canonical native-five-field digest is reused. +ExecutionConfirmationClient now recomputes the served and expected digests and +requires the served digest field to correspond, replacing Python dict equality +which accepted nested True/1, False/0 and integer/float substitutions. + +Before repair seven regression cases wrongly confirmed (four target type +substitutions and three absent/wrong/type-invalid served digest fields); valid +nested types with reordered keys passed. The repair retains this failure history. +Twelve added regression cases cover exact native JSON correspondence, including +substitutions with both retained and recomputed served digests, without +changing the native digest algorithm, request/PDP digest contract or API schema. +Canonical integration/publication and the Platform mediator's final provider +client source pin remain with the coordinator; preparation does not claim them. + +Existing APPROVAL-WP-0004 closure and generated identifiers are untouched. +APPROVAL-WP-0002-T01/T04/T05 remain existing live native acceptance obligations; +none is promoted or completed here. No production auth, approval mutation, +credential, deployment or backend operation was used. This completed low-risk +local source repair introduces no residual native obligation beyond those +existing records and the coordinator's integration of this exact artifact.