informed-decision/docs/decision-services-alignment-answer.md
tegwick 8787e1a14a INFD-WP-0010: record founder decisions 2026-11-01; outcome read for State Hub's pull (T04)
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
2026-11-01 00:37:59 +01:00

15 KiB
Raw Permalink Blame History

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):

"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".