Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
9 KiB
OIDC browser-client registration — informed-decision approver surface
Workplan task: INFD-WP-0001-T07
For: key-cape (KEY-WP-0013-T02, KEY-WP-0013-T05)
Status: submitted 2026-09-10 (INFD-WP-0001-T07 done). The
origin is live. Registration rollout and a native human token accepted by
Approval Engine remain T08; the browser login implementation is described in
browser-authentication.md.
Rulings: GH-DEC-2026-012, GH-DEC-2026-013
Contracts cited: key-cape/docs/approval-engine-auth-contract.md,
key-cape/docs/tenant-claim-contract.md,
approval-engine/docs/approver-surface-requirements.md
1. What this closes
On 2026-09-08 key-cape asked approval-engine for the human approver client's
client_id and full callback URI. approval-engine correctly declined to
invent them: it is a bearer-token resource server with no Ingress, no redirect
endpoint, and no browser client. It recorded that the two strings belong to
whichever browser-facing approver UI presents approval:approve tokens to it.
That component is this repository. These are the two strings.
2. The two strings
| Field | Value |
|---|---|
client_id |
informed-decision-approver |
| Redirect URI | https://decisions.coulomb.social/auth/callback |
client_id is fixed and stable. It will not change.
The origin is settled. 2026-09-10: the operator assigned
decisions.coulomb.social to this surface. It is not the
decide.coulomb.social this document proposed — the assigned name is the one
that counts, and it is recorded here rather than the proposal, because
redirects match exactly at /authorize and a near-miss fails closed. That
exactness is why approval-engine refused to guess in the first place, and it
is the reason the proposed name is corrected here before submission rather
than reconciled afterwards.
- Assigned:
decisions.coulomb.social, givinghttps://decisions.coulomb.social/auth/callback. - Consistent with the estate's existing pattern (
forgejo.coulomb.social,hub.coulomb.social,kc.coulomb.social), TraefikingressClassName, TLS secret per host. - DNS resolves to the cluster address as of 2026-09-10.
- Delivered by
railiance-apps: Ingress and issued certificate, applied 2026-09-10 (manifests/informed-decision-ingress.yaml).
The host answers. This document has been submitted. 2026-09-10 14:32 UTC:
railiance-apps applied the Ingress and cert-manager issued a Let's Encrypt
certificate for decisions.coulomb.social;
GET https://decisions.coulomb.social/auth/callback returns 200 over a
verified chain. The condition this document set for its own submission —
that the origin be real rather than plausible — is met. The path component
/auth/callback was fixed ahead of the host, so nothing else in this
registration moved.
The path is currently served by a placeholder while
INFD-WP-0001-T08 is built. That does not affect the registration: key-cape
matches the redirect URI as a string at /authorize and never fetches it.
3. Client shape
| Property | Value |
|---|---|
| Grant | Authorization code |
| PKCE | Required, S256 |
| Client type | Public |
| Redirect matching | Exact |
| Scopes | openid, approval:read, approval:approve |
mfaRequired |
true |
approval:read is required and was missing from the original specification.
Without it this surface could submit an approval entry it was never able to
display — raised by approval-engine, confirmed by key-cape, now in their
published contract.
approval:consume is never requested. approval-engine refuses it for human
principals, and consumption belongs to the PEP that causes the side effect
(GH-DEC-2026-003).
4. Expected access token
Verified by approval-engine (RS256 against the issuer's /jwks):
| Claim | Value |
|---|---|
aud |
approval-engine — the resource server, never the OAuth client id |
tenant |
tenant:platform — exact string equality, no alias or prefix handling |
principal_type |
human |
sub |
non-empty; becomes the approver identity in the entry |
scope |
non-empty, containing the exact scope for the route |
assurance |
a non-null JSON object — shape below |
iss, exp, iat |
verified, 30s leeway |
The access token is the evidence. id_token is never presented to
approval-engine and appears nowhere in that codebase.
assurance
The shape is key-cape's, defined in their
docs/approval-engine-auth-contract.md — level (aal1|aal2), methods,
mfa, source, at. Cited here rather than restated, so it cannot drift.
Two properties matter to this surface and are recorded rather than assumed:
atis the authentication time, not the token mint time.key-capefixed a defect here on 2026-09-09; the two differ by hours when a browser session is reused, andapproval-enginestores this object verbatim as the only downstream record that MFA happened. Freshness is judged by comparingat, never by assuming it.key-capeemitsassuranceon every human token and never empty. This surface nevertheless requires the object to carry authentication facts rather than relying on it always arriving populated —approval-engineaccepts an empty object, so an empty one would produce an entry evidencing nothing.
5. Tenant provenance — declared, not assumed
This registration declares a tenant the directory record does not carry, and says so here rather than letting it read as a directory-asserted fact.
GH-DEC-2026-013 ruled directory-sourced tenancy the terminal state and granted
key-cape's registration-bound shape (329e48f) as a declared bounded gap —
admissible because its distinguishing case fails closed: registration and
directory disagreeing refuses issuance with 403 tenant_binding rather than
relabelling.
Nothing populates domain.User.Tenant for approver users. So a registration
declaring tenant:platform reaches the token by the gap route by
construction, not by accident. Gate House was explicit: do not declare a tenant
the directory does not carry unless prepared to say so in the record.
Consequences accepted here:
- This is transitional. When the directory carries tenants, the same
key-capecode stops supplying and starts enforcing agreement, with no second migration. This registration is expected to survive that unchanged. - The claim is stored with its provenance. Current
key-capeemitstenant_source:directory,registration, ordefault. The browser adapter preserves the first two as distinct routes and refuses missing, unknown or default provenance for this platform approver flow. It does not relabel registration-supplied tenancy as a fact asserted by the directory. SeePR-09andkey-cape/src/internal/server/oidc/token.go. - The tenant claim is never used as the act-scope.
binding.targetis the act-scope and is committed insideview_hash. Two different facts — which scope this act enters, and which tenant this person belongs to — must not share one field here just because they share one upstream. SeeEvidenceModel.md§8c andPR-08.
6. What this surface does with the token
GET /v1/approvals/{id}and/claim—approval:read, to render.POST /v1/approvals/{id}/entries—approval:approve, to record a binding.- Never
/consume. - Never any path containing
checkor ending/authorize— they 404 by design, and there is no "may this person approve" surface to call.
A 200 from approval-engine is not entitlement to view. A decision is
obtained from access-engine before an approval is rendered to a person
(PR-05) — noting that decision attributability is a declared open gap
(GH-DEC-2026-010, FLEX-WP-0024) and this path is not described as validated
while it stands.
7. Outstanding
| # | Item | Owner |
|---|---|---|
| 1 | decisions.coulomb.social |
railiance-apps / deployment |
| 2 | key-cape |
this repo |
| 3 | Deploy/admit the registration and prove a native human token is accepted by approval-engine |
T08, KEY-WP-0013-T05, APPROVAL-WP-0002-T01 |
8. Humanity provenance correction — 2026-09-10
The earlier assertion that principal_type: human came from registration was
incorrect for the current KeyCape implementation (f9812ab). Its code exchange
consumes a user-authenticated PKCE session, looks up that user and emits the
literal human; its client-credentials handler emits service.
oidc.py verifies the fixed issuer, signature, audiences, nonce, paired subject,
exact scopes, tenant provenance and MFA facts before assigning the
authentication-derived route. The generic provenance guard continues refusing
registration-only assertions. This is a checked source contract, not a claim
that the current deployed issuer or a native human login has been verified here.