key-cape/docs/approval-engine-provisioning-request.yaml

134 lines
7.6 KiB
YAML
Raw Normal View History

# Proposed non-secret admission packet. This is not executable authorization.
# Tenant for both requests is tenant:platform per decision
# 5ed3fb35-eca9-413a-82b9-95171ba85bf6 (landlord zone). Exact spelling required
# across the approval store, these JWT claims and the lifecycle CheckRequest;
# no alias to platform or tenant:coulomb.
status: awaiting-custody-admission
owner: key-cape
resource_audience: approval-engine
issuer: https://kc.coulomb.social
registration_source: config/service-clients.example.yaml
requests:
- client_id: secrets-engine-approval
subject: service:secrets-engine
tenant: tenant:platform
scopes: [approval:read, approval:consume]
lifetime: 15m
proposed_openbao_path: platform/workloads/secrets-engine/approval-client
field: CLIENT_SECRET
proposed_kubernetes_secret: sso/keycape-secrets-engine-approval-client
kubernetes_key: client-secret
keycape_environment: KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
custody_owner: railiance-platform
consumer: secrets-engine
- client_id: approval-engine-operator
subject: service:approval-engine-operator
tenant: tenant:platform
scopes: [approval:create, approval:read, approval:approve, approval:revoke, approval:supersede, approval:observe, approval:emit]
lifetime: 15m
proposed_openbao_path: platform/workloads/approval-engine/operator-client
field: CLIENT_SECRET
proposed_kubernetes_secret: sso/keycape-approval-engine-operator-client
kubernetes_key: client-secret
keycape_environment: KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
custody_owner: railiance-platform
consumer: approval-engine-operator
human_registration:
status: awaiting-exact-callback
blocks_service_client_rollout: false
Answer the approver-client questions, and fix what checking them turned up informed-decision and approval-engine both asked to hear problems with the human approver registration now rather than at handover. Checking their requested shape against the source rather than agreeing it on paper turned up three things. Scope gap, accepted: [openid, approval:approve] cannot render a decision, since GET /v1/approvals/{id} and /claim both need approval:read -- the surface could submit an entry it was never able to display. Published [openid, approval:read, approval:approve]. Reading through a service identity was the alternative and is worse: it weakens the evidence-of-what-this-person-saw claim the component exists to make. approval:consume stays excluded. Assurance shape, published and a defect fixed. Both asked for a documented shape and KeyCape already emitted one, so it is written down rather than renegotiated. Writing it down surfaced that `at` was the token mint time rather than the authentication time. Those differ by hours whenever a browser session is reused, and approval-engine persists this object verbatim as the only downstream record that MFA happened -- so a stored approval could have evidenced MFA at a moment the person proved nothing. PKCESession.AuthTime now carries the original login instant through session reuse, with mint time as the fallback. Blocker found before anyone built on it: a human token cannot carry tenant:platform. effectiveTenant resolves the human tenant from the directory user, no adapter populates User.Tenant, and the per-client tenant field is read only on the client_credentials path -- so every human token defaults to tenant:coulomb, which approval-engine refuses by exact string equality. It would have presented as a failed approval rather than a registration defect. Two resolutions sent to the owners and neither implemented here: the choice decides whether a human's tenant is a property of the person or of the registration, and that is not KeyCape's alone to make. Also recorded ops-warden's answers to KEY-WP-0014-T04, including their finding that `warden plan` returns `autonomous` for a need containing generate and CAS-write, because it has no read-versus-mutate intent. Their standing instruction -- treat a warden plan verdict on any write, rotate or provision need as unreliable until WARDEN-WP-0038 lands -- is recorded in the workplan rather than left in an inbox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 14:25:38 +02:00
# Owner identified 2026-09-09: informed-decision (hub repo cf4c7da8). It
# supplies client_id and callback URI from INFD-WP-0001-T07 once it has a
# deployed origin. approval-engine is a bearer-only resource server and never
# owned those strings.
owner: informed-decision
# approval:read added 2026-09-09 on approval-engine's finding: the surface
# cannot render a decision without GET /v1/approvals/{id} and /claim, so the
# earlier scope set let an approver submit what they could not display.
scopes: [openid, approval:read, approval:approve]
mfa_required: true
client_type: public
Answer the approver-client questions, and fix what checking them turned up informed-decision and approval-engine both asked to hear problems with the human approver registration now rather than at handover. Checking their requested shape against the source rather than agreeing it on paper turned up three things. Scope gap, accepted: [openid, approval:approve] cannot render a decision, since GET /v1/approvals/{id} and /claim both need approval:read -- the surface could submit an entry it was never able to display. Published [openid, approval:read, approval:approve]. Reading through a service identity was the alternative and is worse: it weakens the evidence-of-what-this-person-saw claim the component exists to make. approval:consume stays excluded. Assurance shape, published and a defect fixed. Both asked for a documented shape and KeyCape already emitted one, so it is written down rather than renegotiated. Writing it down surfaced that `at` was the token mint time rather than the authentication time. Those differ by hours whenever a browser session is reused, and approval-engine persists this object verbatim as the only downstream record that MFA happened -- so a stored approval could have evidenced MFA at a moment the person proved nothing. PKCESession.AuthTime now carries the original login instant through session reuse, with mint time as the fallback. Blocker found before anyone built on it: a human token cannot carry tenant:platform. effectiveTenant resolves the human tenant from the directory user, no adapter populates User.Tenant, and the per-client tenant field is read only on the client_credentials path -- so every human token defaults to tenant:coulomb, which approval-engine refuses by exact string equality. It would have presented as a failed approval rather than a registration defect. Two resolutions sent to the owners and neither implemented here: the choice decides whether a human's tenant is a property of the person or of the registration, and that is not KeyCape's alone to make. Also recorded ops-warden's answers to KEY-WP-0014-T04, including their finding that `warden plan` returns `autonomous` for a need containing generate and CAS-write, because it has no read-versus-mutate intent. Their standing instruction -- treat a warden plan verdict on any write, rotate or provision need as unreliable until WARDEN-WP-0038 lands -- is recorded in the workplan rather than left in an inbox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 14:25:38 +02:00
grant: authorization_code + S256 PKCE
never: [approval:consume]
Let a human token carry the zone it is issued into, without relabelling anyone KEY-WP-0013-T05's tenant blocker did not need the decision it was waiting on. The two proposed resolutions differ in where a human's tenant comes from -- the directory record, or the client registration -- and an implementation exists that is correct under either, so the choice can be made later without another migration. A client registration may now declare a tenant. humanTenant() resolves it by four rules: no declaration keeps the directory answer unchanged; a declared zone applies where the directory has placed the user nowhere; agreement passes; and a declared zone conflicting with a directory assignment refuses issuance rather than relabelling the user. The refusal is the design, not an edge case. A registration can bind a zone for unplaced users and can never move a placed one, so this gets the approval chain its tenant:platform without writing a general cross-tenant override into the issuer. It fails closed rather than picking a winner, because either answer would be a silent cross-tenant assertion, and it reports 403 with error_type: tenant_binding so an operator can tell a misconfigured registration from a rejected login. If the owners later populate directory tenants, the same code stops supplying the zone and starts enforcing agreement with it. Safe only because client registrations are static and deployment-owned. The tenant contract records that this rule must be revisited if dynamic client registration is ever admitted. Tests cover all four rules; neutering the conflict check fails the relabel test rather than passing silently. T05 now waits on one thing only: the client_id and callback URI from informed-decision once it has a deployed origin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 14:40:36 +02:00
# Required. Without it the token carries tenant:coulomb from the directory
# default and approval-engine refuses it. See docs/tenant-claim-contract.md.
tenant: tenant:platform
keycape_side_resolved: |
2026-09-09: a human token can now carry tenant:platform. A client
registration may declare a tenant; humanTenant() supplies it when the
directory has not placed the user, requires agreement when it has, and
refuses issuance on conflict rather than relabelling. Safe only because
registrations are static and deployment-owned. Still outstanding from
elsewhere: the exact client_id and callback URI from informed-decision
(INFD-WP-0001-T07), and whether the owners prefer the directory-sourced
resolution instead, which this implementation degrades into cleanly.
verification:
Ship the live-registration check both blocked tasks depend on KEY-WP-0013-T02 and KEY-WP-0014-T04 stay blocked on custody and on ops-warden, but each contains a KeyCape-owned piece that had been left as prose. T02 requires proving "live JWKS verification and denied excess scopes without logging values"; T04 step 4 requires verifying a rotated secret, refusing its predecessor and refusing excess scope. Both were describable and neither was runnable, so the proof would have been improvised by hand against production at the moment custody lands -- the worst possible time for it. keycape verify-client does it in one command. Per registration it checks discovery origin, the client_credentials exchange and its RS256 signature against the deployed JWKS, exact sub/tenant/roles/principal_type, that every -deny-scope is refused, and that the token carries no scope that was not requested. That last check is a real gap: the caller commands prove every requested scope was granted, never that nothing extra came back. -previous-secret-env additionally requires the predecessor to be refused, and treats an unchanged secret as a rotation that did not happen. Nothing is written to disk and no value is printed. Failures name the claim, not the observed value, so running this against production cannot turn a verification into a disclosure. Every check runs before it reports, so one failure does not hide the rest. The exact invocation for each approval client is recorded in the verification block of the provisioning packet, so custody admission hands back a command rather than a description. Tests cover the passing case, an over-broad registration, a live predecessor, an identical rotation and output non-disclosure; neutering mustFail makes the suite fail, so the checks have teeth. Also recorded in KEY-WP-0014: read from ops-warden's catalog rather than waiting for a reply, key-cape-oidc-login is asked-and-waiting on us since 2026-08-28 and is a pointer lane with no programmatic consumers, and the rapp-qonto-keycape-client blocker citing an absent native exchange command went stale when service-token shipped on 2026-09-05. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-08 14:30:43 +02:00
# The first two lines are now one runnable command per client; see
# docs/native-authentication.md, "Verifying a live registration". It writes
# nothing and prints no value, so it is safe to run against production.
- command: |
keycape verify-client -issuer https://kc.coulomb.social
-client-id secrets-engine-approval -audience approval-engine
-scope "approval:read approval:consume"
-secret-env KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET
-expect-subject service:secrets-engine -expect-tenant tenant:platform
-expect-roles secrets-engine
-deny-scope "approval:approve approval:revoke approval:supersede"
- command: |
keycape verify-client -issuer https://kc.coulomb.social
-client-id approval-engine-operator -audience approval-engine
-scope "approval:create approval:read approval:approve approval:revoke approval:supersede approval:observe approval:emit"
-secret-env KEYCAPE_APPROVAL_ENGINE_OPERATOR_CLIENT_SECRET
-expect-subject service:approval-engine-operator -expect-tenant tenant:platform
-expect-roles approval-operator
-deny-scope "approval:consume"
- Check KeyCape and consumer readiness without emitting secrets or tokens.
- Preserve existing registrations and signing key; record versions and image digest.
Ship the live-registration check both blocked tasks depend on KEY-WP-0013-T02 and KEY-WP-0014-T04 stay blocked on custody and on ops-warden, but each contains a KeyCape-owned piece that had been left as prose. T02 requires proving "live JWKS verification and denied excess scopes without logging values"; T04 step 4 requires verifying a rotated secret, refusing its predecessor and refusing excess scope. Both were describable and neither was runnable, so the proof would have been improvised by hand against production at the moment custody lands -- the worst possible time for it. keycape verify-client does it in one command. Per registration it checks discovery origin, the client_credentials exchange and its RS256 signature against the deployed JWKS, exact sub/tenant/roles/principal_type, that every -deny-scope is refused, and that the token carries no scope that was not requested. That last check is a real gap: the caller commands prove every requested scope was granted, never that nothing extra came back. -previous-secret-env additionally requires the predecessor to be refused, and treats an unchanged secret as a rotation that did not happen. Nothing is written to disk and no value is printed. Failures name the claim, not the observed value, so running this against production cannot turn a verification into a disclosure. Every check runs before it reports, so one failure does not hide the rest. The exact invocation for each approval client is recorded in the verification block of the provisioning packet, so custody admission hands back a command rather than a description. Tests cover the passing case, an over-broad registration, a live predecessor, an identical rotation and output non-disclosure; neutering mustFail makes the suite fail, so the checks have teeth. Also recorded in KEY-WP-0014: read from ops-warden's catalog rather than waiting for a reply, key-cape-oidc-login is asked-and-waiting on us since 2026-08-28 and is a pointer lane with no programmatic consumers, and the rapp-qonto-keycape-client blocker citing an absent native exchange command went stale when service-token shipped on 2026-09-05. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-08 14:30:43 +02:00
- Human consume denial is approval-engine's to verify at its resource; KeyCape
proves only that the human client is never issued a consume grant.
Record the custody owner's confirmations and guard a receipt against misreading railiance-platform replied on all three open threads. Recording what they add rather than what we already knew. T02: they reached the same reading of the verifier receipt independently and add human_client_consume_denied: true for both clients, confirm the verifier ran twice (activation and post-rollout) at generation 38 with the signing key unchanged, and state T02 can close against that receipt. They also correct our framing, rightly: the packet says client-side retrieval is unadmitted, which stays true, but the attended operator path is not a client-side read and never required one. This task had been treating those as the same constraint. T04: the live approval clients mean this repo now holds a committed receipt that looks like rotation evidence and is not. Both owners independently state the same two gaps -- no real predecessor rotation, no observed wall-clock expiry. Recorded in T04 rather than only in T02, because verify-client's predecessor rejection is now implemented and unproven, which is a different state from missing or done, and T04 is where that distinction belongs. Answered their open question on CCR-2026-0020, which has no named presenting actor. As issuer: the registration carries both approval:create and approval:approve, so one presenter can create an entry and approve it. That is a separation-of-duties property of the holder, not a defect in the token -- KeyCape issues the grants approval-engine asked for. Recorded both shapes KeyCape can support, and that who holds it is approval-engine's decision and the doctrine question gate-house's, not ours. Also narrowed the operations note: the deployed config has not been written since the activation, so the inspected state is the state that will boot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 20:05:40 +02:00
# Asked by railiance-platform 2026-09-09 (CCR-2026-0020 has no named presenting
# actor in any owner source, and they declined to guess one). This is KeyCape's
# view as the issuer of the identity, not a decision: who may hold the
# credential is approval-engine's call and the doctrine question is
# gate-house's.
presenting_actor_note:
client_id: approval-engine-operator
Record the operator withdrawal and the gate-house tenant conditions approval-engine answered the presenting-actor question we put to them and the answer withdrew the client: nobody presents approval-engine-operator. Nothing in their repository obtains an OAuth token, and scope by scope the bundle never described a single actor -- approve belongs to the human client, emit is redundant, and no requester identity was ever settled for create. Annotated as withdrawn in the example config and the packet rather than deleted, because they asked for it to stay authenticable under CCR-2026-0018 if a presenter appears. The separation-of-duties observation is kept because it constrains the re-request: approve must not travel with the operational scopes. Raised rather than filed: "do not provision" cannot undo a provision. The client went live in the 2026-09-09 attended rollout, so the estate now holds a confidential credential that can create and approve approvals, that nobody presents, and that no consumer awaits. Standing capability with no counterparty is a worse resting state than either provisioning it for a named presenter or disabling it. Disablement is KeyCape's to execute and not KeyCape's to decide alone, so it goes to the owners rather than into a commit. Also records GH-DEC-2026-013 conditions (a) and (b) in the tenant contract, both of which strengthen what we had written about ourselves. Row four is normative, and in the direction we would not have guessed: a future change preferring the DIRECTORY is also void, because "the directory won" is still a winner and picking any winner turns a refusal into a silent cross-tenant assertion. And lifting the dynamic-registration exclusion now VOIDS the registration-bound shape that day rather than reopening it for review -- our "must be revisited" was too weak. The shape is recorded as a declared bounded gap, not the terminal state. Verification note: the Go suite could not be run green for this commit because a peer session is mid-edit on token.go and human_tenant_test.go for the §5 provenance work. Every file in this commit is YAML or Markdown; both YAML files were parsed and the example config's scope set and tenant were asserted unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 23:30:09 +02:00
# ANSWERED 2026-09-09 by approval-engine: the presenter is nobody, and they have
# withdrawn the request. Nothing in their repository obtains an OAuth token; the
# engine only verifies them. They asked railiance-platform to cancel the
# client-side reader CCR-2026-0020 as an owner decision. Verifier custody
# CCR-2026-0018 stands. The separation-of-duties observation below is what
# prompted the question and is retained because it constrains the re-request:
# approval:approve must not travel with the operational scopes.
status: withdrawn-by-requesting-owner
already_live: true # provisioned in the 2026-09-09 attended rollout; withdrawal does not unprovision it
Record the custody owner's confirmations and guard a receipt against misreading railiance-platform replied on all three open threads. Recording what they add rather than what we already knew. T02: they reached the same reading of the verifier receipt independently and add human_client_consume_denied: true for both clients, confirm the verifier ran twice (activation and post-rollout) at generation 38 with the signing key unchanged, and state T02 can close against that receipt. They also correct our framing, rightly: the packet says client-side retrieval is unadmitted, which stays true, but the attended operator path is not a client-side read and never required one. This task had been treating those as the same constraint. T04: the live approval clients mean this repo now holds a committed receipt that looks like rotation evidence and is not. Both owners independently state the same two gaps -- no real predecessor rotation, no observed wall-clock expiry. Recorded in T04 rather than only in T02, because verify-client's predecessor rejection is now implemented and unproven, which is a different state from missing or done, and T04 is where that distinction belongs. Answered their open question on CCR-2026-0020, which has no named presenting actor. As issuer: the registration carries both approval:create and approval:approve, so one presenter can create an entry and approve it. That is a separation-of-duties property of the holder, not a defect in the token -- KeyCape issues the grants approval-engine asked for. Recorded both shapes KeyCape can support, and that who holds it is approval-engine's decision and the doctrine question gate-house's, not ours. Also narrowed the operations note: the deployed config has not been written since the activation, so the inspected state is the state that will boot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uV8zoCKpA1WRAxsKRYbdH Assistant: claude-code Assistant-Model: opus Assistant-Process: 1182213@bnt-lap001 Assistant-Session: 966597b9-ae61-46a4-8b9e-1594ab3ec4ad
2026-09-09 20:05:40 +02:00
observation: |
The registration as issued carries both approval:create and approval:approve,
so a single presenter can create an entry and then approve it. That is a
separation-of-duties property of the credential holder, not a defect in the
token -- KeyCape issues exactly the grants approval-engine requested, and
splitting them would be a change to that request rather than to issuance.
options_keycape_can_support:
- Two registrations with disjoint grants, one creating and one approving, if
approval-engine wants the split enforced at issuance. KeyCape can do this
today; it needs approval-engine to say so, since it changes their contract.
- Keep one registration and constrain the holder by custody instead, which is
where it sits now.
keycape_does_not_decide: |
Who presents this credential, and whether self-approval is acceptable for the
lifecycle operator, are not KeyCape's to settle. Recorded here so the question
is answered from the registration rather than reconstructed from it.
blockers:
- Admit exact custody paths, field delivery, consumer identities and lifecycle authority.
- Resolve attended first-provision authority through the custody owner.
- Verify the actual upstream ID-token issuer before production image rollout.
custody_return:
owner_record: railiance-platform/workplans/RPF-WP-0035-credential-lane-implementation.md#Admit-KeyCape-approval-engine-client-custody-and-delivery
requests: [CCR-2026-0017, CCR-2026-0018]
status: proposed-awaiting-owner-approval
field_correction: CLIENT_SECRET
scope: KeyCape verifier-side copies only; client-side retrieval is not admitted.
human_registration_gate:
- The approver UI owner must supply its real client ID and exact callback.
- This gate blocks human approval entry; it does not block the two independent client_credentials registrations or service startup.