# Caller authentication Production accepts only RS256 JWTs verified against KeyCape JWKS with the exact configured issuer and `approval-engine` audience. `exp`, `iat`, `sub`, `principal_type`, `tenant`, `roles`, `scope`, and `assurance` are mandatory. Missing or unverifiable credentials fail closed. The development static-token mode is explicit, file-backed, and refused with `--production`. The verified `tenant` must exactly match the service's configured store tenant; cross-tenant reads and mutations are rejected before object lookup. **This check is store isolation, and it is not a statement about the person.** It answers "does this caller belong to the store this engine serves", not "is this human a member of the platform zone". `GH-DEC-2026-013` rules that a key-cape human tenant claim may be supplied by the client registration rather than asserted by the directory, and forbids a consumer treating the two as equivalent for a decision turning on a fact about the person. Exact string equality cannot see that difference — a string that matches exactly matches whoever asserted it — so the difference is stated here instead. Consequences this engine accepts deliberately: - A registration-supplied tenant **is** admissible for admission to this store. Admission means "arrived through a channel the platform registered", the approver client is static and deployment-owned, and dynamic client registration is excluded from key-cape by design. - A registration-supplied tenant is **not** admissible for any doctrine that turns on the approver's own membership — "an approver must be a member of the platform zone" is a fact about the person, and this claim cannot carry it. No such doctrine exists today; if `gate-house` issues one, it needs a directory-sourced claim and this check does not become that claim by matching. - When key-cape emits provenance alongside the tenant, this engine records it on the approver entry the way schema v4 records `principal_type` — an evidence reader should not have to infer provenance from a string that cannot carry it. | Route | Required scope | |---|---| | create approval | `approval:create` | | get approval or claim | `approval:read` | | add approval entry | `approval:approve` | | revoke | `approval:revoke` | | supersede | `approval:supersede` | | consume | `approval:consume` and service/agent principal | | cadence, outbox, storage | `approval:observe` | | explicit heartbeat | `approval:emit` | Create additionally requires `binding.actor == sub`. Approval-entry subject, assurance, evidence reference, and **principal type** are derived from the verified JWT, never the request body. `principal_type` is recorded on the entry (schema v4) because `subject_id` alone cannot answer what kind of principal bound the approval — `user:alice` is a naming convention, not a verified claim. This engine does not restrict `/entries` to human principals: whether a service or agent may supply approver evidence is approval doctrine and belongs to `gate-house`, and the `approval-engine-operator` registration holds `approval:approve` today. What this engine owes is that the evidence chain says which it was. Entries written before v4 stay `null` rather than being back-filled into a claim nobody made. KeyCape owns client registration and scope grants; approval-engine only verifies and enforces them. Requested registrations are: - audience/resource server `approval-engine` with the scopes above; - the secrets-engine PEP service client with `approval:read` and `approval:consume`; - separately governed lifecycle/operator clients with only their needed mutation or observation scopes. Client credentials belong in OpenBao/operator custody and must not be placed in manifests, logs, State Hub, or this repository. KeyCape's OpenBao service-auth contract currently emits `aud` as the OAuth `clientId`. That pattern must not be reused here. Tokens presented to this API MUST have resource-server audience `approval-engine`. Requested non-secret client fragments are in `docs/keycape-service-registrations.md`.