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:
parent
62233c7c52
commit
adb5cb0a9a
4 changed files with 109 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
21
docs/evidence/2026-09-10-presentation-exclusion.json
Normal file
21
docs/evidence/2026-09-10-presentation-exclusion.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue