Enforce declared human controls at approval binding

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 19:26:12 +02:00
parent a3c94fb241
commit be1a388a84
17 changed files with 441 additions and 40 deletions

View file

@ -47,15 +47,37 @@ 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:
`principal_type` is recorded from verified identity (since schema v4). Schema v5
implements GH-DEC-2026-016: when an approval declares `human_control: true`,
`/entries` refuses a service, agent or unknown principal with 403 before inserting
an entry or emitting issuance. The request body cannot supply the approver type
or downgrade the declaration. Undeclared approvals retain service-to-service use;
a human entry does not retroactively declare a human control.
**Requesting and binding are separate operations.** `POST /v1/approvals` creates
an unapproved request and records the strict boolean declaration. A service or
agent with `approval:create` may draft that request. The principal contributing
judgment is known only at `POST /entries`; this is the bind/issue boundary where
GH-DEC-2026-016's non-human refusal applies. A draft is never a valid approval.
No statement about the approver is inferred from `binding.principal` or the
requesting actor's type. Dual control also needs its declared `required_count`;
human control alone does not imply two approvers or decide which acts need it.
**Identity provenance:** reviewed KeyCape source
`f9812ab3b2bfe8f0817185f44071e612264ec3ee:src/internal/server/oidc/token.go`
sets `principal_type=human` only after consuming a client/redirect-bound PKCE
session and looking up the current user. Its separate client-credentials path
sets `service`; no registration field supplies a human principal type. This is
different from registration-supplied tenant routing. Production acceptance must
pin and prove that issuer behavior with a real human flow; signed fixtures here
prove engine enforcement, not native identity admission. If an issuer introduces
a registration-supplied route to `human`, GH-DEC-2026-016 §5 requires independent
provenance and refusal of that route before admitting it. A consumer must not
relax the identity contract simply because the JWT verifies.
Entries written before v4 stay null; pre-v5 objects have `human_control: false`.
KeyCape owns client registration and scope grants; approval-engine 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