# 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 refused `403` without 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 a `tenant:platform` store, 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. ```yaml 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`.