src/pqc_ai_governance/authorization.py
| 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 | |