# 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": "", "target": {"admission_id": "", "admission_digest": "sha256:<64 hex>"} }, "validity": {"not_before": "", "expires_at": ""}, "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:`). 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.