diff --git a/docs/approval-claim.md b/docs/approval-claim.md index 8cb0323..f22a266 100644 --- a/docs/approval-claim.md +++ b/docs/approval-claim.md @@ -272,5 +272,25 @@ Local fixtures, workplan ids, and prose are not this claim. ## Examples -See [`../examples/claim.valid.json`](../examples/claim.valid.json) and -[`../examples/claim.revoked.json`](../examples/claim.revoked.json). +| Example | Shape it publishes | +| --- | --- | +| [`claim.valid.json`](../examples/claim.valid.json) | valid, on the PDP path — `pdp_path: true`, `pdp_digest` non-null | +| [`claim.valid.no-pdp.json`](../examples/claim.valid.no-pdp.json) | **valid, with no PDP binding** — `valid_now: true`, `reason_code: ok`, `pdp_path: false`, `pdp_digest: null` | +| [`claim.revoked.json`](../examples/claim.revoked.json) | revoked — `valid_now: false`, `reason_code: revoked` | + +The middle one is the shape most likely to be missing from a consumer's tests, +and it is the one that matters most on a privileged lane: the approval is +genuinely valid and genuinely usable — for a consumer comparing the native +`binding.digest` — while being **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. + +It is published because the first two examples alone would confound two +independent dimensions. With only a valid claim carrying a digest and a revoked +claim carrying none, a reader can reasonably infer 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 +`security-layer-model` §11's both-shapes clause exists to catch. + +`tests/test_examples.py` asserts the decorrelation, not merely that both values +appear somewhere. diff --git a/examples/claim.valid.no-pdp.json b/examples/claim.valid.no-pdp.json new file mode 100644 index 0000000..bf21ee6 --- /dev/null +++ b/examples/claim.valid.no-pdp.json @@ -0,0 +1,33 @@ +{ + "schema_version": "0.1", + "kind": "approval-claim", + "yields_to": "net-kingdom taxonomy request-claim schema (statute §17; unassigned)", + "issuer": "approval-engine", + "approval_id": "7c4e2b91-08da-4f63-b5c7-2a9e6d1f04b3", + "state": "valid", + "valid_now": true, + "consumed": false, + "binding": { + "action": "release.publish", + "target": { + "id": "svc-approval-engine", + "stage": "prod" + }, + "actor": "agt-release-runner", + "principal": "bernd", + "purpose": "cut-0-1-0-release", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "pdp_digest": null, + "pdp_path": false + }, + "freshness": { + "observed_at": "2026-08-29T12:00:00+00:00", + "ttl_seconds": 30, + "not_after": "2026-08-29T12:00:30+00:00" + }, + "validity": { + "not_before": "2026-08-29T11:00:00+00:00", + "expires_at": "2026-08-29T15:00:00+00:00" + }, + "reason_code": "ok" +} diff --git a/tests/test_examples.py b/tests/test_examples.py index 59bf33d..61f5a8c 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -46,6 +46,50 @@ def test_examples_cover_both_pdp_path_declarations(): assert states == {True, False} +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" + ) + + @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."""