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
This commit is contained in:
tegwick 2026-11-01 00:37:59 +01:00
parent ba7a797160
commit 8787e1a14a
7 changed files with 515 additions and 36 deletions

View file

@ -26,6 +26,9 @@ _ACTOR_RE = re.compile(r"service:[a-z0-9][a-z0-9-]{1,62}")
_ACTION_RE = re.compile(r"[a-z0-9][a-z0-9-]{1,62}")
_OUTCOME_PATH = re.compile(r"/v1/memos/([a-z0-9][a-z0-9-]{2,62})/outcome")
class IntakeError(Exception):
def __init__(self, status, code):
self.status, self.code = status, code
@ -72,7 +75,8 @@ def configuration(submitters_path, groups_path):
for username, row in submitters.items():
if (not re.fullmatch(r"system:serviceaccount:[a-z0-9-]+:[a-z0-9-]+", username)
or not isinstance(row, dict)
or set(row) not in (ROW_KEYS, ROW_KEYS | {"present_existing"})
or not ROW_KEYS <= set(row) <= ROW_KEYS | {"present_existing", "outcome_read"}
or ("outcome_read" in row and (row["outcome_read"] is not True or "present_existing" in row))
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):
@ -139,11 +143,33 @@ def verify_existing(approval, submission, *, now=None):
class IntakeService:
def __init__(self, store, authenticator, creator, *, submitters_path, groups_path, reader=None):
"""``reader(approval_id) -> dict`` is a read-only GET used only by present_existing."""
def __init__(self, store, authenticator, creator, *, submitters_path, groups_path, reader=None, issuer=None):
"""``reader(approval_id) -> dict`` is a read-only GET used only by present_existing.
``issuer`` names the human sessions' OIDC issuer in outcome records; without it the
outcome read is unavailable.
"""
self.store, self.authenticator, self.creator = store, authenticator, creator
self.submitters_path, self.groups_path = submitters_path, groups_path
self.reader = reader
self.reader, self.issuer = reader, issuer
def outcome(self, token, memo_id):
"""INFD-WP-0010-T04: the answer to one memo this submitter posed, for the hub's own pull.
Only rows with ``outcome_read: true`` (reviewed deployment data: State Hub's dispatch
worker) may read, and only memos they posed. Read-only; no budget is spent.
"""
username = self.authenticator.authenticate(token)
submitters, _ = configuration(self.submitters_path, self.groups_path)
row = submitters.get(username)
if row is None:
raise IntakeError(403, "submitter_unknown")
if row.get("outcome_read") is not True:
raise IntakeError(403, "outcome_read_not_admitted")
if self.issuer is None:
raise IntakeError(503, "outcome_unavailable")
from .outcome import outcome_for
return outcome_for(self.store, row["submitter_id"], memo_id, issuer=self.issuer)
def pose(self, token, envelope):
username = self.authenticator.authenticate(token)
@ -286,24 +312,30 @@ class IntakeService:
raise IntakeError(409, "approval_already_presented")
def __call__(self, environ, start_response):
from .outcome import OutcomeError
try:
if environ.get("PATH_INFO") != "/v1/memos":
path = environ.get("PATH_INFO", "")
outcome = _OUTCOME_PATH.fullmatch(path)
if path != "/v1/memos" and outcome is None:
raise IntakeError(404, "not_found")
if environ.get("REQUEST_METHOD") != "POST":
if environ.get("REQUEST_METHOD") != ("GET" if outcome else "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:
if outcome is not None:
status, body = self.outcome(authorization[7:], outcome.group(1))
else:
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, OutcomeError) as exc:
status, body = exc.status, {"error": exc.code}
except Conflict:
status, body = 409, {"error": "review_handoff_conflict"}