Revise specs against approval-engine's approver-surface requirements

approval-engine replied to INFD-IN-0001 with docs/approver-surface-requirements.md
(31da1af, 5203f46) and two corrections. Several of my requirements were wrong or
incomplete; revised rather than appended to.

Corrected:
- PR-02 listed only approval:approve. Wrong — the surface also needs
  approval:read to fetch what it renders. As drafted it would have shipped a
  client able to submit an entry it could never display. That changes a
  registration key-cape has already implemented, so it is their call (open
  question A).
- NC-03 implied approval-engine refuses non-human approver entries. It does not;
  only /consume is principal-restricted, and the operator service client holds
  approval:approve. Enforcement of "humans bind, agents draft" is therefore ours
  alone, and is auditable via schema v4's entries[].principal_type — never from
  the shape of subject_id.

Added:
- PR-04 assurance shape. approval-engine persists it verbatim and accepts an
  empty object, so it is the only place MFA survives into the approval record.
  Needs auth method, acr/amr, auth_time, agreed with key-cape.
- PR-05 entitlement. A 200 from the engine is not permission to view; we owe
  access-engine a check before rendering. Consuming a decision, not making one.
- PR-06 response mapping, including 409 duplicate_approver rendered as SUCCESS
  (a browser double-submit is routine and the first entry stands) and 503 as
  fail-closed.
- PR-07 and a matching INTENT wrongness condition: never render `approved` as
  permission to act. That is a PDP in the browser wearing UI copy.
- L-05, L-06 and EvidenceModel 8b: view_hash cannot ride into the entry — the
  POST discards its body by design — so Stage 1 correlates by (approval_id,
  subject, approved_at). DoD-3 is satisfied by the triple, not by a stored hash.

PRD open question 1 is answered by construction: there is no inbox endpoint and
there will not be one, so the approvals-inbox shape is foreclosed upstream.

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 14:20:13 +02:00
parent 23fba00b1b
commit 08b190928f
5 changed files with 210 additions and 17 deletions

View file

@ -189,6 +189,53 @@ between the two. Three candidate outcomes and their costs are set out in
avoided is both digests shipping with no stated authority rule, leaving the
estate with two canonicalizations of one act.
## 8b. Where `view_hash` actually lives (Stage 1)
**It is not in the approval entry, by the engine's deliberate design.**
`POST /v1/approvals/{id}/entries` reads the request body and **discards it**.
Approver identity, `assurance` and `evidence_ref` come only from the verified
token (`evidence_ref` is `jwt-sha256:<digest of the presented token>`). The
reasoning is sound and we adopt it rather than argue it: a caller-supplied
approver field is provenance, not evidence, and that entry is currently the one
record containing no caller-supplied data.
Stage 1 arrangement, requiring no change to `approval-engine`:
1. this repository holds the presentation record and computes both hashes;
2. it emits that record to `audit-core` as its own evidence;
3. correlation to the approval entry is by **`(approval_id, subject,
approved_at)`** — all three returned by the engine after a successful entry.
**Consequence for an auditor, stated plainly:** someone holding only the
approval object cannot reach the presentation. They need this surface's record
or `audit-core` as well. `GOAL.md` DoD-3 — "the approval entry is
reconstructable from a `view_hash`" — is satisfied by the correlation triple,
not by a hash stored on the entry.
Putting the binding *inside* the approval object would be a real change request
against `approval-engine` (an optional, verified-alongside-token `view_hash` on
the entry) and it is doctrine before it is a change request, so it goes to
`gate-house` first. Not raised at Stage 1; recorded here so the option is not
lost.
### `assurance` is the only carrier of authentication facts
`approval-engine` persists the token's `assurance` claim verbatim and
interprets nothing — an empty `{}` is *accepted* and evidences nothing. It is
therefore the single place where "this was bound under MFA" survives into the
approval record. The shape must carry authentication method, `acr`/`amr` and
`auth_time` at minimum, and `informed-decision` and `key-cape` own it between
them. Nothing downstream reconstructs it.
### Principal type is verified evidence; `subject_id` is not
Schema v4 records `entries[].principal_type` from the verified token. Design
principle 10 (humans bind, agents draft) is enforceable from the evidence chain
by reading that field. Never infer it from the shape of `subject_id` — a naming
convention is not a verified claim. Entries written before v4 are `null` and
must not be read as `human`.
## 9. Signed attributes (L4+, horizon)
When AES/QES arrives, the signed attributes carry `memo_id`, `memo_version`,