106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""安全服务:审计裁定、对话安全决策与司命策略加载。"""
|
|
|
|
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)
|