Record the verified principal type on approver entries (schema v4)

An entry stored subject_id, assurance and evidence_ref but nothing about
what kind of principal bound the approval, and subject_id is a naming
convention rather than a verified claim. /entries is not restricted by
principal type — only /consume is — and the approval-engine-operator
client holds approval:approve, so a service can supply approver evidence
today. Whether it may is gate-house doctrine; that it is legible is ours.

Add entries.principal_type, populate it from the verified token, surface
it on the object and the audit evidence path (not the claim, which stays
least-disclosure), and migrate v3 stores leaving legacy rows null rather
than back-filling a claim nobody made.

Also corrects two statements in the requirements issued to
informed-decision: agent tokens are not barred from approval:approve, and
an empty assurance object is accepted rather than refused.

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
This commit is contained in:
tegwick 2026-09-09 14:09:54 +02:00
parent 8b8ada6c4a
commit 31da1af5e4
6 changed files with 160 additions and 20 deletions

View file

@ -53,11 +53,22 @@ the deployed issuer's `/jwks` (RS256) and carry:
The **access** token is what we validate. `id_token` appears nowhere in this
codebase; the ID token belongs to the login client and is never evidence here.
Agents may assemble a memo but must never complete a binding act — that is
`informed-decision`'s own design principle 10, and it matches this engine: an
agent token cannot hold `approval:approve` under the requested registrations,
and an entry recorded under an agent subject would be indistinguishable from a
human's in the evidence chain.
**Correction to our 2026-09-09 message.** We said an agent token cannot hold
`approval:approve` under the requested registrations. That is wrong twice: the
`approval-engine-operator` service client holds `approval:approve`, and this
engine does not restrict `/entries` by principal type at all — only `/consume`
is restricted, to `service`/`agent`. So a non-human principal can supply
approver evidence today.
Whether it *should* be able to is approval doctrine and belongs to
`gate-house`, not to us and not to you. What we have done instead is make it
visible: schema v4 records the verified `principal_type` on every entry
(`tests/test_auth.py::test_entry_records_the_verified_principal_type`,
`::test_non_human_approver_is_recorded_as_such`). Your design principle 10
("humans bind, agents draft") is therefore enforceable in the evidence chain
rather than assumed — read `entries[].principal_type` and do not infer the
answer from the shape of `subject_id`. Entries written before v4 are `null` and
must not be read as `human`.
## 3. Open question A — the human client cannot read the approval it renders
@ -100,9 +111,13 @@ on the human client — the requirement here is that the resulting token's
(authentication method, `acr`/`amr`, `auth_time` at minimum), rather than
leaving MFA as a property of the login that nothing downstream can see.
`informed-decision` and `key-cape` own that shape between them. This engine will
store whatever they agree on and will not silently accept an empty object as
evidence of anything.
`informed-decision` and `key-cape` own that shape between them. To be exact
about what this engine does and does not do: an empty `assurance` object
(`{}`) is *accepted* — validation checks only that the claim is present and is
an object. So an empty assurance is not refused here; it simply produces an
entry that evidences nothing about how the person authenticated. If MFA has to
be *provable* from the approval record, the shape has to carry it, because
nothing downstream reconstructs it.
## 5. Open question C — `view_hash` has nowhere to go today

View file

@ -20,8 +20,17 @@ cross-tenant reads and mutations are rejected before object lookup.
| explicit heartbeat | `approval:emit` |
Create additionally requires `binding.actor == sub`. Approval-entry subject,
assurance, and evidence reference are derived from the verified JWT, never the
request body. KeyCape owns client registration and scope grants; approval-engine
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;