
最近把自己的 Agent 项目做了一次安全专项改造核心就两件事给 Workspace 做沙箱隔离给全链路加审计日志。这个实践我们内部叫 v21.2不是版本号有多玄学而是这个方案从第一版改到现在光事故复盘就做了十几轮。写这篇文章是因为我发现很多做 Agent 开发的同学精力全都扑在模型选型、工具调用和 prompt 调优上反而忽略了“Agent 自主运行之后会闯多大祸”这件事。所以我把这套基于 Workspace 沙箱隔离与全链路审计的安全方案完整拆开讲一遍包括设计思路、核心实现、常见坑和现场排错过程希望对正在做 Agent 开发、接本地大模型、或者负责平台安全的同学有帮助。1. Agent 自主运行到底在怕什么1.1 传统软件和 Agent 的执行边界根本不是一回事我们平时写的 Web 服务权限边界是很清晰的请求进来代码在固定的进程和容器里跑数据库连接串、密钥、文件路径都通过配置注入攻击面大部分集中在网络层和参数校验层。但 Agent 完全不一样它的核心循环是“大模型生成意图 - 工具调用 - 观察结果 - 再生成意图”这个循环意味着模型的每一次输出都有可能变成一次真实的文件操作、命令执行、网络请求。我见过不少团队做 Agent 原型时最典型的心态“先把工具跑通安全问题后面再说。”结果一跑就出问题。比如一个写周报的 Agent设计需求是让它查代码仓库提交记录整理成周报。结果它不知道从哪里拿到一个文件路径直接把整个项目目录压缩传到内部对象存储去了。你说它坏吗不是它只是按照“查询”这个意图自己选了一个最省力的路径然后把所有能拿到的数据都拿走了。这种行为的本质是权限失控开发者给了 Agent 调用工具的能力但没有给工具设定边界。更麻烦的是Agent 还会被外部内容“劫持”。网页上的一段文字、文档里的一句注释只要写得像指令就可能被模型当成新的命令执行这就是大家常说的提示注入。当 Agent 开始自主运行一次误判的代价就不是返回一段错误文本而是实打实地动到你的文件、服务、账单。1.2 核心风险盘点与影响范围我们把 Agent 自主运行阶段的风险做了分类基本可以归纳为五类风险类型典型触发场景直接后果防护思路权限失控Agent 获得读写任意目录能力后误操作文件被覆盖、数据被删除、密钥被读取Workspace 沙箱隔离提示注入网页/文档内容伪装成指令被模型采纳越权调用工具、访问内网地址输入输出过滤 指令边界约束工具链路污染插件或工具依赖被替换成恶意版本执行任意代码、信息窃取工具注册白名单 完整性校验网络越界Agent 在沙箱内访问外部 API 或内网数据外传、内部系统被探测网络策略收敛 审计留痕不可逆操作Agent 调用删除、转账、释放资源接口财产损失、服务不可恢复高危操作二次审批 审计复核需要说明的是这五类风险不是孤立的往往是一环套一环。比如提示注入导致工具链路污染工具链路污染又导致越权读取文件越权读取文件最终引发数据外传。单点防护解决不了问题所以才需要“沙箱隔离 全链路审计”这种组合拳沙箱负责把 Agent 圈在可控边界内审计负责把边界内发生的一切记录下来两者缺一不可。1.3 我们的目标不是不让 Agent 干活而是让它出的每件事都可控做安全最忌讳一刀切。如果因为怕出事就把 Agent 的所有能力都禁掉那这个 Agent 就失去了意义。我给自己定的原则是Agent 照常自主运行但它的每一步都必须发生在可监控、可回滚、可追责的范畴内。Workspace 沙箱隔离解决的是“Agent 能碰什么”全链路审计解决的是“Agent 做了什么为什么这么做”。前者是刹车后者是行车记录仪。没有刹车的车不能上路没有行车记录仪的车出了事说不清这两件事必须一起做。这套方案落地之后我们内部现在允许 Agent 自主跑完一个任务但任何一次工具调用、文件读写、外部请求都能在审计日志里还原出完整的链路。出了问题先看日志再定位原因再调整权限已经成了一个标准的处置流程。2. Workspace 沙箱隔离给 Agent 圈一块地2.1 先回答“为什么不是直接上容器”很多人一听沙箱第一反应就是 Docker、Kubernetes或者用 gVisor 这类运行时隔离方案。我在第一版方案里也确实这么干过每个 Agent 任务都起一个容器跑完就销毁。结果遇到三个很现实的问题第一资源开销大。模型调用已经占了大量内存还要为每个任务维护一个完整容器显卡显存和磁盘 IO 根本扛不住。第二开发调试效率低。Agent 开发本身就是不断试错的过程动不动就要进容器里看日志、改代码把调试时间拉长了一倍不止。第三本地开发场景很难套用。很多 Agent 项目需要读取本机资源比如本地模型、本地数据库、专用硬件设备全容器化意味着这些资源全部要重新映射复杂度非常高。所以第二版方案我换了一种更实际的思路针对“Agent 默认只能操作自己的工作目录”这一点做约束也就是 Workspace 目录级沙箱。它不解决所有安全问题但能挡住绝大多数因为路径越界导致的误操作和数据泄漏。容器化仍然可以作为高权限任务的补充手段但不再作为默认选项。2.2 Workspace 路径设计与最小权限矩阵Workspace 的核心思想很简单每个 Agent 实例、每次任务运行都有一个独立的根目录。Agent 对这个目录拥有全部读写权限对这个目录之外的所有路径默认拒绝访问。目录结构我们是这样约定的workspace_root/ ├── agents/ │ └── agent_8848/ │ ├── work/ # Agent 可任意读写的工作区 │ ├── data/ # 任务输入数据只读挂载 │ ├── output/ # 任务输出结果只读挂载给外部 │ └── tmp/ # 临时文件 ├── shared/ │ └── reference/ # 共享的只读参考资料 └── audit_logs/ # 审计日志Agent 无权限访问每个 Agent 的 workspace 独立后权限矩阵就能设计得很清晰目标路径权限原因自身 workspace/work读 写 执行Agent 主工作区域自身 workspace/data只读提供输入数据防篡改自身 workspace/output写追加外部系统读取 Agent 产物shared/reference只读共享知识库系统目录、用户主目录、其他 Agent 目录默认拒绝核心隔离目标审计日志目录完全拒绝防 Agent 篡改日志这个矩阵是我们反复试出来的结果。最早我给的权限太宽松所有目录都可读写结果两个 Agent 互相把对方的临时文件给清了排查了很久才发现是权限问题。收紧成上面的矩阵后这类问题基本绝迹。2.3 Windows 环境下的落地细节与启动失败排查思路如果你在 Windows 下做 Agent 开发这里有几个坑需要特别注意。我记得最早用一个开源 Agent 运行时它默认的 workspace 路径是C:\Users\administrator\.openclaw\workspace放在用户主目录下这本身就是个隐患Agent 一旦发生路径穿越能直接摸到桌面、文档、下载文件夹这些敏感位置。我的建议是自定义 workspace 到一个独立的盘符或者层级较深的专用目录并给这个目录设置独立的访问权限不要让它继承用户目录的默认权限。还有一个很典型的报错failed to start claudes workspace request error: net::err_connection_timed。这个报错我第一次遇到时以为是模型服务挂了搞了半天才发现是沙箱初始化组件在启动时尝试连接本地端口但防火墙策略把端口挡了。排查思路是这样的先看进程是否存在再看本地端口是否正常监听最后看防火墙和安全软件日志。这种“workspace 启动失败”问题十有八九不是模型问题而是本地环境权限或网络策略问题。另外Windows 的路径处理比 Linux 麻烦得多路径分隔符不统一、盘符大小写不敏感、还有长路径和符号链接的问题。如果你用 python 的Path.resolve()在 Windows 上解析出来的路径可能与你预期不一样很容易让沙箱的路径检查失灵。这个我在第 4 章会给出具体代码示例。3. 全链路审计让每一次工具调用都有据可查3.1 审计到底审什么四个层次沙箱解决了“能做不能做”的问题但用户真正关心的是“到底做了什么”。全链路审计的核心思路是把 Agent 的一次完整运行切分成四个层次每一层都留下记录第一层是请求层。用户输入了什么内容经过哪些预处理是否触发了脱敏或注入检测这些信息必须留痕。第二层是规划层。模型基于输入生成了什么样的行动计划拆成了哪几个步骤每一步的置信度如何。这一层最容易丢因为很多 Agent 框架跑完就完了不会把中间规划过程落盘。第三层是执行层。工具调用、代码执行、文件读写、外部请求每一项都要记录目标、参数、返回结果。第四层是数据层。哪些文件被读取了哪些文件被写入了哪些网络数据包出去了数据量大不大目标地址是什么。这四层合在一起才能完整还原一条链路。比如用户问了“帮我分析一下最近三个月的销售数据”如果只有执行层日志你只能看到 Agent 调用了某个数据查询工具但不知道它为什么调用也不知道它拿了结果之后干了什么。有了规划层日志你才能看到模型的完整推理链从而判断调用是否合理。3.2 审计事件模型与 Trace ID 设计审计日志不能是零散的一堆无意义文本必须有一个统一的模型。我们定义的核心事件字段如下{ trace_id: 8f3a2b9c6d1e4f7a9b2c5d8e1f3a4b5c, event_time: 2025-06-01T14:23:11.208Z, event_type: tool_call, agent_id: agent_8848, session_id: session_20250601_1425, target: read_file, args_digest: sha256:4d7c3f2a9e1b6d8c5a0f2e3b4c5d6e7f, result_code: 0, risk_level: medium, detail: { file_path: /workspace/agents/agent_8848/data/sales.csv, file_size: 204800 } }这里最关键的是trace_id。所有阶段的事件都必须携带同一个 trace_id这样后续排查时只要拿到一个事件里的 trace_id就能把所有相关事件全部串起来。我们的做法是在 Agent 会话开始时生成 uuid然后通过日志上下文统一注入这样不管是模型调用、工具执行还是文件操作只要是在同一个会话里发生的事件都能通过 trace_id 查询到一起。args_digest也是一个容易被忽略的字段。它是对调用参数做哈希后的结果作用是防止敏感参数明文落盘同时又能在出现问题时重新计算哈希做比对。3.3 日志防篡改与存储隔离审计日志本身也是数据如果 Agent 能修改自己的审计日志那这个审计等于白做。所以有两层防护第一层是存储隔离日志必须写入 Agent 权限之外的目录比如独立的审计专用目录或远程日志服务第二层是完整性校验我们会在每条日志落盘时附带一个哈希链前一条日志的哈希值会进到下一条日志的字段里任何中间改动都会破坏哈希链。一开始我把审计日志存在了 workspace 内部结果差点出大事。一个 Agent 在执行清理任务时直接把整个 workspace 删了审计日志也没了。后来才改成独立存储并且把审计目录的写权限从 Agent 运行账号那里彻底拿掉。这里要特别提醒如果审计日志和 Agent 数据在同一个磁盘分区即便是独立目录也可能被误删最好分盘或者放对象存储。4. 实操落地一套最小可用的安全底座4.1 总体架构与 Harness 的角色现在很多 Agent 框架都会把“控制逻辑”和“模型推理”分开控制逻辑通常叫 Harness模型推理部分才叫 Agent。我们这套安全底座的核心思想就是把安全能力全部注入 Harness 层而不是让 Agent 自己去判断“能不能做”。Harness 和 Agent 的区别用大白话讲就是Agent 是那个出主意的人它告诉你“我觉得应该这样做”Harness 是那个把关的人它检查这个主意能不能落地。工具注册、权限校验、审计记录、异常处理都应该在 Harness 层完成。模型只是输出决策真正执行动作的是 Harness 控制的工具层。如果让 Agent 自己决定访问哪个目录就等于让猴子管钥匙迟早出问题。所以我在架构里加了两个核心组件SandboxExecutor沙箱执行器和 AuditRecorder审计记录器。前者拦截所有文件系统访问和工具调用做路径校验和权限判断后者负责为每个执行动作生成审计事件。4.2 沙箱执行器代码示例下面是一段精简但可用的沙箱执行器示例它能解决最核心的路径越界问题# safe_runner.py from pathlib import Path from typing import Union WORKSPACE_ROOT Path(rC:\Users\administrator\.openclaw\workspace).resolve() def resolve_in_workspace(requested_path: Union[str, Path]) - Path: 把请求的路径解析到 workspace 根目录内越界直接抛异常。 raw Path(requested_path) if not raw.is_absolute(): raw WORKSPACE_ROOT / raw candidate raw.resolve() root WORKSPACE_ROOT.resolve() # 核心校验解析后的路径必须位于 workspace 根目录内 if not candidate.is_relative_to(root): raise PermissionError(fPath escape blocked: {requested_path} - {candidate}) return candidate这里面有一个关键点不能直接用原始字符串做前缀判断因为..、符号链接、大小写差异都会绕过前缀检查。必须用resolve()把路径解析成绝对路径之后再做is_relative_to判断。Windows 下还建议统一把路径转成小写再比较因为文件系统大小写不敏感我在这里吃过亏路径检查是大写实际文件系统是小写结果成功绕过沙箱把文件读走了。如果你的 Agent 需要读取共享目录可以在白名单里维护一个允许访问的路径集合ALLOWED_READ_DIRS [ Path(rD:\shared\reference).resolve(), Path(rD:\shared\datasets).resolve(), ] def check_read_permission(requested_path: Path) - bool: candidate requested_path.resolve() if candidate.is_relative_to(WORKSPACE_ROOT): return True for allowed in ALLOWED_READ_DIRS: if candidate.is_relative_to(allowed): return True return False4.3 审计中间件代码示例审计组件建议做成装饰器侵入性最小所有工具函数一装饰就自动落日志# audit.py import functools import hashlib import json import time import uuid class AuditRecorder: def __init__(self, sink_path: str): self.sink_path sink_path def record(self, event: dict): with open(self.sink_path, a, encodingutf-8) as fp: fp.write(json.dumps(event, ensure_asciiFalse) \\n) audit AuditRecorder(/var/log/agent-audit/events.log) def audit_tool(tool_name: str, risk_level: str low): 工具调用审计装饰器。 def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): trace_id kwargs.pop(trace_id, None) or uuid.uuid4().hex start time.time() result None error None try: result func(*args, **kwargs) return result except Exception as e: error str(e) raise finally: # 调试期可以记录完整参数生产环境建议改成参数摘要 audit.record({ trace_id: trace_id, event_time: time.strftime(%Y-%m-%dT%H:%M:%S, time.localtime(start)), event_type: tool_call, target: tool_name, args_digest: hashlib.sha256( json.dumps(kwargs, defaultstr, ensure_asciiFalse).encode() ).hexdigest(), cost_ms: int((time.time() - start) * 1000), result_code: 1 if error else 0, error: error, risk_level: risk_level, }) return wrapper return decorator用起来非常简单audit_tool(read_sales_data, risk_levelmedium) def read_sales_data(path: str): safe_path resolve_in_workspace(path) with open(safe_path, r, encodingutf-8) as fp: return fp.read()要注意的是try/finally里必须把异常重新抛出去不能在这里吞掉异常否则 Agent 会误以为工具执行成功继续执行后续计划问题会被放大。审计记录里同时保存错误信息方便排查。4.4 配置文件示例这套安全底座我建议用独立的 YAML 配置管理方便调整策略不需要改代码sandbox: workspace: C:/Users/administrator/.openclaw/workspace mode: strict # strict / permissive allowed_read_dirs: - D:/shared/reference - D:/shared/datasets denied_suffix: - .env - .pem - id_rsa - .git/config network: enabled: false # 沙箱内是否允许网络请求 allowed_hosts: - api.internal.example.com audit: enable: true sink: file:///var/log/agent-audit/ batch_size: 200 # 达到多少条批量写一次 max_file_size_mb: 500 rotate_days: 7 risk_threshold: medium # 达到该级别的事件立即触发告警这里的denied_suffix是我强烈建议加上的配置。很多 Agent 误操作读文件时最容易泄漏的就是.env、私钥、Git 配置这些敏感文件。在沙箱层前置拦截比事后审计要有效得多。5. 常见问题与排查实录5.1 高频问题速查表问题现象直接原因解决方案workspace 启动失败 / 连接超时沙箱初始化端口被防火墙拦截检查本地端口监听、防火墙放行规则Agent 读不到工作区之外的文件沙箱路径校验拦截确认文件确实在工作区内或加入 allowed_read_dirsAgent 能访问网络致数据外传沙箱网络策略未开启设置 network.enabledfalse 或配置 allowed_hosts审计日志文件过大每条工具调用都写全量参数生产环境用参数摘要哈希缩小日志体积Agent 执行中途提示 execution terminated某一步工具抛了异常且被外层框架捕获通过 trace_id 查询整条链路定位失败步骤沙箱被符号链接绕过路径未做 resolve 处理强制在路径校验前执行 resolve()Agent 误删了其他 Agent 的文件workspace 根目录共用改为每个 Agent 独立目录5.2 三个典型案例的完整排查过程案例一Agent 在 Linux 服务器上通过符号链接读取了/etc/shadow。排查发现沙箱代码只检查了原始路径的前缀没有处理符号链接。攻击者只要在 workspace 里放一个指向/etc的软链接就能绕过检查。修复方案很简单所有路径必须resolve()之后再做归属判断并且禁止 Agent 创建指向工作区外部的符号链接。案例二Agent 执行一个数据处理任务时突然报agent execution terminated due to error。很多人第一反应是模型出错了但我们通过审计日志按 trace_id 查询后发现错误发生在 Python 代码执行阶段原因是数据集里有一个空文件导致脚本崩溃。如果没有审计日志这个问题排查起来至少要半天有了日志五分钟就定位了。案例三某次灰度测试中 Agent 频繁请求外部站点。初看是模型被提示注入诱导调审计日志后发现其实是工具库里的一个依赖在初始化时发起了遥测请求。虽然流量不大但如果不设网络白名单这种隐蔽行为很难被发现。我们现在默认关闭沙箱网络需要联网的 Agent 必须在配置文件里显式声明允许的域名或 IP。5.3 花了大半年才想明白的几个坑第一个坑审计一定要从第一天就开始接不要等系统上线后再补。补审计的代价远比你想象的大因为你必须去改每个工具函数还要说服之前跑得“好好的”同事为什么突然多了一堆日志。最好的做法是从框架层自动注入让业务开发无感知。第二个坑风险阈值不要设置得太低否则告警会刷屏最后大家都麻木了。我一开始把所有低风险事件都设为告警结果一天几百条通知看不过来反而漏掉了真正的高危事件。后来把风险等级分成低中高三级只有中高风险的事件才实时推送低风险事件只在审计平台里留存备查。第三个坑沙箱路径校验用Path.resolve()时一定要处理文件不存在的情况。如果文件不存在resolve()在某些平台上不会展开符号链接导致路径校验通过但 Agent 后续真正访问时实际路径与校验路径不一致。这个问题的处理方式是先检查路径是否存在或者用strictFalse参数配合手工处理上级目录。第四个坑审计日志的批量写要和异常处理解耦。如果审计写入失败不能影响 Agent 主流程但必须有独立的失败标记。我在早期版本里把审计写入放在主逻辑里审计目录磁盘满了导致整个 Agent 任务全部失败这个教训很深刻。现在审计写入是异步的失败会单独记到一个 degraded 目录并触发运维告警。最后多说一句如果你正在做自己的 Agent 项目我的建议是先把安全底座这些事想清楚再动手加功能。说实话我见过太多项目功能做得很炫模型调用很流畅但一次自主运行的误操作就把信任全毁了。沙箱隔离和全链路审计这套组合看起来不性感但它能让你在 Agent 真正闯祸的时候快速定位问题、保住数据、重建信任。个人心得就是安全不是给 Agent 上锁链而是给 Agent 画一个足够大的围栏让它在围栏里自由奔跑出圈立刻拉回来。这套实践 v21.2 的迭代还在继续下一步我准备在审计数据上做更细粒度的行为分析和异常检测期待后续有机会再和大家分享。