diff --git a/docs/approval-claim.md b/docs/approval-claim.md index f22a266..b9a1329 100644 --- a/docs/approval-claim.md +++ b/docs/approval-claim.md @@ -79,6 +79,38 @@ change that JSON and therefore the digest. A decision rendered against approval A for request R cannot be replayed for request R' if the consumer compares digests. +### What this digest is not — answering `INFD-IN-0001` R3 + +`informed-decision` asked whether its `view_hash` and this digest are the same +hash, since both are described as canonicalizing a binding. **They are not, and +they must not be merged.** Three hashes, three questions: + +| Hash | Preimage | Answers | +| --- | --- | --- | +| `binding.digest` (here) | five fields: `action`, `actor`, `principal`, `purpose`, `target` | *which act* is approved | +| `binding.pdp_digest` | the PDP's own `NewDecisionBinding` digest, recorded at issue | *which decision request* this approval corresponds to | +| `view_hash` (`informed-decision`) | a canonicalized binding document additionally covering brief, packet, highlights, locale and UI release | *what a person was shown* when they bound themselves | + +The overlap is only that all three canonicalize *something*. This engine's +digest deliberately covers five fields and no more: it exists so that +wrong-action, wrong-target and wrong-scope are distinguishable, and it has no +opinion about presentation, which does not exist for a machine-to-machine +approval. Widening it to cover a brief or a locale would change the digest of +an act whose act did not change. + +This repository already keeps two digests apart for exactly this reason rather +than as a matter of taste — `binding.digest` and `pdp_digest` answer different +questions and are stored separately precisely so that a reader cannot infer one +property from the other. A third hash answering a third question is the same +pattern, not a duplication of it. + +**The recommended relationship, which avoids two canonicalizations of one act:** +`view_hash`'s binding document should *carry this digest as a field* rather than +re-canonicalize `action`/`actor`/`principal`/`purpose`/`target` itself. Then +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. + ### Mapping from a flex-auth CheckRequest | Claim binding | CheckRequest | diff --git a/docs/caller-authentication.md b/docs/caller-authentication.md index 75694ad..f7a5d03 100644 --- a/docs/caller-authentication.md +++ b/docs/caller-authentication.md @@ -8,6 +8,30 @@ mode is explicit, file-backed, and refused with `--production`. The verified `tenant` must exactly match the service's configured store tenant; cross-tenant reads and mutations are rejected before object lookup. +**This check is store isolation, and it is not a statement about the person.** +It answers "does this caller belong to the store this engine serves", not "is +this human a member of the platform zone". `GH-DEC-2026-013` rules that a +key-cape human tenant claim may be supplied by the client registration rather +than asserted by the directory, and forbids a consumer treating the two as +equivalent for a decision turning on a fact about the person. Exact string +equality cannot see that difference — a string that matches exactly matches +whoever asserted it — so the difference is stated here instead. + +Consequences this engine accepts deliberately: + +- A registration-supplied tenant **is** admissible for admission to this store. + Admission means "arrived through a channel the platform registered", the + approver client is static and deployment-owned, and dynamic client + registration is excluded from key-cape by design. +- A registration-supplied tenant is **not** admissible for any doctrine that + turns on the approver's own membership — "an approver must be a member of the + platform zone" is a fact about the person, and this claim cannot carry it. + No such doctrine exists today; if `gate-house` issues one, it needs a + directory-sourced claim and this check does not become that claim by matching. +- When key-cape emits provenance alongside the tenant, this engine records it on + the approver entry the way schema v4 records `principal_type` — an evidence + reader should not have to infer provenance from a string that cannot carry it. + | Route | Required scope | |---|---| | create approval | `approval:create` | 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 5085ec0..61deffb 100644 --- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md @@ -204,6 +204,39 @@ a service bind legible, but legibility is not permission, and the should-it question remains gate-house's. `secrets-engine-approval` is unaffected: it has a real presenter and is the client that unblocks T05. T01 stays `progress`. +2026-09-10: three inbound items, all answered; one is a live blocker on the +human client and it is not ours to fix. (i) `key-cape` reports that a human +access token **cannot carry `tenant:platform` today**: `effectiveTenant(user)` +resolves the claim from the directory record, `domain.User.Tenant` is populated +by no adapter, and every human token falls back to `tenant:coulomb`; the +per-client `tenant` field is read only on the client-credentials path. Our exact +equality would refuse such a token, surfacing as a rejected approval rather than +a registration defect. We answered **(2) registration-bound**, matching decision +`5ed3fb35-eca9-413a-82b9-95171ba85bf6` and symmetric with the service clients, +and only because registrations are static, deployment-owned, and dynamic client +registration is excluded by design. (ii) `gate-house` `GH-DEC-2026-013` names +the consequence of exactly that: a registration-supplied tenant is the client's +say-so, not the directory's, and exact matching cannot see the difference — the +same shape as our own `pdp_digest` finding one layer down. They were right that +our wording implied a fact about the person. `docs/caller-authentication.md` now +states that the check is **store isolation** and not membership, that a +registration-supplied tenant is admissible for admission but **not** for any +future doctrine turning on the approver's own membership, and that provenance +will be recorded on the entry the way v4 records `principal_type` once the claim +carries it. (iii) `informed-decision` R3, answered in `docs/approval-claim.md`: +`view_hash` and `binding.digest` are **not** the same hash and must not be +merged. Three hashes, three questions — which act (five fields), which decision +request (`pdp_digest`), what a person was shown (`view_hash`). Recommended that +their binding document *carry* our digest as a field rather than re-canonicalize +the same five fields, so the estate has one canonicalization of the act with a +detectable mismatch instead of two independently correct answers. Also accepted +from key-cape: `approval:read` added to the human client, and a documented +`assurance` shape (`level`/`methods`/`mfa`/`source`/`at`) — with `at` fixed to +carry the original authentication instant through session reuse, which matters +here because this engine stores that object verbatim as the only downstream +record that MFA happened. T01 stays `progress`; the human client cannot be +registered until the tenant source is settled by its owner. + ## Harden durable storage and migrations ```task