Founder decisions relayed 2026-11-01: GH-DEC-2026-026 accepted as drafted
(unblocks S4 doctrine; S4 now waits only on access-engine policy), and the
hub pulls decision outcomes through its STATE-WP-0096 dispatch worker.
S3 re-scoped: GET /v1/memos/{id}/outcome on the private listener returns
informed-decision.decision-outcome v1 for memos the caller posed, only for
a submitter row with outcome_read: true. Read-only and value-free.
T05 (enable after STATE-WP-0101) and T06 (retire push write-back) added.
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
223 lines
15 KiB
Markdown
223 lines
15 KiB
Markdown
# 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`.
|
||
|
||
## Founder decisions, 2026-11-01
|
||
|
||
Relayed by the net-kingdom coordinator on 2026-11-01 (quoted as relayed):
|
||
|
||
1. **gate-house GH-DEC-2026-026 accepted as drafted:** decision classes C0–C3; a decision is at least C2 when
|
||
anyone relies on it as authority for an act its standing mandate doesn't cover; C3 reserved; the actor that
|
||
files a C2 approval may never approve it; agents never approve at C1+. This answers open question 1 and removes
|
||
S4's doctrine dependency. S4's only remaining external blocker is access-engine policy work (below).
|
||
2. **State Hub write-back:** the hub's existing STATE-WP-0096 dispatch worker fetches decision outcomes from
|
||
informed-decision (informed-decision gets no hub credential); state-hub needs an outcome read, record
|
||
`informed-decision.decision-outcome` v1 (fields: state-hub `docs/decision-services-alignment-answer.md`
|
||
§5.2). Switch informed-decision's hub write-back on once STATE-WP-0101 lands. This answers open question 2
|
||
and re-scopes S3 (below).
|
||
3. access-engine (review policy for requester `service:nk-bind-executor`) and key-cape (`approval:read` for
|
||
`informed-decision-sitting-requester`) are being asked by separate workers (open questions 3 and 4).
|
||
|
||
## 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. Doctrine settled 2026-11-01; waits only on an access-engine policy action |
|
||
| S3 hub write-back | **Accepted, as a pull** (founder decision 2026-11-01) | Outcome read implemented locally and tested (WP-0010-T04); enablement waits on STATE-WP-0101 (T05) |
|
||
|
||
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:<slug>` 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: <actor>`. 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). **Update 2026-11-01:** items 1 and 3 below are
|
||
answered by GH-DEC-2026-026 (accepted as drafted). A C1 confirm is never authority for an act outside a standing
|
||
mandate (such reliance makes the decision C2), so it needs no approval object and does not bind for execution.
|
||
Item 2, access-engine policy work, is the only remaining blocker. S4 is not being implemented now.
|
||
It was not built because three things had to 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
|
||
|
||
### Outcome read (founder decision 2026-11-01, implemented locally)
|
||
|
||
The hub pulls. `GET /v1/memos/{memo_id}/outcome` on the private workload listener:
|
||
|
||
- **Who:** TokenReview with audience `informed-decision-intake` (the worker's existing token), and a submitter row
|
||
with `outcome_read: true`. Only State Hub's row (`system:serviceaccount:state-hub:state-hub`) is meant to carry
|
||
it; the flag cannot be combined with `present_existing`. Not yet set in shipped deployment data (T05).
|
||
- **What:** only memos that submitter posed (anything else is 404 `memo_not_found`, including other submitters'
|
||
memos), serving a hub decision with `resolves: on-answer` (else 409 `memo_does_not_resolve`).
|
||
- **When:** the first terminal human disposition (accept or decline) on any revision is the answer. An accept is
|
||
reported only once its approval-engine entry is confirmed; before that the answer is 202 `pending`, and a lost
|
||
entry response is 409 `approval_entry_unresolved` (needs the existing recovery, never a guess).
|
||
- **Record** (`informed-decision.decision-outcome` v1, `status: decided`): `hub_decision_id`, `memo_id`,
|
||
`memo_revision`, `verb`, `subject {issuer, sub}`, `assurance {aal, auth_time}` (from the policy observation
|
||
recorded for that act; 409 `assurance_unrecorded` if absent), `decided_at`, `view_hash`, `presentation_id`,
|
||
`approval_id`, and for an accept `approval_entry {approved_at}`; `audit_ref` once audit-core has receipted the
|
||
disposition. Bounded at 8 KiB.
|
||
- **Deliberate differences from state-hub §5.2**, for state-hub to confirm:
|
||
- `approval_entry_id`: approval-engine entries have no id. The record carries `approval_entry.approved_at`; with
|
||
`approval_id` and `subject.sub` it is approval-engine's correlation triple.
|
||
- `option`: not emitted until S4 adds options.
|
||
- `display_name` and `note`: not emitted (value-free; the memo's principal display name is the review group's,
|
||
not the human's). The hub fills `decided_by` from `subject`.
|
||
- **Read-only:** no budget spent, no row written, no outbound call.
|
||
|
||
`INFD_HUB_WRITEBACK` (the push below) stays off under decision 2 and is retired after the pull is live (T06).
|
||
|
||
### Push write-back (INFD-WP-0005, superseded)
|
||
|
||
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: "<reviewer> 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.~~ Answered 2026-11-01:
|
||
GH-DEC-2026-026 accepted as drafted.
|
||
2. ~~**Founder:** enable hub write-back in production now (S3)?~~ Answered 2026-11-01: hub pulls; switch on once
|
||
STATE-WP-0101 lands.
|
||
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".
|