Write the key-cape client registration contract (T07 progress)
Everything except the host is now fixed: client_id informed-decision-approver,
callback path /auth/callback, authorization-code + S256 PKCE public client,
scopes [openid, approval:read, approval:approve], the expected token shape, and
the assurance shape cited from key-cape's contract rather than restated so it
cannot drift.
Section 5 declares the tenant provenance rather than assuming it. Nothing
populates domain.User.Tenant for approver users, so declaring tenant:platform
reaches the token by the gap route by construction rather than by accident.
GH-DEC-2026-013 was explicit that a tenant the directory does not carry must not
be declared unless we are prepared to say so in the record — so it is said,
with the two consequences: the claim is stored with its provenance, and it is
never used as the act-scope.
Deliberately NOT submitted. The origin needs a DNS A record, an Ingress manifest
and a certificate, which is work in railiance-apps rather than here.
decide.coulomb.social is proposed and consistent with the estate's existing
Traefik/TLS-per-host pattern, but proposing a plausible hostname is not the same
as owning one, and submitting a redirect for a host that does not resolve is the
exact failure approval-engine avoided by refusing to invent these strings.
T07 stays progress with the deployment dependency named and owned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 08:06:10 +02:00
|
|
|
# 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:** **draft — not yet submitted.** One input outstanding: the deployed
|
|
|
|
|
origin (§2). Everything else is fixed and stable.
|
|
|
|
|
**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` |
|
2026-09-10 16:17:52 +02:00
|
|
|
| Redirect URI | `https://decisions.coulomb.social/auth/callback` |
|
Write the key-cape client registration contract (T07 progress)
Everything except the host is now fixed: client_id informed-decision-approver,
callback path /auth/callback, authorization-code + S256 PKCE public client,
scopes [openid, approval:read, approval:approve], the expected token shape, and
the assurance shape cited from key-cape's contract rather than restated so it
cannot drift.
Section 5 declares the tenant provenance rather than assuming it. Nothing
populates domain.User.Tenant for approver users, so declaring tenant:platform
reaches the token by the gap route by construction rather than by accident.
GH-DEC-2026-013 was explicit that a tenant the directory does not carry must not
be declared unless we are prepared to say so in the record — so it is said,
with the two consequences: the claim is stored with its provenance, and it is
never used as the act-scope.
Deliberately NOT submitted. The origin needs a DNS A record, an Ingress manifest
and a certificate, which is work in railiance-apps rather than here.
decide.coulomb.social is proposed and consistent with the estate's existing
Traefik/TLS-per-host pattern, but proposing a plausible hostname is not the same
as owning one, and submitting a redirect for a host that does not resolve is the
exact failure approval-engine avoided by refusing to invent these strings.
T07 stays progress with the deployment dependency named and owned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 08:06:10 +02:00
|
|
|
|
|
|
|
|
**`client_id` is fixed and stable.** It will not change.
|
|
|
|
|
|
2026-09-10 16:17:52 +02:00
|
|
|
**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`, giving
|
|
|
|
|
`https://decisions.coulomb.social/auth/callback`.
|
Write the key-cape client registration contract (T07 progress)
Everything except the host is now fixed: client_id informed-decision-approver,
callback path /auth/callback, authorization-code + S256 PKCE public client,
scopes [openid, approval:read, approval:approve], the expected token shape, and
the assurance shape cited from key-cape's contract rather than restated so it
cannot drift.
Section 5 declares the tenant provenance rather than assuming it. Nothing
populates domain.User.Tenant for approver users, so declaring tenant:platform
reaches the token by the gap route by construction rather than by accident.
GH-DEC-2026-013 was explicit that a tenant the directory does not carry must not
be declared unless we are prepared to say so in the record — so it is said,
with the two consequences: the claim is stored with its provenance, and it is
never used as the act-scope.
Deliberately NOT submitted. The origin needs a DNS A record, an Ingress manifest
and a certificate, which is work in railiance-apps rather than here.
decide.coulomb.social is proposed and consistent with the estate's existing
Traefik/TLS-per-host pattern, but proposing a plausible hostname is not the same
as owning one, and submitting a redirect for a host that does not resolve is the
exact failure approval-engine avoided by refusing to invent these strings.
T07 stays progress with the deployment dependency named and owned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 08:06:10 +02:00
|
|
|
- **Consistent with** the estate's existing pattern (`forgejo.coulomb.social`,
|
|
|
|
|
`hub.coulomb.social`, `kc.coulomb.social`), Traefik `ingressClassName`, TLS
|
|
|
|
|
secret per host.
|
2026-09-10 16:17:52 +02:00
|
|
|
- **DNS resolves** to the cluster address as of 2026-09-10.
|
|
|
|
|
- **Still requires, in `railiance-apps` rather than here:** an Ingress manifest
|
|
|
|
|
and an issued certificate. Manifest written 2026-09-10
|
|
|
|
|
(`manifests/informed-decision-ingress.yaml`); not yet applied.
|
Write the key-cape client registration contract (T07 progress)
Everything except the host is now fixed: client_id informed-decision-approver,
callback path /auth/callback, authorization-code + S256 PKCE public client,
scopes [openid, approval:read, approval:approve], the expected token shape, and
the assurance shape cited from key-cape's contract rather than restated so it
cannot drift.
Section 5 declares the tenant provenance rather than assuming it. Nothing
populates domain.User.Tenant for approver users, so declaring tenant:platform
reaches the token by the gap route by construction rather than by accident.
GH-DEC-2026-013 was explicit that a tenant the directory does not carry must not
be declared unless we are prepared to say so in the record — so it is said,
with the two consequences: the claim is stored with its provenance, and it is
never used as the act-scope.
Deliberately NOT submitted. The origin needs a DNS A record, an Ingress manifest
and a certificate, which is work in railiance-apps rather than here.
decide.coulomb.social is proposed and consistent with the estate's existing
Traefik/TLS-per-host pattern, but proposing a plausible hostname is not the same
as owning one, and submitting a redirect for a host that does not resolve is the
exact failure approval-engine avoided by refusing to invent these strings.
T07 stays progress with the deployment dependency named and owned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 08:06:10 +02:00
|
|
|
|
2026-09-10 16:34:38 +02:00
|
|
|
**The host answers. This document is ready to submit.** 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.
|
Write the key-cape client registration contract (T07 progress)
Everything except the host is now fixed: client_id informed-decision-approver,
callback path /auth/callback, authorization-code + S256 PKCE public client,
scopes [openid, approval:read, approval:approve], the expected token shape, and
the assurance shape cited from key-cape's contract rather than restated so it
cannot drift.
Section 5 declares the tenant provenance rather than assuming it. Nothing
populates domain.User.Tenant for approver users, so declaring tenant:platform
reaches the token by the gap route by construction rather than by accident.
GH-DEC-2026-013 was explicit that a tenant the directory does not carry must not
be declared unless we are prepared to say so in the record — so it is said,
with the two consequences: the claim is stored with its provenance, and it is
never used as the act-scope.
Deliberately NOT submitted. The origin needs a DNS A record, an Ingress manifest
and a certificate, which is work in railiance-apps rather than here.
decide.coulomb.social is proposed and consistent with the estate's existing
Traefik/TLS-per-host pattern, but proposing a plausible hostname is not the same
as owning one, and submitting a redirect for a host that does not resolve is the
exact failure approval-engine avoided by refusing to invent these strings.
T07 stays progress with the deployment dependency named and owned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 08:06:10 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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:
|
|
|
|
|
|
|
|
|
|
- `at` is the **authentication** time, not the token mint time. `key-cape` fixed
|
|
|
|
|
a defect here on 2026-09-09; the two differ by hours when a browser session is
|
|
|
|
|
reused, and `approval-engine` stores this object verbatim as the only
|
|
|
|
|
downstream record that MFA happened. Freshness is judged by comparing `at`,
|
|
|
|
|
never by assuming it.
|
|
|
|
|
- `key-cape` emits `assurance` on 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-engine` *accepts* 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:
|
|
|
|
|
|
|
|
|
|
1. **This is transitional.** When the directory carries tenants, the same
|
|
|
|
|
`key-cape` code stops supplying and starts enforcing agreement, with no second
|
|
|
|
|
migration. This registration is expected to survive that unchanged.
|
|
|
|
|
2. **The claim is stored with its provenance.** `key-cape` emits `tenant` as a
|
|
|
|
|
bare string, so a consumer cannot distinguish a directory-asserted tenant from
|
|
|
|
|
a registration-supplied one. Until the claim carries its own provenance
|
|
|
|
|
(`GH-DEC-2026-013` §5, second field deliberately undesigned), this surface
|
|
|
|
|
records which route the value arrived by. See `PR-09`.
|
|
|
|
|
3. **The tenant claim is never used as the act-scope.** `binding.target` is the
|
|
|
|
|
act-scope and is committed inside `view_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. See
|
|
|
|
|
`EvidenceModel.md` §8c and `PR-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 `check` or 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 |
|
|
|
|
|
| --- | --- | --- |
|
2026-09-10 16:34:38 +02:00
|
|
|
| 1 | ~~DNS A record + Ingress + TLS for `decisions.coulomb.social`~~ — **done 2026-09-10**, cert issued, host answers 200 | `railiance-apps` / deployment |
|
Write the key-cape client registration contract (T07 progress)
Everything except the host is now fixed: client_id informed-decision-approver,
callback path /auth/callback, authorization-code + S256 PKCE public client,
scopes [openid, approval:read, approval:approve], the expected token shape, and
the assurance shape cited from key-cape's contract rather than restated so it
cannot drift.
Section 5 declares the tenant provenance rather than assuming it. Nothing
populates domain.User.Tenant for approver users, so declaring tenant:platform
reaches the token by the gap route by construction rather than by accident.
GH-DEC-2026-013 was explicit that a tenant the directory does not carry must not
be declared unless we are prepared to say so in the record — so it is said,
with the two consequences: the claim is stored with its provenance, and it is
never used as the act-scope.
Deliberately NOT submitted. The origin needs a DNS A record, an Ingress manifest
and a certificate, which is work in railiance-apps rather than here.
decide.coulomb.social is proposed and consistent with the estate's existing
Traefik/TLS-per-host pattern, but proposing a plausible hostname is not the same
as owning one, and submitting a redirect for a host that does not resolve is the
exact failure approval-engine avoided by refusing to invent these strings.
T07 stays progress with the deployment dependency named and owned elsewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 08:06:10 +02:00
|
|
|
| 2 | Submit this document to `key-cape`, closing `KEY-WP-0013-T02` | this repo, once 1 lands |
|
|
|
|
|
| 3 | Prove a token issued against the registration is accepted by `approval-engine` | `T08` |
|