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:
parent
8624fa20bb
commit
eece796b8b
3 changed files with 98 additions and 3 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
44
workplans/ADHOC-2026-10-16.md
Normal file
44
workplans/ADHOC-2026-10-16.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue