the-custodian/docs/changes/netkingdom-target-2026-10-16/approval-confirmation/implementation.patch
codex 3add9f8790
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 25s
docs: preserve reviewed NetKingdom session artifacts
2026-11-01 22:45:01 +01:00

140 lines
6.8 KiB
Diff

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.