diff --git a/docs/proposals/2026-10-31-decision-services-alignment-review.md b/docs/proposals/2026-10-31-decision-services-alignment-review.md new file mode 100644 index 0000000..1418d6d --- /dev/null +++ b/docs/proposals/2026-10-31-decision-services-alignment-review.md @@ -0,0 +1,171 @@ +# Decision services alignment: approval-engine, informed-decision and State Hub decisions (review, 2026-10-31) + +**Status:** review and proposal. Nothing here changes another repository; every change named below belongs to its +owner. + +**Trigger:** + +- NK-WP-0056-T17 found that the founder has no path today to approve a Bounded Execution Admission + ([finding](2026-10-31-nk56-t17-founder-approval-path.md)). +- The founder asked for an architecture, code, INTENT and SCOPE review of informed-decision and approval-engine. It + should align them, attach State Hub's decision records, and make the facility proportionate: easy for simple + cases, complete for complex ones (founder, 2026-10-31). + +## 1. Practical needs (what the estate does today) + +| Need | How it is handled now | Problem | +| --- | --- | --- | +| **N1. Executor admissions** (NK-WP-0056-T06): a service files an approval for one exact admission, and the founder approves it at aal2 before a bounded run | No path. The executor must be the approval's actor (approval-engine create rule, BEA), but informed-decision presents only approvals it creates itself | The pilot cannot start | +| **N2. Routine founder decisions**: workplan approvals, adopting owner proposals, choosing between options | An agent writes "approved by the founder in session" into a workplan, a CCR comment or a State Hub decision (`decided_by` is free text) | No binding evidence. The same decision is repeated in several places. The full memo path is too heavy for these, so it is not used | +| **N3. Credential approvals (CCRs)**: platform-owner and net-kingdom-owner approve one request | The `credential-change.py approve` tool appends a review comment; for some types the second approval is recorded by hand (CCR-2026-0058) | Evidence is a YAML comment written by an agent on the founder's instruction | +| **N4. Hub coordination**: an open question waits for a human (`needs_human`, `decision_id`) | State Hub `decisions`, resolved through the API with a rationale | Not linked to any presented or bound evidence. informed-decision's write-back to the hub is disabled | + +## 2. The architecture as built (evidence) + +```text + requester (agent / service) founder (browser, KeyCape aal1/aal2) + │ │ + │ State Hub memo dispatch (STATE-WP-0096, │ decisions.coulomb.social + │ private queue, POST /v1/memos) ▼ + └──────────────► informed-decision ──── presents memo, records view_hash, + intake: creates approval highlights, disposition + actor="informed-decision" │ + │ accept → POST /entries (approval:approve) + ▼ + approval-engine ◄── consume (approval:consume, service) + object, CAS, by whoever holds the scope + outbox → audit-core + + State Hub decisions table: title, rationale, decided_by (string), status open/resolved. No link to the above. +``` + +- **approval-engine** (INTENT, SCOPE, `docs/bounded-execution-admission-carrier.md`): + - It is a PIP. It owns the durable approval object: binding to action, target, actor, principal and purpose, + authenticated entries, atomic consume and supersede, and revocation. + - It is explicitly *not an inbox, workflow or notification surface*. It has get-by-id and no list. + - Only the actor creates (`binding.actor` must equal the token subject). + - Consume does not check consumer = actor (APPROVAL-WP-0007-T02, waiting). + - aal2 is stored, not enforced (APPROVAL-WP-0007-T01). +- **informed-decision** (INTENT, SCOPE, GOAL, code): + - It is the live approver surface. Its thesis is *scale invariance*: one memo model from an L0 informed login to an + L4 contract, with L1 being privileged JIT authorization. Only L3 (management approval) is built; L0 and L2 are + "Stage 2, direction only". + - Intake (`intake.py`) refuses a memo that pins `approval_id` (`approval_pinned_by_submitter`). + - `intake_approval.py` creates every approval with `actor: "informed-decision"`, 24 h validity and + `pdp_path: false`. + - Submitters are configured per username with `memo_classes`, `review_groups` and a `daily_limit` + (`intake_service.configuration`). + - On accept it posts the human's entry, scopes `openid approval:read approval:approve`. + - `view_hash` is not inside the approval entry; correlation is `(approval_id, subject, approved_at)`. +- **State Hub:** + - `decisions` is a coordination record (`made|pending`, `open|resolved|escalated|superseded`, free-text + `decided_by`). + - `memo_dispatch` forwards a validated envelope (`informed-decision.memo-intake` v1) to informed-decision's private + intake. Hub resolution write-back is disabled. + +## 3. Findings + +1. **Creation is coupled to presentation.** informed-decision creates the approval with itself as actor. Two + consequences: + - An actor that will *use* an approval (the executor, secrets-engine's PEP, a deploy agent) cannot have it + presented. That is N1. + - Approvals created this way name the surface, not the user, as actor. That weakens approval-engine's "actor who + may use it" binding, which is the very thing BEA relies on. + + Neither INTENT requires this coupling. informed-decision's INTENT says it "never holds approval current-state", + and approval-engine's says only the actor creates. +2. **One weight for every decision.** Every bound decision takes the full memo path: packet, highlights, + presentation record, view_hash, intake budget. The tiers that would make simple decisions cheap (L0 and L1, and + L2 "ADR accept against the estate's own decision records") are not built. In practice, routine decisions (N2) + bypass the surface entirely and become free text. The heaviness pushes the estate to *less* evidence, not more. +3. **Two decision stores with no link.** A State Hub decision and an informed-decision memo about the same question + do not reference each other. Write-back is off, so a hub decision is resolved by an agent's string. The same + founder decision often lives in three places (hub decision, workplan text, CCR comment) with no binding between + them. +4. **An approval filed by someone else is invisible.** approval-engine has no list (correctly), and informed-decision + lists only its own memos. An executor-filed approval therefore reaches the founder only if a memo is filed for + it. That is the missing contract in Finding 1. +5. **The evidence join is partial.** The approval entry does not carry `view_hash`, and assurance is checked by the + surface, not enforced by the object (APPROVAL-WP-0007-T01). For bounded execution the executor checks aal2 itself + today. + +## 4. Proposed alignment + +### 4.1 Roles (no change of ownership, sharper boundaries) + +| Service | Owns | Does not do | +| --- | --- | --- | +| approval-engine | The object, created **only by the actor who will use it** (or by informed-decision when informed-decision itself is the actor); entries, consume, revoke | Present, list or notify | +| informed-decision | **The one human-facing surface for every bound decision.** It presents questions, collects dispositions and records what was shown. For approvals it adds the human entry to an approval *someone else created*, after verifying its binding | Create approvals on behalf of other actors; hold approval state | +| State Hub decisions | **The index and routing of open questions** for work coordination: what is waiting, for whom, in which workplan. It links to evidence and is resolved by it | Hold binding evidence; be resolved by free text for decisions that need binding | +| gate-house | Doctrine: which decision classes exist, which weight each needs, who may approve | Runtime state | + +### 4.2 Contract: "present an existing approval" (closes Findings 1 and 4, needed for N1) + +1. **Create.** The actor creates the approval in approval-engine, as itself, with `human_control: true`. +2. **Submit.** The actor submits a memo to informed-decision that references `approval_id` and the binding digest. + The memo carries the question, the packet (for an admission: the admission YAML and its digest) and the + highlights (scope ceiling, window, budget, revert). +3. **Verify.** informed-decision accepts the reference only from a registered submitter whose profile allows + `present_existing` for that action class. It then `GET`s the approval and requires all of: + - status `requested`; + - no entries; + - `human_control: true`; + - `binding.actor` equal to the submitter's subject; + - binding digest equal to the memo's; + - the action class allowed for the submitter. +4. **Accept.** Accepting posts the founder's entry with their token (aal2 where the class requires it). + informed-decision never consumes and never creates in this path. +5. **Run.** The actor reads the approval (`GET`) and consumes it. The executor's existing checks (actor, digest, + aal2, listed approvers) still hold. + +### 4.3 Proportional weight: decision classes (closes Finding 2) + +One memo model, as informed-decision's thesis requires, with the *amount of ceremony* set per class by doctrine: + +| Class | Example | Surface | Assurance | approval-engine object | Evidence | +| --- | --- | --- | --- | --- | --- | +| **C0 note** | Record a finding; acknowledge a report | State Hub only | none | no | hub record | +| **C1 confirm** | Approve a workplan; adopt an owner proposal; choose option A/B | informed-decision single-screen confirm: question, one packet item, one required highlight, options | fresh session (aal1) | no | memo acceptance with `view_hash`; resolves the hub decision | +| **C2 bound approval** | Bounded-execution admission; credential lane (CCR); production deploy | informed-decision full memo, presenting an existing approval (4.2) | aal2, fresh | yes, created by the actor | memo + approval entry + audit-core | +| **C3 instrument** | Contract or multi-party | later (informed-decision L4) | per party | per party | as informed-decision Stage 3+ | + +The rule: **anything a service will execute on the strength of the decision is C2.** An admission's `risk_class` can +add highlights or required approvers within C2, but cannot lower it to C1. + +### 4.4 Attaching State Hub decisions (closes Finding 3) + +- A hub decision gains optional links: `memo_id`, `approval_id` and `evidence_ref`. +- When it needs C1 or C2, the hub dispatches a memo (existing STATE-WP-0096 path), and informed-decision's write-back + (now disabled) resolves the hub decision with: + - the authenticated subject; + - the memo revision and `view_hash`; + - for C2, the approval id. +- An agent must not resolve a C1 or C2 decision by text. +- C0 stays as today. + +This makes the hub the place to see *what is waiting*, and informed-decision the place where it is *decided*, without +either duplicating the other. + +## 5. First slice, driven by what blocks today + +| # | Change | Owner | Unblocks | +| --- | --- | --- | --- | +| S1 | informed-decision intake profile `present_existing` (4.2), with tests against approval-engine's real source | informed-decision | N1 / NK-WP-0056-T06 | +| S2 | Executor: KeyCape client `nk-bind-executor-approval`, OpenBao lane for its secret, a `request` phase that creates the approval and submits the memo | net-kingdom (NK-WP-0056-T14) + KeyCape registration + railiance-platform CCR | N1 | +| S3 | Write-back for memos that carry a hub `decision_id`, and decision links in State Hub | informed-decision + state-hub | N4, part of N2 | +| S4 | The C1 single-screen confirm in informed-decision | informed-decision | N2, and N3 later | +| S5 | Doctrine for decision classes C0–C3 and the "executed means C2" rule | gate-house | Every class assignment | +| S6 | approval-engine: aal2 floor (APPROVAL-WP-0007-T01) and `actor_consumes` (T02) | approval-engine | Moves the executor's own checks into the object | + +S1 and S2 are the minimum for the pilot. S3–S5 make routine founder decisions cheaper *and* better evidenced than +today's free text. + +## 6. Open questions + +1. **For the founder:** are the classes C0–C3 and the "executed means C2" rule the right cut? +2. **For informed-decision:** does `present_existing` fit the intake model as a profile, or should it be a separate + route? Should the memo's packet carry the admission YAML itself, or a digest plus link? +3. **For approval-engine:** should an approval created by an actor and presented elsewhere carry a + `presentation_ref` (the memo id) at create, so that the object points at its evidence? (Finding 5) +4. **For State Hub:** decision links as new columns, or as typed `evidence_ref`s? diff --git a/workplans/NK-WP-0056-bounded-executor-path.md b/workplans/NK-WP-0056-bounded-executor-path.md index cf44c4f..7d02469 100644 --- a/workplans/NK-WP-0056-bounded-executor-path.md +++ b/workplans/NK-WP-0056-bounded-executor-path.md @@ -722,6 +722,11 @@ founder approves it, and the executor then consumes it. Needed: `human_control: true`, `pdp_path: false`) and prints its id; - the existing run phase then consumes it. +**2026-10-31:** T17 showed that the founder cannot be presented an executor-filed approval today. The founder asked +for an alignment review of informed-decision, approval-engine and State Hub decisions +([review](../docs/proposals/2026-10-31-decision-services-alignment-review.md)). T14's request phase follows its +"present an existing approval" contract (S1 informed-decision, S2 here). + **Done when:** - the client and the CCR are prepared;