diff --git a/docs/approval-claim.md b/docs/approval-claim.md index b9a1329..38787fa 100644 --- a/docs/approval-claim.md +++ b/docs/approval-claim.md @@ -79,6 +79,27 @@ change that JSON and therefore the digest. A decision rendered against approval A for request R cannot be replayed for request R' if the consumer compares digests. +### Presentation exclusion — GH-DEC-2026-015 §4 + +`binding.digest` MUST cover exactly the five act fields above and MUST NOT cover +presentation material: `view_hash`, brief, packet, highlights, locale, UI release +or a presentation wrapper. Those belong outside the act binding. Callers MUST +NOT smuggle presentation into `target` or another act field; target remains the +actual effect scope, including its nested scope fields. The engine does not +classify arbitrary target values as presentation or silently discard scope. + +This is a compatibility constraint, not design intent. Widening the digest to +presentation would change approval identity on a UI release and could introduce +a mutual hash dependency. `tests/test_claim_contract.py::test_presentation_changes_cannot_change_the_approved_act` +asserts through durable issue/claim that changing presentation leaves the digest +and stored act unchanged, while changing any of the five act fields changes the +digest. A mutation that hashes the whole supplied binding must fail that test. + +This supplies GH-DEC-2026-015's condition for `informed-decision` to carry the +engine's digest in `view_hash`; consumer adoption and its own verification remain +with that repository. GH-DEC-2026-016's declared human-control bind enforcement +is a separate requirement and is not implemented by this contract assertion. + ### What this digest is not — answering `INFD-IN-0001` R3 `informed-decision` asked whether its `view_hash` and this digest are the same diff --git a/docs/evidence/2026-09-10-presentation-exclusion.json b/docs/evidence/2026-09-10-presentation-exclusion.json new file mode 100644 index 0000000..b08b40c --- /dev/null +++ b/docs/evidence/2026-09-10-presentation-exclusion.json @@ -0,0 +1,21 @@ +{ + "ruling": "GH-DEC-2026-015", + "condition": "normative and tested exclusion of presentation from native binding.digest", + "status": "condition-implemented-source", + "test": "tests/test_claim_contract.py::test_presentation_changes_cannot_change_the_approved_act", + "focused_tests": 7, + "full_suite_output": "........................................................................ [ 57%]\n...................................................... [100%]\n=============================== warnings summary ===============================\ntests/test_auth.py::test_jwt_authenticator_rejects_wrong_signature_and_hs256\n /tmp/hfact-attempt-20260910/approval-engine/.venv/lib/python3.12/site-packages/jwt/api_jwt.py:147: InsecureKeyLengthWarning: The HMAC key is 14 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2.\n return self._jws.encode(\n\n-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html\n126 passed, 1 warning in 3.06s", + "mutation": { + "change": "canonical_binding returns the whole supplied input instead of only the act", + "result": "expected test failure", + "exit_code": 1, + "production_mutation_retained": false + }, + "native_algorithm_changed": false, + "live_approval_issued": false, + "residuals": { + "consumer_adoption": "INFD-IN-0004 / INFD-WP-0001", + "human_control_enforcement": "APPROVAL-WP-0002-T01 / GH-DEC-2026-016", + "deployment_and_native_consumption": "APPROVAL-WP-0002-T03/T05" + } +} diff --git a/tests/test_claim_contract.py b/tests/test_claim_contract.py index e993cf7..18607c6 100644 --- a/tests/test_claim_contract.py +++ b/tests/test_claim_contract.py @@ -56,3 +56,36 @@ def test_examples_are_claim_shaped(): assert data["issuer"] == "approval-engine" for forbidden in ("effect", "decision", "allow", "deny"): assert forbidden not in data + + +def test_presentation_changes_cannot_change_the_approved_act(engine): + """GH-DEC-2026-015: presentation cannot enter the native digest preimage.""" + act = binding() + original = approve(engine, extra_binding=act) + original_claim = engine.claim(original.id) + presentation = { + "view_hash": "sha256:" + "12" * 32, + "brief": "First explanation", + "packet": {"sections": ["summary"]}, + "highlights": ["target"], + "locale": "de-DE", + "ui_release": "surface-v1", + "presentation": {"binding_digest": original_claim["binding"]["digest"]}, + } + first = approve(engine, extra_binding={**act, **presentation}) + changed = {key: {"changed": value} for key, value in presentation.items()} + second = approve(engine, extra_binding={**act, **changed}) + for obj in (first, second): + assert engine.claim(obj.id)["binding"]["digest"] == original_claim["binding"]["digest"] + assert obj.binding == original.binding + + # The invariant is about presentation, not permission to ignore act changes. + for field, replacement in { + "action": "secrets.kv.read", + "actor": "different-agent", + "principal": "different-principal", + "purpose": "different-purpose", + "target": {"id": "different-lane", "stage": "prod"}, + }.items(): + other = approve(engine, extra_binding={**act, **presentation, field: replacement}) + assert engine.claim(other.id)["binding"]["digest"] != original_claim["binding"]["digest"] diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md index 61deffb..f8d88d9 100644 --- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: netkingdom created: "2026-09-01" -updated: "2026-09-06" +updated: "2026-09-10" reviewed_at: "2026-09-01" reviewed_against_commit: "ebce5abb276c01ab29ce2526f3b8abb332dc9e90" reviewed_note: >- @@ -237,6 +237,15 @@ here because this engine stores that object verbatim as the only downstream record that MFA happened. T01 stays `progress`; the human client cannot be registered until the tenant source is settled by its owner. +2026-09-10: GH-DEC-2026-016 supplies the previously missing NC-03 doctrine. +Approvals explicitly declared as discharging human-in-the-loop or dual-control +requirements must refuse non-human binds at issue. T01 owns the declaration, +engine/API enforcement, predecessor inheritance and negative legacy migration, +verified human identity provenance, tests and subsequent deployed proof. This +return changes neither existing rows nor production admission. The new rule is +not satisfied merely by the already-recorded principal_type field. The separate +R3 digest-exclusion condition is completed in T06 below. + ## Harden durable storage and migrations ```task @@ -600,3 +609,27 @@ not the evidence of publication; the successful push and remote index lookup are No StatefulSet rollout yet: live KeyCape registrations and audit sender/token admission remain outstanding. APPROVAL-WP-0002-T03 stays wait for those gates. + +## Make presentation exclusion a tested binding contract + +```task +id: APPROVAL-WP-0002-T06 +status: done +priority: high +``` + +GH-DEC-2026-015 §4 conditions informed-decision's nested binding.digest on an +explicit normative presentation exclusion and a regression that exposes widening. +The native digest MUST cover exactly the five act fields and MUST NOT acquire +presentation fields. Callers keep presentation outside effect scope; arbitrary +nested target scope is preserved, not silently filtered. + +Completed 2026-09-10: docs/approval-claim.md states the rule. The durable issue/claim +regression varies view_hash, brief, packet, highlights, locale, UI release and a +presentation wrapper while proving the stored act/digest remain unchanged. It +also varies all five act fields independently and requires the digest to change. +A deliberate whole-binding canonicalization mutation fails the regression; the +production algorithm is unchanged. Evidence: docs/evidence/2026-09-10-presentation-exclusion.json. +This supplies the engine condition for INFD-IN-0004; actual informed-decision +adoption remains its existing work. T01 retains GH-DEC-2026-016 and native identity, +T03/T05 deployment/real consumption remain open. No live approval was issued.