diff --git a/decisions/decisions.md b/decisions/decisions.md index cfbb338..650266b 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -1833,3 +1833,214 @@ Requested by `informed-decision` (`INFD-IN-0001`, hub intake `01a08610`) before its architecture, with three candidate rulings, their costs, the self-dealing objection argued against itself, and an explicit list of what it was *not* asking for. Unblocks `INFD-WP-0001` T05 and T07, and `KEY-WP-0013-T02`. + +## GH-DEC-2026-013 — A client registration may supply a human principal's tenant only as a declared bounded gap, and the claim must carry its provenance + +```yaml +id: GH-DEC-2026-013 +kind: decision +title: A client registration may supply a human principal's tenant only as a declared + bounded gap, and the claim must carry its provenance +status: resolved +owner: Bernd Worsch +repo: gate-house +standard: net-kingdom/canon/standards/security-layer-model_v0.8.md +source_note: key-cape/docs/tenant-claim-contract.md and src/internal/server/oidc/token.go + humanTenant (KEY-WP-0013-T05); relayed by informed-decision +requested_dispositions: +- approved +- revised +- rejected +affects: +- gate-house +- key-cape +- approval-engine +- access-engine +- user-engine +- informed-decision +- net-kingdom +- ops-warden +decided_by: Bernd Worsch +rationale: 'key-cape has no adapter populating the directory user tenant, so every + human token falls back to the default tenant and approval-engine refuses it by exact + match. Two resolutions were possible: source the tenant from the directory, or let + a client registration bind it. Directory-sourced is the terminal state, because + a tenant is a property of the principal and a registration is a statement about + the actor; sourcing a principal attribute from the actor collapses two identities + the estate keeps distinct, and supplying a tenant the directory has not assigned + widens rather than attenuates. The registration-bound shape is nonetheless admissible + as a declared bounded gap rather than refused, because its distinguishing case fails + closed: where the registration and the directory disagree, issuance is refused rather + than resolved in either direction. That is the general property this record states + — a transitional shape is admissible where it fails closed on exactly the case that + distinguishes it from the correct resolution, and inadmissible where it fails open + there, which is why GH-DEC-2026-011 declined ops-warden a transitional fail_open + and this record grants key-cape its transitional binding. Three conditions attach: + refusal on disagreement is normative rather than a design choice, the exclusion + of dynamic client registration is a normative precondition whose lifting voids the + rule rather than reopening it, and the gap is registered with an owner for the directory + adapter. One finding is added that was not asked for: the tenant claim is emitted + as a bare string, so a consumer cannot tell a tenant the directory asserted about + the person from one a registration supplied about the client, and a consumer relying + on it for the first is relying on the second. The claim MUST carry its provenance.' +created: '2026-09-09T21:18:11.679012Z' +updated: '2026-09-09T21:18:11.679012Z' +``` + +## Context + +`key-cape` binds the approval chain to the landlord zone (`tenant:platform`), and +`approval-engine` compares the tenant claim by exact string equality. **No adapter +populates `domain.User.Tenant`**, so every human token fell back to `tenant:coulomb` and +an approver token would have been refused downstream — presenting as a *failed approval* +rather than as a *registration defect*, which is the failure mode that makes this worth +ruling on rather than leaving to implementation. + +Two resolutions were available: source the tenant from the directory, or let a client +registration declare it. `key-cape` declined to choose unilaterally on the ground that +the second writes a cross-tenant capability into the issuer, which reads as doctrine +rather than implementation. That reading is correct and the question is Gate House's: +it is about what may be a source of a principal's identity, which is a Core Rule, not a +runbook. + +It reached us relayed by `informed-decision`, which was asking about its own client +registration and passed on a question that was not its own. `key-cape` has already +built the registration-bound shape, and built it carefully — this record rules on a +design that exists rather than on a proposal. + +## Decision + +### 1. Directory-sourced is the terminal state + +**A principal's tenant is sourced from the identity layer's record of the principal.** +A client registration is a statement about the **actor** — the client the person +authenticated through. A tenant is a property of the **principal**. Sourcing the second +from the first collapses two of the three identities the estate keeps distinct +(principal, actor, runtime identity), and it collapses them in the direction that +widens: a registration supplying a tenant the directory has not assigned gives the +principal an attribute the identity layer never asserted about them. **Delegation +attenuates and never widens.** + +The condition being routed around is a missing adapter, not an absent owner. A missing +implementation in the identity layer is fixed in the identity layer. + +### 2. The registration-bound shape is admissible as a declared, bounded gap + +**Not refused, and not blessed as permanent.** `key-cape` may ship what it has built, +under §3's conditions and registered as a gap. + +The reason it is admissible rather than forbidden is precise, and is not that the design +is careful. It is that **the case distinguishing it from the correct resolution fails +closed.** Where the registration declares a zone and the directory has placed the user +in a different one, `humanTenant` refuses to issue rather than picking a winner. Either +answer would be a silent cross-tenant assertion, and the design says so. A registration +can bind a zone for a user the directory has not placed; it can never relabel one it has. + +`key-cape`'s further observation is what makes the gap bounded rather than open-ended: +the same code turns from *supplying* the zone into *enforcing agreement with* it the +moment the directory carries tenants — no second migration, and no window in which a +stale registration silently wins. A transitional shape that converges on the terminal +state by subtraction is a different object from one that will have to be unwound. + +### 3. The general property, which is why this is granted and `GH-DEC-2026-011` was declined + +**A transitional shape is admissible where it fails closed on exactly the case that +distinguishes it from the correct resolution, and inadmissible where it fails open +there.** + +This is §8's asymmetry applied to transitions, and it is the rule that keeps these two +decisions consistent rather than merely both defensible: + +- `ops-warden` asked for a dated transitional `unknown: fail_open` (`GH-DEC-2026-011`). + **Declined.** The distinguishing case — an unclassified subject reaching an unreachable + engine — is exactly where the transitional shape fails **open**. It is behaviourally + identical to the violation for the whole of its life, so the transition licenses the + thing the rule forbids and merely dates it. +- `key-cape` binds a tenant at registration. **Granted.** The distinguishing case — + registration and directory disagreeing — fails **closed**. The transitional shape and + the terminal state differ only where nobody is served either way. + +A transition is a promise about the future. **A promise that fails open is a permission; +a promise that fails closed is a gap.** Only the second is a thing a register can hold. + +### 4. Three conditions + +1. **Refusal on disagreement is normative.** Row four of `key-cape`'s table is not a + design choice it may later optimise. A registration MUST NOT relabel a + directory-placed principal, and MUST NOT resolve the conflict in either direction. + Any future change that picks a winner — in either direction, including preferring the + directory — voids this permission, because it converts a refusal into a silent + cross-tenant assertion. +2. **The exclusion of dynamic client registration is a normative precondition, and + lifting it VOIDS this rule rather than reopening it.** `key-cape` writes that a + self-service client able to name its users' tenant would be a straightforward + escalation, and that the rule "must be revisited" if the exclusion is lifted. That is + too weak and we are strengthening it: revisiting implies the answer might survive + review. It does not. If dynamic client registration is ever admitted, the + registration-bound shape is void on that day and the directory becomes the only + source, whatever state the adapter is in. Stated that way so that lifting the + exclusion cannot be a small change made by someone who never reads this record. +3. **Registered as a declared gap under §13**, with an intended owner for the directory + adapter, so it cannot quietly become the permanent answer by nobody minding it. Per + §13, an intended owner is a proposal **to** a repository, not an assignment **onto** + it; this record does not name one. + +### 5. The finding that was not asked for: the claim must carry its provenance + +`key-cape` emits `tenant` as a bare string. A consumer cannot tell a tenant **the +directory asserted about the person** from a tenant **a registration supplied about the +client they came through**. `approval-engine` admits an approver by exact-matching that +string, and it is therefore relying on the second while its contract reads as though it +relies on the first. + +**This is the shape of `GH-DEC-2026-010`, one layer down.** There, a PEP held a decision +and could establish which request it was for but not who issued it, while a mechanical +test read as though it discharged both. Here a consumer holds a tenant and can establish +that the string matches, but not whether the identity layer ever said it about this +person. In both cases the check is sound and the property a reader infers from it is not +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*. +Exact-match admission on a registration-supplied tenant is admission on the client's +say-so. + +The mechanism is `key-cape`'s: this record names the property, not the field. Note that +the same rule from `GH-DEC-2026-011` §3 applies to how it is recorded — two sources, one +claim value, and the record must distinguish them even though the string is identical. +It is `unknown` and `absent` again, in the identity layer. + +**Sequencing:** this does not gate `KEY-WP-0013-T02`. The registration may be created +under §2 today. What must not happen is the path being described as validated while a +consumer cannot see which fact it is relying on. + +## What this does not rule + +Not ruled: whether `approval-engine`'s exact-match admission is the right gate, which is +`approval-engine`'s. Not ruled: which repository owns the directory adapter. Not ruled: +the choice of `tenant:platform` itself, which is settled by operator decision +`5ed3fb35-eca9-413a-82b9-95171ba85bf6` and is not doctrine. + +## Reversal condition + +§2 reverses on the day the directory populates the principal's tenant: the permission is +spent, not merely unused, and §1 governs alone. + +§3's general property reverses if a case is found where a transitional shape failing +closed on its distinguishing case is nonetheless the more dangerous option — which would +mean the asymmetry rule has an exception, and §8 would be the thing under review rather +than this record. + +§5 reverses if the two provenances are ever proven indistinguishable in effect, i.e. if +no consumer's decision turns on the difference. That is a claim about every present and +future consumer and we do not expect it to be made good. + +## Provenance + +Raised by `key-cape` (`KEY-WP-0013-T05`, `docs/tenant-claim-contract.md`), which built +the shape, documented its own escalation risk, and declined to ratify its own design. +Relayed by `informed-decision` while asking about its own registration — a repository +passing on a question that was not its to carry. `GH-DEC-2026-011`'s decline and this +record's grant are the two halves of §3, which neither request asked for and which is +the part of this record most likely to matter later.