227 lines
9.0 KiB
Python
227 lines
9.0 KiB
Python
"""Policy and verification logic (fail-closed)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Callable
|
|
|
|
from .errors import ForbiddenError, LockedError
|
|
from .models import ProbeEvidence, ResultEnvelope, TaskRecord
|
|
|
|
|
|
RISK_RANK = {"low": 1, "medium": 2, "high": 3}
|
|
READONLY_WORKER_ACTIONS = frozenset(
|
|
{"claim", "result", "fail", "progress", "heartbeat"}
|
|
)
|
|
ALL_ACTION_CAPABILITIES = frozenset(
|
|
{
|
|
"task.creator",
|
|
"worker.self_register",
|
|
"worker.admin",
|
|
"task.claim",
|
|
"task.cancel",
|
|
"task.delegate",
|
|
"task.read",
|
|
"task.audit",
|
|
}
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class PolicyDecision:
|
|
allowed: bool
|
|
reason: str = ""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReadonlyWorkerProfile:
|
|
"""A narrowly-scoped identity allowed to process safe Shadow tasks only."""
|
|
|
|
worker_id: str
|
|
identity: str
|
|
capabilities: frozenset[str]
|
|
task_types: frozenset[str] = frozenset({"code.review", "test.execute"})
|
|
|
|
def permits_task(self, task: TaskRecord) -> bool:
|
|
plan = task.task_plan
|
|
if plan.task_type not in self.task_types or plan.risk != "low":
|
|
return False
|
|
if plan.data_classification not in {"public", "internal"}:
|
|
return False
|
|
if plan.metadata.get("production_side_effects") is not False:
|
|
return False
|
|
return set(plan.required_capabilities).issubset(self.capabilities)
|
|
|
|
|
|
class IdentityVerifier:
|
|
def __init__(self, trusted_identities: set[str] | None = None):
|
|
self.trusted_identities = trusted_identities
|
|
|
|
def verify(self, identity: str) -> bool:
|
|
if self.trusted_identities is None:
|
|
return True
|
|
return identity in self.trusted_identities
|
|
|
|
|
|
class EvidenceVerifier:
|
|
def __init__(self, mandatory: list[str] | None = None):
|
|
self.mandatory = set(mandatory or [])
|
|
|
|
def verify(self, task: TaskRecord, evidences: list[dict]) -> PolicyDecision:
|
|
required = set(task.task_plan.evidence_requirements or [])
|
|
if not required and not self.mandatory:
|
|
return PolicyDecision(True)
|
|
observed = {e.get("kind") for e in evidences}
|
|
missing = required.union(self.mandatory) - observed
|
|
if missing:
|
|
return PolicyDecision(False, f"missing evidence: {', '.join(sorted(missing))}")
|
|
return PolicyDecision(True)
|
|
|
|
|
|
class PolicyEngine:
|
|
"""
|
|
Fail-closed policy engine with minimal but explicit decisions.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
identity_verifier: IdentityVerifier | None = None,
|
|
evidence_verifier: EvidenceVerifier | None = None,
|
|
execution_enabled: bool = False,
|
|
max_risk: str = "medium",
|
|
result_signature_verifier: Callable[[ResultEnvelope], bool] | None = None,
|
|
probe_attestation_verifier: Callable[[ProbeEvidence], bool] | None = None,
|
|
identity_capabilities: dict[str, set[str]] | None = None,
|
|
admin_identities: set[str] | None = None,
|
|
readonly_worker_execution_enabled: bool = False,
|
|
readonly_worker_profiles: list[ReadonlyWorkerProfile] | None = None,
|
|
) -> None:
|
|
if max_risk not in RISK_RANK:
|
|
raise ValueError("invalid max_risk")
|
|
self.identity_verifier = identity_verifier or IdentityVerifier()
|
|
self.evidence_verifier = evidence_verifier or EvidenceVerifier()
|
|
self.execution_enabled = execution_enabled
|
|
self.max_risk = max_risk
|
|
self.result_signature_verifier = result_signature_verifier
|
|
self.probe_attestation_verifier = probe_attestation_verifier
|
|
self.identity_capabilities = {
|
|
identity: set(capabilities)
|
|
for identity, capabilities in (identity_capabilities or {}).items()
|
|
}
|
|
self.admin_identities = set(admin_identities or ())
|
|
self.readonly_worker_execution_enabled = readonly_worker_execution_enabled
|
|
self.readonly_worker_profiles = {
|
|
profile.identity: profile for profile in (readonly_worker_profiles or [])
|
|
}
|
|
|
|
def _readonly_worker_allows(
|
|
self, action: str, actor: str, task: TaskRecord | None
|
|
) -> bool:
|
|
profile = self.readonly_worker_profiles.get(actor)
|
|
return bool(
|
|
self.readonly_worker_execution_enabled
|
|
and action in READONLY_WORKER_ACTIONS
|
|
and profile is not None
|
|
and (task is None or profile.permits_task(task))
|
|
)
|
|
|
|
def assert_readonly_worker_binding(self, actor: str, worker_id: str) -> None:
|
|
profile = self.readonly_worker_profiles.get(actor)
|
|
if profile is not None and profile.worker_id != worker_id:
|
|
raise ForbiddenError("worker identity binding mismatch")
|
|
|
|
def assert_readonly_worker_registration(self, actor: str, payload: dict) -> None:
|
|
profile = self.readonly_worker_profiles.get(actor)
|
|
if profile is None:
|
|
return
|
|
if (
|
|
str(payload.get("worker_id") or "") != profile.worker_id
|
|
or str(payload.get("identity") or "") != profile.identity
|
|
or set(payload.get("capabilities") or []) != set(profile.capabilities)
|
|
or str(payload.get("trust_zone") or "") != "isolated-review"
|
|
or int(payload.get("max_concurrency") or 0) != 1
|
|
or int(payload.get("heartbeat_interval_seconds") or 0) != 15
|
|
):
|
|
raise ForbiddenError("readonly worker registration mismatch")
|
|
|
|
def verify_result(
|
|
self, result: ResultEnvelope, task: TaskRecord | None = None
|
|
) -> PolicyDecision:
|
|
computed = result.computed_digest()
|
|
if result.result_digest and result.result_digest != computed:
|
|
return PolicyDecision(False, "result digest mismatch")
|
|
sensitive = bool(
|
|
task
|
|
and (
|
|
task.task_plan.data_classification
|
|
in {"confidential", "credential-secret"}
|
|
or any(
|
|
capability.startswith(
|
|
("code.write", "deploy", "secret", "production")
|
|
)
|
|
for capability in task.task_plan.required_capabilities
|
|
)
|
|
)
|
|
)
|
|
if sensitive and not (result.signature or result.attestation_ref):
|
|
return PolicyDecision(False, "trusted result proof required")
|
|
if result.signature is not None or result.attestation_ref is not None:
|
|
if self.result_signature_verifier is None:
|
|
return PolicyDecision(False, "result signature verifier unavailable")
|
|
if not self.result_signature_verifier(result):
|
|
return PolicyDecision(False, "result signature invalid")
|
|
result.trust_status = "trusted_result"
|
|
else:
|
|
result.trust_status = "untrusted_shadow_result"
|
|
return PolicyDecision(True)
|
|
|
|
def authorize(self, action: str, actor: str, task: TaskRecord | None = None, context: dict | None = None) -> PolicyDecision:
|
|
context = context or {}
|
|
if not self.identity_verifier.verify(actor):
|
|
return PolicyDecision(False, "identity untrusted")
|
|
|
|
action_capabilities = {
|
|
"create": "task.creator",
|
|
"read": "task.read",
|
|
"audit": "task.audit",
|
|
"claim": "task.claim",
|
|
"result": "task.claim",
|
|
"fail": "task.claim",
|
|
"progress": "task.claim",
|
|
"heartbeat": "task.claim",
|
|
"cancel": "task.cancel",
|
|
"delegate": "task.delegate",
|
|
"worker.register": "worker.self_register",
|
|
"worker.admin": "worker.admin",
|
|
}
|
|
required_capability = action_capabilities.get(action)
|
|
if required_capability:
|
|
granted = set(
|
|
self.identity_capabilities.get(
|
|
actor, self.identity_capabilities.get("*", set())
|
|
)
|
|
)
|
|
if required_capability not in granted:
|
|
return PolicyDecision(False, f"action {action} not permitted")
|
|
|
|
if action in {"claim", "result", "fail", "cancel", "progress", "delegate", "heartbeat"} and not self.execution_enabled:
|
|
if not self._readonly_worker_allows(action, actor, task):
|
|
return PolicyDecision(False, "execution disabled")
|
|
|
|
if action == "delegate" and task and task.status not in {"COMPLETED", "FAILED_FINAL", "CANCELLED"}:
|
|
risk = str(context.get("risk", task.task_plan.risk))
|
|
if RISK_RANK.get(risk, 2) > RISK_RANK.get(self.max_risk, 1):
|
|
return PolicyDecision(False, f"risk {risk} exceeds policy")
|
|
|
|
return PolicyDecision(True)
|
|
|
|
def assert_allowed(self, action: str, actor: str, task: TaskRecord | None = None, context: dict | None = None):
|
|
decision = self.authorize(action, actor, task, context=context)
|
|
if not decision.allowed:
|
|
if decision.reason == "identity untrusted":
|
|
raise ForbiddenError(decision.reason)
|
|
if decision.reason == "execution disabled":
|
|
raise LockedError("execution disabled")
|
|
raise ForbiddenError(decision.reason)
|