informed-decision/docs/decision-services-alignment-answer.md
tegwick 0e66a33389 INFD-WP-0010: present_existing intake profile (S1) and owner answer to decision services alignment
A registered submitter with a present_existing profile may pose a memo that
pins an approval it created itself in approval-engine. Intake verifies the
object read-only (requested, no entries, human_control, actor == profile
actor, digest == memo, binding == intent, required_count 1, unexpired, one
memo per approval) and never creates, enters or consumes. The recorded
requester reaches review and the access-engine context. Existing submitters
are unchanged.

Answer doc accepts S1 (profile, not route; packet carries the admission),
S4 in principle (waits on gate-house classes and policy) and S3 (exists
behind INFD_HUB_WRITEBACK). Workplan INFD-WP-0010 proposed.

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-10-31 23:07:43 +01:00

11 KiB
Raw 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.

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

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