Architecture Blueprint and rewritten SCOPE; close T05 and T06

T05 written after the ruling rather than before it, which was the point of
gating it. GH-DEC-2026-012 limit 3 did most of the shaping: the evidence copy
must reach audit-core independently of this component, because here the actor
being audited and the evidence source are the same. Booked as four binding
implementation consequences plus O-02, which must be resolved before T08 ships —
"we will add the independent path later" is how limit 3 becomes
limit-3-in-principle.

Other constraints fixed in the blueprint: presentation/ is the only writer of
view_hash; the approval-engine client exposes no validity cache; a fail-closed
outcome is never recorded as an approver's decline, since the human made none;
the assurance shape is cited from key-cape's contract rather than restated so it
cannot drift; and no polling loop may synthesise the inbox approval-engine
refuses to provide.

T06 closed with the SCOPE.md rewrite the ruling unblocked. It carries a "What
this repository does not claim" section, because a scope file listing only
capabilities overstates them: the decision path is not validated while
GH-DEC-2026-010 is open, the residual is not closed, view_hash is not inside the
approval entry, and nothing is deployed.

Two open items block the remainder. O-01, the human token tenant, blocks T07 and
is not ours alone to decide. O-02, the independent evidence path, blocks T08.

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
This commit is contained in:
tegwick 2026-09-09 22:29:06 +02:00
parent be74a6bc35
commit b8e57e8404
4 changed files with 365 additions and 41 deletions

116
SCOPE.md
View file

@ -6,16 +6,15 @@
## Status — 2026-09-09
**Nothing is implemented.** This repository currently contains founding
documents, the preserved founding exploration under `history/`, and
`workplans/INFD-WP-0001`. There is no package, no service, no deployment, and
no UI.
**Specification and declaration are complete; no service is deployed.**
This file exists because the Repo Manager requires it and because an honest
empty boundary is more useful than an imagined one. It is rewritten in
`INFD-WP-0001-T06`, once the `gate-house` layer ruling (T02) and the
specifications (T03T05) have fixed the real boundary. **Do not read the
sections below as describing working code.**
What exists and is tested: the layer and stance declarations (`layer.yaml`,
`pep-stance.yaml`, `informed_decision/stance.py`), the governed canonicalizer
and schema, and the four specs under `docs/specs/`. 46 tests pass.
What does not exist: any HTTP surface, any storage, any UI, any deployment. The
walking skeleton is `INFD-WP-0001-T08` and is gated on an external decision
(§"Open" below).
## One-liner
@ -24,30 +23,52 @@ renders a Decision Memo to the human who holds the mandate, records what was
shown, and binds their identity to the act — and owns the browser-facing
approver UI that `approval-engine` deliberately does not contain.
## Layer
**PEP-shaped**, ruled by `GH-DEC-2026-012`. Not an Engine. Companion §5 owed,
statute §6.4 in full. Declared in `layer.yaml` in this repository's own voice.
It emits one PIP-like fact — *what was presented* — as a claim, under three
limits that are the substance of the permission rather than caveats on it:
1. the claim carries presentation and nothing else, and must never carry,
restate, summarise or imply a decision or verdict;
2. the claim must never be an input to the decision it presents for;
3. the evidence copy reaches `audit-core` **independently** of this repository.
Limit 2 is load-bearing: the argument that a renderer attesting its own
rendering is not self-dealing was accepted *because* that limit holds.
## Core Idea
A decision surface is not a workflow engine and not a decision point. This
repository owns the Decision Memo object, the presentation record, the
canonicalization that produces `view_hash` / `awareness_hash`, the disposition
canonicalization producing `view_hash` / `awareness_hash`, the disposition
vocabulary, and the evidence bundle export. It does not evaluate whether an act
is permitted, does not hold approval current-state, and does not archive the
trail.
## In Scope — first cut (Stage 1, not yet built)
## In Scope — first cut (Stage 1)
- The Decision Memo object and its versions, promoted from
`history/20260909-initial-exploration/` into governed `schemas/`.
- Canonicalization of the binding and awareness documents, with the four
isolation vectors under test.
- The presentation record: what was rendered, to whom, when, in which locale
and UI release.
**Built and tested:**
- Canonicalization of the binding and awareness documents, with the three
published vectors reproducing byte for byte and all four isolation properties
pinned (`tests/test_canonicalize.py`).
- The Decision Memo schema and worked examples, governed under `schemas/`.
- The unreachable-engine stance map, built to v0.8 obligation 3, with
published-equals-shipped asserted by test (`tests/test_layer_conformance.py`).
**Specified, not built:**
- The presentation record: what was rendered, to whom, when, in which locale and
UI release.
- Required-highlight acknowledgment as a precondition of binding.
- The disposition vocabulary — `comment`, `discuss`, `return`, `forward`,
`escalate`, `acknowledge`, `accept`, `decline`, `withdraw`, `configure`
and its legality tables.
- The browser-facing OIDC client for human principals: authorization-code +
PKCE against `key-cape`, yielding a token with `aud=approval-engine`,
`principal_type: human`, scope `approval:approve`.
- The disposition vocabulary and its legality tables. Only `accept` reaches
`approval-engine`; `return`, `discuss`, `escalate` and the rest are
memo-level.
- The browser-facing OIDC client: authorization-code + S256 PKCE against
`key-cape`, scopes `[openid, approval:read, approval:approve]`.
- An L3 approver surface calling `approval-engine`'s approval-entry mutation.
- The evidence bundle as an offline-verifiable export.
@ -55,21 +76,40 @@ trail.
- Authorization decisions — `access-engine`, always and only (statute §6).
- The approval object, its state machine, validity and consumption —
`approval-engine`. This surface never caches validity, never infers
consumption, never requests `approval:consume`.
- Approval doctrine: which acts require approval, how many approvers, which
separations of duty — `gate-house`.
- Identity and authentication — `key-cape`. Identity is imported, never
invented here.
- The evidence archive — `audit-core`. This repository emits and exports.
`approval-engine`. Never cached, never inferred, never `approval:consume`.
- Approval doctrine — `gate-house`.
- Identity and authentication — `key-cape`. Imported, never invented. The
`assurance` shape is `key-cape`'s and is cited, not restated.
- The evidence archive — `audit-core`.
- Credentials materialized after a decision — `secrets-engine`.
- Notification transport, ticketing, and general workflow.
- L4/L5, QES, QTSP integration, qualified archival retention.
- The mandate graph. Stage 1 routes to a named approver and does not maintain a
map of who may bind what — a known, accepted limitation recorded in `GOAL.md`.
- Notification transport, ticketing, general workflow.
- **An approvals inbox.** Foreclosed upstream as well as here:
`approval-engine` exposes get-by-id only and will not add a list.
- L4/L5, QES, QTSP, qualified archival retention.
- The mandate graph — so a Stage 1 `escalate` is an assertion, not a verified
claim.
## Layer placement
## What this repository does not claim
**Provisional and unratified.** The working position is PEP-shaped under
statute §6.4 and companion §5. `layer.yaml` does not exist yet and is written
from the `gate-house` ruling in `INFD-WP-0001-T02`, not from this file.
Stated here because a scope file that only lists capabilities overstates them.
- **The decision path is not validated.** `GH-DEC-2026-010` requires a decision
be attributable to `access-engine`; no consumer can satisfy that today because
the envelope is unsigned (`FLEX-WP-0024`). This surface records
`decision_attributable: false` and must not describe validation as complete.
- **The residual is open.** A compromised surface can present X and attest Y.
`GH-DEC-2026-012` states it is not closed and does not credit this repository
with closing it.
- **`view_hash` is not inside the approval entry.** `POST /entries` discards its
body by design. Correlation is `(approval_id, subject, approved_at)`, so an
auditor holding only the approval object cannot reach the presentation.
- **Nothing is deployed**, so nothing is observed in production and nothing is
contained automatically.
## Open
- **Human token tenant** — blocks `T07`. A human access token cannot carry
`tenant:platform` today. Registration-bound versus directory-sourced; not this
repository's to decide alone.
- **The independent evidence path** — mechanism unchosen; required by limit 3
before `T08` ships.

View file

@ -10,7 +10,7 @@
| --- | --- | --- | --- | --- |
| workplan | INFD-WP-0001 | active | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |
| task | INFD-WP-0001-T01 | done | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |
| task | INFD-WP-0001-T02 | progress | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |
| task | INFD-WP-0001-T02 | done | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |
| task | INFD-WP-0001-T03 | done | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |
| task | INFD-WP-0001-T04 | done | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |
| task | INFD-WP-0001-T05 | todo | — | workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md |

View file

@ -0,0 +1,261 @@
# 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.md``level` (`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.
Open for implementation: whether the independent path is `audit-core` pull,
a separately-credentialed push, or a third party drain. **T08 must not ship
without one 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.** Its mechanism is unchosen. 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.

View file

@ -206,7 +206,7 @@ scale-invariance claim is checkable rather than asserted.
```task
id: INFD-WP-0001-T05
status: todo
status: done
priority: high
state_hub_task_id: "20ea118e-0657-5054-8aa2-b316444f4000"
```
@ -233,11 +233,27 @@ token audiences, scopes and principal types match `approval-engine`'s
stated unavailable-stance; the blueprint names which parts are Stage 1 and which
are placeholders.
Completed 2026-09-09: `docs/specs/ArchitectureBlueprint.md`. Written after the
ruling, as intended — the layer answer shaped it rather than being retrofitted.
The constraint that did most of the work is `GH-DEC-2026-012` limit 3: the
evidence copy must reach `audit-core` independently of this component, because
here the actor being audited and the evidence source are the same. That is
booked as four binding implementation consequences plus an open item (`O-02`)
that must be resolved before T08 ships, rather than as a principle — "we will
add the independent path later" is how limit 3 becomes limit-3-in-principle.
Also fixed: `presentation/` is the only writer of `view_hash`; the
`approval-engine` client must expose no validity cache; a fail-closed outcome is
never recorded as an approver's decline, because the human did not make one; and
the `assurance` shape is cited from `key-cape`'s contract rather than restated,
so it cannot drift.
## Evidence model, schema promotion and canonicalization under test
```task
id: INFD-WP-0001-T06
status: progress
status: done
priority: high
state_hub_task_id: "47cb3f7a-e349-5c81-a304-86275e058a85"
```
@ -299,6 +315,13 @@ Two things worth recording rather than burying:
Remaining for `done`: rewrite `SCOPE.md` after the T02 ruling.
2026-09-09 — **done.** `SCOPE.md` rewritten now the ruling and the specs have
fixed the real boundary. It carries a "What this repository does not claim"
section, because a scope file listing only capabilities overstates them: the
decision path is not validated while `GH-DEC-2026-010` is open, the residual is
not closed, `view_hash` is not inside the approval entry, and nothing is
deployed.
## Publish the OIDC browser-client contract to key-cape
```task