"""司命决策器:按工具策略 + 风险等级 + 是否已确认,输出 allow / ask / deny。 风险分级与权限策略取值见契约字典(risk_level L0–L4、permission_policy allow/ask/always_ask/forbidden);决策枚举对齐 AuditEvent.decision(allow/deny/ask)。 策略来源:``SafetyGovernor()`` 用代码内置默认;``from_policy_dict(data)`` 从已解析字典 加载(文件 IO 由 ``services/safety.load_safety_governor`` 承担,本模块保持纯领域、零 IO)。 """ from __future__ import annotations from dataclasses import dataclass from enum import StrEnum class PermissionPolicy(StrEnum): allow = "allow" ask = "ask" always_ask = "always_ask" forbidden = "forbidden" class SafetyDecision(StrEnum): allow = "allow" ask = "ask" deny = "deny" # 风险等级排序,便于阈值判断 RISK_RANK: dict[str, int] = {"L0": 0, "L1": 1, "L2": 2, "L3": 3, "L4": 4} def default_policy(risk_level: str) -> PermissionPolicy: """无显式工具策略时,按风险等级推导默认策略。""" rank = RISK_RANK.get(risk_level, 2) if rank >= 4: return PermissionPolicy.forbidden if rank >= 3: return PermissionPolicy.always_ask if rank >= 2: return PermissionPolicy.ask return PermissionPolicy.allow @dataclass(frozen=True) class ToolPolicy: """工具的固定策略(由工具定义或配置提供)。""" risk_level: str # L0–L4 permission_policy: PermissionPolicy @dataclass(frozen=True) class SafetyResult: decision: SafetyDecision reason: str risk_level: str policy: PermissionPolicy class SafetyGovernor: """司命决策器。无状态、同步——便于测试与上层组合确认流。""" def __init__(self, policies: dict[str, ToolPolicy] | None = None) -> None: self._policies: dict[str, ToolPolicy] = dict(policies or {}) @classmethod def from_policy_dict(cls, data: dict) -> SafetyGovernor: """从已解析的策略字典构造 Governor(纯逻辑,不读文件)。 文件读取由 ``services/safety.load_safety_governor`` 承担,调用方传入已 ``yaml.safe_load`` 的字典;空字典或缺 ``tools`` 返回默认(空策略)Governor。 """ policies: dict[str, ToolPolicy] = {} tools = data.get("tools") if isinstance(data, dict) else None if isinstance(tools, dict): for name, cfg in tools.items(): if not isinstance(cfg, dict): continue policies[name] = ToolPolicy( risk_level=str(cfg.get("risk_level", "L2")), permission_policy=PermissionPolicy(str(cfg.get("permission_policy", "ask"))), ) return cls(policies) def evaluate( self, *, tool: str, risk_level: str | None = None, confirmed: bool = False, ) -> SafetyResult: policy = self._policies.get(tool) if policy is None: resolved_risk = risk_level or "L2" perm = default_policy(resolved_risk) else: resolved_risk = policy.risk_level perm = policy.permission_policy # L4 一律禁止,即便策略写 allow if RISK_RANK.get(resolved_risk, 2) >= 4: return SafetyResult( SafetyDecision.deny, "L4 风险禁止执行", resolved_risk, PermissionPolicy.forbidden ) if perm is PermissionPolicy.forbidden: return SafetyResult(SafetyDecision.deny, "工具策略为 forbidden", resolved_risk, perm) if perm is PermissionPolicy.allow: return SafetyResult(SafetyDecision.allow, "策略放行", resolved_risk, perm) # ask / always_ask if confirmed: return SafetyResult(SafetyDecision.allow, "用户已确认", resolved_risk, perm) return SafetyResult( SafetyDecision.ask, f"策略 {perm.value} 需用户确认", resolved_risk, perm )