informed-decision/docs/specs/ArchitectureBlueprint.md
tegwick 024946e9da Close INFD-IN-0001; design the independent evidence path; track both blockers
Housekeeping the ruling left behind, plus the one piece of blocked work that was
substantially ours to move.

INFD-IN-0001 closed with its resolution recorded, matching approval-engine's
IN-0002 form. It was still open after GH-DEC-2026-012 answered it.

INFD-IN-0003 and docs/evidence-path-design.md take up O-02, which was sitting in
the blueprint as "mechanism unchosen". Read independence and the local
transactional outbox are settled and not in question. The real question is what
travels, and it is sharper for us than for approval-engine because a
presentation record carries the brief and packet material actually shown to a
human. Three candidates with costs; proposal is commitment-only for Stage 1 —
hashes, principal, timestamps, acks, co-referenced approval id — which
discharges limit 3 and removes our ability to alter the record, while leaving us
able to erase the content. That residual is declared alongside the existing
compromised-surface one rather than papered over.

Deliberately not proposing the full binding document unilaterally: it would put
commercial and personal material into the audit fabric under retention and
export entitlements designed for audit events. That is a meaningful change in
what audit-core holds and is its owner's to accept, not ours to assume. The
third option, a separate evidence store, is refused here because that store has
no owner and inventing one routes around the §16 decision against stronger
archival custody.

Cadence declared and its form argued rather than copied: approval-engine's
heartbeat answer suits genuinely low-volume classes, but ours are mixed —
presentations are one per render, while dispositions and stance applications are
low-volume and are the security-relevant ones. Reconciliation per class as
primary, heartbeat for the low-volume classes. Depends on AUDIT-WP-0009 T04/T06;
declared, not claimed operating.

INFD-IN-0002 files the tenant blocker as a tracked record rather than leaving it
in message threads and a blueprint footnote. T07 and T08 now name their blocking
intakes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-09 23:19:27 +02:00

13 KiB

Architecture Blueprint — Stage 1

Repo: informed-decision Workplan task: INFD-WP-0001-T05 Status: draft, pending review Governing ruling: GH-DEC-2026-012 (answered INFD-IN-0001) Declarations: layer.yaml, pep-stance.yaml Companions: ProductRequirementsDocument.md, UseCaseCatalog.md, EvidenceModel.md


1. What this component is

PEP-shaped, ruled by GH-DEC-2026-012. Browser-facing; causes a protected side effect on the far side of a decision (an approver entry recorded against an approval object). Companion §5 owed, statute §6.4 in full. Not an Engine: it holds no state another layer reads at runtime for a verdict.

It also emits a presentation claim — one fact, what was shown — under the three limits in layer.yaml. No second catalog row.

Nothing in this component renders an authorization decision. No route answers "may this actor do X".


2. Call graph

                         browser (approver)
                                │
                    ┌───────────┴────────────┐
                    │  informed-decision     │   PEP-shaped
                    │  (this repository)     │
                    └──┬────┬────┬────────┬──┘
      auth-code + PKCE │    │    │        │  presentation records,
                       │    │    │        │  dispositions, stance
                       ▼    │    │        ▼  applications
                  key-cape  │    │    audit-core ◄──── independent
                  (identity)│    │                     evidence path
                            │    │                     (limit L3)
                            ▼    ▼
                access-engine   approval-engine
                (decision —     (read + entry;
                 consumed,       never /consume)
                 never made)

2.1 key-cape — identity

Authorization-code + S256 PKCE, public client, exact registered redirect. Access token only; id_token is never evidence. Scopes [openid, approval:read, approval:approve]. Never approval:consume.

Token must carry aud=approval-engine, principal_type: human, non-empty sub and scope, and an assurance object.

assurance shape is key-cape's, adopted as published in their docs/approval-engine-auth-contract.mdlevel (aal1|aal2), methods, mfa, source, at. This blueprint cites that document rather than restating the shape, so it cannot drift. at is authentication time, not token mint time; consumers needing freshness compare at rather than assuming it.

Open — blocking T07. A human access token cannot carry tenant:platform today: the tenant claim on human tokens resolves from the directory user record, which no adapter populates, so tokens fall back to tenant:coulomb. approval-engine compares by exact equality and refuses near-misses. Resolution is registration-bound (preferred here) or directory-sourced; it is not this repository's alone to decide. See §7.

2.2 access-engine — the decision, consumed

A decision is obtained before an approval is rendered to a person. A 200 from approval-engine is not entitlement; that engine never answers "may this actor do X".

Consuming a decision is not rendering one. There is no local fallback: if access-engine is unreachable, the stance applies (§5).

Declared gap, GH-DEC-2026-010. Obligation 1 requires the decision be attributable to access-engine. No consumer can satisfy that today — the envelope is unsigned (FLEX-WP-0024). This surface records decision_attributable: false. The decision path is not validated and must not be described as such while that is open.

2.3 approval-engine — the object and the entry

Route Method Scope
/v1/approvals/{id} GET approval:read
/v1/approvals/{id}/claim GET approval:read
/v1/approvals/{id}/entries POST approval:approve
/v1/approvals/{id}/consume POST never called

Paths containing check or ending in /authorize return 404 by design; there is nothing there to call.

POST /entries discards its request body — approver identity, assurance and evidence_ref come only from the verified token. So view_hash does not ride into the entry. Correlation is (approval_id, subject, approved_at).

No inbox. Get-by-id only; no list, no search, no "awaiting me". Approval ids arrive from whatever requested the approval. This component must never poll.

Only accept becomes a POST …/entries. return, discuss, escalate and the rest are memo-level and are not represented in that engine at all.

2.4 audit-core — evidence, on an independent path

Limit L3 is the architectural constraint of this design. The evidence copy must reach audit-core independently of this repository, because here the actor being audited and the evidence source are the same component. The copy that is evidence must not be reachable only through the party it is evidence about.

Consequences, binding on implementation:

  1. The claim endpoint and the evidence path are separate. Serving a presentation record over this component's API does not discharge the evidence obligation, and emitting evidence does not remove the need for a claim.
  2. A reader must be able to obtain the evidence copy without this component's cooperation. If the only route to a presentation record is an API this component controls, an adversary who controls it controls the evidence about it.
  3. Emission is not best-effort. A presentation or disposition that can be recorded here while its evidence is lost is a state change that never happened as far as an auditor can tell. approval-engine's local-outbox reasoning applies unchanged and is adopted rather than rediscovered: the queue is local, in the same transaction as the state change, with no synchronous audit-core dependency inside it.
  4. Atomicity covers accident, not compromise. It does not close the residual in layer.yaml, and must never be described as doing so.

Cadence is owed, because layer.yaml declares presentation evidence load-bearing (§9.6). The form differs from approval-engine's: their classes are genuinely low-volume, so a heartbeat is the answer. Ours are mixed — presentations are high-volume (one per render), while dispositions and stance applications are low-volume and are the security-relevant ones. Proposed: reconciliation per class as the primary form, plus a heartbeat for the low-volume classes, since a quiet month of dispositions is indistinguishable from suppression by rate alone. Depends on AUDIT-WP-0009 T04/T06; declared, not claimed operating.

What travels is the open question — see docs/evidence-path-design.md and O-02. T08 must not ship without it chosen, because "we will add the independent path later" is how limit 3 quietly becomes limit 3-in-principle.


3. Internal shape

informed_decision/
├── canonicalize.py     view_hash / awareness_hash        (governed, tested)
├── stance.py           the shipped unreachable-engine map (tested == published)
├── memo/               Decision Memo object, versions, lifecycle
├── presentation/       presentation records; the only writer of view_hash
├── disposition/        verb legality, guards G_*, thread
├── evidence/           local outbox, bundle export
└── clients/            key-cape, access-engine, approval-engine, audit-core

presentation/ is the only writer of view_hash. One writer, one canonicalizer, one place to audit. A second path that computes a hash is a defect, not an optimisation.

clients/approval_engine must not expose a validity cache. No memoisation of "is this approval valid". The engine is asked.


4. Storage

Stage 1 holds: memos and their versions; presentations with both hashes and acked highlight ids; dispositions with their coded reasons; thread entries; the local outbox; stance applications.

Properties required:

  • Presentations are append-only. A presentation is a record of an event that happened. Editing one is falsifying evidence.
  • Memo versions are immutable once presented. A change creates version n+1 and invalidates outstanding presentations (guard G_PRES).
  • The outbox is in the same transactional store, written in the same transaction as the state change it evidences.
  • No approval state. Not cached, not mirrored, not derived.

Engine choice is deferred to implementation; the constraint is that it be a single transactional store, so the outbox cannot separate from the mutation.


5. Failure modes — stance per dependency

Default is fail closed. Every stance in pep-stance.yaml is fail_closed, and that is a conclusion: refusing to bind delays a decision, it does not remove a capability anyone needs to keep a system running. There is no continuity argument here of the kind that justifies fail_open elsewhere.

Dependency Unavailable Surface behaviour
key-cape no authentication No session. Nothing rendered.
access-engine no decision Stance applies: fail_closed. Not rendered, not bound. Recorded as a stance application, never as a decline.
approval-engine 5xx / 503 no object, or store down Fail closed. Never present success.
audit-core evidence destination down Rendering and binding continue; evidence queues in the local outbox. An audit-core outage must not block a binding act — the same reasoning that keeps it from blocking a revocation upstream.

A fail-closed outcome is never rendered as a decline by the approver. No disposition is recorded, because the human did not make one. Conflating the two would put a refusal in the record that no person authored.

Engine response mapping — 409 duplicate_approver is success, 409 conflict is terminal, 503 is fail-closed — is specified in PR-06 and is not restated here.


6. Deployment

approval-engine has no Ingress and no external origin, which is why it could not own the browser client. This component has both, and that is the point.

  • Public HTTPS origin, TLS terminated at ingress.
  • Exact registered redirect URI, scheme and path included. Redirects match exactly at /authorize, so a near-miss fails closed.
  • client_id and the callback URI are deployment inputs, not implementation details. Once chosen they are stable; changing one silently breaks login rather than degrading it.
  • Outbound to approval-engine and access-engine on cluster-internal addresses; no public exposure of either is implied by this component's.

The concrete origin is fixed in T07 alongside the client registration, and is gated on the tenant question in §7.


7. Open and blocking

O-01 — Human token tenant. Blocks T07. Registration-bound (this repository's stated preference, as the tenant is then a property of the surface and its registration — which is exactly what the pre-sign binding slice commits) versus directory-sourced (which makes tenant a property of the person and changes it everywhere). Not ours alone; raised with key-cape, approval-engine and gate-house. If registration-bound is chosen, the condition that it holds only because registrations are static and deployment-owned should be written into the contract, not left as reasoning in a message.

O-02 — The independent evidence path. Design and decision request written: docs/evidence-path-design.md, filed as INFD-IN-0003. Read independence and the local transactional outbox are settled; the open question is what travels, because a presentation record carries the brief and packet material actually shown to a human. Proposal is commitment-only (hashes, principal, timestamps, acks, co-referenced approval id) for Stage 1, with the erasure residual declared rather than papered over. Awaiting audit-core on the payload and the sender registration, and gate-house only if the content question is doctrine. Must be resolved before T08 ships.

O-03 — Requester identity for an engine-originated memo. approval-engine does not model a requester. Likely the approval's principal; needs confirming against the object.

O-04 — abstain. In the state tables and real in Mitzeichnung practice, but with no obvious approval-engine counterpart. Stage 1 scope undecided.


8. What would make this architecture wrong

  • A route that answers whether an act is permitted.
  • A validity cache for approvals.
  • A second writer of view_hash.
  • An evidence path reachable only through this component.
  • view_hash nested inside, or recomputed from, approval-engine's binding digest — co-reference only (GH-DEC-2026-012 R3).
  • A polling loop against approval-engine synthesising an inbox.
  • A fail-closed outcome recorded as an approver's decline.
  • Describing the decision path as validated while GH-DEC-2026-010 is open.