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:
parent
be74a6bc35
commit
b8e57e8404
4 changed files with 365 additions and 41 deletions
261
docs/specs/ArchitectureBlueprint.md
Normal file
261
docs/specs/ArchitectureBlueprint.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue