key-cape reports the withdrawal came too late: approval-engine-operator was provisioned in the 2026-09-09 custody window and is wired into a pod. A live credential that can create and approve approvals, presented by nobody, is a worse resting state than either extreme. As requesting owner: disable, do not delete. CCR-2026-0018 stands so it is restorable under the narrowing constraint once a presenter exists. A recorded expiry was rejected — it promises to act later on a capability that is dangerous now, guarding exactly the case where nobody is watching. 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
12 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. As of 2026-09-09 that flow is informed-decision's
(INFD-WP-0001); see docs/approver-surface-requirements.md.
approval-engine-operator — requested, unpresented, do not provision
Answering railiance-platform (RPF-WP-0035-T06, CCR-2026-0020) 2026-09-09.
Their question was the right one and we could not answer it from this source
either, because the answer is nobody.
Nothing in this repository presents this client, and nothing is deployed that
could. This engine only verifies bearer tokens; it never obtains one. The
sole outbound credential it holds is the mounted audit-token used for
audit-core delivery (approval_engine/audit.py), which is not an OAuth client.
approval_engine/pep.py is a consumer-side helper for PEPs such as
secrets-engine, which present their own tokens. There is no client-credentials
grant anywhere in the package, and deploy/approval-engine.yaml is unapplied.
Scope by scope, the bundle we originally requested does not describe one actor:
| Scope | Who would present it |
|---|---|
approval:create |
whoever requests an approval. Not named anywhere in the estate today; gate-house doctrine says which acts need one, but no requester identity has been settled |
approval:approve |
informed-decision's human client, per its own registration. A service principal holding this is exactly the question gate-house has not ruled on — see the principal_type note below |
approval:read |
anything rendering or inspecting an approval |
approval:revoke, approval:supersede |
attended lifecycle operations |
approval:observe |
monitoring and operations |
approval:emit |
redundant in normal operation — the running server emits its own heartbeat on --heartbeat-seconds with no token; the route exists only to force one |
So this was a convenience bundle written when this repo assumed a single
"lifecycle operator" would exist, and it aged badly: it now carries the widest
scope set in the pair, including approval:approve, for an identity nobody has
named.
Decision, option (b): cancel CCR-2026-0020. No client-side reader is
wanted, because there is no presenter. This is an owner decision and not a
deferral — recorded here so the request does not rot in_flight. Verifier
custody (CCR-2026-0018) is unaffected and remains correct: KeyCape must be
able to authenticate this client if it is ever presented.
When a presenter is actually named, the registration should be re-requested
narrowed to that presenter's scopes, not reinstated as this bundle. In
particular approval:approve should not travel with the operational scopes:
schema v4 records entries.principal_type so a service bind is legible, but
legibility is not permission, and whether a non-human may supply approver
evidence at all is gate-house's to rule on.
secrets-engine-approval above is unaffected — it has a real presenter, a
narrow scope pair, and is the client that unblocks T05.
It was already live — disable the registration, keep the custody
2026-09-10: key-cape reports the withdrawal came too late. The client was
provisioned in the 2026-09-09 attended custody window and verified there
(docs/evidence/2026-09-09-keycape-verifier-admission.json records
exact_claims_verified and a working exchange for approval-engine-operator),
its secret is in the namespace and wired into a pod.
So the estate holds a live confidential credential that can create and approve approvals, that nobody presents, and that no consumer is waiting on. Standing capability with no counterparty is a worse resting state than either extreme: it serves no one, and it is the shape found later by someone other than its owner.
Decision, as requesting owner: disable the registration, keep the custody.
Not delete. CCR-2026-0018 verifier custody stands, so the client remains
authenticable and restorable the moment a presenter is named; the re-request
constraint above still applies — narrowed to that presenter, and
approval:approve must not travel with the operational scopes. key-cape
executes the disablement with railiance-platform as the deployment change.
The middle option — leaving it standing with a recorded expiry — was rejected deliberately. An expiry is a promise to act later on a capability that is dangerous now, and the thing it protects against is precisely the case where nobody is watching.
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.