diff --git a/decisions/decisions.md b/decisions/decisions.md index 8312a13..da4a844 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -2004,6 +2004,18 @@ present. **The `tenant` claim MUST carry its provenance** — whether the value was asserted by the directory about the principal, or supplied by the registration — and a consumer MUST NOT treat the two as equivalent for any decision that turns on a fact about the *person*. + +**Amended 2026-09-10 — `key-cape` emitted three values where this record named two, and +it was right to.** Its third is `default`: nobody asserted a zone and the profile's +non-empty fallback supplied one. Folding that into `directory` would have reproduced this +very finding one level down, since a consumer would read an assertion the identity layer +never made. `key-cape` applied A-16 to a route this record did not examine, and asked to +be corrected rather than assume — the correction is that there was nothing to correct. + +Its second judgement is also endorsed: where registration and directory **agree**, the +value resolves to `directory`, because the directory did assert it about the person and +reporting the weaker source would understate what is known. That also makes the claim +strengthen on its own the day the adapter lands, with no reissue. Exact-match admission on a registration-supplied tenant is admission on the client's say-so. @@ -2035,7 +2047,35 @@ same fact as *which tenant is this person a member of*, which is a property of t The trouble is that one claim named `tenant` is being asked to carry both. That is why the registration-bound shape feels correct to `informed-decision` and wrong to the -identity layer: each is looking at a different fact through the same field. A binding +identity layer: each is looking at a different fact through the same field. + +**Amended 2026-09-10: there are three facts, not two.** `approval-engine` answered this +record by naming the one it actually uses, and it is neither of the two above: + +1. **Store isolation** — *did this caller arrive through a channel this store serves.* A + property of the **channel**. This is what `approval-engine`'s exact-match gate tests, + and it is what a registration-supplied tenant **is** adequate for: admission means + arrived through a registered channel, the approver client is static and + deployment-owned, and dynamic client registration is excluded. +2. **Act scope** — *which scope is this act being entered into.* A property of the + **act**. `informed-decision`'s binding slice needs this, and it resolved the point by + finding that its own `binding.target` already commits it — so the ruling required no + new field, only a statement that `binding.target` is the act-scope and MUST NOT be + derived from the token's `tenant` claim. +3. **Principal membership** — *is this person a member of this zone.* A property of the + **principal**. This is the one a registration-supplied tenant cannot carry, and the + one §1 reserves to the directory. + +`approval-engine` committed to the consequence without being asked: a registration- +supplied tenant is admissible for (1) and **not** for any doctrine turning on (3), and it +asked to be held to that as *"the point at which a sound check silently becomes an unsound +one"*. Held. Note `GH-DEC-2026-016` §5 is the first place it bites. + +Three facts in one field is a stronger case for §5 than two was. It also explains why +each party's intuition was locally correct: `approval-engine` reading (1) was right that +the registration suffices, `informed-decision` reading (2) was right that the fact is +act-scoped, and the identity layer reading (3) was right that a registration cannot supply +it. Nobody was wrong about their own fact; the field was wrong to hold all three. A binding slice that must commit the scope being entered should **commit that scope**, not borrow the principal's membership claim to stand in for it.