From b8e57e8404629612e1c58b0a1220a5adb7e5ff60 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 9 Sep 2026 22:29:06 +0200 Subject: [PATCH] Architecture Blueprint and rewritten SCOPE; close T05 and T06 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- SCOPE.md | 116 +++++--- WORK-RECORDS.md | 2 +- docs/specs/ArchitectureBlueprint.md | 261 ++++++++++++++++++ ...ounding-specs-and-approver-ui-ownership.md | 27 +- 4 files changed, 365 insertions(+), 41 deletions(-) create mode 100644 docs/specs/ArchitectureBlueprint.md diff --git a/SCOPE.md b/SCOPE.md index 21b6775..06905ef 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -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 (T03–T05) 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. diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index a0bfe6c..9bda84a 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -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 | diff --git a/docs/specs/ArchitectureBlueprint.md b/docs/specs/ArchitectureBlueprint.md new file mode 100644 index 0000000..ce4b7f9 --- /dev/null +++ b/docs/specs/ArchitectureBlueprint.md @@ -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. diff --git a/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md b/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md index 760da69..2d8a7bd 100644 --- a/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md +++ b/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md @@ -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