Agent 工具权限模型:最小权限原则要先于功能实现
Agent 工具权限模型最小权限原则要先于功能实现一、Agent 被赋予执行任意 Shell 命令的权限然后删掉了半个项目一个代码助手 Agent 被设计为可以执行 Shell 命令来完成部署操作。权限配置是一个简单的布尔开关allow_shellTrue。用户说清理所有临时文件Agent 执行了rm -rf /tmp/*——但它也在用/tmp目录保存用户正在编辑的未提交代码。问题不在于 Agent 的能力而在于权限模型是全局二值化的。Agent 要么拥有执行一切命令的权限要么什么都没有。真实世界的安全模型从不是这样工作的——操作系统有用户组/ACL、数据库有 GRANT/REVOKE、网络有 ACL/firewall rule。二、Agent 权限模型的分层架构flowchart TB Request[用户请求] -- Auth{身份认证br/Who?} Auth -- RBAC{角色检查br/What Role?} RBAC -- Policy{策略评估br/Allowed?} Policy --|允许| ScopedExec[作用域执行br/Where/When/How?] Policy --|拒绝| Deny[拒绝执行br/ 审计日志] ScopedExec -- Audit[操作审计br/记录完整调用链] subgraph PolicyLayers[策略层次] direction TB P1[全局策略br/系统级规则] P2[工具策略br/每个工具的能力边界] P3[运行时策略br/当前会话的临时限制] end Policy -- PolicyLayers style Deny fill:#ffcdd2 style Audit fill:#e3f2fd权限模型的三个嵌套层全局策略系统级定义 Agent 可以访问哪些系统资源文件系统范围、网络、进程工具策略工具级每个工具的权限声明如文件读取工具只读 /workspace/ 目录运行时策略会话级用户可以在调用 Agent 时临时收紧权限如这次只允许创建文件不允许删除三、Agent 权限模型的实现import os import re import json import fnmatch from typing import List, Dict, Optional, Set, Callable from dataclasses import dataclass, field from enum import Enum from pathlib import Path class Action(Enum): 可执行的操作类型 设计理念白名单模式。只定义允许的操作类型 不在白名单中的操作默认拒绝。 为什么不用黑名单因为工具和能力的扩展是不可预测的 黑名单总会漏掉新的危险操作 FILE_READ file_read FILE_WRITE file_write FILE_DELETE file_delete FILE_LIST file_list SHELL_EXEC shell_exec NETWORK_HTTP network_http NETWORK_SOCKET network_socket PROCESS_SPAWN process_spawn dataclass class ScopedPath: 路径作用域——为每个操作绑定一个允许的路径范围 支持两种约束 - allowed_paths: 白名单路径列表支持 glob 语法 - blocked_paths: 黑名单路径列表优先级高于白名单 黑名单的存在是因为某些危险路径即使在整个允许目录内 也不应该被访问——如 /workspace/.git/config allowed_paths: List[str] field(default_factorylist) blocked_paths: List[str] field(default_factorylist) def is_path_allowed(self, target_path: str) - bool: 判断目标路径是否在允许范围内 real_path os.path.realpath(target_path) # 黑名单优先检查 for blocked in self.blocked_paths: if fnmatch.fnmatch(real_path, blocked): return False # 白名单检查 for allowed in self.allowed_paths: if fnmatch.fnmatch(real_path, allowed): return True return False # 不在任何白名单中拒绝 dataclass class ToolPermission: 单个工具的权限声明 设计原则每个工具在注册时必须声明其需要的权限集合。 Agent 运行时每条工具调用都会与此声明交叉验证。 这不是事后审计而是事前拦截 tool_name: str required_actions: List[Action] # 操作类型 scoped_paths: Dict[Action, ScopedPath] field(default_factorydict) max_runtime_seconds: int 60 # 单次调用最大执行时间 require_user_approval: bool False # 是否需要用户确认 class AgentPermissionManager: Agent 权限管理器 最核心的设计决策默认拒绝Deny by Default。 任何未被显式声明的操作都返回 PermissionError。 这个决策不是技术性的而是安全工程的铁律—— 宁可让 Agent 少做一件事也不让它做一件不该做的事 def __init__(self, global_scopes: Optional[ScopedPath] None): self._tool_permissions: Dict[str, ToolPermission] {} self._session_restrictions: Dict[str, List[Action]] {} # session_id - blocked_actions self.global_scopes global_scopes or ScopedPath() self._audit_log: List[Dict] [] # 审计日志 def register_tool(self, permission: ToolPermission): 注册工具权限 必须在 Agent 启动时完成运行中不允许动态注册。 这条规则的目的是防止工具通过调用其他工具来提权—— 这是 Agent 权限模型中一个隐蔽的攻击面 if permission.tool_name in self._tool_permissions: raise ValueError(f工具 {permission.tool_name} 已经注册不允许覆盖) self._tool_permissions[permission.tool_name] permission def add_session_restriction( self, session_id: str, blocked_actions: List[Action] ): 会话级限制——实时收紧权限 可以用来取消对本次调用中已注册但本次不需要的能力。 例如虽然 Agent 被赋予了 FILE_DELETE 权限 但用户在某个下游任务中明确说这次不要删文件 self._session_restrictions[session_id] blocked_actions def authorize( self, tool_name: str, action: Action, target: Optional[str] None, session_id: Optional[str] None, ) - bool: 权限验证的入口方法 验证顺序 1. 工具是否已注册 2. 该工具是否被授予了此操作类型 3. 目标路径是否在允许范围内 4. 会话级限制是否禁止了此操作 任何一步失败整体拒绝。在拒绝时记录审计日志 以便事后追溯——这对 Agent 安全性至关重要 因为用户可能几天后才发现某个操作不该被执行 # 1. 工具注册检查 tool_perm self._tool_permissions.get(tool_name) if not tool_perm: self._audit(DENIED, tool_name, action, target, 工具未注册) return False # 2. 操作类型检查 if action not in tool_perm.required_actions: self._audit(DENIED, tool_name, action, target, f工具 {tool_name} 未被授予 {action.value} 权限) return False # 3. 路径范围检查 if target and action in tool_perm.scoped_paths: scoped tool_perm.scoped_paths[action] if not scoped.is_path_allowed(target): # 同时也检查全局范围 if not self.global_scopes.is_path_allowed(target): self._audit(DENIED, tool_name, action, target, 路径超出允许范围) return False # 4. 会话级限制 if session_id and session_id in self._session_restrictions: if action in self._session_restrictions[session_id]: self._audit(DENIED, tool_name, action, target, 会话级限制) return False self._audit(ALLOWED, tool_name, action, target, ) return True def _audit( self, decision: str, tool_name: str, action: Action, target: Optional[str], reason: str ): 记录审计信息 import datetime self._audit_log.append({ timestamp: datetime.datetime.now().isoformat(), decision: decision, tool: tool_name, action: action.value, target: target, reason: reason, }) def get_audit_report(self, last_n: int 50) - List[Dict]: 获取最近的审计记录 return self._audit_log[-last_n:] # # 使用示例 # if __name__ __main__: manager AgentPermissionManager( global_scopesScopedPath( allowed_paths[/workspace/**], blocked_paths[/workspace/.git/**, /workspace/.env*], ) ) # 注册文件读取工具 manager.register_tool(ToolPermission( tool_namefile_reader, required_actions[Action.FILE_READ, Action.FILE_LIST], scoped_paths{ Action.FILE_READ: ScopedPath(allowed_paths[/workspace/**]), Action.FILE_LIST: ScopedPath(allowed_paths[/workspace/**]), }, )) # 注册 Shell 工具——仅允许在 /workspace/build 下执行 manager.register_tool(ToolPermission( tool_nameshell_executor, required_actions[Action.SHELL_EXEC], scoped_paths{ Action.SHELL_EXEC: ScopedPath( allowed_paths[/workspace/build/**], blocked_paths[/workspace/build/.cache/**], ), }, max_runtime_seconds30, require_user_approvalTrue, # Shell 操作必须用户确认 )) # 允许在 workspace 目录下读文件 assert manager.authorize(file_reader, Action.FILE_READ, /workspace/src/main.py) # 拒绝尝试读取 /etc 目录 assert not manager.authorize(file_reader, Action.FILE_READ, /etc/passwd) # 拒绝shell_executor 没有 FILE_READ 权限 assert not manager.authorize(shell_executor, Action.FILE_READ, /workspace/test.py) print(所有权限验证通过) print(f审计日志条数: {len(manager.get_audit_report())}) ## 四、Agent 权限模型的实现权衡与安全边界 **白名单模式 vs 黑名单模式**。 白名单默认拒绝只放行声明过的操作在安全性上远优于黑名单默认允许只阻止禁止列表中的操作。但白名单模式的代价是每次添加新工具需要同时完成功能实现和权限注册两步操作而两步操作由同一个开发者完成且没有审查机制时白名单的效果等同于形式化——作用退化为提醒开发者别忘了声明而非实质性的安全屏障。 **路径范围约束的绕行风险**。 os.path.realpath() 可以解析符号链接的真路径但无法防御先创建符号链接指向允许目录外的文件然后请求访问该符号链接的时序攻击。更严格的做法是在 Agent 操作目录下禁用符号链接创建权限或每次路径验证时同时检查符号链接链中的每一个中间节点。 **审计日志的完整读可用性**。 审计日志的价值不在于记录了什么而在于出事后能从日志中还原事件链。这意味着审计日志必须具备不可篡改性append-only、完整性包含操作前后的上下文、可检索性按时间/工具/操作类型/用户查询。见证奇迹的时刻不是日志记录了第 10000 条操作而是当安全事件发生后 72 小时你仍然能通过审计日志完整还原 Agent 的每一步操作及其当时的权限检查决策。 ## 五、总结 Agent 权限模型的核心原则 1. **默认拒绝Deny by Default**任何未在工具注册时声明的操作都应直接拒绝不为方便而牺牲安全性。 2. **路径约束需要多层防护**全局范围 工具级范围 黑名单三层交叉验证降低绕行概率。 3. **审计日志是事后防线**格式化的、不可篡改的操作记录是出现安全事故后唯一的事实依据。