Close the binding-correspondence gap as GH-DEC-2026-008

access-engine raised, and declined to solve locally, a hole in the split
GH-DEC-2026-005 ruled on. approval-claim verification item 4 is a
disjunction and neither limb delivers "approved for THIS request" on the
PDP path: limb one requires translating between two engines' vocabularies
and no mapping is published, limb two (pdp_digest) is optional. Where the
digest is absent a consumer can hold valid_now true, receive an ALLOW,
consume and act with nothing establishing that approval and decision
concern the same action and target.

Ruled: the PDP digest is the correspondence and is required on that path;
a claim without one fails closed; the native limb survives only for
consumers already in approval-engine's vocabulary, including T-06. No
mapping is published — a translation can be wrong while still producing a
confident answer, it fails open, it would be owned by neither engine, and
recomputing another layer's binding is the re-derivation GH-DEC-2026-005
already forbids. The cost is stated: an approval issued without a bound
CheckRequest is unusable on this path, which is correct behaviour.

Also: adopted hub row b606e8ce as canonical for GH-DEC-2026-005 rather
than registering a duplicate; recorded approval-engine's narrowing of the
approver-threshold consequence (distinctness is a UNIQUE storage
invariant, so the PEP stopped checking that the engine applied its own
invariant, not whether dual control could be forged); and drafted A7/T08,
a §11 marking obligation and §12 consumer rule for derived summaries,
after four instances in one week across four repositories.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtJBr77gMFLrN93iEevqQJ

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 425128@bnt-lap001
Assistant-Session: f5944d8b-dac4-4e1a-87eb-8b3d8f314a63
This commit is contained in:
tegwick 2026-09-06 09:32:20 +02:00
parent 865bab3955
commit f0f888ca7e
4 changed files with 196 additions and 4 deletions

View file

@ -978,3 +978,97 @@ decided_by: Bernd Worsch
created: '2026-09-06T07:30:42.213794Z'
updated: '2026-09-06T07:30:42.213794Z'
```
## Context
Raised by `access-engine` while implementing `GH-DEC-2026-005`, and declined by it
rather than solved locally — the right call, and the reason it reached a decision
record instead of a commit.
`approval-engine/docs/approval-claim.md` verification item 4 is a **disjunction**:
> `binding.digest` equals the digest of the binding the consumer computed from the
> proposed action, **or** `binding.pdp_digest` equals the `NewDecisionBinding` digest
> of that request.
Both limbs are load-bearing for one property — *approved for **this** request*, as
distinct from *approved*. Neither limb currently delivers it on the PDP path.
**Limb one requires a mapping nobody owns.** The claim's `binding.action` and
`binding.target` are in `approval-engine`'s vocabulary, over
`{action, actor, principal, purpose, target}`. `access-engine`'s policy package and
`CheckRequest` are in its own. To compute limb one, a consumer must translate between
them. No mapping is published. `access-engine` declined to invent one on the correct
ground that a wrong mapping fails **open** in the worst way available: it silently
accepts a claim approved for something else.
**Limb two is optional.** `binding.pdp_digest` is present only *"when recorded at
issue"*.
So on the path this estate actually runs, correspondence rests entirely on an optional
field, and where that field is absent a consumer can hold a claim with
`valid_now: true`, receive an ALLOW, consume, and act — with nothing having
established that the approval and the decision concern the same action and target.
The claim would not be lying; `valid_now` answers a different question.
## Decision
**`binding.pdp_digest` is the binding correspondence on the `GH-DEC-2026-003` path,
and it is required there. No cross-engine vocabulary mapping is published.**
1. **A consumer on the PDP path MUST verify `claim.binding.pdp_digest` equals the
decision's `NewDecisionBinding.request_digest`.** It is no longer a preferred
comparison; it is the comparison.
2. **A claim carrying no `pdp_digest` MUST NOT be used on that path.** The consumer
fails closed. It is not an error to be worked around, and specifically it is not a
licence to fall back to limb one.
3. **Limb one survives only where the consumer is natively in `approval-engine`'s
vocabulary** — that engine's own tests, and the `T-06` assurance case, which uses
the native binding precisely because no PDP digest exists there. It is not a
fallback for a PDP-path consumer.
4. **`approval-engine` MUST record the PDP digest at issue for any approval intended
for use on the PDP path**, and the claim schema states which approvals those are
rather than leaving it to the requester's memory.
### Why not publish the mapping
`access-engine` offered to co-author one. Declining is the substantive half of this
record.
**A mapping is a translation; a digest is an identity.** A translation can be wrong in
a way that still produces a confident answer — the failure mode `access-engine` named,
and it fails open. A digest comparison has no wrong-but-plausible result: it matches
or it does not.
**A published mapping would be owned by neither engine.** It would sit between two
vocabularies, each of which is its own repository's to evolve under §2, and it would
have to be revised whenever either side changed. That artifact is a third authority on
what a request *is* — the shape §6 exists to prevent, arriving as plumbing rather than
as a decision point.
**And computing limb one is the re-derivation `GH-DEC-2026-005` forbids.** A consumer
recomputing `approval-engine`'s native binding from its own vocabulary is a consumer
deriving another layer's data instead of validating against the layer that owns it.
The rule this estate just adopted answers this question on its own terms.
### The cost, stated
An approval requested without a known `CheckRequest` cannot be used on the PDP path,
because there was no PDP digest to record. That is a real constraint and it is
**correct behaviour, not a defect**: an approval granted against an unspecified action
does not become an approval for a specific one because a consumer later found a use
for it. Any workflow needing detached approval must either bind the request at issue
or accept that its approvals are not usable here.
This narrows what `approval-engine` may issue for this path. It does not narrow what
it may issue.
## Reversal
Revert if a legitimate workflow genuinely cannot bind the `CheckRequest` at approval
time — a standing human authorization for an action whose exact parameters are not
knowable until execution is the plausible candidate. The falsifier is a concrete
workflow, not the possibility of one. The expected resolution is that such a case
needs a *class* binding with its own contract, not a relaxation of this one to an
optional field, because an optional correspondence is indistinguishable at the
consumer from no correspondence at all.