src/pqc_ai_governance/authorization.py
2.2 KB · 67 lines · python Raw
1 """AuthorizationChain - passed ConsensusResults that authorize an agent/model to act."""
2
3 from __future__ import annotations
4
5 from dataclasses import dataclass, field
6 from typing import Any
7
8 from pqc_ai_governance.errors import GovernanceError
9 from pqc_ai_governance.proposal import ProposalKind
10 from pqc_ai_governance.round import ConsensusRound, ConsensusResult
11
12
13 # Map each AUTHORIZE_* kind to its canonical REVOKE_* counterpart.
14 _REVOKE_MAP: dict[ProposalKind, ProposalKind] = {
15 ProposalKind.AUTHORIZE_MODEL: ProposalKind.REVOKE_MODEL,
16 ProposalKind.AUTHORIZE_AGENT: ProposalKind.REVOKE_AGENT,
17 }
18
19
20 @dataclass
21 class AuthorizationGrant:
22 """A passed proposal that confers authority on a subject (model DID or agent DID)."""
23
24 subject_id: str
25 kind: ProposalKind
26 result: ConsensusResult
27 scope: dict[str, Any] = field(default_factory=dict)
28
29 def is_passed(self) -> bool:
30 return self.result.decision == "passed"
31
32 def verify(self) -> bool:
33 """Verify the underlying ConsensusResult's ML-DSA signature."""
34 return ConsensusRound.verify_result(self.result)
35
36
37 @dataclass
38 class AuthorizationChain:
39 """Ordered set of ``AuthorizationGrant`` records referencing a single subject."""
40
41 subject_id: str
42 grants: list[AuthorizationGrant] = field(default_factory=list)
43
44 def add(self, grant: AuthorizationGrant) -> None:
45 if grant.subject_id != self.subject_id:
46 raise GovernanceError(
47 f"grant subject {grant.subject_id} != chain subject {self.subject_id}"
48 )
49 self.grants.append(grant)
50
51 def is_authorized(self, kind: ProposalKind) -> bool:
52 """Return True if there is at least one passed ``AUTHORIZE_*`` grant of
53 this kind and no subsequent matching ``REVOKE_*``."""
54 authorized = False
55 revoke_kind = _REVOKE_MAP.get(kind)
56 for grant in self.grants:
57 if not grant.is_passed():
58 continue
59 if grant.kind == kind:
60 authorized = True
61 elif revoke_kind is not None and grant.kind == revoke_kind:
62 authorized = False
63 return authorized
64
65 def __len__(self) -> int:
66 return len(self.grants)
67