Skip to content

Commit 7860a39

Browse files
committed
feat: add admissibility proof spine evaluator
1 parent 12dc0e2 commit 7860a39

1 file changed

Lines changed: 238 additions & 0 deletions

File tree

admissibility_proof_spine.py

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"""Admissibility Proof Spine v0.1.
2+
3+
A small executable demonstration of one bounded claim:
4+
5+
Clean evidence does not make an inadmissible transition admissible.
6+
7+
This module does not execute external side effects. It produces deterministic
8+
verdict receipts for inspection and testing.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import hashlib
14+
import json
15+
from dataclasses import asdict, dataclass
16+
from datetime import datetime, timezone
17+
from enum import Enum
18+
from typing import Any
19+
20+
from stop_machine import State, StopMachine
21+
22+
23+
class Verdict(str, Enum):
24+
"""Possible admissibility outcomes for the demonstrated path."""
25+
26+
ALLOW = "ALLOW"
27+
HOLD = "HOLD"
28+
DENY = "DENY"
29+
30+
31+
@dataclass(frozen=True)
32+
class Authority:
33+
"""Declared authority attached to a proposed action."""
34+
35+
authority_id: str
36+
scope: str
37+
expires_at: str
38+
39+
40+
@dataclass(frozen=True)
41+
class Evidence:
42+
"""Evidence completeness flags.
43+
44+
Completeness is deliberately separate from admissibility.
45+
A complete evidence object may still describe an invalid action.
46+
"""
47+
48+
timestamp_present: bool
49+
actor_present: bool
50+
scope_present: bool
51+
replay_data_present: bool
52+
receipt_fields_complete: bool
53+
54+
def is_complete(self) -> bool:
55+
"""Return True only when every required evidence field is present."""
56+
57+
return all(
58+
(
59+
self.timestamp_present,
60+
self.actor_present,
61+
self.scope_present,
62+
self.replay_data_present,
63+
self.receipt_fields_complete,
64+
)
65+
)
66+
67+
68+
@dataclass(frozen=True)
69+
class PriorChainState:
70+
"""Prior receipt-chain state read by the next admissibility decision."""
71+
72+
chain_head_verified: bool
73+
prior_verdict: str
74+
rebind_resolved: bool
75+
76+
77+
@dataclass(frozen=True)
78+
class ProposedTransition:
79+
"""A proposed consequence-bearing transition."""
80+
81+
actor: str
82+
action: str
83+
required_scope: str
84+
attempted_at: str
85+
authority: Authority
86+
evidence: Evidence
87+
prior_chain_state: PriorChainState
88+
reference_surface_eligible: bool = True
89+
current_state_supports_transition: bool = True
90+
91+
92+
@dataclass(frozen=True)
93+
class DecisionReceipt:
94+
"""Deterministic local receipt for the demonstrated decision."""
95+
96+
scenario_id: str
97+
actor: str
98+
action: str
99+
verdict: str
100+
reason: str
101+
stop_state: str
102+
consequence_bound: bool
103+
evidence_complete: bool
104+
authority_id: str
105+
receipt_hash: str
106+
claim_boundary: dict[str, bool]
107+
108+
109+
def _parse_utc(value: str) -> datetime:
110+
"""Parse an ISO-8601 UTC timestamp ending in Z."""
111+
112+
if not value.endswith("Z"):
113+
raise ValueError("Timestamp must be UTC and end with 'Z'.")
114+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
115+
return parsed.astimezone(timezone.utc)
116+
117+
118+
def _canonical_hash(payload: dict[str, Any]) -> str:
119+
"""Return a deterministic SHA-256 hash for a JSON-compatible payload."""
120+
121+
encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode(
122+
"utf-8"
123+
)
124+
return hashlib.sha256(encoded).hexdigest()
125+
126+
127+
def _terminal_stop() -> StopMachine:
128+
"""Return a StopMachine driven to RED using explicit transitions."""
129+
130+
machine = StopMachine()
131+
machine.advance() # GREEN -> AMBER
132+
machine.advance() # AMBER -> RED
133+
return machine
134+
135+
136+
def evaluate_transition(
137+
transition: ProposedTransition,
138+
*,
139+
scenario_id: str = "clean_evidence_invalid_action_v0_1",
140+
) -> DecisionReceipt:
141+
"""Evaluate a proposed transition and return a deterministic receipt.
142+
143+
The evaluation is FIRST_FAIL. Evidence completeness is checked, but it does
144+
not override authority, state, reference-surface, or chain failures.
145+
"""
146+
147+
verdict = Verdict.ALLOW
148+
reason = "admissible"
149+
machine = StopMachine()
150+
151+
attempted_at = _parse_utc(transition.attempted_at)
152+
expires_at = _parse_utc(transition.authority.expires_at)
153+
154+
if not transition.evidence.is_complete():
155+
verdict = Verdict.HOLD
156+
reason = "evidence.incomplete"
157+
elif not transition.reference_surface_eligible:
158+
verdict = Verdict.HOLD
159+
reason = "reference_surface.not_eligible"
160+
elif expires_at <= attempted_at:
161+
verdict = Verdict.HOLD
162+
reason = "authority.expired"
163+
elif transition.authority.scope != transition.required_scope:
164+
verdict = Verdict.DENY
165+
reason = "authority.scope_mismatch"
166+
elif not transition.current_state_supports_transition:
167+
verdict = Verdict.HOLD
168+
reason = "state.changed_before_execution"
169+
elif not transition.prior_chain_state.chain_head_verified:
170+
verdict = Verdict.HOLD
171+
reason = "chain.head_unverified"
172+
elif (
173+
transition.prior_chain_state.prior_verdict == "REBIND_REQUIRED"
174+
and not transition.prior_chain_state.rebind_resolved
175+
):
176+
verdict = Verdict.HOLD
177+
reason = "chain.rebind_required_unresolved"
178+
179+
consequence_bound = verdict == Verdict.ALLOW
180+
181+
if not consequence_bound:
182+
machine = _terminal_stop()
183+
184+
receipt_payload = {
185+
"scenario_id": scenario_id,
186+
"actor": transition.actor,
187+
"action": transition.action,
188+
"verdict": verdict.value,
189+
"reason": reason,
190+
"stop_state": machine.state.value,
191+
"consequence_bound": consequence_bound,
192+
"evidence_complete": transition.evidence.is_complete(),
193+
"authority_id": transition.authority.authority_id,
194+
"claim_boundary": {
195+
"does_not_prove_production_readiness": True,
196+
"does_not_prove_compliance": True,
197+
"does_not_prove_universal_path_coverage": True,
198+
},
199+
}
200+
201+
return DecisionReceipt(
202+
**receipt_payload,
203+
receipt_hash=_canonical_hash(receipt_payload),
204+
)
205+
206+
207+
def receipt_to_dict(receipt: DecisionReceipt) -> dict[str, Any]:
208+
"""Return a JSON-compatible receipt dictionary."""
209+
210+
return asdict(receipt)
211+
212+
213+
def clean_evidence_invalid_action_fixture() -> ProposedTransition:
214+
"""Return the canonical v0.1 adversarial fixture."""
215+
216+
return ProposedTransition(
217+
actor="agent.synthetic",
218+
action="send_external_email",
219+
required_scope="external_send",
220+
attempted_at="2026-05-17T21:00:00Z",
221+
authority=Authority(
222+
authority_id="auth.expired.demo",
223+
scope="internal_draft_only",
224+
expires_at="2026-05-17T20:00:00Z",
225+
),
226+
evidence=Evidence(
227+
timestamp_present=True,
228+
actor_present=True,
229+
scope_present=True,
230+
replay_data_present=True,
231+
receipt_fields_complete=True,
232+
),
233+
prior_chain_state=PriorChainState(
234+
chain_head_verified=True,
235+
prior_verdict="REBIND_REQUIRED",
236+
rebind_resolved=False,
237+
),
238+
)

0 commit comments

Comments
 (0)