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

@ -144,6 +144,25 @@ supplied by the requester.
**Verbs:** `accept` (submits the approval entry), `decline`, `return` with coded
reasons, `discuss`, `forward`, `escalate`, `comment`.
**Only `accept` reaches the engine.** `approval-engine` models entries against
an approval object and knows nothing of memo dispositions. `accept` becomes a
`POST /v1/approvals/{id}/entries`; `return`, `discuss`, `escalate` and the rest
are memo-level and must not be represented in that engine at all. This is the
correct separation, and it means the approval object alone does not show that a
human sent the question back — see `EvidenceModel.md` §8 and PRD L-06.
**Routes used:** `GET /v1/approvals/{id}` and `GET /v1/approvals/{id}/claim`
(scope `approval:read`), `POST /v1/approvals/{id}/entries` (scope
`approval:approve`). Never `/consume`. Any path containing `check` or ending in
`/authorize` returns 404 by design — there is no "may this person approve"
surface to call.
**Entitlement:** a `200` from the engine is not a statement that this human may
see the approval. `access-engine` is asked before rendering (PR-05).
**No inbox.** The engine exposes get-by-id only — no list, no search, no
"awaiting me". Approval ids arrive from whatever requested the approval.
**Boundary — the part that must not drift:**
- The surface calls `approval-engine`'s approval-entry mutation with a token
@ -217,10 +236,24 @@ of informedness the model has.
### NC-03 — Agent attempts a disposition
**Attempt:** a principal with `actor.kind = agent` submits any binding verb.
**Required behaviour:** refused at the API, not merely hidden in the UI.
**Required behaviour:** refused at *this repository's* API, not merely hidden in
the UI.
**Invariant protected:** humans bind, agents draft.
`guard G_NOAGENT · P-10 · W-"An agent binds"`
**Corrected 2026-09-09.** The first draft implied the refusal was also
`approval-engine`'s. It is not: that engine restricts only `/consume` by
principal type, and its `approval-engine-operator` service client holds
`approval:approve`, so a non-human principal can supply approver evidence there
today. Whether it should is `gate-house` doctrine, not ours.
Two consequences. First, enforcement of P-10 is **this surface's own
obligation** — there is no upstream backstop. Second, it is *auditable*:
schema v4 records the verified `entries[].principal_type` from the token. Read
that field. **Never** infer the answer from the shape of `subject_id`, which is
a naming convention, not a verified claim; entries predating v4 are `null` and
must not be read as `human`.
### NC-04 — Tenant switch requiring a new bind
**Attempt:** after an L0/L1 bind, the subject selects a tenant marked