凌晨两点的告警,不是CPU飙高,也不是磁盘告满,而是一行刺眼的日志:你的 AI Agent 在自动跑完一轮清理任务之后,顺手把对应的执行Trace记录也一起"清理"掉了。
这还真不是段子。我身边做自动化运维的朋友近期就撞上过这个场景——Agent按照既定流程删除了无用资源日志,但由于会话里被授予了过高的操作权限,删除命令的执行记录连同被引用到的历史Trace一并没了。排查时你根本说不清它是"误删"还是"按指令执行",因为连"它到底干了什么"的凭证都被它自己抹掉了。
这个现象背后藏着一个非常关键的问题:Agent 既然连 Trace 都能删,那给它套上的 Agent Harness 到底凭什么号称能保证安全?
这篇文章我想认真梳理一下这个事。会拆解Agent为什么能删Trace、Harness在架构上应当如何与Agent划清边界、审计数据怎样做到"Agent自己删不掉",以及一套能落地的最小权限模型和端到端加固方案。无论你是正在搭Agent服务,还是已经上了LangChain、FastAPI这类工作流又在被安全问题追着跑,这篇都值得往下看。
1. 当 Agent 能删 Trace,暴露的其实不是"误操作"
表面上看,这是一次权限配置事故。但往深里挖,它暴露的是三个层级的安全失效:权限边界失守、审计与执行耦合、信任假设错误。
1.1 权限边界失守:Agent 的执行上下文吞掉了观测上下文
为什么Agent能删Trace?最直接的原因,是它拿到的执行凭证里,既包含"操作业务资源"的权限,也包含"修改自身追踪记录"的权限。很多团队在给Agent签发服务账号或临时凭证时,会图省事直接挂一个"管理员"角色,Agent调用的每个工具都带一套宽泛的读改写权限。一个毁灭性的后果就是:Agent在执行任务时可以顺带访问日志、Trace、甚至安全配置。
我在实际项目里见过一种极常见的配置:
- Agent 被授予了云存储桶的
DeleteObject权限; - Trace 数据恰好也存放在该存储桶的一个前缀路径下;
- Agent 清理旧日志时,工具调用所携带的上下文包含了桶内读写资源配置;
- 一次模糊匹配的路径清理,直接把Trace前缀下的对象一起删了。
这不是Agent"学坏了",是权限模型没有做任何域隔离。或者说,是我们在架构上没有把"Agent能碰的东西"和"记录Agent行为的东西"放进两个不同的权限空间。
1.2 审计与执行同域:裁判和运动员坐在同一边
Trace本身是观测系统的一部分,理论上是"局外人"——它负责记录Agent的行为,相当于比赛录像。但问题在于,如果Agent的执行进程、工具调用、数据访问都跑在同一个权限身份下,而这个身份同时能操作观测数据的存储,那么这套录像就失去了公正性。主动权一旦握在"被记录者"手里,"记录"这件事本身就变成了可以被操纵的资源。
这就好像比赛录像带放在球员休息室的柜子里,球员心情不好顺手就能烧掉。Trace存得再好,也经不住Agent自己抬手一删。
1.3 信任假设错误:默认Agent是"可控的",所以没做外部强制
第三个失效点是团队默认Agent不会乱来。Agent输出的每一个动作都是通过策略工具执行的,但策略工具本身如果只做"能不能调用"的静态判断,而不做"调用完之后发生了什么"的动态跟踪,那很多破坏性操作其实是拦不住的。
说到底,这类事故的共性在于:我们把"安全"押注在了Agent的自律上,而没有押注在"不可触碰的外部约束"上。而Agent Harness要解决的,恰好就是如何把约束从"自律"变成"他律"。
2. Agent Harness 的本质:不是护城河,是缰绳和笼头
很多人一听到Agent Harness,下意识觉得它是一堵墙、一个护城河,用来把Agent围起来。这个理解方向对,但位置偏了。Harness的本意是"马具"——缰绳、笼头、肚带这些东西。它的核心作用不是不让马跑,而是让马无论怎么跑,方向都还在骑手手里。
2.1 Harness 与 Agent 的职责边界
在一个典型的Agent应用里,Agent承担的是"思考与决策":理解用户意图、拆分任务、选择工具、组装下一步动作。而Harness承担的是"边界与记录":限定Agent能调用什么工具、限制它的凭证范围、拦截危险操作、且强制记录所有关键行为。
简单说:
- Agent 是那个"做决定的人";
- Harness 是那个"决定什么能做、做了必留痕的路口"。
没有Harness的Agent,就像没有铁轨的火车——不是不能跑,而是一旦跑偏,没人能拦住。
2.2 Harness 必须独立于 Agent 的进程边界之外
这里有一条被我反复验证过的架构铁律:Harness 坚决不能作为 Agent 内部的一段代码来运行。
为什么?因为Harness的安全逻辑如果和Agent跑在同一个进程里、享有同一个权限域,那么Agent一旦通过工具调用拿到了系统级命令执行能力,它就完全有能力去关闭、绕过或者篡改自己身边的Harness。这就像让一个小偷自己保管监控室钥匙,监控自然形同虚设。
我把Harness的部署形态分为三档,按可信强度从小到大:
| 部署形态 | 可信强度 | 说明 | 适用场景 |
|---|---|---|---|
| 进程内库(in-process) | 低 | Harness代码打进Agent进程依赖里,拦截靠函数级钩子 | Demo演示、快速原型 |
| Sidecar 独立进程 | 中 | Harness跑在独立进程/容器里,通过本地接口与Agent通信,具备隔离的崩溃域 | 生产环境的中小规模Agent服务 |
| 外部策略网关 | 高 | Harness作为独立服务,在Agent的所有工具调用、凭证签发、事件上报链路上强制裁决 | 多Agent、高安全要求、合规场景 |
如果你已经在生产环境跑Agent,我的建议是无论如何不要停在第一档。进程内库看起来方便,但它把"监督者"放进了"被监督者"的肚子里面,Agent一旦因为注入、提示词操控或者工具bug获得了原生代码执行能力,这个库连挣扎的机会都没有。
Sidecar 是我个人认为的性价比最优解——有隔离,又不需要把整个调用链拉太长。至于外部策略网关,适合平台级产品,它能做跨Agent的统一策略,但也要付出更高的链路复杂度和延迟代价。
2.3 Harness 的三个基本安全动作:拦截、改写、终止
Harness真正对Agent动作的管控,不是靠"提示词里多写几行'要小心'",而是靠三个在任何成熟执行引擎里都存在的原语:
- 拦截:工具调用在发出之前,先经过策略引擎检查,不符合策略的直接不执行。注意,"拦截"必须作用在工具调用边界,而不是 Agent 的"意图表达"边界。Agent聊天里说"我想删掉生产库"没关系,真正要拦的是它调用数据库删除工具的那一刻。
- 改写:有的动作本身是合理的,但参数需要被收窄。比如Agent想"清理临时文件",Harness可以把目标路径强制限定为
/tmp之下,防止路径穿越。改写通常也叫参数规范化。 - 终止:持续的口令失控、连续失败、或者检测到超高危操作(比如删除审计数据本身),Harness直接终止整个会话的运行,并立即告警。
我经常用一句话跟团队讲:Agent负责天马行空,Harness负责把天马行空变成"在地图上跑"。拦截、改写、终止就是地图的边界。
3. 让 Trace "删不掉":审计数据要设计成黑匣子
回到文章开头那个事故:如果当时他们的Trace存储在设计上就满足了"Agent没有删除路径",那即便Agent误入歧途,删除操作也会因为权限不足直接失败。想让Trace真正具备"删不掉"的属性,不是靠日志软件有什么防删除开关,而是靠架构上把执行存储和审计存储彻底分离。
3.1 执行同理,审计同果:写路径与读路径的分离设计
很多团队理解的"Trace存储隔离",是给Trace库单独建一个实例、单独分配一组账号,但Agent的执行角色仍然保留着对Trace库的读取和删除权限。这不算隔离,它只是换了个名字的同一把钥匙。
我建议按这样的方式做:
- 写路径:Agent执行关键动作时,Harness通过受信任的适配器,把事件(时间、用户、工具名、参数摘要、结果状态)写入独立的审计存储。Agent自身不直接持有审计存储的写入凭证。
- 读路径:Agent没有读取审计存储的权限,只有安全团队、运维后台、或者独立的审计分析服务才有读取通道。
- 管理路径:审计存储的删除、轮转、归档权限,只属于专门的存储管理员身份,和Agent会话凭证、业务账号完全剥离。
这里的关键词是"权限域分离"。Agent在容器里跑得再欢,它的临时凭证只对它接触的业务资源有效;对它自己的Trace存储,它的权限应该等于零。这也是整个安全设计里我认为最核心的一条:把观测数据的权限,从被执行者的权限中完全剥离。
3.2 黑匣子设计:追加写入、哈希链、独立保留策略
飞机的黑匣子之所以可靠,不是因为飞行员够自觉,而是因为飞行员根本打不开它。Trace存储也要做成这个逻辑:
- 只追加写入:审计存储只开放append接口,不开放update和delete。技术上可以用只读文件系统挂载、对象存储的不可变对象版本、或者专门的WORM(Write Once Read Many)存储来实现。
- 哈希链防篡改:每条Trace记录在写入时携带前一条记录的哈希值,篡改任何一条都会破坏整条链的完整性。这个不需要引入重型区块链,一个简单的累加哈希就能让"偷偷改记录"的行为在后续校验时立刻现出原形。
- 独立保留策略:审计数据的生命周期(轮转、归档)由存储管理员统一控制,Agent任务再多也碰不到这个开关。
用一句大白话总结:Trace要存放在Agent"看得见但摸不着"的地方。Agent也许能通过工具观察到自己在被记录,但绝对没有能力把这些记录从存储里抠掉。
3.3 如果现有 Trace 系统不支持隔离,怎么办
有读者会说:我们的Trace就是用现成的可观测系统,数据就存在那个平台的存储里,Agent的服务账号确实有访问权限,改架构很难。这种情况我理解,但至少有三种过渡做法,都能显著降低"Agent删Trace"的风险:
- 强制导出副本:把Trace数据通过管道实时同步到外部的、Agent不可达的存储(比如独立的日志归档桶),同步目的存储的写权限只归Harness。
- 收紧工具清单:在Agent的工具注册表里,彻底去掉"删除日志""清理Trace""删除存储对象"这类高危工具。哪怕Agent有对应的云凭证,没有工具能调用它,就少一个攻击面。
- 独立事件流兜底:如果连导出都做不到,那至少让Harness在每次Agent调用工具前,把一条精简的"动作摘要"写入一个Agent无权访问的独立消息队列。它不完整,但它是可信的。
要注意,这些过渡手段都有各自的覆盖死角,真正稳妥的还是把"执行存储"和"审计存储"从权限模型上掰开。
4. 权限的收与放:最小权限模型在 Agent 流水线上的落地规则
权限模型做得好不好,决定了Harness到底是在"表演安全"还是在"执行安全"。下面这五条规则是我在实际搭建Agent流水线时反复验证过的,照着做,能避免绝大多数"Agent干了不该干的事"的麻烦。
4.1 从"给所有工具"改为"每步只给必要工具"
很多Agent框架为了方便,直接把一大串工具名扔给Agent去选择。表面上Agent灵活了,实际上攻击面也平铺了。Agent根本不需要在第一步就拥有"删除数据库"的能力——它只需要在当前任务步骤里,访问当前步骤指定的几个工具。
我之前搭过一个内部数据整理Agent,第一版给它注册了二十多个工具,结果它在一次误识别用户意图时,直接调用了"批量删除"工具,好在环境是测试的。后来我把工具拆成"步骤级"白名单:任务解析阶段只能读,数据整理阶段能写临时区,删除操作必须显式申请并二次确认。从那之后,类似的失控一次都没再发生过。
做法上,推荐在Harness里维护一张工具注册表,每个工具附带允许的入参schema和可调用的会话状态范围:
tools: - name: file_cleanup allowed_when: - step: "cleanup_temp" - user_approved: true allowed_paths: - "/tmp/**" - "!/home/**" risk_level: high上面这个配置表达的意思很明确:file_cleanup工具只在cleanup_temp这一步、且用户已确认时才可调用,路径只允许/tmp下的内容,/home被显式排除。这就是将工具调用从"全量可用"压缩成"场景化可用"。
4.2 凭证要时效化、作用域化
Agent证书的最大问题,不是发得太多,而是有效期太长、作用域太宽。我强烈建议给Agent的每个任务会话签发独立的临时凭证,有效期从分钟级到小时级,而不是用长时间有效的服务账号。
同时,凭证的作用域要配合工具白名单一起做。比如云对象存储的凭证只授予指定前缀的读写;数据库凭证只授予指定库表的SELECT和INSERT;凡是涉及DROP、DELETE的命令,一律不给Agent默认权限。这样即便Agent被恶意提示词诱导,它能造成的破坏也被限制在极小的范围内。
4.3 动态授权:每次工具调用都过一遍策略引擎
静态权限解决的是"能不能",动态授权解决的是"这一次能不能"。Agent在运行过程中,上下文是变化的:用户意图可能被污染、任务参数可能被篡改、调用频率可能异常。如果权限是一次性发出去就不再管控,那跟把门禁卡贴门口没什么区别。
正确的做法是:每次工具调用都交给Harness的策略引擎做一次实时裁决。裁决的输入包括当前用户意图、会话状态、工具参数、调用频率、上下文风险分。输出是allow、rewrite或deny。
# 伪代码示例 async def policy_check(context: SessionContext, call: ToolCall) -> CheckResult: risk_score = compute_risk(context, call) if risk_score > HIGH_RISK_THRESHOLD: return CheckResult(action=DENY, reason="high_risk_score") if call.tool.risk_level == "high" and not context.user_approved: return CheckResult(action=DENY, reason="need_user_confirmation") if call.tool.name in context.step_allowed_tools: return CheckResult(action=ALLOW) return CheckResult(action=DENY, reason="tool_not_in_step_whitelist")代码写得比较简,但核心思想到位:一切调用都以当前步骤的策略为准,而不是以Agent的"自我规划"为准。
4.4 熔断机制:连续失败、敏感操作、异常频率触发暂停
任何时候都要有熔断。我最推荐的是下面这三类触发条件:
- 连续失败熔断:Agent在短时间内连续调用工具失败超过N次,说明可能存在异常循环,立刻暂停会话。
- 敏感操作熔断:任何涉及删除、权限变更、凭证读取、数据导出的操作,一律进入"高安全模式",需要人工审批。
- 频率熔断:单位时间内的工具调用次数远超该任务正常水平时,按异常流量处理。
熔断的意义不只是"拦住坏人",更重要是"给失控留出时间"。Agent一旦进入异常循环,每一秒都在扩大损失,早停一秒就少一分伤害。
4.5 管理面与控制面分离:Agent 永远碰不到安全策略
最后这条很多人忽略:Agent不仅不能执行危险命令,它更不能修改"哪些命令算危险"。安全策略、Harness配置文件、策略引擎的规则库,这些都属于"管理面",必须与Agent的运行空间完全隔离。
实际中我遇到过团队把策略文件放在Agent工作目录旁边的,结果Agent有一次在做文件整理时直接把整个目录权限改了。这不是Agent故意,是它确实"看见"了配置文件并且认为自己有权处理。所以,管理面文件不但要隔离,最好放在Agent根本摸不到的独立挂载或独立服务里。
5. 一个最小可信的 Harness 落地示例
前面讲了不少原则,这一章直接上一套我最近在项目里搭的配置骨架,你可以当作参考模板去对照自己的系统。
5.1 运行环境层:容器级隔离和最小网络
Agent不管多个别多听,最终是要在某个环境里执行工具的。我的建议是最低限度满足下面几点:
- 独立命名空间:每个任务一个一次性容器/沙箱,任务结束即销毁,避免残留态污染下一个任务。
- 根文件系统只读:Agent运行的镜像根文件系统挂载为只读,只有挂载出的临时目录可写。
- 无出站网络(除非白名单):Agent没有任意访问公网的能力,只能访问预先配置的服务端点,比如内部API、向量数据库、公司内网网关。
- 资源上限:CPU、内存、磁盘IO全部设置Quota,防止Agent在失控状态下把宿主机打爆。
容器会带来一些性能损耗,但对安全来说这是值得的——隔离性越好,Agent"越狱"的成本越高。
5.2 调用链策略层:所有工具调用过网关
在Agent和真实API之间,用一层独立的策略网关做强制代理转发。Agent不再直接握有业务API的地址和凭证,它只能向网关发起意图请求,由网关进行鉴权、参数校验、完成真正的调用。
policy_engine: endpoint: internal://harness-policy-engine rules: - id: deny_trace_delete match: tool: "tracking.*" operation: "delete" action: deny reason: "trace_data_is_protected" - id: require_approval_for_batch_delete match: tool: "storage.batch_delete" action: require_user_approval approver_channel: "ops_wechat" - id: rewrite_path_to_tmp match: tool: "file.remove" rewrite: target_path: "regex_replace($param.path, '^/home', '/tmp')"另外,策略网关必须做完整的调用审计:谁调用的、什么时候、目标是什么、参数是什么、结果是什么。每一条都给外部审计存储。
5.3 监测告警层:这几个事件必须实时推给人
有些事件不能等事后去查日志,必须实时告警。我把告警事件分为三级:
| 告警级别 | 事件类型 | 示例 |
|---|---|---|
| P0 | 安全机制被触碰 | 尝试访问Harness配置、尝试删除Trace、出现工具未注册调用 |
| P1 | 高危操作未批准 | 批量删除、导出数据、权限变更、外部站点请求 |
| P2 | 异常行为模式 | 高频调用、夜间大量执行、上下文突变、连续失败 |
P0 必须电话或短信级别通知,P1 至少要在审批群里弹卡,P2 进日报就行。我曾经就因为少设了一条"Trace被删除尝试直接告警"的规则,让一个手工误操作白白跑了一整天才被发现。
5.4 收尾核对:任务结束后必须"对账"
Agent执行完任务,不是点个"完成"就结束。Harness要拿着执行计划跟实际动作做比对:计划里说要删3个文件,实际删了5个;计划里说只读数据库,实际出现了写操作。这些偏差是判断Agent是否偏离任务边界的重要信号。
对账不复杂,可以做成一个简单的比对表:
预期动作清单: - read /tmp/data.csv - write /tmp/result.json 实际动作清单: - read /tmp/data.csv - write /tmp/result.json - delete /var/log/trace_buffer.log <- 不在计划内,标记异常任何不在预期清单里的动作,都应该被标记并触发人工审查。这一步就是Harness真正发挥作用的临门一脚。
6. 我踩过的三个坑,和最后一点个人体会
理论说了不少,最后分享几个我在真实项目里踩过的坑,希望能帮你少走一段弯路。
第一个坑:给Agent的凭证顺手挂上了"存储管理员"角色,结果Agent在清理临时文件时清理到了Trace对象。那之后我把所有Agent凭证跟管理员角色彻底拆开,并且把权限模型改成了"默认拒绝、显式放行"。
第二个坑:一开始偷懒把Trace只写到stdout,靠容器日志收集。结果环境和应用重启,历史记录全丢了,连排查事故的依据都没了。后来我强制要求所有Trace和审计事件走独立审计存储的追加写通道,并做了循环校验。
第三个坑:策略规则写得过于宽松——本来要对高危操作开启人工审批,结果把批量删除操作漏掉了,Agent在半夜一次误判中真的跑了批量删除。好在因为限流规则触发熔断,才没有造成更大问题。从那以后我把高危操作清单单独维护,每次上线新工具都会先过一遍规则清单。
说到底,Agent Harness的"安全"从来不在于它能让Agent变乖,而在于它把失控的代价抬高、把作恶的路径拉长,并且确保无论Agent做什么,它都做不到销毁自己作恶的证据。如果你正在设计Agent系统,请一定记住这句话:监督必须放在被监督者不可触及的地方,记录必须存放在被记录者不可删除的位置。
把这句刻进架构里,你就能拍着胸脯说,删Trace这种事,在你这儿永远发生不了。