From 16c1d46d5d2e9fc784e47573aa9b2a4b76464723 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 9 Sep 2026 23:20:42 +0200 Subject: [PATCH] Rule the tenant-provenance question: bounded gap, and the claim must say which MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit key-cape has no adapter populating the directory user tenant, so every human token fell back to the default and approval-engine refused it by exact match — presenting as a failed approval rather than as a registration defect. It built a registration-bound resolution, then declined to ratify its own design because the second option writes a cross-tenant capability into the issuer. That reading was right and the question is ours: what may be a source of a principal's identity is a Core Rule, not a runbook. Directory-sourced is the terminal state. A registration is a statement about the actor; a tenant is a property of the principal; sourcing the second from the first collapses two identities the estate keeps distinct, in the direction that widens. The registration-bound shape is admissible anyway, as a declared bounded gap, and the reason is not that the design is careful. It is that the case distinguishing it from the correct resolution fails closed: where registration and directory disagree, issuance is refused rather than resolved either way. And the same code turns from supplying the zone into enforcing agreement with it the moment the directory carries tenants, so it converges by subtraction. That general property is section 3 and neither request asked for it. 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. It is section 8's asymmetry applied to transitions, and it is what makes this grant and GH-DEC-2026-011's decline one rule rather than two defensible calls: a promise that fails open is a permission, a promise that fails closed is a gap, and only the second is a thing a register can hold. Two conditions strengthen what key-cape wrote about itself. Refusal on disagreement is normative, including against a future change that prefers the directory — picking any winner converts a refusal into a silent cross-tenant assertion. And lifting the dynamic-registration exclusion voids the rule rather than reopening it; key-cape wrote "must be revisited", which implies the answer might survive review, and it would not. The finding they did not ask for is section 5. The tenant claim is a bare string, so a consumer cannot tell a tenant the directory asserted about the person from one a registration supplied about the client they came through. approval-engine exact-matches that string and is relying on the second while its contract reads as the first. That is GH-DEC-2026-010 one layer down — a sound check whose reader infers a property it does not carry. The claim must carry its provenance; the mechanism is key-cape's. Gap registered at net-kingdom@f9e1611. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012viPor8WJNCbV64ipwewrm Assistant: claude-code Assistant-Model: opus Assistant-Process: 1754332@bnt-lap001 Assistant-Session: 9c8ac536-ff5e-46a3-8ab1-a548bde25fc0 --- decisions/decisions.md | 211 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) 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.