沙箱不等于权限模型:多智能体系统授权链路设计与实践

沙箱不等于权限模型:多智能体系统授权链路设计与实践 在构建多智能体系统multi-agent systems时一个高频误区是把 sandbox 当成权限模型。很多团队在开发 AI Agent、自动化工作流或多角色协作系统时会给每个 Agent 配置独立的 sandbox认为“代码跑在沙箱里越权问题就不存在了”。实际运行后却发现一个 Agent 仍然可能读到另一个 Agent 的文件甚至执行了本应被拒绝的工具调用。原因通常不是 sandbox 没有生效而是 sandbox 只解决了“代码能在什么范围内执行”的问题没有解决“哪个主体有权执行哪个动作”的问题。本文会围绕“A sandbox is not a permission model”这条主线先解释 sandbox 和 permission model 的区别再通过一个最小可运行案例展示“只有沙箱没有权限模型”时会发生的越权现象然后给出生产环境的分层设计方案、验证方法和排错链路。读完以后你可以把文章里的思路直接用在 Agent 工具调用、文件访问控制、外部脚本执行和策略审计等场景里。1. Sandbox 与 Permission Model先厘清两种边界1.1 Sandbox 回答“能否执行”Permission Model 回答“能否授权”Sandbox 是一种执行隔离机制。它把一段不太可信的代码放进受限运行环境限制它能访问的文件、命令、网络、系统调用和资源用量。比如 Docker、gVisor、WASM、seccomp、Windows Sandbox甚至一个受限的系统账号都可以被当作沙箱。沙箱的核心目标是即使代码本身有问题它也不能突破预设的资源边界。Permission model 是一种策略决策机制。它把一次请求抽象成“谁、要做什么、操作哪个资源、在什么条件下做”然后根据已经定义的策略判断该请求是否被允许。典型的实现包括 ACL、RBAC、ABAC以及 OPA、Cedar 这类外部策略引擎。权限模型的核心目标是在业务语义上控制访问而不是在系统调用层面限制能力。区别在于沙箱即使完美运行也只能阻止代码访问边界之外的资源它并不会判断“当前这个调用者是否有权限访问边界之内的资源”。如果两个 Agent 都能在同一个沙箱内读文件那么沙箱本身并不知道哪个 Agent 应该读哪个目录。这就是为什么 sandbox 不能替代 permission model。维度SandboxPermission Model核心问题代码能访问哪些资源主体是否有权执行动作判断对象进程行为、资源边界主体、动作、资源、上下文典型实现seccomp、namespace、Docker、WASMACL、RBAC、ABAC、策略引擎典型失败沙箱内代码越过了资源边界未授权主体执行了敏感操作在多智能体系统中的角色执行隔离层授权决策层如果把两者混为一谈就会出现一种常见情况沙箱配置得很严格目录只开放了/data/workspace于是开发者认为“所有 Agent 只能访问工作目录安全了”。但权限模型没有定义“哪个 Agent 可以访问工作目录下的哪个子目录”结果就是任意 Agent 都能读取工作目录里所有文件。1.2 用户看到的“创建 sandbox”提示不代表权限已配置不少桌面 AI 客户端在安装或启动时会提示“正在创建 sandbox”这是一个很容易被误解的信息。用户看到这个提示会以为产品已经自动完成了安全隔离和权限配置。实际上这个提示通常只表示程序把自己的子进程放进了受限执行环境和“当前用户是否有权读取某个文件、调用某个工具”是两回事。安装提示和产品实际权限能力之间没有必然关系。如果一个多智能体系统只在启动时创建了沙箱却没有在工具调用入口做权限判断那么用户看到的“sandbox 已创建”就只是一个执行环境准备动作不是策略配置动作。真正需要关注的是Agent 的身份从哪来工具调用由谁拦截权限判断在哪个环节发生决策结果在哪里留下记录。注意sandbox 可以作为权限模型的执行层但不能替代权限模型的决策层。先想清楚授权请求的构成再决定沙箱放在哪一层。2. 多智能体授权链路权限判断应该在执行沙箱之前2.1 一次授权请求包含四个要素在多智能体系统中一次工具调用最终会落到某个资源上。为了让权限判断可复现、可审计需要把请求拆成四个要素。要素示例常见错误主体 principalagent_idalice只使用角色名不区分具体 Agent动作 actionread_file、write_file、run_command动作名称不统一大小写混用资源 resource/data/workspace/alice/notes.md未做路径规范化存在符号链接绕过上下文 context项目环境、数据分级、会话、委托链忽略委托关系导致权限放大上下文在多智能体场景里尤其关键。Agent A 可能调用 Agent B 的工具这时候如果系统只记录最终执行者 B而忽略了原始发起者 A权限模型就无法判断这次调用是否越权。正确做法是在调用链中保留原始主体并且把委托链写入日志。2.2 调用链上的判断顺序不能颠倒一个规范的多智能体调用链应该是Agent 请求 - 身份认证 - 权限决策PDP - 策略执行PEP - 沙箱执行 - 审计日志权限决策必须发生在沙箱执行之前。原因很简单如果先进入沙箱再在沙箱内部做权限判断那么判断逻辑和策略数据就暴露在不可信代码可影响的范围里。沙箱内的代码可能通过注入、竞态、文件内容、环境变量等方式影响判断结果。另外沙箱内进程能看到的身份信息不一定可信如果只根据whoami或进程参数来判断权限调用者很容易伪造身份。正确做法是在可信的宿主侧完成身份解析和权限决策把允许执行的命令、允许访问的资源作为参数传给沙箱执行器沙箱内部只负责“按给定资源执行”不再做业务授权判断。2.3 多智能体场景比单进程更复杂单进程应用里权限判断通常只需要区分用户角色。多智能体系统则要处理多个独立主体、动态工具组合、跨 Agent 委托、共享资源和并行执行。一个 Agent 可能被赋予工具 A工具 A 内部又会调用工具 B如果每个工具都单独判断权限而不考虑原始主体就会出现“横向越权”。所以多智能体系统的权限模型不能只写成“角色可以调用哪些工具”还要定义“角色可以在哪些资源上执行哪些动作”。资源范围必须和沙箱的执行边界联动但两者又必须分开配置权限模型负责业务授权沙箱负责执行兜底。3. 最小可运行案例只有沙箱时越权访问如何发生3.1 定义策略文件下面用一个简化案例演示沙箱和权限模型的差异。系统里有 Alice 和 Bob 两个 AgentAlice 可以读写自己的工作目录Bob 只能读取报告目录。策略文件先定义 Agent 能力和资源范围。{ agents: { alice: { allowed_actions: [read_file, write_file], allowed_paths: [/data/workspace/alice] }, bob: { allowed_actions: [read_file], allowed_paths: [/data/reports] } } }这个 JSON 代表的是权限模型的一部分。它已经明确表达了 Alice 和 Bob 的资源边界。如果没有这个文件只有一个允许访问/data/workspace/alice和/data/reports的沙箱那么 Bob 读取 Alice 的目录在沙箱层面并不会被阻止。3.2 写一个只有沙箱的执行器先看一个完全没有权限模型、只有沙箱文件路径校验的 Python 示例。它只检查命令是否在允许列表内、路径是否在允许根目录内。import shlex import subprocess from pathlib import Path ACTION_TO_COMMAND { read_file: cat } class SandboxExecutor: def __init__(self, allowed_roots, allowed_commands(cat, ls, grep)): self.allowed_roots [Path(p).resolve() for p in allowed_roots] self.allowed_commands allowed_commands def resolve_path(self, target): target_path Path(target).expanduser().resolve() for root in self.allowed_roots: if target_path root or root in target_path.parents: return target_path raise PermissionError(fsandbox path denied: {target}) def run(self, action, resource): if action not in ACTION_TO_COMMAND: raise PermissionError(faction not supported: {action}) command ACTION_TO_COMMAND[action] if command not in self.allowed_commands: raise PermissionError(fsandbox command denied: {command}) resolved self.resolve_path(resource) parts [command, str(resolved)] result subprocess.run( parts, capture_outputTrue, textTrue, timeout5 ) return { returncode: result.returncode, stdout: result.stdout, stderr: result.stderr }这里的关键点是resolve_path只检查“路径是否在沙箱允许的根目录内”不检查“当前请求来自哪个 Agent”。如果沙箱允许的根目录是/data/workspace/alice和/data/reports那么 Bob 请求读取 Alice 目录下的文件时沙箱会直接放行因为路径本身在允许范围内。sandbox SandboxExecutor( allowed_roots[/data/workspace/alice, /data/reports] ) result sandbox.run(read_file, /data/workspace/alice/notes.txt) print(result)在没有权限模型的情况下这段代码会成功输出/data/workspace/alice/notes.txt的内容。问题不在沙箱配置而在沙箱根本不具备“Bob 不能读 Alice 目录”的业务判断能力。3.3 在沙箱前引入访问控制现在把权限模型加到工具调用入口。工具执行前先做一次策略判断只有判断通过后才进入沙箱。class AccessControl: def __init__(self, policy): self.policy policy def check(self, agent_id, action, resource): agent self.policy[agents].get(agent_id) if not agent: return False, funknown agent: {agent_id} if action not in agent[allowed_actions]: return False, f{agent_id} cannot execute {action} if not any(resource.startswith(prefix) for prefix in agent[allowed_paths]): return False, f{agent_id} cannot access {resource} return True, def invoke_tool(agent_id, action, resource, acl, executor): allow, reason acl.check(agent_id, action, resource) if not allow: return {denied: True, reason: reason} return executor.run(action, resource) acl AccessControl(POLICY) response invoke_tool( bob, read_file, /data/workspace/alice/notes.txt, acl, sandbox ) print(response)这次输出会变成{denied: True, reason: bob cannot access /data/workspace/alice/notes.txt}同样的沙箱配置加上权限模型后越权请求在沙箱执行之前就被拦截了。这个案例说明沙箱负责限制“代码能做什么”权限模型负责限制“当前主体能做什么”。两者叠加才构成完整的安全链路。注意上面的路径匹配使用了startswith只用于演示思路。生产环境必须做路径规范化并处理符号链接、相对路径、大小写差异等问题。4. 生产环境的分层设计身份、策略、执行、审计各司其职4.1 每一层都有明确职责生产环境不能只写一个AccessControl类也不能只靠沙箱配置。更合理的做法是把安全能力拆成四个层次每个层次只解决一个问题。层次职责常见实现失败后果身份层识别 Agent 身份和调用链JWT、内部身份令牌、委托链身份伪造、越权审计失效策略层根据策略输出 allow/denyACL、RBAC、ABAC、OPA授权规则混乱默认放行执行层限制代码运行边界seccomp、namespace、WASM、容器代码一旦被攻破可访问宿主资源审计层记录决策和执行结果结构化日志、指标、链路追踪问题无法定位合规缺失在实际项目中策略层和沙箱层必须分开配置。策略层输出的是业务决策允许哪个 Agent 读哪个目录沙箱层输出的是执行边界允许哪些命令、哪些路径、是否允许网络、内存上限是多少。策略允许的路径范围可以比沙箱允许的范围更小但不能反过来让沙箱去理解业务授权。4.2 一份可对照的 YAML 配置下面是一个贴合案例的分层配置示例。它分成sandbox和permissions两个独立段落方便团队分别维护。sandbox: runtime: subprocess allowed_commands: - cat - ls - grep allowed_roots: - /data/workspace/alice - /data/reports allow_network: false max_output_bytes: 1048576 timeout_seconds: 10 permissions: default_deny: true policy_model: list_acl agents: alice: allowed_actions: - read_file - write_file allowed_roots: - /data/workspace/alice bob: allowed_actions: - read_file allowed_roots: - /data/reports这里的sandbox.allowed_roots是执行边界permissions.agents.alice.allowed_roots是授权边界。两者可以独立配置但生产环境仍然建议遵循最小权限原则沙箱允许的目录尽量收窄不要为了省事把整个磁盘都挂进去。如果一个 Agent 的业务权限是只能访问/data/workspace/alice但沙箱允许了/data/workspace那么一旦策略层配置错误或策略判断被跳过沙箱仍然存在放行其他子目录的风险。反之如果沙箱允许范围过窄即使授权允许读取也会被沙箱挡住造成误报。4.3 权限决策不放进沙箱内一个常见的错误设计是在沙箱内部加载策略文件然后让沙箱内代码自己判断谁可以访问什么。这样做会导致两个问题。第一策略文件如果放在沙箱允许的目录里沙箱内不可信代码就有可能读到它。第二如果沙箱内代码可以根据请求参数自行决定是否允许攻击者可以把一个未授权请求伪装成授权请求。正确的做法是宿主侧的工具调度器负责验证身份和策略然后只把“允许执行的动作和资源”传给沙箱执行器沙箱内部不再读取策略也不再做业务授权判断。在这个分层结构里沙箱可以继续承担系统调用限制、资源限额和进程隔离的职责但它不参与“允许或拒绝”的业务决策。权限决策是宿主侧可信组件的职责。5. 验证与排错用“策略拒绝”和“沙箱拒绝”两类日志定位问题5.1 构造三类验证用例一个完整的权限体系至少要通过三类用例验证合法授权请求应该放行业务越权请求应该在策略层被拒绝资源越界请求应该在沙箱层被拒绝。cases [ { agent: alice, action: read_file, resource: /data/workspace/alice/notes.txt, expected: allow, }, { agent: bob, action: read_file, resource: /data/workspace/alice/notes.txt, expected: acl_deny, }, { agent: alice, action: read_file, resource: /data/reports/budget.csv, expected: acl_deny, }, ] for case in cases: resp invoke_tool( case[agent], case[action], case[resource], acl, sandbox ) print(case[agent], case[resource], , resp)预期结果是Alice 读自己的文件ACL 通过沙箱也放行工具执行成功。Bob 读 Alice 的文件ACL 拒绝沙箱没有机会执行。Alice 读报告目录ACL 拒绝因为 Alice 的授权范围不包含报告目录。这里还要做一个对照实验用同一个沙箱直接执行 Bob 的越权请求不经过 ACL。如果沙箱仍然放行就说明“沙箱本身没有业务授权能力”。这个对照实验最好写进自动化测试防止后续有人误改配置后把沙箱当成权限模型。5.2 排错链路先判断是哪一层拒绝遇到 Agent 访问异常时不要先改沙箱配置。先按下面的链路排查。确认请求有没有进入权限决策层。查看审计日志里是否有policy_decision记录。如果日志显示acl_deny优先检查 Agent 身份、动作名、资源前缀配置。如果日志显示sandbox_deny再检查沙箱的allowed_commands、allowed_roots、网络策略和资源限制。如果策略层和沙箱层都没有日志说明调用链可能绕过了统一执行入口需要检查代码里是否有直接访问资源的路径。检查路径规范化。符号链接、..、~、大小写都可能让路径匹配失效。检查沙箱进程的宿主环境。如果沙箱没有真正限制挂载、网络或句柄继承那么它只是一个形式上的沙箱。5.3 用结构化日志区分两类拒绝生产环境建议把策略层和沙箱层的日志区分开至少包含layer、decision、request_id、agent_id、action、resource字段。这样定位问题时可以直接根据layer缩小范围。策略层拒绝示例{ request_id: req-1024, layer: policy, decision: deny, reason: bob not in allowed_roots, agent_id: bob, action: read_file, resource: /data/workspace/alice/notes.txt }沙箱层拒绝示例{ request_id: req-1025, layer: sandbox, decision: deny, reason: path outside allowed_roots, agent_id: alice, action: read_file, resource: /etc/passwd }这两类日志必须配套使用。只记录沙箱拒绝日志看不到业务授权判断无法回答“Bob 为什么不能读 Alice 的文件”只记录策略拒绝日志看不到沙箱实际阻断了什么无法判断执行边界是否真正生效。6. 常见误区、最佳实践与扩展方向6.1 四个容易踩中的误区误区错误表现为什么是错的正确做法沙箱能防止越权只配置沙箱不开权限模型沙箱不感知主体身份在工具调用前增加权限判断角色名等于身份用roleadmin代替agent_id多个 Agent 可能共享同一角色每个 Agent 必须有唯一可信标识路径前缀匹配足够用startswith(/data/workspace)无法处理符号链接和路径绕过使用路径解析后的规范路径做匹配沙箱内部自己判断权限沙箱内加载策略文件不可信代码可能影响判断结果权限决策放在宿主侧沙箱只执行6.2 可直接落地的检查清单在把多智能体系统发布到测试或生产环境之前可以按下面的清单逐项检查。[ ] 每个 Agent 是否有唯一可信的身份标识调用链上的委托关系是否保留。[ ] 工具调用是否经过统一的策略执行入口是否存在绕过入口直接访问资源的代码。[ ] 权限模型是否采用默认拒绝未匹配策略的请求是否会返回 deny。[ ] 沙箱是否配置了最小命令集、最小资源路径、网络白名单和超时时间。[ ] 策略层和沙箱层是否分别输出结构化日志是否都带上request_id。[ ] 路径解析是否使用规范化结果是否处理了符号链接和相对路径。[ ] 沙箱是否限制输出大小避免一次工具调用返回过量数据。[ ] 权限策略和沙箱配置是否分开管理是否有单独的变更评审流程。这份清单适用于大多数 Agent 工具调用场景。如果系统中有文件上传、代码执行、外部命令调用、跨 Agent 委托还需要额外补充对应场景的安全测试。6.3 扩展方向如果项目已经跑通了“权限模型 沙箱执行”的基础分层下一步可以往几个方向扩展。一是引入外部策略引擎。把 ACL 和 RBAC 规则从代码中抽出来交给 OPA、Cedar 这类通用策略引擎管理可以让权限规则独立于业务代码发布和测试。二是引入 ABAC。当资源带有数据分级、项目环境、敏感度标签时可以基于属性判断权限而不是简单维护“Agent 到路径”的列表。三是强化沙箱隔离能力。如果 Agent 需要执行不可信代码可以在 Linux 上组合使用 seccomp、namespace、只读文件系统或者使用 WASM 运行时、gVisor 等隔离方案。沙箱越强权限模型误判时造成的损失就越小但两种能力仍然要分别建设。四是建立权限回归测试。把“合法请求放行、越权请求拒绝、资源越界拒绝”写成自动化用例每次策略变更和沙箱配置变更后自动运行。多智能体系统最大的风险不是静态配置错误而是运行过程中权限规则被动态修改后没有回归验证。具体实践时可以先画一条工具调用链路标出身份解析点、权限决策点、沙箱执行点和日志输出点再决定是否引入外部策略引擎。只要权限决策点始终在沙箱执行点之前并且两类拒绝日志清晰可查就不会再犯“sandbox is a permission model”这个错误。