# Owner answer: decision services alignment (S1, S3, S4) **Date:** 2026-10-31 **Answering:** net-kingdom `docs/proposals/2026-10-31-decision-services-alignment-review.md` (commit `b764561`), trigger `docs/proposals/2026-10-31-nk56-t17-founder-approval-path.md`, hub message `6811164f`. **Workplan:** `workplans/INFD-WP-0010-present-existing-and-proportionate-decisions.md`. ## Summary | Item | Answer | Status | | --- | --- | --- | | S1 `present_existing` | **Accepted, adapted** as a submitter *profile* on the existing `/v1/memos` route | Implemented locally and tested; not deployed. Two owner dependencies outside this repository before live use (below) | | S4 C1 single-screen confirm | **Accepted in principle** as a lighter memo profile on the same memo model | Not built. Needs gate-house class doctrine (S5) and an access-engine policy action first | | S3 hub write-back | **Accepted.** The write-back already exists (`hub_writeback.py`, INFD-WP-0005) behind `INFD_HUB_WRITEBACK=1` | Enablement is a founder decision; structured links need state-hub's decision-link fields | The founder's direction (one surface, weight proportional to risk, a clear split with approval-engine) fits `INTENT.md` directly: one memo model at every level is the thesis, and "never own approval state" makes the proposal's role split the correct one. Intake creating approvals as `informed-decision` was a Stage 1 convenience, not a principle. It stays for existing submitters (state-hub), because those approvals are used by nobody but the human review itself. ## S1: present an existing approval ### Fit - `INTENT.md` and `AGENTS.md`: informed-decision never owns approval state, never consumes, never decides. Presenting an object someone else created keeps all three. It removes informed-decision from the actor role, which it should never have had for approvals that a service will execute against. - The review side already re-reads the approval with the human's token on every open/accept, and compares the binding digest and actor to the intake record (`review.py` `_authorize`). Only intake and the recorded requester had to change. ### Profile, not route (answer to review §6 Q2a) `present_existing` is a property of the **registered submitter**, not of the request body. Same route (`POST /v1/memos` on the private workload listener), same envelope (`informed-decision.memo-intake` v1). Reasons: one admission path (TokenReview, daily budget, idempotency, secret scan, packet hashing all unchanged), and a submitter cannot choose its own weight. A row is either a create-path submitter or a present-existing submitter, never both. ### Contract (as implemented) **Registry row** (`intake-submitters.json`, reviewed deployment data, reloaded on every request): ```json "system:serviceaccount:sso:nk-bind-executor": { "submitter_id": "nk-bind-executor", "memo_classes": ["bounded-execution"], "review_groups": ["net-kingdom-admins"], "daily_limit": 10, "present_existing": { "approval_actor": "service:nk-bind-executor", "actions": ["bounded-execution"] } } ``` The key is the Kubernetes service account (TokenReview, audience `informed-decision-intake`). The `approval_actor` is the KeyCape subject the same workload uses towards approval-engine. The registry is the only place that joins those two identities; the body never names the actor. `approval_actor` must match `service:` and must not be `service:informed-decision`. `actions` is a non-empty list of slugs. **Envelope differences for a present-existing submitter:** - `memo.approval_id` (UUID) and `memo.approval_binding_digest` (`sha256:<64 hex>`) are **required**. They remain forbidden (`approval_pinned_by_submitter`) for every other submitter. - `approval_intent.action` must be in the profile's `actions` (`action_not_admitted`). - `approval_intent.target` may be any flat object of 1–8 string fields (BEA uses `{admission_id, admission_digest}`). For other submitters it stays `{id, type, system}`. - `approval_intent.{action, principal, purpose, target}` restate the approval's binding; the actor is taken from the profile. **Verification** (`intake_service.verify_existing`), by a read-only `GET /v1/approvals/{id}`; every condition must hold, the first failure is the refusal (HTTP 409 unless noted): | Check | Refusal | | --- | --- | | returned `id` equals `memo.approval_id` | `approval_mismatch` | | `status == "requested"` | `approval_not_requested` | | `entries == []` | `approval_has_entries` | | `binding.human_control is true` | `approval_not_human_controlled` | | `binding.actor ==` the profile's `approval_actor` | `approval_actor_mismatch` | | `binding.digest == memo.approval_binding_digest` | `approval_digest_mismatch` | | `binding.{action, principal, purpose, target}` equal the intent | `approval_binding_mismatch` | | `required_count == 1` (Stage 1 presents one approver) | `approval_required_count_unsupported` | | `validity.expires_at` in the future | `approval_expired` | | GET failed or no reader configured | `approval_unreadable` / `present_existing_unavailable` (503) | | another memo already presents this approval | `approval_already_presented` | The action-class check happens at validation, before any read. On refusal the reservation is released (no engine side effect happened), so a corrected submission can be retried. In this path informed-decision never calls create, entries or consume. The receipt has the same shape as today (`memo_id`, `approval_id`, `approval_binding_digest`). **After intake:** the ledger records `profile: present_existing` and `approval_requester: `. The review then checks on every open and accept that the live object's actor equals that requester, and it passes `approval_requester` to access-engine as the attribute context. Accept posts the founder's entry with their own token, as today. The executor then GETs and consumes the approval itself. **Code:** `informed_decision/intake.py` (`PresentExisting`, pinned validation), `intake_service.py` (registry profile, `verify_existing`, one approval per memo), `intake_approval.py` (`IntakeApprovalReader`, GET-only), `requester.py` (read-scoped exchange, `compose_intake` wiring), `store.py` (recorded requester). **Tests:** `tests/test_intake_present_existing.py` (25 cases, one of them opt-in against approval-engine's real store with `INFD_APPROVAL_ENGINE_SOURCE`). Full suite: 676 passed, 83 skipped; with the real engine 735 passed, 24 skipped. Existing submitters behave exactly as before (asserted by test). ### What still blocks live use (outside this repository) 1. **access-engine policy.** `informed-decision.compact-sitting` v13 hard-codes `requester_ok if input.context.approval_requester == "informed-decision"`. A present-existing memo is therefore refused at review time with `requester_mismatch`, which fails closed, as it should. A new policy version must admit the `(intake_submitter, approval_requester, memo_class)` triple, here `(nk-bind-executor, service:nk-bind-executor, bounded-execution)`, and should require `aal2` for `bounded-execution`. This needs an access-engine/flex-auth owner change, followed by a pin update here. 2. **KeyCape requester client scope.** The intake-time GET needs `approval:read` on `informed-decision-sitting-requester`, which today holds only `approval:create`. Until key-cape grants it, the read exchange is refused and only present-existing submitters fail (503 `approval_unreadable`). 3. **Deployment data and network.** The registry row above, `memo_class` / submitter in the policy's admitted sets, a NetworkPolicy from `sso/nk-bind-executor` to the private workload listener (8081), and a projected token with audience `informed-decision-intake` on the executor Job. The founder runs these under the usual release. ### Packet content (answer to review §6 Q2b) **The packet carries the admission YAML itself**, not a digest plus link. A required highlight must point at the scope ceiling, the window, the budget and the revert. The view_hash covers what the founder was shown. A link would leave the shown content outside the evidence and outside informed-decision's custody, which the evidence model forbids for a binding memo. A later, optional check (WP-0010-T02) can require for `bounded-execution` that one packet item's digest equals `target.admission_digest`. This is open only because the admission digest's canonical form needs confirming with net-kingdom. ## S4: C1 single-screen confirm **Accepted in principle, adapted.** A C1 confirm is the memo model with less ceremony, not a second model: - one question, one packet item, one required highlight, two to four options; - fresh session (aal1, existing 900-second freshness); - `view_hash` and disposition as today, and no approval-engine object; - it resolves the hub decision it serves (S3). This is what `INTENT.md` calls the lower depth levels (L1/L2). It is not built because three things must exist first: 1. gate-house doctrine for the C0–C3 classes and the rule that anything executed on the strength of a decision is C2 (review S5). informed-decision must not decide which class a decision has; 2. an access-engine policy action for a confirm without an approval object (today review refuses `missing_act_binding`); 3. a founder answer on whether C1 acceptances count as "binding" for N2 (workplan approvals, owner proposals). ## S3: hub write-back **Accepted.** It exists already: an intake envelope's `serves: {hub_decision_id, resolves: on-answer}` queues a resolution atomically with a terminal human disposition (accept or decline). The worker posts `/decisions/{id}/resolve` with `decided_by: " via informed-decision"` and a rationale naming the memo, the version, the approval, the binding digest, the `view_hash` and the disposition. Remaining work: - the founder's decision to set `INFD_HUB_WRITEBACK=1` in production (a release); - with state-hub: structured fields (`memo_id`, `approval_id`, `view_hash`, `evidence_ref`) once state-hub has decision links (review §6 Q4), replacing the parse-the-rationale idempotency check; - state-hub enforcing that a C1/C2 decision is resolved only by this write-back, not by text (state-hub owns that rule). ## Open questions 1. **Founder:** are C0–C3 and "executed means C2" the right cut? This gates S4. 2. **Founder:** enable hub write-back in production now (S3)? 3. **access-engine owner:** policy version admitting present-existing requesters (blocker 1). Should aal2 be required per memo class in policy, or by approval-engine's `required_assurance` once APPROVAL-WP-0007-T01 enforces it? 4. **key-cape owner:** add `approval:read` to the sitting requester client (blocker 2), or register a separate read-only client. 5. **approval-engine (review §6 Q3):** a `presentation_ref` set at create would let the object point at its memo. informed-decision would welcome it, but it cannot be checked at intake because the memo does not exist yet. A post-accept back-link is a better shape, and it is approval-engine's call. 6. **Multi-approver approvals** (`required_count > 1`) are refused for now. Supporting them means one memo per approver against the same approval, which needs a deliberate change to "one approval, one memo".