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:
parent
a3c94fb241
commit
be1a388a84
17 changed files with 441 additions and 40 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
47
docs/evidence/2026-09-10-human-control.json
Normal file
47
docs/evidence/2026-09-10-human-control.json
Normal 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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue