Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1029c-c003-7283-acc9-930b434e6cae
22 KiB
Frictionless decision intake
Status: proposed design, 2026-10-03. Workplan: INFD-WP-0005.
Driver: CUST-WP-0075, security close-down checklist row 2.
Founder direction (2026-10-03): "The idea about having decisions.coulomb.social
is to make it easy and frictionless to pose and answer decisions. It makes no
sense to have manual steps to provide the decisions in the first place."
Target: posing a decision takes no human step. Only answering is human.
This is an addendum to the Stage 1 specs, not a new product. It keeps the
Decision Memo, the review UI, CompactSignoffBatches.md and its
anti-requirements. It removes the steps that put a drafted memo in front of the
reviewer, and does not touch the step where the reviewer answers.
1. What posing costs today
Example: docs/batches/2026-10-03-provisioning-window/OPERATOR.md.
| Step | What happens | Why it is manual |
|---|---|---|
| A3 | The founder runs warden access informed-decision-sitting-requester-login --exec -- tools/create_sitting_approvals.sh |
The create-only client secret (informed-decision-sitting-requester, approval:create only, sub=informed-decision) is held in attended reader custody, so a human has to log in only to read it |
| A4 | Someone hand-edits access-engine/examples/informed-decision-sitting/policy.md (new exact record, version bump), CI builds it, the pin is redeployed, deploy/sitting-admission.json is bumped and the review service rolls out |
Admission is an exact-record list: each memo id, approval id and binding digest is a policy value |
| A5 | An operator runs attach_compact_bindings.py and load_sitting_memos.py against the live store |
There is no intake: memos are files that reach the store through an operator CLI |
| A6 | The founder signs in with fresh MFA and answers | This is the only step that has to be human |
So the attended human appears three times before the one act that needs him. None of A3 to A5 adds judgment about the question. A3 is custody friction, A4 duplicates facts approval-engine already holds, and A5 is plumbing.
2. Target flow
agent / service informed-decision (intake) approval-engine access-engine founder
| POST /v1/memos ------------> authenticate workload
| (memo + packet) validate (schema, 1 question,
| secret scan, packet digests)
| idempotency check
| POST /v1/approvals ------------> draft, digest
| (own create-only client) (human_control)
| pin approval_id + digest on memo
| save memo + packet in live store
| audit-core: memo.posed
| <-- 201 {memo_id, url} ----- notify founder (hub message)
| opens link
| GET memo -> approval-engine get-by-id (human token)
| check digest/actor/human_control
| POST /v1/check (attribute rule) ---------------> allow/deny
| render; view_hash
| ack + accept
| fresh MFA check, POST /entries (human token)
| audit-core: disposition
| write-back: resolve hub decision (memo id in rationale)
3. Intake API
POST /v1/memos on a separate in-cluster listener of the review service
(not on the public decisions.coulomb.social ingress). The browser surface
stays read-and-answer only.
3.1 Submitter identity (workload, not human)
- The caller presents a projected Kubernetes service-account token with
audience
informed-decision-intake. The service verifies it with TokenReview, the same pattern access-engine uses for its own callers. - The verified
system:serviceaccount:<ns>:<sa>is looked up indeploy/intake-submitters.json, a deployment-owned allow list mapping each submitter to asubmitter_idand to the memo classes and review groups it may pose. An unknown or unmapped caller is refused (submitter_unknown). - The submitter id is never read from the body. The body may carry
drafted_by(for exampleclaude@the-custodian), which is shown to the reviewer as asserted provenance, not as authenticated identity. - First submitter: State Hub. Most decisions are drafted by coding agents on
the workstation, which have no workload identity. They already talk to the
hub. The hub (
system:serviceaccount:state-hub:state-hub) forwards a memo attached to a hub decision, soserves.hub_decision_idis known and authenticated by construction. In-cluster services (activity-core, secrets-engine, ...) may call intake directly once they are listed. - No human token is ever accepted on this route. A human posing a decision goes through an agent or the hub like everyone else; there is no "human submitter" shortcut that could become a bind.
3.2 Envelope
Schema: schemas/memo-intake.schema.json (kind informed-decision.memo-intake,
version 1). Closed object:
| Field | Meaning |
|---|---|
idempotency_key |
Submitter-chosen, 8 to 128 chars [A-Za-z0-9._:-] |
memo_class |
One of the classes in the submitter's allow list (for example credential-lane, work-record-accept, spend-envelope, adr-accept) |
review_group |
For example net-kingdom-admins. Must be allowed for the submitter |
drafted_by |
Optional, asserted provenance string |
serves |
Optional {hub_decision_id, resolves: "on-answer" | "never"} |
approval_intent |
{action, principal, purpose, target{id,type,system}}; actor is not accepted, intake sets it to informed-decision |
memo |
The memo record in the operational shape records.memo_from reads (as in docs/batches/*/…memo.json) |
packet |
[{item_id, media_type, content}], UTF-8 text, at most 256 KiB each |
3.3 Validation (all fail closed, first failure wins, stable reason codes)
- Shape. Closed envelope, closed memo keys, known enums.
- Unsigned draft.
version == 1,sealed == false,approval_idandapproval_binding_digestarenull(intake fills them),binding.principal.id == "pending-human-session". A submitter cannot pre-bind or pin an approval. - One question per memo.
questionis one line, at most 400 chars, ends with?and contains exactly one?.requested_actis one value. A memo whose target or highlights reference more than one work record of different acts is refused (bundled_acts). This is a mechanical floor; the anti-requirement still binds drafters. - Highlights. Unique ids, each
item_idnames a packet item. At least one highlight withrequired_ackforcriticalseverity is preserved as given; intake never drops or reorders highlights. - Packet integrity. Every
memo.packet[*]has exactly one packet entry with the sameitem_id, andsha256(content)equals the memo'shash. No orphan packet content. - Secret-marker scan over every string in the memo, the intent and the
packet: PEM private keys,
Bearertokens, JWT-shaped strings, OpenBao/Vault tokens (hvs.,s.), provider key prefixes (sk-,sk-or-,ghp_,github_pat_,glpat-,AKIA),password=/secret=/token=assignments, credentials in URLs. A hit refuses the whole submission withsecret_markerand the field path only; the matched text is never echoed, logged or stored. - Authorization of the pose.
memo_classandreview_groupare in the submitter's allow list.
3.4 Idempotency
submission_digest = sha256(canonical JSON of {submitter_id, envelope without idempotency_key}). The ledger key is (submitter_id, idempotency_key):
- new key → accept;
- same key, same digest → replay: return the stored
{memo_id, approval_id}, create nothing; - same key, different digest →
409 idempotency_conflict; - a memo id already in the store under another key →
409 memo_id_taken.
The ledger row is written before the approval-engine POST with state
creating, and finished after. A crash before receipt retention leaves a creating
row that refuses automatic retry; see §4 for recovery requirements.
A new version of a memo is a new submission with a new memo id or an explicit
supersedes relation in a later version of this contract; intake never
mutates a stored memo.
4. Approval creation without a human login
approval-engine already supports this. docs/caller-authentication.md:
"A service or agent with approval:create may draft that request." Create
needs binding.actor == sub, and the requester client
informed-decision-sitting-requester exists (CCR-2026-0026/0027, create-only,
sub=informed-decision, principal_type=service, 15 min tokens).
The only reason A3 is attended is custody: the client secret is readable only through the attended reader lane. The change:
- railiance-platform delivers
CLIENT_SECRETfromplatform/workloads/informed-decision/sitting-requesterto the review workload by ESO (a workload read grant, the shapesecrets-engine-requesteralready has). No new KeyCape client, no new scope. - The intake does
client_credentials→POST /v1/approvalswithhuman_control: true,required_count: 1,binding.actor = "informed-decision"and the submitter'sapproval_intent. - The submitting workload is the requester; the human is only the
approver. Approval-engine records
informed-decisionas requester. The originating submitter anddrafted_byare recorded on the memo and in audit-core, not invented into the approval binding. - GH-DEC-2026-016 still holds:
POST /entrieson ahuman_controlapproval refuses any non-human principal at bind time, independently of us.
What approval-engine would ideally add (requested, not blocking):
- An
Idempotency-KeyonPOST /v1/approvals, so a retried create after a crash returns the same draft instead of leaving an orphan. Until then acreatingrow without a receipt requires explicit owner-assisted reconciliation and never automatically repeats creation. Acreatedrow with a durable receipt retries local loading only. An orphan draft is never bound and expires. - An optional, non-binding
requested_viaevidence field so the engine's own record names the original submitter.
5. Admission without a per-memo policy deploy
5.1 What the exact-record list protects, and what replaces it
The exact-record list (informed-decision.compact-sitting v1–v6) does two
jobs:
- (a) the human sees exactly what is bound: the memo version, approval id and binding digest the reviewer acts on are the ones an operator pinned;
- (b) posing is operator-gated: nothing reaches a reviewer unless someone edited policy.
(b) is the friction the founder wants removed. (a) must stay. (a) does not need
a policy value per memo: approval-engine already holds the binding digest, and
the review service already re-reads the approval live and compares it with the
memo pin before rendering and before every act (approval_client). The
view_hash covers memo content; the binding digest covers the act. Both stay
distinct and non-substitutable (open hub decision 8c3c0d14).
5.2 The attribute rule (access-engine, new branch or package)
Allow read, acknowledge, accept, return, discuss, decline when:
- the identity bar is unchanged: human,
authentication-derivedprincipal type,net-kingdom-admins(or the memo'sreview_group) in verified groups, KeyCape AAL2["pwd","otp"], MFA age at most 900 s (12 h forlist); resource.system == "informed-decision",type == "decision-memo",resource.idmatchesmemo:[a-z0-9][a-z0-9-]{2,62};context.review_groupis in the subject's groups (eligibility, not a bind);context.memo_classis inadmitted_classes(generated set);context.intake_submitteris inadmitted_submitters(generated fromdeploy/intake-submitters.json, same pattern astools/gen_ops_warden_fetch.py: separate data,--checkdrift test);context.approval_requester == "informed-decision",context.approval_human_control == true;context.approval_idis a UUID andcontext.approval_binding_digestmatchessha256:[0-9a-f]{64};context.memo_version >= 1.
Deny by default, first failing reason named. The existing exact records stay as a second branch until their memos are terminal, so nothing admitted today loses admission. A new memo needs no policy edit, no image build and no pin redeploy. A new memo class or submitter changes generated data and, until §5.3 lands, an image.
5.3 Who asserts the context attributes
Build mode: informed-decision is the PEP and carries approval_requester,
approval_human_control and the digest as read live from approval-engine
(the get-by-id it already does with the human's own token), plus
intake_submitter and memo_class from its own intake ledger. This is the
consumer asserting facts that the PDP decides on. The exact-record list avoided
that by having an operator repeat the facts in policy. It is recorded as
relaxation R3.
Close-down: the PDP verifies the approval fact itself from the
approval-engine claim (GET /v1/approvals/{id}/claim, approval-claim.md),
signed by approval-engine or fetched by access-engine as PIP, and verifies the
intake fact from a signed intake receipt. Then the PEP carries only ids.
5.4 What stays fail-closed
- No allow without a live approval-engine read that matches the memo pin.
- Unknown class, unknown submitter, wrong requester,
human_controlfalse, malformed digest, stale MFA: deny. - PDP unreachable: refuse (no local allow fallback, as today).
listkeeps its own rule; alistallow satisfies nothing else.
6. Load and notification
- Intake runs inside the review service, so a validated, approval-pinned memo
is written with
Store.put_document+Store.save_memoin the same transaction as the ledger row. No CLI, nobound/directory. - The binding principal is resolved from deployment config
(
deploy/review-groups.json: review group → named recipient KeyCapesub), never from memo content. Draft principalpending-human-sessionstays in the unsigned record; the live bind still substitutes the authenticated subject. - Notification: a State Hub message to the founder's inbox (
to_agentconfigured, defaultbernd) with memo id, question, class, submitter and the review link. The signed-in overview (INFD-WP-0003) lists it as pending. No packet content, no highlights, no secrets in the notification. Email/push is a later channel owned elsewhere. - audit-core receives
informed-decision.memo.posed(submitter, class, memo id, approval id, binding digest, submission digest; no content).
7. Invariants kept
| Invariant | Where it is enforced after the change |
|---|---|
| Humans bind; agents never accept/decline/seal | Intake has no bind route; workload tokens are refused on every browser route; approval-engine GH-DEC-2026-016 refuses non-human entries on human_control approvals; the requester client has no approval:approve |
| MFA freshness on answer | Unchanged PDP identity bar (900 s) plus the review service's own act-time check |
| One question per memo | Intake validation §3.3(3); anti-requirement unchanged |
| No approve-all | No batch act exists; intake creates one approval per memo; acts are per memo |
| Human sees exactly what is bound | view_hash per presentation; live approval read and digest compare before render and act; digest carried to PDP |
| audit-core evidence | memo.posed added; presentation, disposition and evidence path unchanged |
| No secret values | Secret-marker scan at intake; notifications carry no content; client secret only via ESO into the pod |
| Membership is not a bind | review_group is eligibility only; no auto-approval |
8. Write-back to State Hub
When a memo with serves.hub_decision_id and resolves: "on-answer" gets a
terminal human disposition (accept or decline; return and discuss do
not resolve), the review service calls the sanctioned hub write:
POST /decisions/{hub_decision_id}/resolve
{"decided_by": "<reviewer display name> via informed-decision",
"rationale": "<verb> in informed-decision memo <memo_id> v<version>; approval <approval_id>;
binding <digest>; view_hash <view_hash>; disposition <id> at <time>",
"write_log": true}
- Behind
INFD_HUB_WRITEBACK=1(default off). Off means the payload is built and recorded, not sent. - Idempotent:
409 already resolvedwith our memo id in the hub rationale is success; a 409 without it is a recordedconflictfor a human to look at. Transport errors retry from a local outbox; the human's act is never undone by a write-back failure. - One decision, one resolving memo. A packet that spans several lanes (like D1)
uses
resolves: "never"on its per-lane memos and gets a progress note instead; the hub decision is resolved by its own resolving memo. - The hub is a read model: the write-back reflects an act that already happened in informed-decision. It never decides.
9. Build mode now, close-down later
| # | Relaxation accepted now | Re-arm at close-down |
|---|---|---|
| R1 | Posing is no longer operator-gated: any listed submitter can put a memo in front of the founder | Keep, but the submitter list and class list are reviewed data with a signed regeneration path |
| R2 | Workstation agents pose through State Hub, whose API is unauthenticated on the loopback tunnel; drafted_by is asserted |
Hub authenticates its writers (agent workload/OIDC identity) before forwarding; the forwarded submitter carries that identity |
| R3 | PEP-carried approval and intake attributes in the PDP request (§5.3) | access-engine verifies a signed approval-engine claim and a signed intake receipt itself |
| R4 | The requester client secret moves from attended to workload custody (ESO into the review pod) | Rotation schedule, ESO read audit, and proof the pod cannot read sibling paths |
| R5 | Admitted classes/submitters baked into the access-engine image | Runtime-loaded policy data with integrity checks, or a reviewed regeneration pipeline (same as checklist row 6) |
| R6 | Hub write-back uses the unauthenticated hub API | Hub requires the informed-decision workload identity for resolve and records it |
| R7 | No Idempotency-Key on approval create; orphan drafts possible after a crash |
approval-engine idempotent create |
| R8 | Notification only via hub inbox | Authenticated notification channel; no content beyond id and question |
Never relaxed, build mode or not: humans bind, fresh MFA on answer, one question per memo, no approve-all, no secret values, audit-core evidence.
10. Migration and ownership
| Phase | What | Owner | Needs |
|---|---|---|---|
| 0 (now, offline) | Intake validation module + envelope schema + tests; hub write-back adapter behind a flag | informed-decision | nothing |
| 1 | Intake listener, TokenReview, submitter config, ledger, store write, notification, memo.posed |
informed-decision | railiance-platform (SA/NetworkPolicy), audit-core (event type) |
| 2 | Requester secret to the pod by ESO; intake creates approvals | railiance-platform, informed-decision | founder go for the custody change |
| 3 | Attribute admission branch + generator + fixtures; deploy | access-engine | founder go for deploy |
| 4 | State Hub forwards decision memos as a submitter | state-hub | — |
| 5 | Write-back flag on | informed-decision | phases 1–3 live |
| Close-down | R1–R8 | each owner | founder |
Unchanged: key-cape (the create-only client already exists; no new scope). gate-house is asked to confirm that attribute admission plus live digest checks satisfy the doctrine the exact-record list was serving.
The operator scripts (create_sitting_approvals.*, attach_compact_bindings.py,
load_sitting_memos.py) stay as the fallback until phase 3 is live, then
retire.
11. Doctrine response and local implementation — 2026-10-03
Gate House reply 4a290496-5aa7-4488-a01d-f8fde925a54b confirms no human act
is required at posing time. Draft GH-DEC-2026-023 remains proposed and awaits
the founder's ruling. The response imposes these requirements now:
- C1: PDP allow is eligibility only. MUST read the approval live and compare approval id and native digest with the memo pin; never obtain approval fact from an allow.
- C2: view_hash MUST NEVER enter PDP context. The binding digest MUST come from the engine's live response and MUST NOT be recomputed here.
- C3: every intake-created approval MUST have human_control true; no class may override it. PDP MUST deny false. Principal identity stays authentication-derived.
- C4: removing a submitter from runtime data MUST refuse the next pose without a policy image rebuild, including attempts to replay previously accepted data.
- C5: submitter/class additions MUST be reviewed commits by someone other than the submitter being admitted. Local allow-list refusal grants no PDP permission.
- C6: state-hub submitter proves authenticated forwarding only. drafted_by MUST remain visibly asserted; loopback hub access does not authenticate the drafter.
Findings F1–F3 remain open: PEP-asserted admission facts require independently verified signed claims/receipts at close-down; ungated posing can flood human attention; hub notifications can impersonate a review request. Intake now requires a configured positive per-submitter daily budget. At close-down the budget is mandatory, the authenticated overview is authoritative, notifications carry ids, and terminal exact-record memos lose the compatibility branch. R2/R5 re-arming includes authenticated drafting identity and independent grant review. The unsigned PDP attribution gap remains explicitly declared.
The offline service and its durable integration worker are implemented; shipped
registries are empty and transports are injected. Runtime composition and live
acceptance remain blocked on the owner requirements in
docs/intake-dependency-requirements.md. These requirements do not arm a live
control or grant custody/deployment approval.