From a0a602976eef818f36dde35f76f7f2e589bd051b Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 10 Sep 2026 22:43:33 +0200 Subject: [PATCH] Principal is the requesting party, not the approver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit informed-decision asked whether its binding.principal and ours name the same field, since both GH rulings said its slice canonicalizes "two of the five". They do not: ours is the party on whose behalf the act is performed, taken from the decision request's subject, while theirs is the approver — which appears here only as an entry and never in binding. Only target overlaps. Records the four-role distinction normatively in docs/approval-claim.md and guards it with a test asserting the act digest is insensitive to the approver while the approver stays recorded on the entry. Folding approver identity into the digest would now break view_hash's assumption here rather than silently in that repository. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QDzGbdDFnVJBxDgdp7RvpH Assistant: claude-code Assistant-Model: opus Assistant-Process: 2191554@bnt-lap001 Assistant-Session: d69bb7c3-b7b2-41c4-8287-6baef48c0993 --- docs/approval-claim.md | 27 ++++++++++++++++++ tests/test_claim_contract.py | 28 ++++++++++++++++++- ...duction-readiness-and-consumer-adoption.md | 27 ++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/docs/approval-claim.md b/docs/approval-claim.md index f28cc74..01199d4 100644 --- a/docs/approval-claim.md +++ b/docs/approval-claim.md @@ -132,6 +132,33 @@ there is exactly one canonicalization of the act, computed here, referenced by the presentation hash — and a mismatch is detectable rather than being two independently correct answers about the same approval. +### `principal` is the party on whose behalf, not the approver + +`informed-decision` asked (`principal_role_overlap`) whether this engine's +`binding.principal` and its own `binding.principal` name the same field, since +both rulings used the shorthand that its binding slice canonicalizes "two of the +five". **They are different roles, and the shorthand covered only one of them.** + +| Field | Role | Shape | +| --- | --- | --- | +| `binding.principal` (here) | the party **on whose behalf** the act is performed — the requesting side, taken from the decision request's subject | scalar identifier | +| `binding.actor` (here) | the identity that may **use** the approval | scalar identifier | +| approver | the identity that **bound itself** to the act, with its verified `principal_type` and assurance | an entry, one per approver, never part of the binding | +| `binding.principal` (`informed-decision`) | the person **being bound** — the approver | party object | + +Only `target` overlaps between the two binding slices. The approver does not +appear in this engine's binding at all, and therefore does not enter +`binding.digest`: two approvals of the same act bound by different people share +one digest. A presentation hash that must commit to *who was shown this* cannot +obtain that from our digest, so `view_hash` keeping its own approver field is +correct and is not a second canonicalization of the act. + +`tests/test_claim_contract.py::test_approver_identity_is_not_in_the_act_digest` +asserts both halves: the digest is insensitive to the approver, and the approver +remains separately recorded on the entry. A future change that folded approver +identity into the digest would break `informed-decision`'s assumption here +rather than silently in that repository. + ### Mapping from a flex-auth CheckRequest | Claim binding | CheckRequest | diff --git a/tests/test_claim_contract.py b/tests/test_claim_contract.py index 18607c6..b8b743a 100644 --- a/tests/test_claim_contract.py +++ b/tests/test_claim_contract.py @@ -2,7 +2,7 @@ import json from pathlib import Path from approval_engine.binding import binding_digest -from tests.conftest import approve, binding +from tests.conftest import approve, binding, validity ROOT = Path(__file__).resolve().parents[1] @@ -89,3 +89,29 @@ def test_presentation_changes_cannot_change_the_approved_act(engine): }.items(): other = approve(engine, extra_binding={**act, **presentation, field: replacement}) assert engine.claim(other.id)["binding"]["digest"] != original_claim["binding"]["digest"] + + +def test_approver_identity_is_not_in_the_act_digest(engine): + """`binding.principal` is the party on whose behalf, never the approver. + + Answers `informed-decision`'s `principal_role_overlap` question. The + approver is recorded as an entry, so two approvals of the same act bound by + different people share one digest — which is why a presentation hash that + must commit to *who was shown this* cannot get that from our digest. + """ + act = binding() + first = engine.create(act, validity(), required_count=1) + first = engine.add_entry(first.id, "user:approver-a") + second = engine.create(act, validity(), required_count=1) + second = engine.add_entry(second.id, "user:approver-b") + + expected = binding_digest(act) + assert engine.claim(first.id)["binding"]["digest"] == expected + assert engine.claim(second.id)["binding"]["digest"] == expected + + # The approver is still distinguishable — recorded, not hashed into the act. + assert [e["subject_id"] for e in first.entries] == ["user:approver-a"] + assert [e["subject_id"] for e in second.entries] == ["user:approver-b"] + + # And changing who the act is *for* does change the digest. + assert binding_digest(binding(principal="someone-else")) != expected diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md index 4534719..b26d6c5 100644 --- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md @@ -275,6 +275,33 @@ must not be treated as satisfying it. Evidence: `docs/evidence/2026-09-10-human-control.json`. No production approval or database migration occurred during these tests. +2026-09-10 (third): `informed-decision` activated GH-DEC-2026-015 — `view_hash` +now carries our `binding.digest` and no longer canonicalizes the act-scope, so +the estate has one canonicalization of the act. They verified our exclusion +condition by running the test themselves rather than taking it on report, and +asked one open question (`principal_role_overlap`). Answered: **the two +`binding.principal` fields are different roles and theirs must stay.** Ours is +the party *on whose behalf* the act is performed, taken from the decision +request's subject; `actor` is who may use the approval; the approver is an entry +and is not in `binding` at all. Only `target` actually overlaps, so the "two of +the five" shorthand now carried in two rulings was wrong about principal. +`docs/approval-claim.md` records the four-role distinction normatively, and +`tests/test_claim_contract.py::test_approver_identity_is_not_in_the_act_digest` +asserts both halves — the digest is insensitive to the approver, and the approver +stays recorded on the entry. It is a regression guard, not a fix: folding +approver identity into our digest would now break their hash loudly here rather +than silently there, which is the narrowing half of the two-sided pinning they +credited on the act-scope. Not added to `layer.yaml`: that file is the +security-layer-model declaration and has no invariant-ledger section. +Also answered their flag that `principal_type: human` is the same shape as the +GH-DEC-2026-013 tenant gap — it is not today (`effectiveTenant` reads a +registration field; the reviewed KeyCape source sets `human` only on the +PKCE/user path, and no registration field supplies it), but the distinction rests +on a source review rather than production proof, which is exactly the native-proof +item T01 still owns. Suite: 156 tests pass. T01 stays `progress`; the remaining +work is unchanged — exact requester/human registration, gated on key-cape +settling the human tenant source, and native issuer proof with a real human flow. + 2026-09-10 (second): key-cape reports the `approval-engine-operator` withdrawal came too late — the client was provisioned and verified in the 2026-09-09 attended custody window, secret in the namespace and wired into a pod. A live