APPROVAL-WP-0007-T02 done: actor_consumes; T05 principal_approves split out

Founder decision 2026-11-01, relayed by the net-kingdom coordinator: "Agree to
split APPROVAL-WP-0007-T02: build actor_consumes now (opt-in at issue; the
bounded executor will set it); principal_approves keeps waiting for the KeyCape
subject mapping."

- Optional actor_consumes (boolean) at issue, schema v8. Consume refuses with
  403 unless the consuming subject equals binding.actor, also on idempotent
  retries; internal calls without a consumer fail closed. Inherited on
  supersession (existing-successor mismatch 409), in audit details, served in
  GET/claim only when declared so undeclared claims keep exact bytes. Legacy
  rows migrate without the flag.
- GH-DEC-2026-026 Ruling 6: under human_control, add_entry refuses (403) the
  binding actor as approver. human_control already covered a service actor;
  this covers a human actor.
- Workplan: T02 done (actor_consumes); new T05 principal_approves (wait).
- Not released or deployed: needs a v8 image and founder-run rollout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
tegwick 2026-11-01 00:39:34 +01:00
parent e9e9f93317
commit 5c45d3c157
11 changed files with 344 additions and 36 deletions

View file

@ -425,3 +425,34 @@ entry, never a request body:
Migration leaves existing rows NULL (no floor). The declaration is separate from
the five-field act digest. Whether KeyCape issues `aal2` for a step-up login is
KeyCape's question.
### Declared consumer = actor — `binding.actor_consumes`
APPROVAL-WP-0007-T02 (actor half), schema v8 store. `POST /v1/approvals` may
declare `"actor_consumes": true` (a JSON boolean; anything else is 422; `false`
is the same as absent). The flag is stored on the object, served in `GET` and
the claim as `binding.actor_consumes: true`, carried on audit events in
`details.actor_consumes`, and inherited on supersession (linking an existing
successor with a different declaration is a 409).
The member is **present only when declared**, so undeclared objects keep their
exact pre-v8 bytes. Absence means any tenant service or agent holding
`approval:consume` may consume, as before.
When declared, `POST /consume` refuses with **403** unless the authenticated
consuming subject equals `binding.actor`. The check runs before the idempotent
branch, so another service gets no receipt from a retry either. An internal
call with no authenticated consumer fails closed. The check does not change
`valid_now`: the claim describes the object, not who is reading it.
Migration leaves existing rows without the flag. The declaration is separate
from the five-field act digest.
### Actor is never an approver under `human_control`
GH-DEC-2026-026 Ruling 6. Under `human_control: true`, `POST /entries` refuses
(403) an entry whose verified subject equals `binding.actor`. For a service
actor the existing human-only rule already made this impossible; the check
covers a human actor, who may create the request but not approve it. Objects
without `human_control` are unchanged. Entries persisted before this check
are not re-evaluated by the claim or consume.

View file

@ -40,6 +40,8 @@ unchanged:
"validity": {"not_before": "<window start>", "expires_at": "<window end>"},
"required_count": 1,
"human_control": true,
"required_assurance": {"level": "aal2"},
"actor_consumes": true,
"pdp_path": false
}
```
@ -64,7 +66,16 @@ Things the requester must know:
- **Declare `human_control: true`.** Then `add_entry` refuses any non-human
approver and consume refuses without the required human entries
(`Engine.add_entry`, `Engine.consume`). This keeps the service actor from
approving its own request.
approving its own request. GH-DEC-2026-026 Ruling 6 (the actor is never an
approver of its own approval) is therefore already met for this service
actor by `human_control` alone. For a human actor it is not, so `add_entry`
also refuses (403), under `human_control`, an entry whose subject equals
`binding.actor` (`docs/approval-claim.md`).
- **Declare `actor_consumes: true`** (APPROVAL-WP-0007-T02, schema v8, in
source, not yet deployed). Consume then refuses with 403 unless the
consuming subject equals `binding.actor` (see §3). The `required_assurance`
and `actor_consumes` members are accepted only by a v7/v8 image; until one is
live, omit them and keep the executor's own checks.
- **`pdp_path: false` for the pilot.** `pdp_path: true` requires a PDP request
digest at issue (GH-DEC-2026-008). There is no access-engine decision for
executor operations yet, so the pilot is off the PDP path. When access-engine
@ -72,8 +83,10 @@ Things the requester must know:
- **`principal` is a bound label, not an approver constraint.** The engine does
not check that the approving human equals `binding.principal`. Any human
holding `approval:approve` for the tenant can approve. Proposal
APPROVAL-WP-0007-T02 lets an issue declare that correspondence; until then
the executor checks the entry subject itself (see §3).
APPROVAL-WP-0007-T05 (`principal_approves`, split from T02 by founder
decision 2026-11-01) would let an issue declare that correspondence; it
waits for the KeyCape subject mapping. Until then the executor checks the
entry subject itself (see §3).
## 2. Assurance (BEA-C4)
@ -143,11 +156,14 @@ either one.
`GET /v1/approvals/{id}` (scope `approval:read`) is the existing authoritative
read surface for confirming consumption
(`docs/task-execution-consumption-contract.md`, "Existing route and trust
boundary"). The engine does not check whether a digest is equivalent and does
not check that the consuming subject equals `binding.actor`. Any service in
the tenant that holds `approval:consume` could spend the object. That cannot
make anything run, but it can deny the run. Proposal APPROVAL-WP-0007-T02 adds
an opt-in check that the consumer is the actor. The reading we intend for the
boundary"). The engine does not check whether a digest is equivalent. Without a
declaration it does not check that the consuming subject equals
`binding.actor`: any service in the tenant that holds `approval:consume` could
spend the object. That cannot make anything run, but it can deny the run.
**Update (APPROVAL-WP-0007-T02, in source, not yet deployed):** issue with
`"actor_consumes": true` and consume refuses (403) every other subject,
including on idempotent retries; GET and the claim serve
`binding.actor_consumes: true`. The reading we intend for the
executor:
1. Before consuming, GET the object and require: `status == "approved"`; the