approval-engine/docs/bounded-execution-admission-carrier.md
tegwick 5028fe2231 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
2026-10-31 14:19:29 +01:00

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 (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.