Answer NK-WP-0056-T02 bounded-execution carrier request; propose APPROVAL-WP-0007
Owner answer in docs/bounded-execution-admission-carrier.md: binding, consume and revoke are supported today; aal2 is recorded but not enforced. Workplan APPROVAL-WP-0007 proposes an assurance floor, opt-in approver/consumer correspondence, revoker evidence and a synthetic carrier proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 994874@bnt-lap001 Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
This commit is contained in:
parent
297e866e2c
commit
5028fe2231
2 changed files with 295 additions and 0 deletions
203
docs/bounded-execution-admission-carrier.md
Normal file
203
docs/bounded-execution-admission-carrier.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
# Bounded Execution Admission — Approval Engine carrier answer
|
||||
|
||||
Status: owner answer to NetKingdom NK-WP-0056-T02 (hub message
|
||||
`40344380-ea33-4670-9b27-dafca1699d36`), 2026-10-31. Source reviewed at the
|
||||
commit that adds this file. This is an engine-owner technical answer, not a
|
||||
founder decision, identity registration or runtime admission. Follow-up work:
|
||||
`workplans/APPROVAL-WP-0007-bounded-execution-carrier.md`.
|
||||
|
||||
Inputs: NetKingdom `canon/standards/bounded-execution-admission_v0.1.md`
|
||||
(proposed), its schema and validator, the example
|
||||
`examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml`, and
|
||||
`docs/proposals/2026-10-31-nk56-t02-owner-requests.md`.
|
||||
|
||||
## Summary
|
||||
|
||||
| Question | Answer |
|
||||
| --- | --- |
|
||||
| 1. Binding | **Supported today**, with one consequence: the executor (the `actor`) must create the request itself |
|
||||
| 2. aal2 | **Recorded today, not enforced.** Enforcement at issue/entry/consume needs a change (APPROVAL-WP-0007-T01) |
|
||||
| 3. Consume | **Supported today.** Scope `approval:consume`, service principal; proposed `request_digest` = the admission digest. The independent GET comparison is the intended reading |
|
||||
| 4. Revoke | **Supported today** for unconsumed approvals. The revoker's identity is not yet recorded in evidence (APPROVAL-WP-0007-T03) |
|
||||
|
||||
## 1. Binding
|
||||
|
||||
The native binding has exactly five fields — `action`, `actor`, `principal`,
|
||||
`purpose`, `target` — and `target` must be an object of any shape
|
||||
(`approval_engine/binding.py` `canonical_binding`). The native digest is
|
||||
SHA-256 over their sorted-key JSON (`binding_digest`). So this binding fits
|
||||
unchanged:
|
||||
|
||||
```json
|
||||
{
|
||||
"binding": {
|
||||
"action": "bounded-execution",
|
||||
"actor": "service:nk-bind-executor",
|
||||
"principal": "admin",
|
||||
"purpose": "<spec.purpose>",
|
||||
"target": {"admission_id": "<spec.id>", "admission_digest": "sha256:<64 hex>"}
|
||||
},
|
||||
"validity": {"not_before": "<window start>", "expires_at": "<window end>"},
|
||||
"required_count": 1,
|
||||
"human_control": true,
|
||||
"pdp_path": false
|
||||
}
|
||||
```
|
||||
|
||||
Validity is a separate `not_before`/`expires_at` pair (`store.py`
|
||||
`Engine.create`); consume enforces it with one post-lock instant
|
||||
(not-before inclusive, expiry exclusive; `Engine.consume`). Nothing in the
|
||||
engine interprets `action`, so `bounded-execution` needs no registration here.
|
||||
The binding is immutable after create: no route updates it (`api.py`
|
||||
`dispatch`).
|
||||
|
||||
Things the requester must know:
|
||||
|
||||
- **The actor creates.** `POST /v1/approvals` (scope `approval:create`) refuses
|
||||
unless `binding.actor` equals the authenticated token subject
|
||||
(`api.py` `dispatch`, "binding.actor must match the authenticated subject").
|
||||
The executor's service identity therefore files the request in `requested`
|
||||
state; the founder then approves it. Neither the founder nor NetKingdom
|
||||
tooling can create an approval whose actor is the executor. The string
|
||||
`service:nk-bind-executor` must equal the `sub` KeyCape issues for that
|
||||
client; the engine does no mapping.
|
||||
- **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.
|
||||
- **`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
|
||||
adds one, issue with `pdp_path: true` and its `pdp_digest`.
|
||||
- **`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).
|
||||
|
||||
## 2. Assurance (BEA-C4)
|
||||
|
||||
**Recorded today.** `POST /v1/approvals/{id}/entries` (scope
|
||||
`approval:approve`) takes no body fields for identity. It stores the verified
|
||||
token's `subject`, `principal_type`, `tenant_source`, `evidence_ref` and the
|
||||
whole `assurance` claim (for example `{"level":"aal2","amr":["pwd","otp"],
|
||||
"source":"key-cape"}`) as JSON (`api.py` `dispatch`, `auth.py`
|
||||
`identity_from_claims`, `store.py` `Engine.add_entry`). A caller cannot spoof it
|
||||
in the body (`tests/test_auth.py`, spoofed-assurance test). The entry and its
|
||||
assurance appear in `GET /v1/approvals/{id}` (`Approval.as_dict`) and in the
|
||||
`approval.issuance` outbox event threshold evidence (`_threshold_evidence`).
|
||||
|
||||
**Not enforced today.** The engine stores whatever level the token carries.
|
||||
Nothing at issue declares a minimum level, and `add_entry`, `_evaluate` and
|
||||
`consume` do not compare levels. An aal1 approval of a bounded-execution
|
||||
request would become `approved` and consumable.
|
||||
|
||||
**Change (in owner scope): APPROVAL-WP-0007-T01.** Add an optional
|
||||
`required_assurance` (minimum level, for example `aal2`) declared at issue and
|
||||
stored with the object. `add_entry` refuses a weaker entry, consume and the
|
||||
claim refuse when the requirement is unmet, the claim reports it, and
|
||||
supersession carries it forward. The level comes only from the verified token.
|
||||
Whether KeyCape puts `aal2` into the `assurance` claim for a step-up login is
|
||||
KeyCape's question (identity owner), not this engine's.
|
||||
|
||||
**Until T01 ships,** the executor's GET check (§3) must require that every
|
||||
entry has `principal_type: "human"` and `assurance.level == "aal2"`. Note that
|
||||
this makes the executor the only enforcement point.
|
||||
|
||||
## 3. Consume
|
||||
|
||||
**Scope and principal.** `POST /v1/approvals/{id}/consume` requires scope
|
||||
`approval:consume`, the store's tenant, and `principal_type` `service` or
|
||||
`agent` (`api.py` `dispatch`). For the executor's service principal (pilot
|
||||
`service:nk-bind-executor`), KeyCape registers `approval:create`,
|
||||
`approval:read` and `approval:consume`. The founder's human identity needs
|
||||
`approval:approve` and `approval:revoke`. Registration belongs to KeyCape.
|
||||
|
||||
**`request_digest` (proposal).** The engine requires only syntax
|
||||
(`sha256:<64 lowercase hex>`, `binding.py` `require_digest`). It does not
|
||||
compare the digest with anything (`Engine.consume`, and
|
||||
`docs/task-execution-consumption-contract.md` "Exact binding
|
||||
responsibilities"). The normal meaning (`docs/approval-consumption.md`) is the
|
||||
PDP decision's request digest, but this pilot has no PDP decision. The owner
|
||||
proposes:
|
||||
|
||||
- `request_digest` = the admission digest (`target.admission_digest`, already
|
||||
`sha256:<hex>`). The consumption record then names exactly what was run. A
|
||||
retry with the same admission is idempotent (`idempotent: true`, no second
|
||||
use event), and any other digest conflicts with 409.
|
||||
- `decision_id` = the executor's `run_id` (optional provenance; stored and
|
||||
returned on idempotent retries).
|
||||
- Once access-engine gates executor operations, move to the PDP path:
|
||||
`pdp_path: true`, `pdp_digest` at issue, and the PDP request digest at
|
||||
consume.
|
||||
|
||||
This choice belongs to NetKingdom (it owns the standard). The engine accepts
|
||||
either one.
|
||||
|
||||
**Independent GET comparison: yes, that is the intended reading.**
|
||||
`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
|
||||
executor:
|
||||
|
||||
1. Before consuming, GET the object and require: `status == "approved"`; the
|
||||
served `binding` (with `binding.digest`) equals the binding the executor
|
||||
computes from the admission document it holds, with `target.admission_digest`
|
||||
equal to its own digest of that document; `actor` is its own identity;
|
||||
`validity` covers now; `human_control: true`; entries are human, at `aal2`
|
||||
(until T01), and come from the expected approver. Use the entries only in
|
||||
memory and discard them. Never copy approver entries into receipts or logs.
|
||||
2. Consume once with the request digest above. Validate the direct response:
|
||||
exact `approval_id`, `status: consumed`, the same `request_digest`, and a
|
||||
strict boolean `idempotent`. `execution_confirmation.py`
|
||||
`ApprovalHTTPClient.consume_exact` does this. Treat `idempotent: true` as
|
||||
historical, never as new authority to run.
|
||||
3. If the outcome is unknown (timeout), do not consume again to find out. GET
|
||||
and compare `consumption.request_digest` and `consumed_at`. An unknown
|
||||
outcome stops the run (`on_unknown: stop`).
|
||||
|
||||
The staged `confirm_consumption` helper requires `pdp_path` and
|
||||
`human_control` both true, so it does not fit the off-PDP pilot as written.
|
||||
The executor runtime (NetKingdom-owned) should implement the checks above. A
|
||||
pilot-mode confirmation helper can come later if the runtime wants one.
|
||||
|
||||
Revocation, supersession and expiry all leave the binding immutable, and
|
||||
consume refuses every non-`approved` status. That makes GET-then-consume safe:
|
||||
if the founder revokes between the two calls, consume returns 409.
|
||||
|
||||
## 4. Revocation (kill switch before start)
|
||||
|
||||
**Supported today.** `POST /v1/approvals/{id}/revoke` (scope `approval:revoke`)
|
||||
moves `requested` or `approved` to terminal `revoked` by compare-and-swap. It
|
||||
writes an `approval.revocation` outbox event in the same transaction
|
||||
(`store.py` `Engine.revoke` → `_terminal`). It needs no cooperation from the
|
||||
holder. Every later consume refuses (`cannot consume from status revoked`),
|
||||
and the claim reports `valid_now: false`, `reason_code: revoked`. An
|
||||
`audit-core` outage does not block a revocation (outbox is local; SCOPE.md).
|
||||
|
||||
Limits:
|
||||
|
||||
- **A consumed approval cannot be revoked.** Consumption is terminal, so
|
||||
after the executor's first-step consume, stopping it mid-run is the
|
||||
executor's own kill switch (T07, NetKingdom runtime). This engine has no
|
||||
"stop after start".
|
||||
- **The engine does not record who revoked.** The event carries the bound
|
||||
`actor` and the binding digest, not the revoker's subject or assurance, and
|
||||
the engine does not check that the revoker is the approver or principal.
|
||||
APPROVAL-WP-0007-T03 records the verified revoker identity, assurance and an
|
||||
optional reason in the revocation event.
|
||||
- Supersession (`approval:supersede`) also stops an unconsumed approval and
|
||||
links a successor. For the pilot, revoke is the right kill switch.
|
||||
|
||||
## Not owned here
|
||||
|
||||
- Executor and founder scope registrations, the client `sub` and `aal2` in
|
||||
`assurance`: KeyCape.
|
||||
- A policy decision for executor operations: access-engine.
|
||||
- Execution journal, budget, in-flight stop and receipts: NetKingdom executor
|
||||
runtime and audit-core.
|
||||
92
workplans/APPROVAL-WP-0007-bounded-execution-carrier.md
Normal file
92
workplans/APPROVAL-WP-0007-bounded-execution-carrier.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
---
|
||||
id: APPROVAL-WP-0007
|
||||
type: workplan
|
||||
title: "Bounded-execution admission carrier: assurance floor, correspondence and revoker evidence"
|
||||
domain: infotech
|
||||
repo: approval-engine
|
||||
status: proposed
|
||||
flavor: implementation
|
||||
owner: approval-engine
|
||||
topic_slug: netkingdom
|
||||
created: "2026-10-31"
|
||||
updated: "2026-10-31"
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
Demand: NetKingdom NK-WP-0056-T02 owner request (hub message
|
||||
40344380-ea33-4670-9b27-dafca1699d36) to carry a Bounded Execution Admission
|
||||
v0.1 (proposed) as an approval. Owner answer:
|
||||
`docs/bounded-execution-admission-carrier.md`. The binding, consume and
|
||||
revoke already work. This plan closes the engine-side gaps the answer found.
|
||||
Every change is opt-in at issue, so existing approvals and consumers keep
|
||||
their behavior. Native digest, CAS consumption and no-unconsume are unchanged.
|
||||
|
||||
## Declared minimum approver assurance
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0007-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Add an optional `required_assurance` (minimum `level`, ordered aal1 < aal2 <
|
||||
aal3) to `POST /v1/approvals`. Store it and serve it in the object and the
|
||||
claim, and inherit it on supersede. `add_entry` refuses an entry whose
|
||||
verified token `assurance.level` is weaker or missing. `_evaluate` and
|
||||
`consume` refuse when an existing entry does not meet it. Schema migration
|
||||
leaves old rows NULL, meaning no floor. Tests cover aal1 refused, aal2
|
||||
accepted, missing level refused, supersede inheritance and the claim
|
||||
reason_code. Serves BEA-C4. Whether KeyCape emits `aal2` is KeyCape's
|
||||
question, outside this plan.
|
||||
|
||||
## Opt-in approver and consumer correspondence
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0007-T02
|
||||
status: wait
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Proposal for owner and Gate House agreement before implementation, because
|
||||
it gives `principal` and `actor` an enforced meaning. Two optional issue
|
||||
flags:
|
||||
|
||||
- `principal_approves`: every entry subject must equal `binding.principal`.
|
||||
- `actor_consumes`: the consuming subject must equal `binding.actor`.
|
||||
|
||||
Today any human with `approval:approve` can approve, and any tenant service
|
||||
with `approval:consume` can spend the object (a denial-of-run risk). Wait
|
||||
reason: semantic decision. NetKingdom's actor and principal strings must also
|
||||
match KeyCape subjects exactly.
|
||||
|
||||
## Revoker identity in revocation evidence
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0007-T03
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Record the verified revoker's subject, principal_type and assurance, plus an
|
||||
optional bounded `reason`, in the `approval.revocation` outbox event. Persist
|
||||
them on the object (the `revoked_by` fields) without changing the CAS or
|
||||
terminal semantics. This is the evidence for the founder's pre-start kill
|
||||
switch (NK-WP-0056-T07). Do not require the revoker to equal the principal:
|
||||
revocation must stay possible without the holder.
|
||||
|
||||
## Synthetic bounded-execution carrier proof
|
||||
|
||||
```task
|
||||
id: APPROVAL-WP-0007-T04
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Add an in-process test and an example request covering the full path: the
|
||||
executor (service) creates the request (`action: bounded-execution`,
|
||||
`target: {admission_id, admission_digest}`, `human_control: true`,
|
||||
`pdp_path: false`); a human aal2 entry approves it; GET binding comparison;
|
||||
consume with `request_digest` = admission digest; idempotent retry; a
|
||||
different digest returns 409; revoke before consume gives 409 on consume;
|
||||
consumption outside the window is refused. Synthetic fixtures only, with no
|
||||
live service.
|
||||
Loading…
Add table
Add a link
Reference in a new issue