approval-engine/docs/decision-services-alignment-answer.md
tegwick e9e9f93317 APPROVAL-WP-0007-T01 done: required_assurance floor; executor ingress; alignment answer
- 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
2026-10-31 23:07:05 +01:00

6.2 KiB

Decision services alignment — Approval Engine owner answer

Status: owner answer to the NetKingdom review net-kingdom/docs/proposals/2026-10-31-decision-services-alignment-review.md (hub message 81cafbf3-8d54-44c6-9235-6f46e95aaf74), 2026-10-31. Also answers the pod-scoped ingress request (hub 39a3bffa-2c7c-4ca6-8a57-61e4ed94820f). This is an engine-owner technical position, not a founder decision. Nothing here is deployed.

Summary

Item Position
Roles (§4.1) Accepted. No change of role is needed here
Present an existing approval (§4.2) Works on today's API. Step 3 needs only GET /v1/approvals/{id} with approval:read
S6 / T01 aal2 floor Implemented in source (schema v7). Not yet released or deployed
S6 / T02 actor_consumes Supported, still waiting. Owner proposes splitting it from principal_approves
§6.3 presentation_ref at create Declined as proposed. Counter-proposal: the link goes on the entry, from the presenter
Executor ingress Prepared in source: deploy/networkpolicies.yaml, policy approval-engine-bounded-executor. Live apply is founder-run

1. Roles

The proposal matches this engine's INTENT and SCOPE:

  • The engine keeps the object: binding, entries, consume, supersede, revoke and outbox evidence.
  • Only the actor creates. POST /v1/approvals already refuses unless binding.actor equals the token subject. informed-decision stays a legitimate actor for approvals it uses itself.
  • The engine stays not-an-inbox: get by id, no list and no notification. Discovery belongs to State Hub (the index) and informed-decision (the surface).

For the §4.2 verify step, informed-decision reads everything it needs from one GET: status, entries, binding.human_control, binding.actor and binding.digest. It should also check binding.required_assurance (T01, once deployed) when a C2 class requires aal2. With human_control: true, the engine already refuses an entry posted under informed-decision's own service token. The founder's entry must carry the founder's token, as it does today.

One addition to §4.2 step 3: compare validity as well, so a memo cannot present an approval whose window has already passed or is far wider than the packet says.

2. S6

T01 (aal2 floor): done in source.

  • POST /v1/approvals accepts an optional required_assurance: {"level": "aal1"|"aal2"|"aal3"}.
  • POST /entries refuses a weaker, missing or unknown level from the verified token.
  • The claim reports assurance_unsatisfied, and consume refuses a persisted entry below the floor.
  • The floor is inherited on supersession and is not part of the act digest.
  • Legacy rows migrate to NULL, meaning no floor.

The member appears in GET and the claim only when declared. The access fact reader rejects unknown members, so undeclared claims keep their exact bytes. Contract: docs/approval-claim.md. It needs a v7 image release and a founder-run rollout with a verified pre-v7 backup. Until then the executor keeps its own aal2 check. Whether KeyCape issues aal2 stays KeyCape's question.

T02 (correspondence): supported, not implemented. The owner proposes splitting it:

  • actor_consumes (consumer subject must equal binding.actor). It is low-risk and closes the denial-of-run gap: today any tenant service holding approval:consume can spend the object. It adds no new identity mapping, because the actor is already pinned to the creator's token subject. The owner is ready to implement it as an opt-in issue flag once Gate House (or the founder) agrees that actor means "the only consumer" when the flag is set.
  • principal_approves (entry subject must equal binding.principal). It stays waiting. NetKingdom's principal labels (for example admin) must first equal KeyCape subjects exactly, and C2 doctrine (S5) may name approvers that are not the principal.

3. Open question 6.3 — presentation_ref at create

Declined as proposed, for three reasons:

  1. Ordering. In §4.2 the actor creates the approval before it submits the memo, so no memo id exists at create. Setting the field later would make the object mutable outside its state machine. The binding is immutable by design.
  2. Wrong asserter. A reference set by the actor is the actor's claim about where it will be presented. The evidence that matters is what the approver was shown, and only the presenting surface can state that.
  3. The join already exists in one direction. The memo carries approval_id and the binding digest (§4.2 step 2), and audit-core can join on (approval_id, subject, approved_at).

Counter-proposal (needs informed-decision agreement; not implemented): an optional entry-level presentation record. The approving surface posts it with the entry, for example {"memo_id", "revision", "view_hash"}. The engine stores it on that entry, serves it in GET and emits it in the issuance and use threshold evidence. It is labelled surface-asserted: the engine checks its shape, not its truth. Identity, principal type and assurance still come only from the token. This closes Finding 5 (view_hash not inside the entry) without changing creation. If agreed, it becomes a new APPROVAL-WP-0007 task.

4. Executor ingress

approval-engine-bounded-executor admits TCP 8080 to the approval-engine pod from pods labelled app.kubernetes.io/name=bounded-executor in namespace sso. The rule needs both selectors to match: they sit in one from element. It does not label sso and does not change approval-engine-callers. tests/test_networkpolicies.py holds that shape, so a split into two peers, which would admit all of sso, fails the test. Network admission grants no authority: the executor still needs its KeyCape token and scopes. To apply (founder-run):

kubectl apply -f deploy/networkpolicies.yaml

Founder decisions needed

  1. Release and roll out the v7 image (T01) when the pilot needs the engine-side floor.
  2. Agree the T02 split, and that actor_consumes may be implemented now.
  3. Accept or decline the entry-level presentation counter-proposal (with informed-decision).
  4. Apply the executor ingress policy.