Implement offline workload intake and record blocked owner dependencies

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a1029c-c003-7283-acc9-930b434e6cae
This commit is contained in:
tegwick 2026-10-03 18:45:10 +02:00
parent c1a75e85aa
commit c1cf6cf54c
13 changed files with 790 additions and 25 deletions

View file

@ -255,3 +255,19 @@ Stated here because a scope file that only lists capabilities overstates them.
human proof (`INFD-WP-0001-T08`) and `audit-core` custody/live delivery
(`AUDIT-IN-0003`, `AUDIT-WP-0009`) — both live and observed in production as
of this Status section.
## Offline workload intake — 2026-10-03
INFD-WP-0005 implements a separate workload-only WSGI intake application,
injected TokenReview and create-only approval transports, per-request grant
reload, per-submitter daily limits, and a schema-3 durable ledger. It atomically
loads pinned memos and packet bytes and queues notification/posed jobs. Human
terminal dispositions atomically queue optional hub resolution; a leased worker
retries delivery with stable ids and leaves disabled/unconfigured adapters pending.
An uncertain approval create never reposts automatically; retained receipts can
resume local loading. Tests cover schema-1/2 upgrade, rollback, replay/restart,
revocation and write-back. This is offline implementation, not a deployed intake
listener. Owner contracts, workload custody and runtime composition are tracked
as blocked tasks in INFD-WP-0005, with acceptance requirements in
`docs/intake-dependency-requirements.md`.

View file

@ -10,7 +10,7 @@
| --- | --- | --- | --- | --- |
| workplan | INFD-WP-ADHOC-2026-10-03 | blocked | — | workplans/ADHOC-2026-10-03.md |
| workplan | INFD-WP-0003 | finished | — | workplans/INFD-WP-0003-my-decisions-overview.md |
| workplan | INFD-WP-0005 | proposed | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| workplan | INFD-WP-0005 | blocked | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md |
| task | INFD-WP-ADHOC-2026-10-03-T02 | wait | — | workplans/ADHOC-2026-10-03.md |
| task | INFD-WP-0003-T01 | done | — | workplans/INFD-WP-0003-my-decisions-overview.md |
@ -20,12 +20,12 @@
| task | INFD-WP-0005-T01 | done | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T02 | done | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T03 | done | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T04 | todo | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T04 | done | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T05 | wait | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T06 | wait | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T07 | todo | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T07 | wait | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T08 | wait | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T09 | wait | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T09 | done | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| task | INFD-WP-0005-T10 | wait | — | workplans/INFD-WP-0005-frictionless-decision-intake.md |
| intake | INFD-IN-0001 | closed | blue | intakes/intakes.md |
| intake | INFD-IN-0002 | closed | blue | intakes/intakes.md |

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,126 @@
# Intake implementation and dependency acceptance — 2026-10-03
Work record: INFD-WP-0005. These are requirements for owner implementation;
this document grants no deployment or credential-custody change.
Local implementation: `intake_service.IntakeService` is a separate WSGI app
with only `POST /v1/memos`; `TokenReview` checks an injected Kubernetes response;
`intake_approval.IntakeApprovalClient` sends create-only approval requests through
a bounded transport. Store schema 3 migrates existing schema 1/2 databases and
adds the submission ledger and integration outbox. Packet bytes, pinned memo,
ready ledger and posed/notification jobs commit in one transaction. Terminal
human dispositions queue hub resolution in their existing evidence transaction.
`IntakeDeliveryWorker` dispatches injected audit, notification and hub transports
with leases, stable ids, bounded retries and a disabled-by-default write-back.
No listener is composed into the deployed runtime yet; shipped registries are
empty, so no caller is admitted. No live deployment occurred.
## railiance-platform — T05, T10
Existing request: `cd811073-d6db-41cf-8a57-10df0b360eab`.
Deliver a reviewed deployment change with:
- ESO custody for `platform/workloads/informed-decision/sitting-requester`,
mounted read-only to the intake workload; exact-path read grant, sibling-path
refusal, safe secret metadata handling and no values in evidence.
- Separate private intake port and Service, absent from public ingress;
NetworkPolicy admitting only registered submitter namespaces/service accounts.
- TokenReview RBAC for the review workload, API-server CA verification and an
authenticated, bounded TokenReview transport. Project submitter tokens for
audience `informed-decision-intake`; do not reuse browser or audit tokens.
- Project the reviewed JSON registries so removal is visible on the next request
without image rebuild. Review additions independently of the admitted submitter.
- Workload OIDC client-credentials exchange yielding only `approval:create`,
subject `informed-decision`, platform tenant and service principal provenance.
Bind it to `IntakeApprovalClient`; no approve/read/consume scopes.
Acceptance: wrong audience, unknown SA, removed grant and absent custody fail
closed; one approved dev pose creates one human-control draft. Record immutable
image/config references and founder approval before any live change.
## access-engine — T06, T10
Existing request: `306bcdd2-4fe4-4ccf-8aac-590c2abe51c4`.
Deliver the §5.2 attribute admission branch, generated class/submitter data,
drift check, exact-record compatibility branch and versioned request/response
fixtures. Confirm field placement for review_group, memo_class, intake_submitter,
approval_requester, approval_human_control, memo_version, approval_id and
approval_binding_digest. Keep human authentication-derived identity, verified
groups and act-time MFA freshness. No view_hash in PDP context. The approval
fact must be read live and matched to the memo pin independently of an allow.
Acceptance: unknown class/submitter, requester mismatch, false human_control,
digest mismatch and stale MFA refuse; existing exact-record memos still work.
Consumer changes must update both policy request construction and Store's strict
policy-observation context comparison, with live engine reads before checks.
Do not activate consumer attributes against the current exact-only pin. Signed
PDP attribution remains the separately declared FLEX-WP-0024 gap.
## approval-engine — T05 recovery and production close-down
Existing request: `59c5ee82-c706-4432-bab4-24af64031d39`.
Confirm native service creation response fields and optional requested_via.
Provide idempotent creation or a lookup/recovery contract for lost create replies.
Current local behavior reserves before POST: `creating` without a receipt returns
`approval_create_unresolved` and never reposts. `created` with a persisted receipt
can retry the atomic local load without creating another approval. An unresolved
create needs owner-assisted receipt reconciliation; never manufacture a digest
or infer approval state. Idempotent creation is a close-down requirement, not a
condition on offline tests or initial dev rollout.
## audit-core — T07
Existing request: `23b670ac-809e-49bf-9233-e0b62cf0a039`.
Register `informed-decision.memo.posed` for source informed-decision and tenant
platform; accept the locally retained event envelope containing memo/approval
ids, carried binding digest, authenticated forwarding submitter, memo class and
submission digest. No packet, brief, or secret content. Confirm sender custody,
Idempotency-Key receipts and heartbeat/reconciliation requirements for the new
class. Provide positive/duplicate/refusal fixtures. Only then connect posed
jobs to AuditCoreSink and include the class in runtime readiness/reconciliation.
## state-hub — T07, T08
Existing request: `e30dea31-90e3-4fc0-954a-ed8412b52aca`.
Deliver forwarding with the projected intake-audience SA token, bounded envelope,
stable idempotency key, durable retry and receipt retention. Identify the actual
SA to review into the submitter registry. Forwarding proves the hub identity;
drafted_by remains asserted and must be visibly described that way. Add a dev
fixture for pose/replay/conflict and a deduplicated notification contract using
our stable delivery id. Notifications carry only id/question/class/submitter;
the signed-in overview is the authoritative review entry point.
Confirm resolve semantics: terminal human accept/decline only; return/discuss
and resolves: never do not resolve. A 409 must allow reading an existing
resolution to correlate it. Delivery failure retains local evidence and retries;
other-memo conflicts block for review. Bind the hub transports and schedule
IntakeDeliveryWorker in the runtime after these contracts are accepted. Keep
INFD_HUB_WRITEBACK unset until deployment approval and end-to-end evidence.
## gate-house and founder — T09, T10
Reply `4a290496-5aa7-4488-a01d-f8fde925a54b` answers the doctrine check: no
human act is required when posing. GH-DEC-2026-023 is still proposed; this reply
is not a ratified decision or live rollout approval. Spec now records C1–C6 and
F1–F3. Production close-down additionally requires authenticated drafting
identity, independent list review, signed claims/receipts, volume budgets and
retirement of exact-record admission after its memos become terminal.
T10 resumes after T05–T08 acceptance and founder approval for the concrete
rollout. Record dev positive/negative traces first, then one live human answer
with audit and write-back receipts. The provisioning sitting's human binding
remains blocked in INFD-WP-ADHOC-2026-10-03-T02.
## Verification
`uv run --extra dev python -m pytest -q`: 437 passed, 67 skipped (optional
component fixtures are not enabled in this run). After the final receipt-recovery
concurrency guard and nonempty-entry response refusal, the affected intake,
runtime migration, durable store and write-back suites: 63 passed, 1 skipped.
`git diff --check` passed. State Hub consistency reconciled the blocked plan,
completed T04/T09 and waiting T07; no registrar fallback was required.

View file

@ -137,8 +137,8 @@ idempotency_key})`. The ledger key is `(submitter_id, idempotency_key)`:
- 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 between the two leaves a `creating`
row that a retry resumes; see §4 for the orphan-draft gap.
`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
@ -172,9 +172,10 @@ only through the attended reader lane. The change:
What approval-engine would ideally add (requested, not blocking):
1. An `Idempotency-Key` on `POST /v1/approvals`, so a retried create after a
crash returns the same draft instead of leaving an orphan. Until then intake
resumes `creating` rows by looking up its own receipt, and an orphan draft
is harmless (never bound, expires).
crash returns the same draft instead of leaving an orphan. Until then a `creating` row without a receipt requires explicit
owner-assisted reconciliation and never automatically repeats creation. A
`created` row with a durable receipt retries local loading only. An orphan
draft is never bound and expires.
2. An optional, non-binding `requested_via` evidence field so the engine's
own record names the original submitter.
@ -337,3 +338,38 @@ 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.

View file

@ -0,0 +1,34 @@
"""Create-only approval adapter; token custody/exchange belongs to workload setup."""
from datetime import datetime, timedelta, timezone
from .http_transport import JSONTransport, fixed_origin
from .intake import canonical
from .intake_service import IntakeError
class IntakeApprovalClient:
def __init__(self, origin, token_provider, *, transport=None, allow_internal_http=False):
self.origin = fixed_origin(origin, allow_internal_http=allow_internal_http)
self.token_provider = token_provider
self.transport = transport or JSONTransport(allow_internal_http=allow_internal_http)
def __call__(self, request):
if (set(request) != {'binding', 'human_control', 'required_count'}
or request['human_control'] is not True or type(request['required_count']) is not int
or request['required_count'] != 1 or request['binding'].get('actor') != 'informed-decision'):
raise IntakeError(400, 'invalid_approval_intent')
try:
token = self.token_provider()
if (not isinstance(token, str) or not token or len(token) > 16384
or not token.isascii() or any(c.isspace() for c in token)):
raise ValueError()
now = datetime.now(timezone.utc)
body = {**request, 'pdp_path': False, 'validity': {
'not_before': now.isoformat(), 'expires_at': (now + timedelta(hours=24)).isoformat()}}
status, result = self.transport.request('POST', self.origin + '/v1/approvals',
headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'}, body=canonical(body))
if status != 201:
raise ValueError()
return result
except Exception:
raise IntakeError(503, 'approval_create_unresolved') from None

View file

@ -0,0 +1,68 @@
"""Leased local integration outbox. Owner composition supplies bounded transports.
Unknown adapters leave rows pending; hub resolutions remain pending while the
write-back flag is off. Dispatch failure never rolls back a human disposition.
Audit/notification receivers must deduplicate using the stable delivery id.
"""
import json
import time
import uuid
from .hub_writeback import Answer, enabled, write_back
class IntakeDeliveryWorker:
def __init__(self, store, *, audit=None, notify=None, hub_transport=None,
hub_base_url='', env=None, clock=time.time):
self.store, self.audit, self.notify = store, audit, notify
self.hub_transport, self.hub_base_url = hub_transport, hub_base_url
self.env, self.clock = env, clock
def run_once(self, limit=10):
if type(limit) is not int or not 1 <= limit <= 100:
raise ValueError('delivery limit must be 1..100')
kinds = []
if self.audit:
kinds.append('memo-posed')
if self.notify:
kinds.append('notification')
if self.hub_transport and enabled(self.env):
kinds.append('hub-resolution')
if not kinds:
return 0
delivered = 0
for _ in range(limit):
now, lease = self.clock(), str(uuid.uuid4())
with self.store._transaction() as db:
placeholders = ','.join('?' for kind in kinds)
row = db.execute(f"SELECT * FROM intake_deliveries WHERE kind IN ({placeholders}) "
"AND state IN ('pending','in_flight') AND next_attempt<=? "
"AND (lease_until IS NULL OR lease_until<=?) ORDER BY rowid LIMIT 1", (*kinds, now, now)).fetchone()
if row is None:
break
db.execute("UPDATE intake_deliveries SET state='in_flight',lease=?,lease_until=?,attempts=attempts+1 WHERE id=?",
(lease, now + 30, row['id']))
state = 'pending'
try:
payload = json.loads(row['payload'])
if row['kind'] == 'memo-posed':
self.audit.deliver(row['id'], row['payload'])
state = 'delivered'
elif row['kind'] == 'notification':
# notify(id, metadata) returns only after a deduplicated receiver receipt.
if self.notify(row['id'], payload) is True:
state = 'delivered'
else:
result = write_back(Answer(**payload['answer']), payload['serves'],
base_url=self.hub_base_url, transport=self.hub_transport, env=self.env)
if result.status in ('resolved', 'already_resolved'):
state = 'delivered'
elif result.status == 'conflict':
state = 'blocked'
except Exception:
pass # Never persist upstream bodies, tokens, or exception text.
with self.store._transaction() as db:
db.execute("UPDATE intake_deliveries SET state=?,lease=NULL,lease_until=NULL,next_attempt=? "
"WHERE id=? AND lease=?", (state, now + min(3600, 2 ** min(row['attempts'] + 1, 11)), row['id'], lease))
delivered += state == 'delivered'
return delivered

View file

@ -0,0 +1,218 @@
"""Separate workload-only WSGI intake. No browser session or binding route.
The creator and TokenReview transports are injected by the workload composition.
An uncertain create is never automatically repeated: its durable reservation
requires receipt recovery until approval-engine supports idempotent creation.
"""
from dataclasses import replace
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import re
import uuid
from .intake import IntakeRefused, SubmitterGrant, canonical, idempotency_outcome, validate_submission
from .memo import Principal
from .records import dumps
AUDIENCE = "informed-decision-intake"
MAX_BODY = 1024 * 1024
class IntakeError(Exception):
def __init__(self, status, code):
self.status, self.code = status, code
super().__init__(code)
class TokenReview:
"""call(body) performs an authenticated Kubernetes TokenReview using pod credentials.
The adapter must verify the API server's CA and suppress tokens and responses
in its logs. TokenReview is performed afresh on every pose, including replay.
"""
def __init__(self, call):
self.call = call
def authenticate(self, token):
try:
result = self.call({"apiVersion": "authentication.k8s.io/v1", "kind": "TokenReview",
"spec": {"token": token, "audiences": [AUDIENCE]}})
status = result["status"]
user = status["user"]
username = user["username"]
if (status.get("authenticated") is not True or status.get("error")
or AUDIENCE not in status.get("audiences", [])
or not user.get("uid") or not isinstance(username, str)
or not re.fullmatch(r"system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+", username)):
raise ValueError()
return username
except Exception:
raise IntakeError(401, "workload_authentication_failed") from None
def configuration(submitters_path, groups_path):
"""Reload reviewed deployment data on every request so removal revokes immediately.
Empty shipped registries deliberately admit no workload or human recipient.
Entries identify actual service accounts; drafted_by is only an assertion.
"""
try:
submitters = json.loads(Path(submitters_path).read_text())
groups = json.loads(Path(groups_path).read_text())
if not isinstance(submitters, dict) or not isinstance(groups, dict):
raise ValueError()
for username, row in submitters.items():
if (not re.fullmatch(r"system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+", username)
or set(row) != {"submitter_id", "memo_classes", "review_groups", "daily_limit"}
or not isinstance(row["submitter_id"], str)
or not row["submitter_id"] or type(row["daily_limit"]) is not int
or row["daily_limit"] < 1):
raise ValueError()
for field in ("memo_classes", "review_groups"):
if not isinstance(row[field], list) or any(not isinstance(x, str) for x in row[field]):
raise ValueError()
if len({r["submitter_id"] for r in submitters.values()}) != len(submitters):
raise ValueError()
for row in groups.values():
if (set(row) != {"subject", "display_name"} or not all(isinstance(x, str) and x for x in row.values())):
raise ValueError()
return submitters, groups
except Exception:
raise IntakeError(503, "intake_configuration_unavailable") from None
class IntakeService:
def __init__(self, store, authenticator, creator, *, submitters_path, groups_path):
self.store, self.authenticator, self.creator = store, authenticator, creator
self.submitters_path, self.groups_path = submitters_path, groups_path
def pose(self, token, envelope):
username = self.authenticator.authenticate(token)
submitters, groups = configuration(self.submitters_path, self.groups_path)
row = submitters.get(username)
if row is None:
raise IntakeError(403, "submitter_unknown")
grant = SubmitterGrant(row["submitter_id"], frozenset(row["memo_classes"]), frozenset(row["review_groups"]))
submission = validate_submission(envelope, submitter_id=grant.submitter_id, grant=grant)
recipient = groups.get(submission.review_group)
if recipient is None:
raise IntakeError(503, "review_group_unconfigured")
metadata = {"memo_class": submission.memo_class, "review_group": submission.review_group,
"drafted_by": submission.drafted_by, "serves": submission.serves}
recovered = None
# Reserve before any network side effect. SQLite serializes concurrent poses.
with self.store._transaction() as db:
previous = db.execute("SELECT * FROM intake_ledger WHERE submitter=? AND key=?",
(submission.submitter_id, submission.idempotency_key)).fetchone()
if previous:
idempotency_outcome(previous["digest"], submission.submission_digest)
if previous["state"] == "ready":
return 200, json.loads(previous["receipt"])
if previous["state"] == "created":
recovered = json.loads(previous["receipt"])
else:
raise IntakeError(409, "approval_create_unresolved")
if not previous and (db.execute("SELECT 1 FROM memos WHERE id=?", (submission.memo.id,)).fetchone()
or db.execute("SELECT 1 FROM intake_ledger WHERE memo_id=?", (submission.memo.id,)).fetchone()):
raise IntakeError(409, "memo_id_taken")
# Budget counts attempts as well as completed poses; retries do not spend twice.
day = datetime.now(timezone.utc).date().isoformat()
count = db.execute("SELECT COUNT(*) FROM intake_ledger WHERE submitter=? AND json_extract(metadata,'$.day')=?",
(submission.submitter_id, day)).fetchone()[0]
if not previous and count >= row["daily_limit"]:
raise IntakeError(429, "submitter_budget_exhausted")
metadata["day"] = day
if not previous:
db.execute("INSERT INTO intake_ledger VALUES (?,?,?,?,?,?,NULL)",
(submission.submitter_id, submission.idempotency_key, submission.submission_digest,
submission.memo.id, "creating", dumps(metadata)))
if recovered is None:
try:
approval = self.creator({"binding": submission.approval_binding,
"human_control": True, "required_count": 1})
binding = approval["binding"]
approval_id, digest = approval["id"], binding["digest"]
if (not re.fullmatch(r"[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}", approval_id)
or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest)
or binding.get("human_control") is not True
or any(binding.get(k) != v for k, v in submission.approval_binding.items())
or approval.get("status") != "requested" or approval.get("entries")):
raise ValueError()
except Exception:
raise IntakeError(503, "approval_create_unresolved") from None
# Preserve the receipt before local load: recovery can locate this engine object.
receipt = {"memo_id": submission.memo.id, "approval_id": approval_id, "approval_binding_digest": digest}
with self.store._transaction() as db:
db.execute("UPDATE intake_ledger SET state='created',receipt=? WHERE submitter=? AND key=?",
(dumps(receipt), submission.submitter_id, submission.idempotency_key))
else:
receipt = recovered
approval_id, digest = receipt["approval_id"], receipt["approval_binding_digest"]
memo = replace(submission.memo, approval_id=approval_id, approval_binding_digest=digest,
binding=replace(submission.memo.binding, principal=Principal(
recipient["subject"], "person", recipient["display_name"])))
with self.store._transaction() as db:
current = db.execute("SELECT state,receipt FROM intake_ledger WHERE submitter=? AND key=?",
(submission.submitter_id, submission.idempotency_key)).fetchone()
if current["state"] == "ready":
return 200, json.loads(current["receipt"])
for item in envelope["packet"]:
data = submission.packet[item["item_id"]]
document_digest = "sha256:" + hashlib.sha256(data).hexdigest()
existing = db.execute("SELECT * FROM documents WHERE digest=?", (document_digest,)).fetchone()
if existing and (existing["media_type"] != item["media_type"] or existing["content"] != data):
raise IntakeError(409, "document_conflict")
db.execute("INSERT OR IGNORE INTO documents VALUES (?,?,?)", (document_digest, item["media_type"], data))
db.execute("INSERT INTO memos VALUES (?,?,?)", (memo.id, memo.version, dumps(memo)))
event_id = str(uuid.uuid4())
event = {"id": event_id, "type": "informed-decision.memo.posed",
"source": "informed-decision", "tenant": "tenant:platform",
"subject": memo.id, "correlation_id": approval_id,
"occurred_at": datetime.now(timezone.utc).isoformat(),
"data": {**receipt, "submitter": submission.submitter_id,
"memo_class": submission.memo_class,
"submission_digest": submission.submission_digest}}
notification = {"memo_id": memo.id, "question": memo.question,
"memo_class": submission.memo_class, "submitter": submission.submitter_id}
for kind, payload in (("memo-posed", event), ("notification", notification)):
db.execute("INSERT INTO intake_deliveries(id,kind,payload) VALUES (?,?,?)",
(event_id if kind == "memo-posed" else str(uuid.uuid4()), kind, dumps(payload)))
db.execute("UPDATE intake_ledger SET state='ready' WHERE submitter=? AND key=?",
(submission.submitter_id, submission.idempotency_key))
return 201, receipt
def __call__(self, environ, start_response):
try:
if environ.get("PATH_INFO") != "/v1/memos":
raise IntakeError(404, "not_found")
if environ.get("REQUEST_METHOD") != "POST":
raise IntakeError(405, "method_not_allowed")
authorization = environ.get("HTTP_AUTHORIZATION", "")
if not authorization.startswith("Bearer ") or not authorization[7:] or len(authorization) > 16384:
raise IntakeError(401, "workload_token_required")
if environ.get("CONTENT_TYPE", "").split(";")[0] != "application/json":
raise IntakeError(415, "json_required")
length = int(environ.get("CONTENT_LENGTH") or 0)
if length < 1 or length > MAX_BODY:
raise IntakeError(413, "submission_too_large")
raw = environ["wsgi.input"].read(length)
if len(raw) != length:
raise IntakeError(400, "invalid_json")
status, body = self.pose(authorization[7:], json.loads(raw))
except IntakeError as exc:
status, body = exc.status, {"error": exc.code}
except IntakeRefused as exc:
status = 409 if exc.reason == "idempotency_conflict" else 400
body = {"error": exc.reason} # No client-controlled field paths in logs/responses.
except (ValueError, TypeError, KeyError, UnicodeError):
status, body = 400, {"error": "invalid_submission"}
except Exception:
status, body = 503, {"error": "intake_unavailable"}
from http import HTTPStatus
raw = canonical(body)
start_response(f"{status} {HTTPStatus(status).phrase}",
[("Content-Type", "application/json"), ("Content-Length", str(len(raw))),
("Cache-Control", "no-store")])
return [raw]

View file

@ -27,7 +27,7 @@ from .provenance import Claim, Route, assert_human_control_dischargeable
from .stance import resolve
from .records import disposition_from, dumps, memo_from, presentation_from
SCHEMA_VERSION = 2
SCHEMA_VERSION = 3
_SCHEMA = """
CREATE TABLE documents (digest TEXT PRIMARY KEY, media_type TEXT NOT NULL, content BLOB NOT NULL);
CREATE TABLE memos (id TEXT NOT NULL, version INTEGER NOT NULL, body TEXT NOT NULL, PRIMARY KEY(id,version));
@ -99,7 +99,7 @@ class Store:
triggers += (f"CREATE TRIGGER immutable_{table}_{operation} BEFORE {operation} ON {table} "
"BEGIN SELECT RAISE(ABORT,'immutable evidence'); END;\n")
db.executescript("BEGIN IMMEDIATE;" + _SCHEMA + triggers + "PRAGMA user_version=1; COMMIT;")
elif version not in (1, SCHEMA_VERSION):
elif version not in (1, 2, SCHEMA_VERSION):
raise StoreError("unsupported evidence schema version")
if version in (0, 1):
db.executescript("""BEGIN IMMEDIATE;
@ -113,6 +113,19 @@ class Store:
BEGIN SELECT RAISE(ABORT,'immutable evidence'); END;
PRAGMA user_version=2; COMMIT;""")
if version in (0, 1, 2):
db.executescript("""BEGIN IMMEDIATE;
CREATE TABLE intake_ledger (
submitter TEXT NOT NULL, key TEXT NOT NULL, digest TEXT NOT NULL,
memo_id TEXT NOT NULL UNIQUE, state TEXT NOT NULL,
metadata TEXT NOT NULL, receipt TEXT,
PRIMARY KEY(submitter,key));
CREATE TABLE intake_deliveries (
id TEXT PRIMARY KEY, kind TEXT NOT NULL, payload TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending', attempts INTEGER NOT NULL DEFAULT 0,
next_attempt REAL NOT NULL DEFAULT 0, lease TEXT, lease_until REAL);
PRAGMA user_version=3; COMMIT;""")
@contextmanager
def _connection(self):
db = sqlite3.connect(self.path.as_uri() + "?mode=rw", uri=True, timeout=5, isolation_level=None)
@ -320,6 +333,18 @@ class Store:
db.execute("INSERT INTO dispositions VALUES (?,?,?,?,?)", (d.id, operation_id, request, p.id, dumps(d)))
if verb is Verb.ACCEPT:
db.execute("INSERT INTO submissions(disposition_id,approval_id,subject) VALUES (?,?,?)", (d.id, memo.approval_id, actor.sub))
intake = db.execute("SELECT metadata FROM intake_ledger WHERE memo_id=? AND state='ready'", (memo.id,)).fetchone()
if intake and verb in (Verb.ACCEPT, Verb.DECLINE):
from .hub_writeback import Answer, build_resolution
serves = json.loads(intake["metadata"]).get("serves")
if serves and serves.get("resolves") == "on-answer":
answer = Answer(memo.id, memo.version, verb.value, d.id, d.at,
memo.binding.principal.display_name, memo.approval_id,
memo.approval_binding_digest, p.view_hash, actor.kind.value)
payload = {"answer": answer.__dict__, "serves": serves,
"resolution": build_resolution(answer)}
db.execute("INSERT INTO intake_deliveries(id,kind,payload) VALUES (?,?,?)",
(d.id, "hub-resolution", dumps(payload)))
c = commit_disposition(d, custody="informed-decision:dispositions:" + d.id)
self._event(db, c, {"disposition": json.loads(dumps(d)), "presentation": json.loads(dumps(p)),
"policy_observation_id": policy_id},

View file

@ -0,0 +1,201 @@
import io
import json
import pytest
from informed_decision.intake_service import IntakeError, IntakeService, TokenReview, AUDIENCE
from informed_decision.store import Store
from test_intake import envelope
USERNAME = 'system:serviceaccount:state-hub:state-hub'
APPROVAL = '12345678-1234-1234-1234-123456789abc'
def token_review(body):
assert body['spec'] == {'token': 'projected-token', 'audiences': [AUDIENCE]}
return {'status': {'authenticated': True, 'audiences': [AUDIENCE],
'user': {'username': USERNAME, 'uid': 'workload-uid'}}}
@pytest.fixture
def setup(tmp_path):
private = tmp_path / 'private'
private.mkdir(mode=0o700)
store = Store(private / 'review.sqlite')
submitters, groups = tmp_path / 'submitters.json', tmp_path / 'groups.json'
submitters.write_text(json.dumps({USERNAME: {'submitter_id': 'state-hub',
'memo_classes': ['credential-lane'], 'review_groups': ['net-kingdom-admins'], 'daily_limit': 2}}))
groups.write_text(json.dumps({'net-kingdom-admins': {'subject': 'keycape-human', 'display_name': 'Reviewer'}}))
calls = []
def create(body):
calls.append(body)
assert body['human_control'] is True and body['required_count'] == 1
return {'id': APPROVAL, 'status': 'requested', 'binding': {
**body['binding'], 'human_control': True, 'digest': 'sha256:' + 'a' * 64}}
service = IntakeService(store, TokenReview(token_review), create,
submitters_path=submitters, groups_path=groups)
return service, store, calls, submitters
def request(service, body, token='projected-token', path='/v1/memos'):
raw = json.dumps(body).encode()
environ = {'PATH_INFO': path, 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'application/json',
'CONTENT_LENGTH': str(len(raw)), 'HTTP_AUTHORIZATION': 'Bearer ' + token,
'wsgi.input': io.BytesIO(raw)}
response = []
result = b''.join(service(environ, lambda status, headers: response.append(status)))
return int(response[0].split()[0]), json.loads(result)
def test_pose_replay_and_restart(setup):
service, store, calls, _ = setup
status, receipt = request(service, envelope())
assert status == 201
assert receipt['approval_id'] == APPROVAL
memo = store.memo(receipt['memo_id'])
assert memo.binding.principal.id == 'keycape-human'
assert memo.sealed is False
assert store.memos_for('keycape-human') == [memo]
service.store = Store(store.path)
assert request(service, envelope()) == (200, receipt)
assert len(calls) == 1
with store._connection() as db:
assert db.execute('SELECT COUNT(*) FROM documents').fetchone()[0] == 1
assert db.execute('SELECT COUNT(*) FROM dispositions').fetchone()[0] == 0
def test_revocation_refuses_even_replay(setup):
service, _, calls, config = setup
assert request(service, envelope())[0] == 201
config.write_text('{}')
assert request(service, envelope()) == (403, {'error': 'submitter_unknown'})
assert len(calls) == 1
def test_conflicts_precede_create(setup):
service, _, calls, _ = setup
assert request(service, envelope())[0] == 201
changed = envelope()
changed['memo']['brief'] += ' Changed'
assert request(service, changed) == (409, {'error': 'idempotency_conflict'})
changed = envelope()
changed['idempotency_key'] = 'different-key'
assert request(service, changed) == (409, {'error': 'memo_id_taken'})
assert len(calls) == 1
def test_uncertain_create_never_reposts_or_loads(setup):
service, store, calls, _ = setup
def lost(body):
calls.append(body)
raise OSError('sensitive upstream body')
service.creator = lost
assert request(service, envelope()) == (503, {'error': 'approval_create_unresolved'})
service.store = Store(store.path)
assert request(service, envelope()) == (409, {'error': 'approval_create_unresolved'})
assert len(calls) == 1
with store._connection() as db:
assert db.execute('SELECT COUNT(*) FROM memos').fetchone()[0] == 0
assert db.execute('SELECT COUNT(*) FROM documents').fetchone()[0] == 0
def test_created_receipt_recovers_without_create(setup):
service, store, calls, _ = setup
# Fail the load transaction, after the receipt has been persisted.
with store._connection() as db:
db.execute("CREATE TRIGGER fail_load BEFORE INSERT ON memos BEGIN SELECT RAISE(ABORT,'failure'); END")
assert request(service, envelope())[0] == 503
with store._connection() as db:
assert db.execute('SELECT COUNT(*) FROM documents').fetchone()[0] == 0
assert db.execute('SELECT state FROM intake_ledger').fetchone()[0] == 'created'
db.execute('DROP TRIGGER fail_load')
assert request(service, envelope())[0] == 201
assert len(calls) == 1
def test_budget_and_no_browser_routes(setup):
service, _, calls, config = setup
data = json.loads(config.read_text())
data[USERNAME]['daily_limit'] = 1
config.write_text(json.dumps(data))
assert request(service, envelope())[0] == 201
another = envelope()
another['memo']['id'] = 'another-memo'
another['idempotency_key'] = 'another-key'
assert request(service, another) == (429, {'error': 'submitter_budget_exhausted'})
assert request(service, envelope(), path='/review')[0] == 404
assert len(calls) == 1
@pytest.mark.parametrize('status', [
{'authenticated': False},
{'authenticated': True, 'audiences': ['wrong'], 'user': {'username': USERNAME, 'uid': 'x'}},
{'authenticated': True, 'audiences': [AUDIENCE], 'user': {'username': 'human', 'uid': 'x'}},
{'authenticated': True, 'audiences': [AUDIENCE], 'user': {'username': USERNAME}},
])
def test_token_review_fails_closed(status):
with pytest.raises(IntakeError) as error:
TokenReview(lambda body: {'status': status}).authenticate('hidden-token')
assert error.value.code == 'workload_authentication_failed'
def test_notifications_retry_and_disabled_writeback_stays_pending(setup):
from informed_decision.intake_delivery import IntakeDeliveryWorker
service, store, _, _ = setup
assert request(service, envelope())[0] == 201
seen = []
now = [100.0]
def notify(delivery_id, payload):
seen.append((delivery_id, payload))
return len(seen) > 1
worker = IntakeDeliveryWorker(store, notify=notify, clock=lambda: now[0])
assert worker.run_once() == 0
now[0] += 10
assert worker.run_once() == 1
assert seen[0] == seen[1]
assert 'brief' not in seen[0][1] and 'packet' not in seen[0][1]
with store._connection() as db:
assert db.execute("SELECT state FROM intake_deliveries WHERE kind='memo-posed'").fetchone()[0] == 'pending'
def test_terminal_disposition_queues_writeback_atomically(setup):
from informed_decision.disposition import Actor, ActorKind, Verb
from informed_decision.provenance import Claim, Route
from informed_decision.intake_delivery import IntakeDeliveryWorker
service, store, _, _ = setup
env = envelope()
env['serves']['resolves'] = 'on-answer'
status, receipt = request(service, env)
assert status == 201
p = store.present(receipt['memo_id'], principal_sub='keycape-human',
tenant=Claim('tenant:platform', Route.REGISTRATION),
principal_type=Claim('human', Route.AUTHENTICATION))
actor = Actor('keycape-human', ActorKind.PERSON)
store.acknowledge(p.id, actor, store.memo(receipt['memo_id']).required_ack_ids)
d = store.record_disposition(p.id, actor, Verb.DECLINE, operation_id='decline-once', reasons=('wrong_scope',))
store.record_disposition(p.id, actor, Verb.DECLINE, operation_id='decline-once', reasons=('wrong_scope',))
calls = []
def transport(method, url, payload):
calls.append(payload)
return 200, {}
worker = IntakeDeliveryWorker(store, hub_transport=transport, hub_base_url='http://hub', env={})
assert worker.run_once() == 0 and calls == []
worker.env = {'INFD_HUB_WRITEBACK': '1'}
assert worker.run_once() == 1
assert d.id in calls[0]['rationale']
assert worker.run_once() == 0
def test_create_adapter_has_only_create_route():
from informed_decision.intake_approval import IntakeApprovalClient
calls = []
class Transport:
def request(self, method, url, *, headers, body):
calls.append((method, url, json.loads(body)))
assert headers['Authorization'] == 'Bearer workload-token'
return 201, {'id': APPROVAL}
client = IntakeApprovalClient('https://engine.example', lambda: 'workload-token', transport=Transport())
assert client({'binding': {'actor': 'informed-decision'}, 'human_control': True, 'required_count': 1}) == {'id': APPROVAL}
assert calls[0][0:2] == ('POST', 'https://engine.example/v1/approvals')
assert calls[0][2]['pdp_path'] is False
assert 'validity' in calls[0][2]

View file

@ -12,11 +12,16 @@ from test_durable_store import storage,present
from test_durable_component import receiver
def test_schema_one_upgrade_preserves_existing_evidence(storage):
@pytest.mark.parametrize("version", [1, 2])
def test_schema_upgrade_preserves_existing_evidence(storage, version):
store,memo=storage;p=present(store,memo)
before=store.evidence()
with sqlite3.connect(store.path) as db:
db.execute('DROP TABLE policy_observations');db.execute('PRAGMA user_version=1')
db.execute('DROP TABLE intake_deliveries')
db.execute('DROP TABLE intake_ledger')
if version == 1:
db.execute('DROP TABLE policy_observations')
db.execute(f'PRAGMA user_version={version}')
reopened=Store(store.path)
assert reopened.retrieve_presentation(p.id)[0]==memo and reopened.evidence()==before
assert reopened.policy_observations()==[]

View file

@ -4,7 +4,7 @@ type: workplan
title: "Frictionless decision intake — agents pose memos, only answering is human"
domain: infotech
repo: informed-decision
status: active
status: blocked
flavor: implementation
owner: claude
topic_slug: netkingdom
@ -23,11 +23,11 @@ state_hub_workstream_id: "c9407d2e-fd51-5afe-be4c-24483d1b2dbc"
Design: `docs/specs/FrictionlessDecisionIntake.md`.
Lifecycle review (2026-10-03): active implementation. T01–T03 have shipped
design and offline implementation, and T04 is actionable without live access.
The remaining owner responses, credential custody, doctrine review, and rollout
approvals remain explicit task gates; this status correction grants none of
those approvals. T07 still depends on T04.
Lifecycle review (2026-10-03): blocked after completing the local intake
service, durable ledger/outbox and doctrine-response incorporation. The remaining
runtime composition, custody, policy, receiver contracts and rollout require
owner deliveries or founder approval. Requirements and acceptance evidence are
in `docs/intake-dependency-requirements.md`; no live change was made.
Today a drafted memo reaches the review page only after an attended requester
login (A3), a hand-edited exact-record admission package with CI build and pin
@ -49,7 +49,7 @@ Owner messages sent 2026-10-03 (hub message ids):
| gate-house | `6ab42744-b651-4136-9794-c0dc092509b7` | doctrine check: no human act required at posing time |
| the-custodian | `35a87432-b0d5-4301-9fb9-d6a99bac4e0c` | close-down checklist rows R1–R8 |
## Tasks
## Intake design
```task
id: INFD-WP-0005-T01
@ -63,6 +63,8 @@ identity, validation, idempotency, approval creation without a human login,
attribute admission, load/notify, invariants, write-back, build-mode
relaxations, migration.
## Offline validation
```task
id: INFD-WP-0005-T02
status: done
@ -75,6 +77,8 @@ Offline intake validation: `informed_decision/intake.py` and
(closed envelope, unsigned draft, one question, bundled acts, packet digests,
secret-marker scan without echo, submitter/class/group grants, idempotency).
## Hub write-back adapter
```task
id: INFD-WP-0005-T03
status: done
@ -86,9 +90,11 @@ Hub write-back adapter `informed_decision/hub_writeback.py` behind
`INFD_HUB_WRITEBACK=1` (default off), with tests in `tests/test_hub_writeback.py`.
Not wired into the disposition path yet (T07).
## Authenticated intake service and ledger
```task
id: INFD-WP-0005-T04
status: todo
status: done
priority: high
state_hub_task_id: "2fe63827-25cb-5a2d-88a7-35414f5718ae"
```
@ -99,6 +105,8 @@ the projected SA token (audience `informed-decision-intake`),
written in one transaction, principal resolved from `deploy/review-groups.json`.
Offline-testable with a fake TokenReview and fake approval client.
## Requester custody and runtime composition
```task
id: INFD-WP-0005-T05
status: wait
@ -115,6 +123,8 @@ Intake creates the approval itself with `informed-decision-sitting-requester`
`binding.actor = informed-decision`), pins approval id and engine digest on the
memo, and records the receipt. Replaces OPERATOR step A3.
## Attribute admission consumer
```task
id: INFD-WP-0005-T06
status: wait
@ -130,9 +140,13 @@ Consumer side of attribute admission: carry `review_group`, `memo_class`,
update `docs/flex-auth-review-contract.md`; keep exact-record memos working.
Replaces OPERATOR step A4.
## Notification, audit and write-back delivery
```task
id: INFD-WP-0005-T07
status: todo
status: wait
blocked_on: message-from:audit-core
blocking_reason: "Local queued notification/posed events and disposition write-back implemented; audit-core event registration and State Hub deduplicated notification contracts required for runtime wiring (docs/intake-dependency-requirements.md)"
priority: medium
depends_on: [INFD-WP-0005-T04]
state_hub_task_id: "22d361cc-4bf2-59d4-9b97-728d42b1aeee"
@ -144,6 +158,8 @@ Notification and evidence: hub message to the founder on each posed memo
wire `hub_writeback.write_back` into the terminal-disposition path with a local
outbox. Replaces OPERATOR step A5.
## State Hub forwarding acceptance
```task
id: INFD-WP-0005-T08
status: wait
@ -156,17 +172,19 @@ state_hub_task_id: "ce2b36b4-ef82-53b7-aee7-e0063855370b"
Register state-hub as a submitter and run an end-to-end pose from a hub
decision through to the founder's review page in a dev stack.
## Doctrine response
```task
id: INFD-WP-0005-T09
status: wait
status: done
priority: medium
blocked_on: message-from:gate-house
blocking_reason: "Doctrine check that no human act is required at posing time (msg 6ab42744)"
state_hub_task_id: "403b0a7a-26c8-5893-b7ab-84579877d540"
```
Record gate-house's answer in the spec; adjust §5 if doctrine requires it.
## Approved live rollout
```task
id: INFD-WP-0005-T10
status: wait
@ -192,3 +210,19 @@ class grant was found; the real draft refuses submitter_unknown without a grant.
The alternative authenticated intake remains T04–T10. Gate House's T09 reply
4a290496 is described with its proposed-record conditions, without changing
authority or task states. No submission, provisioning or human answer occurred.
## Implementation evidence — 2026-10-03
T04: `intake_service.py`, `intake_approval.py`, Store schema 3 and empty reviewed
registries. Offline tests cover authentication/audience refusal, runtime
revocation, replay/restart, collisions, daily budget, atomic rollback, uncertain
create refusal and receipt-based load recovery. No deployed listener is enabled.
T05 local create-only transport is implemented; workload token exchange and
custody remain external. T07 local integration jobs are durable and terminal
human dispositions enqueue resolution atomically; the worker preserves stable
ids and leaves unconfigured adapters and disabled write-back pending. Receiver
contracts and scheduled runtime composition remain blocked.
T09: reply 4a290496 incorporated into the spec, with proposed-decision status
preserved. No founder ruling or deployment permission is inferred.