跳到主要内容

三重门把关 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() 不会自动经过这里的检查.