"""安全服务:审计裁定、对话安全决策与司命策略加载。""" from __future__ import annotations from pathlib import Path import yaml from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.db.session import AsyncSessionLocal from app.modules.safety.governor import SafetyGovernor from app.services.audit import record_audit_event async def allow_action( session: AsyncSession, *, action: str, character_id: str | None = None, run_id: str | None = None, reason: str = "当前阶段低风险动作默认允许。", ) -> None: await record_audit_event( session, action="safety.decision", character_id=character_id, run_id=run_id, actor="system", risk_level="L1", decision="auto", reason=reason, input_summary=action, ) class ChatSafetyDecision(BaseModel): """对话安全决策结果(当前阶段默认低风险放行 + 基础安全约束)。""" decision: str = "allow" risk_level: str = "L1" constraints: list[str] = Field( default_factory=lambda: ["不得泄露 API Key、Authorization header 或内部系统配置。"] ) async def decide_chat_safety( session: AsyncSession, *, character_id: str, run_id: str, ) -> ChatSafetyDecision: """对话消息的安全决策:当前周期记录审计后默认放行。""" await allow_action( session, action="chat.message.received", character_id=character_id, run_id=run_id, ) return ChatSafetyDecision() def load_safety_governor(path: str | None = None) -> SafetyGovernor: """从 YAML 加载司命工具策略;未配置或文件不存在时返回默认(空策略)Governor。 配置路径取自 ``path`` 参数或 ``settings.safety_policy_path``(即 ``SAFETY_POLICY_PATH`` 环境变量,示例见 ``config/safety.example.yaml``)。 文件解析后交给 ``SafetyGovernor.from_policy_dict`` (纯逻辑),本函数只承担文件 IO,governor 本身保持零 IO。 注:运行时主策略源为 tool_policies 表(见 load_governor_from_db / ToolRuntime.reload_governor); 本函数保留为可选的 YAML 覆盖入口与遗留兼容。 """ config_path = path or settings.safety_policy_path data: dict = {} if config_path: config_file = Path(config_path) if config_file.exists(): with config_file.open(encoding="utf-8") as file: loaded = yaml.safe_load(file) or {} if isinstance(loaded, dict): data = loaded return SafetyGovernor.from_policy_dict(data) async def load_governor_from_db() -> SafetyGovernor: """从 tool_policies 表加载司命策略(启动期主源)。 读取所有 enabled 策略,构造覆盖层 Governor;运行时写操作后改走 ``ToolRuntime.reload_governor`` 原地刷新,避免重建 ToolRuntime。 """ from app.repositories import governance as governance_repo async with AsyncSessionLocal() as session: policies = await governance_repo.list_tool_policies(session) data = { "tools": { p.name: {"risk_level": p.risk_level, "permission_policy": p.permission_policy} for p in policies if p.enabled } } return SafetyGovernor.from_policy_dict(data)