- 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
115 lines
6.2 KiB
Markdown
115 lines
6.2 KiB
Markdown
# 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):
|
|
|
|
```bash
|
|
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.
|