三重门把关 Agent 工具权限
通过硬规则、风险等级和用户确认三重检查,让 Agent 在执行工具前判断是否允许操作。
为什么要检查工具权限
模型可以提出工具调用, 但真正执行操作的是本地程序. 读取文件, 写入文件和执行 Shell 命令的影响不同, 所以程序需要在执行前检查权限.
本文参考 permissions-2.py, 把检查分成三重: 硬规则 → 风险等级 → 用户确认. 只有通过检查, 才调用工具函数.
定义核心类型和配置
风险等级
用 ToolRisk 枚举表示三种处理方式:
from enum import Enum
class ToolRisk(Enum):
SAFE = "safe" # 直接放行
DANGEROUS = "dangerous" # 需要用户确认
DENY = "deny" # 禁止执行
再用字典给工具分配等级:
TOOL_PERMISSIONS = {
"read_file": ToolRisk.SAFE,
"get_weather": ToolRisk.SAFE,
"write_file": ToolRisk.DANGEROUS,
"delete_file": ToolRisk.DANGEROUS,
"run_shell": ToolRisk.DANGEROUS,
}
未配置风险等级的工具默认按 DANGEROUS 处理. 但工具仍需先注册到 TOOL_NAMES, 否则无法执行.
统一检查结果
用数据类统一表示每一步是否通过, 以及拒绝的原因:
from dataclasses import dataclass
@dataclass
class PermissionCheckResult:
allowed: bool
deny_reason: str | None = None
例如, PermissionCheckResult(allowed=True) 表示通过; 拒绝时设置 allowed=False 并填写 deny_reason.
三重检查
第一重: 硬规则
硬规则先检查禁止执行的操作. 如果命中, 直接拒绝, 不再进入用户确认. 当前代码检查 Shell 命令是否为空, 以及是否匹配禁止模式:
def hard_rules_check(tool_name, args):
if tool_name == "run_shell":
command = args.get("command", "")
if not command.strip():
return PermissionCheckResult(False, "缺少 shell 命令")
for pattern in DENY_SHELL_PATTERNS:
if re.search(pattern, command, re.IGNORECASE):
return PermissionCheckResult(False, f"命中禁止规则: {pattern}")
return PermissionCheckResult(True)
例如, 模型调用 run_shell 时传入 {"command": "rm test.txt"}, 会匹配删除命令的禁止规则. 参数名是 command, 因为工具注册时使用了这个名字.
⚠️ 当前代码尚未实现工作区路径限制. Shell 正则黑名单也可能被绕过, 不能作为完整的安全沙箱.
第二重: 风险等级
通过硬规则后, 根据工具名称查询等级. 没有配置时, get() 返回默认值 DANGEROUS:
risk = TOOL_PERMISSIONS.get(tool_name, ToolRisk.DANGEROUS)
if risk == ToolRisk.SAFE:
return PermissionCheckResult(allowed=True)
if risk == ToolRisk.DENY:
return PermissionCheckResult(allowed=False, deny_reason="工具被禁止执行")
if tool_name in ALWAYS_ALLOWED:
return PermissionCheckResult(allowed=True)
return user_cofirm(tool_name, args)
以上是 tool_risk_check() 的核心逻辑. 危险工具如果没有被记住, 就进入第三重确认.
第三重: 用户确认
user_cofirm() 展示工具名称和参数, 再读取用户输入:
if user_answer == "y":
return PermissionCheckResult(allowed=True)
if user_answer == "n":
return PermissionCheckResult(allowed=False, deny_reason="用户拒绝执行")
if user_answer == "always" and tool_name in REMEMBERABLE_TOOLS:
ALWAYS_ALLOWED.add(tool_name)
return PermissionCheckResult(allowed=True)
y: 只允许本次执行.n: 拒绝本次执行.always: 仅允许可记住的工具使用, 本次进程中后续不再询问.
当前只有 write_file 在 REMEMBERABLE_TOOLS 中. run_shell 不支持 always, 每次都要确认. Shell 也能删除文件, 所以单独限制 delete_file 并不能覆盖所有删除方式.
接入工具执行
总入口 check
check() 先检查硬规则. 如果通过, 再检查风险等级; 风险检查内部按需调用用户确认:
def check(tool_name, args):
result = hard_rules_check(tool_name, args)
if not result.allowed:
return result
return tool_risk_check(tool_name, args)
在 excute_tool 中调用
执行器先检查工具是否已注册, 再调用 check(). 只有检查通过, 才从 TOOL_NAMES 取出函数执行:
def excute_tool(tool_name, args):
if tool_name not in TOOL_NAMES:
return f"工具 {tool_name} 不存在"
result = check(tool_name, args)
if not result.allowed:
return f"权限检查未通过: {result.deny_reason}"
try:
return TOOL_NAMES[tool_name](**args)
except Exception as error:
return f"工具调用发生异常: {str(error)[:1000]}"
例如 excute_tool("read_file", {"path": "note.txt"}) 会先检查权限, 再调用 read_file(path="note.txt"). Agent 循环将执行结果或拒绝原因作为工具结果返回给模型.
📌 权限检查在
excute_tool()中. 直接调用read_file()或run_shell()不会自动经过这里的检查.