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
15 KiB
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):
- 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).
- 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-outcomev1 (fields: state-hubdocs/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). - access-engine (review policy for requester
service:nk-bind-executor) and key-cape (approval:readforinformed-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.mdandAGENTS.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):
"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) andmemo.approval_binding_digest(sha256:<64 hex>) are required. They remain forbidden (approval_pinned_by_submitter) for every other submitter.approval_intent.actionmust be in the profile'sactions(action_not_admitted).approval_intent.targetmay 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)
- access-engine policy.
informed-decision.compact-sittingv13 hard-codesrequester_ok if input.context.approval_requester == "informed-decision". A present-existing memo is therefore refused at review time withrequester_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 requireaal2forbounded-execution. This needs an access-engine/flex-auth owner change, followed by a pin update here. - KeyCape requester client scope. The intake-time GET needs
approval:readoninformed-decision-sitting-requester, which today holds onlyapproval:create. Until key-cape grants it, the read exchange is refused and only present-existing submitters fail (503approval_unreadable). - Deployment data and network. The registry row above,
memo_class/ submitter in the policy's admitted sets, a NetworkPolicy fromsso/nk-bind-executorto the private workload listener (8081), and a projected token with audienceinformed-decision-intakeon 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_hashand 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:
- 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;
- an access-engine policy action for a confirm without an approval object (today review refuses
missing_act_binding); - 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 withoutcome_read: true. Only State Hub's row (system:serviceaccount:state-hub:state-hub) is meant to carry it; the flag cannot be combined withpresent_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 withresolves: on-answer(else 409memo_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 409approval_entry_unresolved(needs the existing recovery, never a guess). - Record (
informed-decision.decision-outcomev1,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; 409assurance_unrecordedif absent),decided_at,view_hash,presentation_id,approval_id, and for an acceptapproval_entry {approved_at};audit_refonce 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 carriesapproval_entry.approved_at; withapproval_idandsubject.subit is approval-engine's correlation triple.option: not emitted until S4 adds options.display_nameandnote: not emitted (value-free; the memo's principal display name is the review group's, not the human's). The hub fillsdecided_byfromsubject.
- 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=1in 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
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.Founder: enable hub write-back in production now (S3)?Answered 2026-11-01: hub pulls; switch on once STATE-WP-0101 lands.- 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_assuranceonce APPROVAL-WP-0007-T01 enforces it? - key-cape owner: add
approval:readto the sitting requester client (blocker 2), or register a separate read-only client. - approval-engine (review §6 Q3): a
presentation_refset 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. - 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".