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

@ -347,3 +347,26 @@ false, and the example set is what would have taught them — the failure
`tests/test_examples.py` asserts the decorrelation, not merely that both values
appear somewhere.
### Declared human judgment — `binding.human_control`
New producers always state this boolean. `true` records that the requester
explicitly declared a human-in-the-loop or dual-control requirement at issue;
`valid_now: true` then also requires the declared count of distinct verified
human approvers. Non-human binds are refused before insertion. Undeclared objects
remain useful for service approvals. Historical absence is undeclared, never
proof of human judgment; a consumer needing the property must require exactly
true, rather than infer it from an approver's name or from a valid generic claim.
The declaration stays separate from the five act fields and does not change the
native binding digest. It is inherited on supersession, emitted on audit events,
and cannot be downgraded by linking an existing successor. The additive claim
property stays within schema 0.1; old consumers that do not require this property
retain their existing behavior. Consumer adoption of the new requirement and
native issuer/deployment proof remain necessary before the factory human path.
`claim.valid-human-control.json` shows a valid declared human control;
`claim.valid.json` shows a valid ordinary approval. For an inconsistent persisted
human-control object, `valid_now` is false with `human_control_unsatisfied` and
consume refuses. Neither the declaration nor a claim is an authorization verdict.

View file

@ -53,22 +53,21 @@ 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.
**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.
**GH-DEC-2026-016 is now implemented in the source candidate.** An approval
explicitly declared with `human_control: true` refuses service/agent binds at
`/entries`, irrespective of what the caller submits in the body. The verified
human identity supplies the entry. The UI retains its own humans-bind-agents-draft
rule and must require the declared human-control object for that workflow; an
undeclared historical approval must not be relabelled from its human entries.
Request a new declared object when needed. The requester may be a service; it
creates an unapproved request and cannot supply human judgment.
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`.
The engine still admits service-to-service approvals when no human control was
declared. The flag neither grants a scope nor identifies which acts require a
human; those remain owner doctrine. See [caller-authentication.md](caller-authentication.md)
for the issuer's human-type provenance and remaining native acceptance. The
source candidate requires schema v5 and a new image; the current production
manifest is not evidence that this enforcement is deployed.
## 3. Open question A — the human client cannot read the approval it renders

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

View file

@ -0,0 +1,47 @@
{
"status": "source-tests-passed",
"ruling": "GH-DEC-2026-016",
"schema_version": 5,
"new_cases_failed_before": 22,
"new_cases_passed_after": 22,
"full_suite_passed": 152,
"tests": "tests/test_human_control.py",
"semantics": {
"create": "Records declared intent on an unapproved request; service/agent requesters remain permitted under approval:create",
"bind": "Verified human required before insertion/issuance for declared human controls",
"legacy": "False, no inference from existing human entries",
"supersession": "Declaration inherited; mismatched existing successor conflicts atomically",
"identity": "RS256/JWKS verifies principal_type; entry body cannot spoof it",
"issuer_contract_source": "f9812ab3b2bfe8f0817185f44071e612264ec3ee:src/internal/server/oidc/token.go",
"persistence_proof": "Temporary on-disk v4 database migrated to v5; preserved records/digests",
"outbox": "Human bind and issuance roll back together",
"act_digest": "Unchanged five-field digest"
},
"consumer_compatibility": {
"status": "passed",
"cases": [
{
"human_control": false,
"existing_consumer_accepts_additive_field": true
},
{
"human_control": true,
"existing_consumer_accepts_additive_field": true
}
],
"native_calls": 0,
"human_control_required_by_existing_consumer": false,
"limit": "This proves wire compatibility only. The pilot requester/PEP must explicitly declare and require human_control; current consumer admission is not changed."
},
"native_issuer_proof": false,
"production_migration": false,
"factory_attempts": 0,
"residual_records": [
"APPROVAL-WP-0002-T01",
"APPROVAL-WP-0002-T03",
"APPROVAL-WP-0002-T05",
"SECRETS-WP-0009-T03",
"INFD-WP-0001-T08",
"HFACT-WP-0001-T01/T03/T04/T05"
]
}

View file

@ -60,3 +60,21 @@ Downgrade is not supported: an older server refuses the store on the version
check rather than reading the column, which is the intended direction. Restore
from a v4 backup onto a v3 release requires re-pinning forward, not editing
`user_version`.
## Schema v5 — explicit human-control declaration
GH-DEC-2026-016 adds `approvals.human_control`, constrained to 0 or 1, with default
0. Existing objects remain undeclared regardless of recorded approver types;
there is no retrospective declaration or change to their five-field binding
digest. A successor inherits the declaration. Linking an existing successor with
a different declaration conflicts and rolls back the parent transition.
New human-control entries require verified type human at the engine boundary.
The declaration is retained on object/claim and audit events. Claim and consume
also refuse inconsistent persisted human evidence. Tests cover a persistent v4
upgrade and preservation, signed API refusal, quorum and outbox rollback. Take
and verify the existing backup before migration. The old v3 image must not serve
a v5 database; rollback requires the matching pre-migration backup and the
existing single-writer recovery procedure. No live database is migrated by the
source test run.