From d5d1e410359d6a3580e049b7daa9838b1bae332a Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 7 Sep 2026 13:48:00 +0200 Subject: [PATCH] Publish the valid-but-unbound claim; the missing shape was confounded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- docs/approval-claim.md | 24 +++++++++++++++-- examples/claim.valid.no-pdp.json | 33 ++++++++++++++++++++++++ tests/test_examples.py | 44 ++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 examples/claim.valid.no-pdp.json 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."""