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
11 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.
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.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). It is not built because three things must 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
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=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.
- Founder: enable hub write-back in production now (S3)?
- 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".