flex-auth asked whether this engine should publish an action/target
mapping between the claim binding's vocabulary (secrets.kv.destroy,
{"id": "lane-openbao-root"}) and a policy package's (destroy, lane:...),
since their package makes no cross-check that a claim was approved for
the action being decided.
Answered no. A PIP asserting that one vocabulary's action means
another's would author policy semantics it does not own, over
vocabularies it does not own, and the failure mode is asymmetric: a wrong
mapping silently accepts a claim approved for a different action, which
is worse than no mapping. binding.pdp_digest is the correspondence and
sidesteps vocabulary entirely -- it compares the PDP's own digest to the
PDP's own digest, with no translation by anyone.
Implemented the part that was ours. pdp_digest was emitted only when
recorded, so a consumer could not distinguish "not issued against a
decision" from "we forgot to look". It is now always present and null in
that case, required-but-nullable in the schema, and documented as
something a PEP on a privileged lane must refuse. This engine states the
fact; enforcing the lane's policy stays with the consumer.
Both published examples were already contradicting the updated schema by
omitting the field -- the same fixture-versus-contract defect flex-auth
hit twice this week and that secrets-engine implemented. Fixed both, made
them cover the PDP-bound and unbound shapes so neither is inferred from
the other, and added tests/test_examples.py to validate every example
against the schema so the class cannot recur here. jsonschema added as a
dev dependency.
94 tests pass (6 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
GH-DEC-2026-005 moved the distinct-approver check off the PEP onto this
engine's valid_now. secrets-engine has implemented the split and reports
it no longer verifies the threshold independently. Gate House accepted
that as correct on layering AND as a genuine reduction in defence in
depth, and named the compensating control: not a second check at the PEP,
which is the duplication the split removes, but reconstructability at the
issuer under §9.6.
The emitted events could not support that. approval.issuance carried
required_count but never who satisfied it, and approval.use carried no
threshold evidence at all, so an auditor replaying the stream could not
recompute the evaluation without reading live rows -- rows that may since
have been superseded, revoked, or expired.
Both events now carry a threshold object: required_count,
distinct_approver_count, threshold_met, and approvers with approved_at
plus assurance and evidence_ref when recorded. Tests prove reconstruction
from the use row alone, and that the claim still discloses no approver
identities -- they are evidence for audit-core, not consumer-facing, and
the claim keeps disclosing the least it can.
Writing the tests showed distinctness is already a storage invariant:
entries is UNIQUE on (approval_id, subject_id), so a repeat approver is
refused at insert and a separate entry_count could never differ from the
distinct count. Dropped that field rather than ship a number that cannot
vary, and the test now asserts the refusal instead.
88 tests pass (4 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
Close remaining in-repo APPROVAL-WP-0002 gaps: drive GH-DEC-2026-003 against
the real HTTP surface, fail closed on JWT/human-consume/static-token paths,
treat audit 200 duplicates as drained, and ask KeyCape for the production
audience and client grants.
Assistant: grok
Assistant-Session: 01a06253-e557-7971-93d9-4f4c2cfbf455
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9