test: enforce the presentation exclusion contract for approval digests

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-10 18:39:30 +02:00
parent 62233c7c52
commit adb5cb0a9a
4 changed files with 109 additions and 1 deletions

View file

@ -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

View file

@ -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"
}
}

View file

@ -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"]

View file

@ -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.