fix: confirm exact canonical consumed act binding

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 09:20:33 +02:00
parent 8624fa20bb
commit eece796b8b
3 changed files with 98 additions and 3 deletions

View file

@ -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:

View file

@ -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

View file

@ -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.