Origin and evidence path both landed today. T07 origin: railiance-apps deployed decisions.coulomb.social and corrected the hostname in this repo — not the decide.coulomb.social this workplan proposed. Verified here rather than taken on report: both paths 200, TLS verify 0, Let's Encrypt cert valid to 2026-12-09. T08: audit-core registered the source with every field as proposed and landed the detection half. INFD-IN-0003 closed. Their refinements booked — reconciliation on the high-volume class too, since rate detects a stream stopping but never a stream missing the particular renders that mattered, which is exactly our threat model; and PR-12, the custody locator must be a stable non-secret identifier because redact scans data and an existence declaration arriving without its pointer looks complete while being useless. INFD-IN-0004 ruled as GH-DEC-2026-015: gate-house reversed itself and nesting is permitted for this pair. The decisive ground was not the cycle argument we led with — our binding slice canonicalizes principal and target, two of the five digest fields, so co-reference left us performing a partial recomputation of one act in a second vocabulary, closer to the translation R3 forbade than nesting is. Our ordering objection was withdrawn as mistaken. The permission is conditioned and NOT ACTIVE until approval-engine states its presentation exclusion as normative and tested. layer.yaml is deliberately unchanged and carries nesting_permission_active false — we do not activate on our own initiative. GH-DEC-2026-016 ruled NC-03. Its §5 is live rather than hypothetical and is booked as PR-11: principal_type: human is a property of the client registration, structurally the same shape as the gap-route tenant, so a human-in-the-loop control must not be discharged on it as verified humanity. T07 stays progress: the submission to key-cape is written but unsent, blocked by the local permission classifier rather than by any repository. 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
540 lines
26 KiB
Markdown
540 lines
26 KiB
Markdown
# Product Requirements Document — Stage 1
|
|
|
|
**Repo:** informed-decision
|
|
**Stage:** 1 — the L3 approval approver surface
|
|
**Workplan task:** `INFD-WP-0001-T03`
|
|
**Status:** draft. **Revised 2026-09-09** against
|
|
`approval-engine/docs/approver-surface-requirements.md` (commits `31da1af`,
|
|
`5203f46`) and two corrections `approval-engine` sent with it. Revised
|
|
requirements are marked **[rev-1]**
|
|
**Companion specs:** `UseCaseCatalog.md`, `ArchitectureBlueprint.md` (gated on
|
|
`INFD-WP-0001-T02`), `EvidenceModel.md`
|
|
|
|
---
|
|
|
|
## 1. Scope
|
|
|
|
Stage 1 delivers **one** thing: a browser-facing surface on which a human
|
|
approver is shown an `approval-engine` approval request, understands it, acts on
|
|
it with the full disposition vocabulary, and leaves a reconstructable record of
|
|
what they were shown when they bound themselves.
|
|
|
|
Everything in this document is L3 on the depth spectrum. L0, L1, L2, L4 and L5
|
|
appear only in `UseCaseCatalog.md`, and only to constrain the schema so it
|
|
cannot fork later.
|
|
|
|
**Traceability.** Every requirement below carries a `trace:` line pointing at
|
|
either a `GOAL.md` definition-of-done item (`DoD-n`), an `INTENT.md` principle
|
|
(`P-n`) or wrongness condition (`W-n`), or a named external contract. A
|
|
requirement that traces to nothing is out of scope by construction.
|
|
|
|
**Testability.** Every requirement states an observable pass condition. "The UI
|
|
should be clear" is not a requirement; "a bind attempt with an unacknowledged
|
|
required highlight returns an error and creates no disposition" is.
|
|
|
|
---
|
|
|
|
## 2. Personas
|
|
|
|
### 2.1 Approver (primary)
|
|
|
|
Holds a mandate. Receives a memo because the routing said they are the person
|
|
who may bind this act — not because they were available. Is usually busy, is
|
|
frequently not a specialist in the subject matter, and is accountable for the
|
|
outcome afterwards.
|
|
|
|
What they need: to know why this is on their desk, what happens if they say yes,
|
|
what happens if they say nothing, and what they are actually binding. What they
|
|
must be able to do without leaving the object: ask a question, send it back, and
|
|
push it upward.
|
|
|
|
### 2.2 Requester (secondary)
|
|
|
|
Assembled the memo, or an agent assembled it on their behalf. Needs to know
|
|
where it is, who is holding it, and what was asked for when it comes back.
|
|
Stage 1 does **not** give the requester an authoring UI — memos arrive from
|
|
`approval-engine`. Authoring is Stage 2+.
|
|
|
|
### 2.3 Observer / auditor (tertiary)
|
|
|
|
Arrives months later with a question of the form *"who decided this, having seen
|
|
what?"* Never uses the live surface. Consumes the evidence bundle export and
|
|
`audit-core`. Their requirement is that the answer exists and verifies offline.
|
|
|
|
### 2.4 Non-persona: the agent
|
|
|
|
An agent may assemble and enrich a memo. It has **no** disposition capability.
|
|
This is stated as a persona exclusion because the temptation to add "auto-approve
|
|
low-risk items" arrives in every approval product, and it is the single change
|
|
that would void the identity half of the promise.
|
|
`trace: P-10, W-"An agent binds", guard G_NOAGENT`
|
|
|
|
---
|
|
|
|
## 3. Functional requirements
|
|
|
|
### 3.1 Identity and session
|
|
|
|
**PR-01 — Identity is imported, never issued.**
|
|
Every principal is authenticated by `key-cape` via authorization-code + PKCE.
|
|
The surface holds no password, no local credential store, and issues no
|
|
assurance level of its own.
|
|
*Pass:* no code path creates a session not derived from a `key-cape` token;
|
|
authentication failure yields no session.
|
|
`trace: DoD-2, P-9, GOAL invariant "Identity is imported"`
|
|
|
|
**PR-02 [rev-1] — Token shape is fixed by the resource server.**
|
|
Tokens presented to `approval-engine` carry `aud=approval-engine` (the resource
|
|
server, never the OAuth client id), `principal_type: human`, `tenant:
|
|
tenant:platform` compared by **exact string equality** (no alias, no prefix
|
|
handling — `platform` and `tenant:coulomb` are refused), non-empty `sub` and
|
|
`scope`, `assurance` as a non-null JSON object, and RS256 verification against
|
|
the issuer's `/jwks`. The access token is the evidence; `id_token` is never
|
|
presented here.
|
|
|
|
Scopes required: **`approval:read`** to fetch the approval and its claim, and
|
|
**`approval:approve`** to record an entry. The surface never holds
|
|
`approval:consume`.
|
|
|
|
*Pass:* a token request including `approval:consume` fails a build-time check;
|
|
`approval-engine`'s verifier accepts the issued token on both routes.
|
|
`trace: approval-engine docs/approver-surface-requirements.md §1-2; DoD-2`
|
|
|
|
*Correction from the first draft:* this requirement previously listed only
|
|
`approval:approve`. That was wrong and would have shipped a surface able to
|
|
submit an entry it was never able to display — `key-cape` specified the human
|
|
client as `[openid, approval:approve]`. Raised as `approval-engine` open
|
|
question A; see PR-05.
|
|
|
|
**PR-04 [rev-1] — `assurance` carries authentication facts in a documented
|
|
shape.**
|
|
`approval-engine` persists the token's `assurance` claim verbatim into the
|
|
approver entry and interprets nothing. It is therefore the **only** place where
|
|
"this approval was bound under MFA" survives into the approval record. An empty
|
|
object `{}` is *accepted* by the engine and evidences nothing.
|
|
|
|
The shape must carry, at minimum: authentication method, `acr`/`amr`, and
|
|
`auth_time`. `informed-decision` and `key-cape` own it between them.
|
|
|
|
*Pass:* a documented `assurance` shape exists and is agreed with `key-cape`; an
|
|
entry produced by this surface allows MFA to be established from the approval
|
|
record alone.
|
|
`trace: approval-engine §4 open question B and its 2026-09-09 correction`
|
|
|
|
**PR-05 [rev-1] — Entitlement to view is checked before rendering.**
|
|
`approval-engine` returning `200` is **not** a statement that this human is
|
|
entitled to see the approval — it never answers "may this actor do X". The
|
|
surface owes `access-engine` a check before rendering an approval to a person.
|
|
|
|
Consuming a decision is not rendering one: this requirement does not make the
|
|
surface a PDP, and satisfying it must not be done by local judgment.
|
|
*Pass:* no approval is rendered without a decision obtained from
|
|
`access-engine`; no local rule substitutes for it when `access-engine` is
|
|
unavailable (fail closed — see PR-06).
|
|
`trace: approval-engine §3; DoD-8; W-"It starts deciding"`
|
|
|
|
**PR-06 [rev-1] — Engine responses map to the vocabulary, and are not swallowed.**
|
|
|
|
| Status | Reason | Required surface behaviour |
|
|
| --- | --- | --- |
|
|
| 401 | `unauthenticated` | re-authenticate; never retry blindly |
|
|
| 403 | `forbidden` | wrong scope/tenant; surface as a boundary error, not a decline |
|
|
| 404 | `not_found` | no such approval in this tenant |
|
|
| 409 | `duplicate_approver` | **render as SUCCESS.** This subject already has an entry; the first stands |
|
|
| 409 | `conflict` | approval is terminal (revoked/superseded/consumed/expired); the act is no longer available |
|
|
| 422 | `unprocessable` | malformed request — a defect in this surface |
|
|
| 503 | `store_unavailable` | **fail closed.** Never present success |
|
|
|
|
A browser double-submit producing `duplicate_approver` is routine; rendering it
|
|
as a failure is a lie to the approver.
|
|
*Pass:* each row has a test; `duplicate_approver` produces a success state and
|
|
`503` produces no success state under any timing.
|
|
`trace: approval-engine §7`
|
|
|
|
**PR-07 [rev-1] — `approved` is never rendered as permission to act.**
|
|
`approved` is a state of an object. `approval-engine` actively refuses to
|
|
serialize a decision (`_assert_not_decision`). A surface that renders "approved"
|
|
as "you may now do the thing" has re-implemented a PDP in the browser.
|
|
Approval status is read from the response body, never inferred from a `200`.
|
|
*Pass:* no copy or affordance in the surface presents approval status as
|
|
authorization; status is read from the response object.
|
|
`trace: approval-engine §7; W-"It starts deciding"`
|
|
|
|
**PR-03 — The surface displays which identity and which scope are being used.**
|
|
Before any binding act, the approver sees the identity they are acting as and
|
|
the tenant/scope they are acting in, as committed values — not as inferred
|
|
context.
|
|
*Pass:* both values appear in the rendered binding document and therefore in
|
|
`view_hash`.
|
|
`trace: P-11, EvidenceModel binding slice`
|
|
|
|
### 3.2 Presentation
|
|
|
|
**PR-08 [rev-2] — The act-scope is committed; the membership claim is not
|
|
borrowed to stand in for it.**
|
|
`binding.target` — the tenant, environment and system being entered — is the
|
|
authoritative act-scope and is inside `view_hash`. The token's `tenant` claim is
|
|
a membership fact about the *principal* and is never used as the act-scope.
|
|
*Pass:* no code path derives `binding.target` from the token's `tenant` claim;
|
|
changing `binding.target` changes `view_hash` (existing isolation vector 3).
|
|
`trace: GH-DEC-2026-013 §5; EvidenceModel §8c`
|
|
|
|
**PR-09 [rev-2] — The `tenant` claim is stored with its provenance.**
|
|
`key-cape` emits `tenant` as a bare string, so a consumer cannot distinguish a
|
|
tenant the *directory asserted about the person* from one a *registration
|
|
supplied about the client they came through*. Until the claim carries its own
|
|
provenance, this surface records which route the value arrived by.
|
|
|
|
This surface must not declare a tenant the directory record does not carry
|
|
without saying so in the record — doing so asserts a fact about a person on this
|
|
registration's authority, which `GH-DEC-2026-013` permits only as a bounded gap.
|
|
*Pass:* every stored tenant value carries a provenance marker; a value with an
|
|
undifferentiated or absent provenance is a validation failure, not a default.
|
|
`trace: GH-DEC-2026-013 §5; key-cape 329e48f`
|
|
|
|
**PR-11 [rev-3] — A human-in-the-loop control is never discharged on an
|
|
unverified assertion of humanity.**
|
|
`GH-DEC-2026-016` requires that where an approval is *declared* as discharging a
|
|
human-in-the-loop control, the approver must be a human principal and
|
|
`approval-engine` must refuse at bind time. Its §5 lands here: what makes a
|
|
principal `human` belongs to the identity layer and **inherits A-16** — if
|
|
`human` is reachable by two routes, the control must not be discharged on a
|
|
registration-supplied claim. Refusing a service principal while accepting an
|
|
unverified assertion of humanity moves the defect rather than closing it.
|
|
|
|
**This is live for us, not hypothetical.** `principal_type: human` is a property
|
|
of the *client registration*, the same shape as our registration-supplied
|
|
`tenant`. Until its provenance is distinguishable, this surface treats it as
|
|
registration-supplied and does not present it as verified humanity.
|
|
*Pass:* `principal_type` is stored with its provenance like `tenant` (PR-09); no
|
|
copy, export field or evidence record describes a bind as human-verified on the
|
|
strength of the claim alone.
|
|
`trace: GH-DEC-2026-016 §5; A-16 with the marker-independence rider; PR-09`
|
|
|
|
**PR-12 [rev-3] — The custody locator is a stable non-secret identifier.**
|
|
`audit-core` applies `secret_policy: redact`, which scans `data`. A
|
|
credentialed URL or secret-shaped path in the custody field is redacted out and
|
|
the existence declaration (PR-53) arrives without its pointer. It fails visibly
|
|
— `details.redaction.paths` records it — but the declaration is then useless.
|
|
*Pass:* the custody locator is an identifier the custodian resolves, never a
|
|
credentialed URL; a redaction finding on the custody field is a build-breaking
|
|
defect, not a warning.
|
|
`trace: audit-core docs/informed-decision-source-registration.md`
|
|
|
|
**PR-10 — A memo renders question, requested act, binding level, brief and
|
|
consequences before any action control is reachable.**
|
|
*Pass:* the disposition controls are not operable until the brief region has
|
|
been rendered; a memo missing a question fails validation and does not render.
|
|
`trace: INTENT "One question per memo"; GOAL invariant`
|
|
|
|
**PR-11 — The packet is always reachable in full.**
|
|
Highlights are an aid to attention, never a filter. Every document in the packet
|
|
is openable from the memo, and the count of packet items is visible without
|
|
interaction.
|
|
*Pass:* for a memo with *n* packet items, all *n* are reachable; no item is
|
|
hidden behind a highlight-only view.
|
|
`trace: P-8, W-"Highlights shrink the legal object"`
|
|
|
|
**PR-12 — The UI states unmistakably that the whole instrument is bound.**
|
|
Acknowledging highlights must never read as binding only the acknowledged
|
|
sections. This is a legal exposure, not a copy preference.
|
|
*Pass:* the bind control carries scope language naming the whole instrument;
|
|
reviewed and signed off explicitly as a requirement, not left to implementation.
|
|
`trace: P-8, W-"Highlights shrink the legal object"; exploration open question 5`
|
|
|
|
**PR-13 [rev-1] — Every render creates a presentation record.**
|
|
Carrying memo id, memo version, locale, UI release, timestamp, principal, and
|
|
the computed `view_hash` and `awareness_hash`.
|
|
*Pass:* rendering a memo twice creates two presentation records; a disposition
|
|
references exactly one.
|
|
`trace: DoD-3, DoD-5, guard G_PRES`
|
|
|
|
*Constraint discovered 2026-09-09:* `POST /entries` **reads the request body and
|
|
discards it**. Approver identity, `assurance` and `evidence_ref` come only from
|
|
the verified token, deliberately — a caller-supplied approver field is
|
|
provenance, not evidence. So `view_hash` cannot ride into the approval entry.
|
|
Stage 1 therefore holds the presentation record here, emits it to `audit-core`,
|
|
and correlates to the entry by **`(approval_id, subject, approved_at)`**, all
|
|
three returned by the engine after a successful entry. Putting the hash *inside*
|
|
the approval object is a change request against `approval-engine` and is
|
|
doctrine first — it goes to `gate-house`. `GOAL.md` DoD-3 is satisfied by the
|
|
correlation triple, not by a hash stored on the entry.
|
|
|
|
**PR-14 — A disposition binds to a presentation of the same memo version.**
|
|
A bind attempt against a stale version is refused, not silently upgraded.
|
|
*Pass:* incrementing the memo version invalidates outstanding presentations; a
|
|
bind against the old presentation returns a version conflict.
|
|
`trace: guard G_PRES; INTENT invariant "Binding acts reference a presentation of
|
|
the same version"`
|
|
|
|
### 3.3 The acknowledgment gate
|
|
|
|
**PR-20 — Required highlights must be acknowledged before a binding verb.**
|
|
Where `highlights[].required_ack` is true, `accept`, `decline` on a bind step,
|
|
and `acknowledge` are unavailable until every required highlight id is in the
|
|
presentation's acked set.
|
|
*Pass:* a bind attempt with an unacknowledged required highlight fails closed,
|
|
creates no disposition, and returns an error naming the outstanding ids.
|
|
`trace: DoD-4, guard G_ACK`
|
|
|
|
**PR-21 — Acknowledgment is an explicit act, never inferred.**
|
|
Scroll position, time on page, focus events and viewport intersection MUST NOT
|
|
acknowledge a highlight.
|
|
*Pass:* no acknowledgment is recorded by any event other than a deliberate
|
|
control activation.
|
|
`trace: P-12, INTENT "Informedness without surveillance"`
|
|
|
|
### 3.4 Disposition vocabulary
|
|
|
|
**PR-30 — The surface implements the full verb set for the step kind, not
|
|
approve/reject.**
|
|
Overlay verbs `comment` and `discuss` are available whenever the memo status
|
|
allows. `return`, `forward` and `escalate` are available to the assignee on
|
|
every step kind. `accept` and `decline` follow the legality table.
|
|
*Pass:* every verb in `state-transitions.md` §B2 legal for the step kind is
|
|
reachable in the UI for that step.
|
|
`trace: DoD-6, INTENT §"The Disposition Vocabulary"`
|
|
|
|
**PR-31 — `accept` is not offered on a weak step.**
|
|
On `inform`, `comment`, `review` and `acknowledge` steps, `accept` is absent —
|
|
not present-and-disabled. Kenntnisnahme is not approval.
|
|
*Pass:* the control does not exist in the rendered UI for weak steps; an API
|
|
call attempting it is refused.
|
|
`trace: state-transitions §B2 ("accept: —" on weak); INTENT design principle 6`
|
|
|
|
**PR-32 — `return` carries at least one coded reason.**
|
|
Free text alone is not a return. Return is a first-class success path and must
|
|
be as easy to reach as accept.
|
|
*Pass:* a return with zero coded reasons is refused (guard `G_REASONS`); the
|
|
return control has equal visual weight to accept.
|
|
`trace: DoD-6, P-4, guard G_REASONS`
|
|
|
|
**PR-33 — `return` and `decline` are distinguishable in the record.**
|
|
A memo sent back for improvement and a memo refused are different outcomes and
|
|
must not collapse to one status.
|
|
*Pass:* `returned` and `completed/declined` are distinct in the stored record
|
|
and in the export.
|
|
`trace: DoD-6, GOAL definition of done 6`
|
|
|
|
**PR-34 — `discuss` keeps the dialogue on the object.**
|
|
A question to the requester creates a thread entry on the memo. The surface
|
|
provides no "email the requester" affordance.
|
|
*Pass:* no outbound side-channel exists; the thread is part of the exported
|
|
evidence.
|
|
`trace: P-5`
|
|
|
|
**PR-35 — `escalate` requires a strictly higher mandate target.**
|
|
Where the target's mandate is not strictly higher for this act, the verb is
|
|
`forward`, not `escalate`.
|
|
*Pass:* guard `G_ESC` is evaluated; a same-level target is refused as an
|
|
escalation.
|
|
`trace: guard G_ESC`
|
|
*Known Stage 1 limitation:* without the mandate graph, `G_ESC` cannot be fully
|
|
evaluated. Stage 1 MUST record the distinction the approver asserted rather than
|
|
silently treating escalate as forward. See §6.
|
|
|
|
### 3.5 The binding / awareness split
|
|
|
|
**PR-40 — Awareness fields never enter `view_hash`.**
|
|
Proposed role, other available roles, other tenants, and last-session summary
|
|
are rendered on the same surface, hashed into `awareness_hash`, and excluded
|
|
from the signed binding document.
|
|
*Pass:* isolation vector 2 — editing an awareness field does not change
|
|
`view_hash` — is green in CI.
|
|
`trace: DoD-4, P-11, W-"Awareness leaks into the signature", EvidenceModel`
|
|
|
|
**PR-41 — Awareness is visually distinguished from binding.**
|
|
The approver can tell, without reading documentation, which part of the surface
|
|
they are committing to and which part is orientation.
|
|
*Pass:* the two regions are separately labelled; the labelling is part of the
|
|
reviewed design, not incidental styling.
|
|
`trace: P-11`
|
|
|
|
**PR-42 — Promotion is explicit and versions the memo.**
|
|
Moving a field from awareness into binding creates a new memo version and
|
|
invalidates outstanding presentations.
|
|
*Pass:* guard `G_PROMOTE`; a presentation taken before promotion cannot be bound
|
|
against after it.
|
|
`trace: guard G_PROMOTE, INTENT invariant on `awareness_promoted``
|
|
|
|
### 3.6 Evidence
|
|
|
|
**PR-50 — An evidence bundle exports and verifies offline.**
|
|
Containing the memo, its versions, the presentations with both hashes, the
|
|
dispositions, the thread, and the hash chain.
|
|
*Pass:* an exported bundle verifies with no network access; verification detects
|
|
a mutated field.
|
|
`trace: DoD-7, P-7`
|
|
|
|
**PR-51 — The export is the evidence, not a screenshot.**
|
|
No requirement is satisfied by a rendered image of the surface.
|
|
*Pass:* the bundle is structured data; the canonicalizer reproduces `view_hash`
|
|
from it.
|
|
`trace: P-7`
|
|
|
|
**PR-52 — The surface states the bound of its own guarantee.**
|
|
Documentation and the export state that attestation covers accident and later
|
|
tampering, not a compromised surface presenting X and attesting Y.
|
|
*Pass:* the residual appears in `EvidenceModel.md` and in the bundle metadata.
|
|
`trace: gate-house decision request §3; approval-engine's equivalent residual`
|
|
|
|
**PR-53 [rev-2] — Emitted evidence asserts that content exists and where custody
|
|
sits.**
|
|
Commitment-only emission carries, alongside the hashes, an assertion that the
|
|
committed content exists and the custodian holding it, so that failure to
|
|
produce at retrieval is a **conformance failure attributable to the custodian**
|
|
rather than an unremarkable blank. Without it, a reviewer cannot distinguish
|
|
*erased*, *withheld*, *lost* and *never held*.
|
|
*Pass:* every emitted commitment carries `content_exists` and `custody`; a
|
|
retrieval failure produces a finding naming the custodian.
|
|
`trace: GH-DEC-2026-014 §4 — a granting condition, not a reversal candidate`
|
|
|
|
**PR-54 [rev-2] — Commitment-only evidence is never described as
|
|
reconstructability.**
|
|
It satisfies **non-alteration**. It does not carry what it commits to, and
|
|
establishes what was committed to only conditionally — if a document is later
|
|
produced, whether it is the one.
|
|
*Pass:* no document, export field or conformance claim in this repository
|
|
describes the independent evidence path as reconstructing content.
|
|
`trace: GH-DEC-2026-014`
|
|
|
|
### 3.7 Locale and accessibility
|
|
|
|
**PR-60 — German and English, with German as a first-class source language.**
|
|
The *Umlaufmappe* vocabulary — Kenntnisnahme, Mitzeichnung, Schlusszeichnung,
|
|
Rücksprache, Rückgabe — is the domain vocabulary, not a translation of English
|
|
product terms.
|
|
*Pass:* both locales render every verb; locale is part of the presentation
|
|
record and therefore of `view_hash`.
|
|
`trace: DoD-6, INTENT §"The Disposition Vocabulary"; exploration open question 7`
|
|
|
|
**PR-61 — Locale changes the memo version's presentation, not its meaning.**
|
|
Because locale enters `view_hash`, a memo read in German and one read in English
|
|
produce different presentations of the same binding. This is intended and must
|
|
be documented, not smoothed over.
|
|
*Pass:* the two presentations differ in `view_hash` and reference the same memo
|
|
version.
|
|
`trace: canonicalize.md allow-list`
|
|
|
|
**PR-62 — Keyboard-operable and screen-reader-navigable throughout.**
|
|
An approver using assistive technology can read the brief, open packet items,
|
|
acknowledge highlights and bind, without a mouse.
|
|
*Pass:* every disposition control and every acknowledgment control is reachable
|
|
and operable by keyboard; the acknowledgment state is programmatically
|
|
determinable.
|
|
`trace: P-8; PR-21 (acknowledgment must remain explicit under AT too)`
|
|
|
|
---
|
|
|
|
## 4. Anti-requirements
|
|
|
|
These are requirements *not* to build things a reasonable implementer would
|
|
otherwise add. Each is testable as an absence.
|
|
|
|
**PR-70 — No dwell timers.** Minimum reading time, "you must spend 30 seconds on
|
|
this page", and countdown-gated buttons are forbidden.
|
|
|
|
**PR-71 — No keystroke, mouse-movement or attention analytics.** Informedness is
|
|
evidenced by the hash of the presented view plus explicit acknowledgment, never
|
|
by behavioural telemetry.
|
|
|
|
**PR-72 — No dark patterns.** Accept is not visually privileged over return.
|
|
Decline is not hidden behind a secondary menu. No pre-checked acknowledgments.
|
|
No confirm-shaming copy.
|
|
|
|
**PR-73 [rev-1] — No auto-approval, no delegation-to-agent, no "approve all".**
|
|
Bulk selection may exist for reading; it must not exist for binding.
|
|
|
|
*Correction:* the first draft assumed `approval-engine` would refuse a non-human
|
|
approver entry. It does not — only `/consume` is restricted by principal type,
|
|
and the `approval-engine-operator` service client holds `approval:approve`. So a
|
|
non-human principal **can** supply approver evidence today. Whether it should is
|
|
`gate-house` doctrine. Enforcement of "humans bind, agents draft" is therefore
|
|
this surface's own obligation, and is *auditable* via schema v4's
|
|
`entries[].principal_type`, which records the verified value from the token.
|
|
Read that field; **never** infer it from the shape of `subject_id`, which is a
|
|
naming convention, not a verified claim. Entries written before v4 are `null`
|
|
and must not be read as `human`.
|
|
|
|
**PR-74 — No approval-state caching.** The surface never stores or serves whether
|
|
an approval is valid. It asks `approval-engine`.
|
|
`trace: W-"It reimplements approval-engine", GOAL invariant`
|
|
|
|
**PR-75 — No authorization endpoint.** Nothing in this repository answers "may
|
|
this actor do X".
|
|
`trace: DoD-8, W-"It starts deciding"`
|
|
|
|
*Pass for §4:* each is a review checklist item and, where mechanically checkable,
|
|
a test asserting the absence.
|
|
|
|
---
|
|
|
|
## 5. Non-requirements for Stage 1
|
|
|
|
Not deficiencies — deliberate exclusions carried from `GOAL.md`:
|
|
|
|
- Memo authoring. Memos arrive from `approval-engine`.
|
|
- The mandate graph.
|
|
- L4/L5, QES, QTSP, qualified archival retention.
|
|
- Multi-party parallel Mitzeichnung.
|
|
- EUDI Wallet / eID identity import.
|
|
- Notification transport. A memo may be announced elsewhere; the dialogue stays
|
|
on the object.
|
|
- Mobile-native clients. Responsive web only.
|
|
- Offline operation.
|
|
|
|
---
|
|
|
|
## 6. Known limitations, stated up front
|
|
|
|
**L-01 — `escalate` without a mandate graph is forwarding.** Guard `G_ESC`
|
|
requires knowing that the target's mandate is strictly higher. Stage 1 has no
|
|
such map. The surface records the approver's asserted intent to escalate and
|
|
marks it unverified rather than either blocking the verb or pretending the guard
|
|
passed. `GOAL.md` records this as accepted; `EvidenceModel.md` must state that
|
|
an escalation in a Stage 1 bundle is an assertion, not a verified claim.
|
|
|
|
**L-02 — `view_hash` is computed by the renderer.** A compromised surface can
|
|
present one thing and attest another. Not closed, not closable at this layer,
|
|
and not claimed to be closed.
|
|
|
|
**L-03 — Binding level is organizational only.** Stage 1 produces no AES or QES
|
|
signature. `binding_level` is carried in the object and in `view_hash` so that
|
|
higher levels are expressible without a schema change, but nothing above
|
|
organizational is implemented or claimed.
|
|
|
|
**L-04 — `view_hash` versus `approval-engine`'s binding digest is unresolved.**
|
|
Filed as `INFD-IN-0001` R3. Until ruled, the surface computes `view_hash` and
|
|
does not assert any relationship to the approval claim's digest.
|
|
|
|
**L-05 — `view_hash` is not inside the approval entry.** By the engine's
|
|
deliberate design (PR-13). Correlation is by `(approval_id, subject,
|
|
approved_at)`. An auditor holding only the approval object cannot reach the
|
|
presentation without also holding this surface's record or `audit-core`.
|
|
|
|
**L-06 — A memo `return` is invisible to `approval-engine`.** `accept` is the
|
|
only disposition that becomes a `POST …/entries`. `return`, `discuss`,
|
|
`escalate` and the rest are dispositions of a *memo* and are not represented in
|
|
that engine at all. Correct separation, but it means the approval object alone
|
|
does not show that a human sent the question back.
|
|
|
|
---
|
|
|
|
## 7. Open questions for review
|
|
|
|
1. ~~Does the approver need to see other pending memos?~~ **Answered
|
|
2026-09-09 by construction.** `approval-engine` exposes get-by-id only: no
|
|
list, no search, no "awaiting me" query, and there will not be one. Approval
|
|
ids reach this surface from whatever requested the approval, never by
|
|
polling. A listing surface would itself be an authorization question and
|
|
would need scoping by something other than that engine's judgment. The
|
|
approvals-inbox shape is foreclosed upstream, which is the outcome this
|
|
repository wanted anyway.
|
|
2. What does the surface do when `approval-engine` is reachable but
|
|
`audit-core` is not? Fail closed on binding, or bind and queue? The
|
|
`ArchitectureBlueprint` must answer this per-dependency (T05).
|
|
3. Who is the requester for an `approval-engine`-originated memo, given the
|
|
engine does not model one? Likely the approval's `principal`, but this needs
|
|
confirming against the approval object.
|
|
4. Is `abstain` in Stage 1? It is in the state tables and it is real in
|
|
Mitzeichnung practice, but it has no obvious `approval-engine` counterpart.
|