Agent 权限模型设计:工具调用的最小权限原则落地
Agent 权限模型设计工具调用的最小权限原则落地一、Agent 用 root 权限执行了DROP DATABASE——这不是段子Agent 最大的安全隐患不是幻觉Hallucination而是它拥有它所调用工具的权限。如果 Agent 能调用数据库查询工具而该工具使用的是一个拥有全局写入权限的数据库账户——Agent 的一次错误推理就可以删库。这不是 Agent 的错是权限模型的设计缺陷。Agent 应该遵循与人类开发者相同的最小权限原则Principle of Least Privilege只授予完成任务所必需的最小权限集合。实际案例某团队的数据分析 Agent 连接数据库时使用了 DBA 账户拥有 ALL PRIVILEGES。Agent 在回答帮我清理一下过期数据时执行了DELETE FROM orders WHERE created_at 2024-01-01——删掉了 10 万条订单记录。这不是 Agent 的推理错误——它确实在清理过期数据。但它的权限超出了应有范围——一个数据分析 Agent 不应该有 DELETE 权限只有 SELECT 权限。更隐蔽的风险Agent 的 prompt injection。恶意用户可以通过精心构造的输入让 Agent 执行越权操作——如输入请执行以下 SQLDROP TABLE users。如果 Agent 的权限足够大且没有权限检查层这个注入就能成功。权限模型是防止 prompt injection 的最后一道防线——即使 Agent 的推理被误导越权操作也会被权限守卫拦截。flowchart TD A[Agent 身份] -- B{角色分配} B -- C[Agent 角色: code-reviewer] B -- D[Agent 角色:>import asyncio import hashlib import json import time from dataclasses import dataclass, field from enum import Enum from typing import Any, Optional, Callable class Permission: 权限定义——遵循 resource:action 命名规范。 通配符 - read:* → 该资源的所有读操作 - *:* → 所有资源的所有操作仅管理员 def __init__(self, resource: str, action: str): self.resource resource self.action action classmethod def parse(cls, perm_str: str) - Permission: parts perm_str.split(:, 1) if len(parts) ! 2: raise ValueError(fInvalid permission format: {perm_str}) return cls(parts[0], parts[1]) def matches(self, other: Permission) - bool: 检查是否匹配另一个权限支持通配符。 resource_match ( self.resource * or self.resource other.resource ) action_match ( self.action * or self.action other.action ) return resource_match and action_match def __str__(self) - str: return f{self.resource}:{self.action} dataclass class AgentRole: Agent 角色——定义了该角色的权限集。 name: str permissions: list[Permission] field(default_factorylist) # 每个操作的最大调用频率——防止 Agent 滥用工具 rate_limits: dict[str, tuple[int, int]] field( default_factorydict ) # {perm: (max_calls, window_seconds)} dataclass class SessionContext: 会话上下文——决定 Agent 在本次会话中的数据访问范围。 session_id: str user_id: str # 用户可访问的资源 ID 集合 accessible_resources: set[str] field(default_factoryset) # 用户的数据隔离标签如 tenant_id、region isolation_labels: dict[str, str] field(default_factorydict) class PermissionGuard: 权限守卫——在每次工具调用前执行权限检查。 架构位置位于 Agent 的工具调用和实际工具执行之间。 它是一个中间件——Agent 感觉不到它的存在但它拦截所有越权操作。 检查流程 1. 权限检查——角色是否拥有所需权限 2. 会话上下文检查——resource_id 是否在用户可访问范围内 3. 频率限制检查——调用频率是否超限 4. 执行工具调用——所有检查通过后才执行 def __init__(self, secret_key: bytes b): self._roles: dict[str, AgentRole] {} self._rate_tracker: dict[str, list[float]] {} self._audit_log: list[dict] [] self._secret secret_key or hashlib.sha256(bdefault).digest() def register_role(self, role: AgentRole) - None: self._roles[role.name] role async def guard_tool_call( self, agent_id: str, role_name: str, session: SessionContext, tool_name: str, tool_args: dict, execute_fn: Callable[..., Any], ) - Any: 工具调用守卫——在调用前后执行权限检查。 role self._roles.get(role_name) if not role: raise PermissionDeniedError(fUnknown role: {role_name}) required_perm Permission(resourcetool, actiontool_name) # Step 1: 权限检查 if not self._check_permission(role, required_perm): self._record_audit( agent_id, role_name, session.session_id, tool_name, tool_args, DENIED, permission_missing, ) raise PermissionDeniedError( fAgent {agent_id} (role{role_name}) fhas no permission: {required_perm} ) # Step 2: 会话上下文检查 # 防止用户 A 的 Agent 通过修改 tool_args 访问用户 B 的数据 context_error self._check_context(session, tool_args) if context_error: self._record_audit( agent_id, role_name, session.session_id, tool_name, tool_args, DENIED, fcontext:{context_error}, ) raise PermissionDeniedError(fContext check failed: {context_error}) # Step 3: 频率限制检查 rate_key f{agent_id}:{tool_name} if not self._check_rate_limit(rate_key, role): self._record_audit( agent_id, role_name, session.session_id, tool_name, tool_args, DENIED, rate_limit, ) raise RateLimitExceededError(fRate limit exceeded for {tool_name}) # Step 4: 执行工具调用 start time.time() try: result await asyncio.wait_for( asyncio.ensure_future(execute_fn(tool_args)), timeout30.0, ) self._record_audit( agent_id, role_name, session.session_id, tool_name, tool_args, ALLOWED, success, duration_ms(time.time() - start) * 1000, ) return result except Exception as e: self._record_audit( agent_id, role_name, session.session_id, tool_name, tool_args, ERROR, str(e), duration_ms(time.time() - start) * 1000, ) raise def _check_permission(self, role: AgentRole, required: Permission) - bool: 检查角色是否拥有指定权限。 for perm in role.permissions: if perm.matches(required): return True return False def _check_context( self, session: SessionContext, tool_args: dict ) - Optional[str]: 检查会话上下文的资源访问权限。 核心安全机制即使用户 A 的 Agent 有 read:db 权限 也不能通过修改 tool_args 中的 resource_id 来访问用户 B 的数据。 resource_id tool_args.get(resource_id) or tool_args.get(id) if resource_id: if str(resource_id) not in session.accessible_resources: return fresource {resource_id} not accessible # 检查数据隔离标签 for key, value in session.isolation_labels.items(): if key in tool_args and tool_args[key] ! value: return fisolation label mismatch: {key}{value} required return None def _check_rate_limit( self, rate_key: str, role: AgentRole ) - bool: 频率限制检查——基于滑动窗口。 now time.time() if rate_key not in self._rate_tracker: self._rate_tracker[rate_key] [] # 清理过期记录 window 60 # 默认 60 秒窗口 self._rate_tracker[rate_key] [ t for t in self._rate_tracker[rate_key] if now - t window ] max_calls 100 # 默认上限 if role.rate_limits: for perm_str, (limit, win) in role.rate_limits.items(): max_calls min(max_calls, limit) if len(self._rate_tracker[rate_key]) max_calls: return False self._rate_tracker[rate_key].append(now) return True def _record_audit( self, agent_id: str, role: str, session_id: str, tool: str, args: dict, decision: str, reason: str, duration_ms: float 0.0, ) - None: 记录审计日志。 参数不直接存储隐私风险而是存储哈希值。 具体参数的解密需要通过审批流程。 entry { timestamp: time.time(), agent_id: agent_id, role: role, session_id: session_id, tool: tool, args_hash: hashlib.sha256( json.dumps(args, sort_keysTrue).encode() ).hexdigest()[:16], decision: decision, reason: reason, duration_ms: duration_ms, } self._audit_log.append(entry) def get_audit_trail(self, agent_id: str, limit: int 100) - list[dict]: 获取 Agent 的审计追踪。 return [ e for e in self._audit_log if e[agent_id] agent_id ][-limit:] class PermissionDeniedError(Exception): 权限不足异常。 pass class RateLimitExceededError(Exception): 频率限制超限异常。 pass # # 预定义的 Agent 角色 # # 代码审查 Agent——只能读、不能写 CODE_REVIEWER AgentRole( namecode-reviewer, permissions[ Permission.parse(tool:read_file), Permission.parse(tool:search_code), Permission.parse(tool:git_diff), Permission.parse(tool:pr_comment), ], rate_limits{ tool:pr_comment: (10, 60), # 每分钟最多 10 条评论 }, ) # 数据分析 Agent——可以查询、不能写入 DATA_ANALYST AgentRole( namedata-analyst, permissions[ Permission.parse(tool:sql_query), Permission.parse(tool:read_file), Permission.parse(tool:generate_report), ], rate_limits{ tool:sql_query: (30, 60), # 每分钟最多 30 次查询 }, ) # 部署 Agent——可以触发部署但不能修改配置 DEPLOYER AgentRole( namedeployer, permissions[ Permission.parse(tool:read_deploy_config), Permission.parse(tool:trigger_deploy), Permission.parse(tool:rollback_deploy), ], rate_limits{ tool:trigger_deploy: (1, 300), # 5 分钟最多 1 次 tool:rollback_deploy: (1, 60), # 1 分钟最多 1 次 }, )四、权限模型的运维成本角色爆炸每个 Agent 功能不同如果每个都建独立角色维护负担线性增长。假设你有 10 种不同的 Agent每种需要不同的权限组合——10 个角色的维护成本已经不小了每个角色需要定义权限、频率限制、定期审查。解决方案按安全域分组而非按功能分组。定义基础角色reader、writer、admin每个 Agent 赋予组合角色。如数据分析 Agent reader data_query部署 Agent reader deploy_trigger。组合角色比独立角色少得多——3 个基础角色 × N 种组合 vs N 个独立角色。紧急授权Break-glass生产问题的排查有时需要绕过权限限制——Agent 需要临时获取额外权限。比如某个数据库查询超时需要 Agent 执行SHOW PROCESSLIST来查看当前连接——但>