State pdp_digest explicitly; decline to publish a vocabulary mapping
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
2026-09-06 09:32:11 +02:00
|
|
|
"""The published examples must satisfy the published schema.
|
|
|
|
|
|
|
|
|
|
flex-auth shipped two defects in one day from fixtures that contradicted their
|
|
|
|
|
own contracts, and secrets-engine built a validator against one of them. A
|
|
|
|
|
contract whose examples contradict its prose will be implemented as its
|
|
|
|
|
examples, so the examples are tested rather than trusted.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
jsonschema = pytest.importorskip("jsonschema")
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
SCHEMA = json.loads((ROOT / "schemas" / "approval_claim.schema.json").read_text())
|
|
|
|
|
EXAMPLES = sorted((ROOT / "examples").glob("claim.*.json"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_examples_exist():
|
|
|
|
|
assert EXAMPLES, "no claim examples found to validate"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("path", EXAMPLES, ids=lambda p: p.name)
|
|
|
|
|
def test_example_matches_schema(path):
|
|
|
|
|
jsonschema.validate(json.loads(path.read_text()), SCHEMA)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize("path", EXAMPLES, ids=lambda p: p.name)
|
|
|
|
|
def test_example_states_pdp_digest_explicitly(path):
|
|
|
|
|
"""Absence must be a stated null, never a missing key."""
|
|
|
|
|
assert "pdp_digest" in json.loads(path.read_text())["binding"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_examples_cover_both_pdp_binding_states():
|
|
|
|
|
"""An implementer must see both shapes, not infer one from the other."""
|
|
|
|
|
states = {
|
|
|
|
|
json.loads(p.read_text())["binding"]["pdp_digest"] is None for p in EXAMPLES
|
|
|
|
|
}
|
|
|
|
|
assert states == {True, False}
|
Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.
Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.
Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.
Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 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
2026-09-06 14:51:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_examples_cover_both_pdp_path_declarations():
|
|
|
|
|
states = {json.loads(p.read_text())["binding"]["pdp_path"] for p in EXAMPLES}
|
|
|
|
|
assert states == {True, False}
|
|
|
|
|
|
|
|
|
|
|
Publish the valid-but-unbound claim; the missing shape was confounded
The example set satisfied §11's both-shapes clause only by accident. Both
values of pdp_digest and pdp_path appeared, but they appeared in perfect
correlation with validity: claim.valid carried a digest with pdp_path true,
claim.revoked carried null with pdp_path false, and nothing else existed.
Two independent dimensions presented as one. A reader could reasonably conclude
that pdp_digest is null because the claim is revoked, or that pdp_path tracks
validity. Both are false, and the example set is what would have taught them --
the failure the both-shapes clause exists to catch, which this repo proposed and
then shipped a case of.
The missing shape is the consequential one: a claim that is entirely valid --
valid_now true, reason_code ok, not consumed -- and carries no PDP binding. It
is usable for a consumer comparing the native binding.digest and unusable on the
GH-DEC-2026-003 path, where a PEP MUST refuse it. valid_now true is not
permission to proceed on that lane. A consumer writing that refusal previously
had no published shape to test against and would have had to invent a fixture,
which is the drift §12 names.
examples/claim.valid.no-pdp.json publishes it. The tests now assert the
decorrelation rather than mere presence: one requires a valid claim with no PDP
binding to exist, the other requires valid claims to cover both pdp_path
declarations. Verified both fail when the new example is removed, so they hold
the property rather than restating today's file list. 121 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-07 13:48:00 +02:00
|
|
|
def test_pdp_binding_is_not_confounded_with_validity():
|
|
|
|
|
"""Both shapes present is not enough if they only ever co-vary.
|
|
|
|
|
|
|
|
|
|
With only `claim.valid` (pdp_digest set, pdp_path true) and `claim.revoked`
|
|
|
|
|
(null, false), the two dimensions are perfectly correlated and an
|
|
|
|
|
implementer can reasonably infer that pdp_digest is null *because* the claim
|
|
|
|
|
is revoked, or that pdp_path tracks validity. Both inferences are wrong and
|
|
|
|
|
the example set is what teaches them.
|
|
|
|
|
|
|
|
|
|
The shape that must exist is a claim that is entirely valid -- `valid_now`
|
|
|
|
|
true, `reason_code` ok, not consumed -- and still carries no PDP binding.
|
|
|
|
|
That is the claim a privileged-lane PEP MUST refuse under
|
|
|
|
|
`GH-DEC-2026-008`, and it is the one a consumer would otherwise have to
|
|
|
|
|
invent a fixture for.
|
|
|
|
|
"""
|
|
|
|
|
claims = [json.loads(p.read_text()) for p in EXAMPLES]
|
|
|
|
|
valid_without_pdp = [
|
|
|
|
|
c
|
|
|
|
|
for c in claims
|
|
|
|
|
if c["valid_now"]
|
|
|
|
|
and c["reason_code"] == "ok"
|
|
|
|
|
and not c["consumed"]
|
|
|
|
|
and c["binding"]["pdp_digest"] is None
|
|
|
|
|
and c["binding"]["pdp_path"] is False
|
|
|
|
|
]
|
|
|
|
|
assert valid_without_pdp, (
|
|
|
|
|
"no example of a valid claim with no PDP binding; a consumer writing the "
|
|
|
|
|
"privileged-lane refusal has no published shape to test against"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_valid_claims_cover_both_pdp_path_declarations():
|
|
|
|
|
"""The decorrelation stated as coverage rather than as one instance."""
|
|
|
|
|
paths = {
|
|
|
|
|
c["binding"]["pdp_path"]
|
|
|
|
|
for c in (json.loads(p.read_text()) for p in EXAMPLES)
|
|
|
|
|
if c["valid_now"]
|
|
|
|
|
}
|
|
|
|
|
assert paths == {True, False}, (
|
|
|
|
|
f"valid examples declare only pdp_path={paths}; both are reachable states "
|
|
|
|
|
"for a valid approval"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.
Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.
Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.
Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 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
2026-09-06 14:51:23 +02:00
|
|
|
@pytest.mark.parametrize("path", EXAMPLES, ids=lambda p: p.name)
|
|
|
|
|
def test_pdp_path_examples_always_carry_a_digest(path):
|
|
|
|
|
"""GH-DEC-2026-008: pdp_path true guarantees pdp_digest non-null."""
|
|
|
|
|
binding = json.loads(path.read_text())["binding"]
|
|
|
|
|
if binding["pdp_path"]:
|
|
|
|
|
assert binding["pdp_digest"] is not None
|