key-cape (KEY-WP-0013-T02) asked for two exact strings for the human approver registration. This engine is a bearer-token resource server: no redirect endpoint, no authorization-code/PKCE path, and no Ingress or external origin, so neither string exists here. Record why, name the missing owner (a browser-facing approver UI outside this repo), and confirm that the access token — never the ID token — is what this resource validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275879@bnt-lap001 Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
7.2 KiB
Requested KeyCape registrations
Status: requested by APPROVAL-WP-0002-T01. Non-secret. KeyCape owns issuance,
client disablement, and the exact claim contract. This file is a consumer
request, not a live registration.
Tokens presented to approval-engine MUST use resource-server audience
approval-engine. Do not reuse the OpenBao service-auth pattern that sets
aud to the OAuth clientId.
Required claims remain those in docs/caller-authentication.md: iss, sub,
aud, exp, iat, principal_type, tenant, roles, scope, assurance.
principal_type for consume callers must be service or agent.
Resource server
| Field | Value |
|---|---|
| Audience | approval-engine |
| Issuer | the deployed KeyCape issuer (manifest uses https://kc.coulomb.social) |
| JWKS | GET /jwks on the KeyCape service |
| Scopes | approval:create, approval:read, approval:approve, approval:revoke, approval:supersede, approval:consume, approval:observe, approval:emit |
Tenant — resolved: exact tenant:platform
The operator accepted tenant:platform as the platform management,
administration and services tenant (the landlord zone). Decision
5ed3fb35-eca9-413a-82b9-95171ba85bf6, recorded in
glas-harness/docs/platform-tenant-decision.md and relayed by glas-harness
2026-09-06.
The spelling is the contract. ApiApplication.identity compares the
verified JWT tenant claim to the engine's configured store tenant with exact
string equality and raises Forbidden before any object lookup
(approval_engine/api.py:66). There is no mapping table, no normalisation, and
no prefix handling anywhere in this engine — deliberately. platform is not
an accepted alias for tenant:platform, and neither is tenant:coulomb.
Rendered values, all three now exactly tenant:platform:
| Value | Where it is set | Content |
|---|---|---|
| Store tenant | deploy/approval-engine.yaml --tenant |
tenant:platform |
| Store tenant default | approval_engine/cli.py --tenant, Engine(tenant=…) |
tenant:platform |
JWT tenant claim |
the client registrations below | tenant:platform |
| CheckRequest tenant | flex-auth policy subject | tenant:platform |
The CLI and Engine defaults were moved off platform in the same change. A
default that differs from the sanctioned value is a trap: a serve invocation
that omits --tenant would have come up healthy and then refused every
authenticated call, which is the failure this decision exists to prevent.
The flex-auth CheckRequest tenant now matches by spelling, but note it still does not participate in this comparison — it is a PDP policy subject this engine never reads. Alignment there is a property of the decision, not a mechanism here.
Denial evidence:
tests/test_auth.py::test_wrong_tenant_is_forbidden— a signature-valid token whose tenant differs from the store tenant is refused403without mutation.tests/test_auth.py::test_near_miss_tenant_spellings_are_forbidden— pins the decision's "no alias" clause directly:platform,tenant:coulomb,TENANT:PLATFORM,tenant:platform(trailing space) and `` (empty) are each refused against atenant:platformstore, while the exact spelling is admitted. Without a test that varies the tenant, a suite of fixtures all carrying the sanctioned value proves nothing about the field.
This resolves the choice of value only. Verification and the remaining admission
gates — KeyCape actually owning these registrations, credential materialization,
and the T03 rollout evidence — are unchanged and still open.
Tracked against APPROVAL-WP-0002-T01; related KEY-WP-0013-T02,
SECRETS-WP-0009-T03, GLAS-WP-0015.
Clients
Confidential client secrets stay in OpenBao/operator custody. secretRef
names below are placeholders for that custody path.
clients:
- clientId: secrets-engine-approval
displayName: secrets-engine PEP consume client
audience: approval-engine
allowedScopes: [approval:read, approval:consume]
grantTypes: [client_credentials]
clientType: confidential
secretRef: env:KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
serviceSubject: service:secrets-engine
principal_type: service
tenant: tenant:platform
roles: [secrets-engine]
tokenLifetime: 15m
- clientId: approval-engine-operator
displayName: approval-engine lifecycle operator
audience: approval-engine
allowedScopes:
- approval:create
- approval:read
- approval:approve
- approval:revoke
- approval:supersede
- approval:observe
- approval:emit
grantTypes: [client_credentials]
clientType: confidential
secretRef: env:KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
serviceSubject: service:approval-engine-operator
principal_type: service
tenant: tenant:platform
roles: [approval-operator]
tokenLifetime: 15m
Human approvers use the existing KeyCape human flow with approval:approve
only, still with aud=approval-engine. They must not receive
approval:consume.
Human approver client — not supplied (2026-09-08)
KEY-WP-0013-T02 asked for two exact strings for the human approver
registration (public client, authorization code + S256 PKCE, exact redirect,
aud=approval-engine, allowedScopes: [openid, approval:approve],
mfaRequired: true): the client_id and the full callback URI.
This repository cannot supply either, and must not guess one. Redirects are
matched exactly at /authorize, so a fabricated value fails closed — or, worse,
registers a redirect no deployed component owns.
Why there is nothing to give:
- approval-engine is a bearer-token resource server only.
approval_engine/api.pyis a WSGI app whose every non-health route readsAuthorization: Bearer …(approval_engine/auth.py). There is no/callback, no redirect handling, no authorization-code or PKCE code anywhere in the package. - Nothing here is a browser client. The requested registrations above are both
client_credentialsconfidential service clients. - The service is not publicly addressable:
deploy/approval-engine.yamlhas a Namespace, ClusterIP Service and StatefulSet — no Ingress, no external hostname — so nohttps://…/callbackorigin exists to register.
The human approver flow therefore belongs to whichever browser-facing
approver UI presents approval:approve tokens to this engine. That component
is not in this repo and is not named in APPROVAL-WP-0002. The two strings must
come from its owner once it exists and has a deployed origin; approval-engine
only needs the resulting access token to carry aud=approval-engine,
principal_type: human, tenant: tenant:platform and scope approval:approve.
Token confirmed: access token, never the ID token
ApiApplication.identity verifies the access token presented as
Authorization: Bearer at this resource — RS256 against the deployed issuer's
/jwks, checking iss, aud=approval-engine, exp/iat (30s leeway), sub,
principal_type, tenant (exact equality with the store tenant), roles,
scope and assurance. id_token appears nowhere in the codebase; the ID
token belongs to the login client and is never evidence here. Human principals
are additionally refused approval:consume.