- Optional required_assurance {"level": aal1|aal2|aal3} at issue, schema v7.
Enforced at add_entry (403), claim (assurance_unsatisfied) and consume (409)
from the verified token level only; inherited on supersession; legacy rows
NULL. Served only when declared so undeclared claims keep exact bytes.
- deploy/networkpolicies.yaml: pod-scoped ingress for sso +
app.kubernetes.io/name=bounded-executor on 8080 (hub 39a3bffa). Not applied.
- docs/decision-services-alignment-answer.md: owner answer to the NetKingdom
decision services review (hub 81cafbf3): roles accepted, T02 split proposed,
presentation_ref at create declined with an entry-level counter-proposal.
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
11 KiB
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:
{
"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(scopeapproval:create) refuses unlessbinding.actorequals the authenticated token subject (api.pydispatch, "binding.actor must match the authenticated subject"). The executor's service identity therefore files the request inrequestedstate; the founder then approves it. Neither the founder nor NetKingdom tooling can create an approval whose actor is the executor. The stringservice:nk-bind-executormust equal thesubKeyCape issues for that client; the engine does no mapping. - Declare
human_control: true. Thenadd_entryrefuses 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: falsefor the pilot.pdp_path: truerequires 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 withpdp_path: trueand itspdp_digest.principalis a bound label, not an approver constraint. The engine does not check that the approving human equalsbinding.principal. Any human holdingapproval:approvefor 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.
Update (APPROVAL-WP-0007-T01, in source, not yet deployed): the engine now
accepts required_assurance: {"level": "aal2"} at issue and enforces it at
entry, claim and consume (docs/approval-claim.md). The executor should issue
with it once the v7 image is live, and keep its own check until then.
Until T01 is deployed, 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, alreadysha256:<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'srun_id(optional provenance; stored and returned on idempotent retries).- Once access-engine gates executor operations, move to the PDP path:
pdp_path: true,pdp_digestat 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:
- Before consuming, GET the object and require:
status == "approved"; the servedbinding(withbinding.digest) equals the binding the executor computes from the admission document it holds, withtarget.admission_digestequal to its own digest of that document;actoris its own identity;validitycovers now;human_control: true; entries are human, ataal2(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. - Consume once with the request digest above. Validate the direct response:
exact
approval_id,status: consumed, the samerequest_digest, and a strict booleanidempotent.execution_confirmation.pyApprovalHTTPClient.consume_exactdoes this. Treatidempotent: trueas historical, never as new authority to run. - If the outcome is unknown (timeout), do not consume again to find out. GET
and compare
consumption.request_digestandconsumed_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
actorand 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
subandaal2inassurance: KeyCape. - A policy decision for executor operations: access-engine.
- Execution journal, budget, in-flight stop and receipts: NetKingdom executor runtime and audit-core.