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
103 lines
5.9 KiB
Python
103 lines
5.9 KiB
Python
"""Decision outcome read for a submitter's own memos (INFD-WP-0010-T04, founder decision 2026-11-01).
|
|
|
|
State Hub's dispatch worker (STATE-WP-0096) pulls the human's answer to a memo it posed and
|
|
carries it to the hub; informed-decision holds no hub credential. This module builds the
|
|
``informed-decision.decision-outcome`` v1 record (state-hub
|
|
``docs/decision-services-alignment-answer.md`` section 5.2) from evidence already recorded here.
|
|
|
|
Read-only. Value-free: identifiers, digests, the verb, the deciding subject and its assurance.
|
|
No packet content, brief, note, reasons, display name or token ever leaves through this record.
|
|
It is a projection of evidence; nothing may execute on the strength of it.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
import json
|
|
|
|
from .evidence import EventClass
|
|
from .store import EvidenceUnavailable
|
|
|
|
DISPOSITION = EventClass.DISPOSITION.value
|
|
|
|
KIND = "informed-decision.decision-outcome"
|
|
VERSION = 1
|
|
TERMINAL = ("accept", "decline")
|
|
MAX_RECORD = 8 * 1024
|
|
|
|
|
|
class OutcomeError(Exception):
|
|
def __init__(self, status, code):
|
|
self.status, self.code = status, code
|
|
super().__init__(code)
|
|
|
|
|
|
def _assurance(db, disposition_id):
|
|
"""The assurance the PDP evaluated for this act, from its recorded policy observation."""
|
|
row = db.execute("SELECT content FROM evidence WHERE class=? "
|
|
"AND json_extract(envelope,'$.data.disposition_id')=? ORDER BY rowid LIMIT 1",
|
|
(DISPOSITION, disposition_id)).fetchone()
|
|
policy_id = json.loads(row["content"]).get("policy_observation_id") if row else None
|
|
observation = db.execute("SELECT request,outcome FROM policy_observations WHERE id=?",
|
|
(policy_id,)).fetchone() if policy_id else None
|
|
if observation is None or observation["outcome"] != "allow":
|
|
return None
|
|
attributes = json.loads(observation["request"]).get("subject", {}).get("attributes", {})
|
|
assurance = attributes.get("assurance")
|
|
if (not isinstance(assurance, dict) or not isinstance(assurance.get("level"), str)
|
|
or type(assurance.get("at")) is not int or assurance["at"] <= 0):
|
|
return None
|
|
auth_time = datetime.fromtimestamp(assurance["at"], timezone.utc).isoformat(timespec="seconds")
|
|
return {"aal": assurance["level"], "auth_time": auth_time.replace("+00:00", "Z")}
|
|
|
|
|
|
def outcome_for(store, submitter_id, memo_id, *, issuer):
|
|
"""(status, body) for one memo. 404 hides memos this submitter did not pose."""
|
|
with store._connection() as db:
|
|
intake = db.execute("SELECT metadata FROM intake_ledger WHERE submitter=? AND memo_id=? AND state='ready'",
|
|
(submitter_id, memo_id)).fetchone()
|
|
if intake is None:
|
|
raise OutcomeError(404, "memo_not_found")
|
|
metadata = json.loads(intake["metadata"])
|
|
serves = metadata.get("serves")
|
|
if metadata.get("profile") == "present_existing" or not serves or serves.get("resolves") != "on-answer":
|
|
raise OutcomeError(409, "memo_does_not_resolve")
|
|
# The first terminal human answer on any revision is the answer (resolves: on-answer).
|
|
rows = db.execute("SELECT d.id,d.body FROM dispositions d JOIN presentations p ON p.id=d.presentation_id "
|
|
"WHERE p.memo_id=? ORDER BY d.rowid", (memo_id,)).fetchall()
|
|
answer = None
|
|
for row in rows:
|
|
body = json.loads(row["body"])
|
|
if body["verb"] in TERMINAL and body["actor"]["kind"] == "person":
|
|
answer = body
|
|
break
|
|
if answer is None:
|
|
return 202, {"kind": KIND, "version": VERSION, "memo_id": memo_id, "status": "pending"}
|
|
presentation = json.loads(db.execute("SELECT body FROM presentations WHERE id=?",
|
|
(answer["presentation_id"],)).fetchone()["body"])
|
|
assurance = _assurance(db, answer["id"])
|
|
if assurance is None:
|
|
raise OutcomeError(409, "assurance_unrecorded")
|
|
record = {"kind": KIND, "version": VERSION, "status": "decided",
|
|
"hub_decision_id": serves["hub_decision_id"],
|
|
"memo_id": memo_id, "memo_revision": answer["memo_version"],
|
|
"verb": answer["verb"],
|
|
"subject": {"issuer": issuer, "sub": answer["actor"]["sub"]},
|
|
"assurance": assurance, "decided_at": answer["at"],
|
|
"view_hash": presentation["view_hash"], "presentation_id": answer["presentation_id"],
|
|
"approval_id": answer.get("approval_id")}
|
|
if answer["verb"] == "accept":
|
|
submission = db.execute("SELECT state,approved_at FROM submissions WHERE disposition_id=?",
|
|
(answer["id"],)).fetchone()
|
|
if submission is None or submission["state"] in ("prepared", "in_flight"):
|
|
# The human accepted; the approval entry is not yet confirmed. Not an answer yet.
|
|
return 202, {"kind": KIND, "version": VERSION, "memo_id": memo_id, "status": "pending"}
|
|
if submission["state"] != "confirmed":
|
|
raise OutcomeError(409, "approval_entry_unresolved")
|
|
# approval-engine entries carry no id; (approval_id, subject, approved_at) is the correlation.
|
|
record["approval_entry"] = {"approved_at": submission["approved_at"]}
|
|
audit = db.execute("SELECT o.receiver_reference FROM evidence e JOIN outbox o ON o.id=e.id "
|
|
"WHERE e.class=? AND json_extract(e.envelope,'$.data.disposition_id')=? "
|
|
"AND o.state='delivered' ORDER BY e.rowid LIMIT 1", (DISPOSITION, answer["id"])).fetchone()
|
|
if audit is not None and audit["receiver_reference"]:
|
|
record["audit_ref"] = audit["receiver_reference"]
|
|
if len(json.dumps(record).encode()) > MAX_RECORD:
|
|
raise EvidenceUnavailable("outcome record exceeds its bound")
|
|
return 200, record
|