几个月前,我让一个 AI 编程代理管理一台个人服务器。它拥有 shell 访问权限,可以读写我的用户账号能够触及的任何内容,也可以抓取任何它想访问的 URL。它运行得很好——直到一天下午,我看到它在一项与 SSH 密钥毫无关系的任务中,把读取 ~/.ssh/id_rsa 当作了一个完全附带的步骤。它没有拿这个文件做任何恶意的事。它不需要这么做。问题在于它有能力这样做,而在我碰巧看着终端之前,我没有任何记录能够知道它碰过哪些内容。

这个项目就是从那一刻开始的。不是抽象地说“AI 代理很危险”——这类观点我已经读过——而是一个具体、平淡的认识:我给了一个进程超过任务所需的权限,却没有办法审计它用这些权限做过什么。这不是一个新问题。sudo、SELinux,以及安全领域中的每一种最小权限模型,都是为了解决同一个问题。只是对于这种特定的新型进程,这个问题当时还没有得到解决。

问题的形状

一个使用工具的 AI 代理(读取文件、运行命令、抓取 URL)在这里有三种需要关注的失效模式。虽然它们常常被笼统归入“AI 安全”,但实际上是不同的问题:

  1. 代理做了任务并不需要它做的事。并非出于恶意——只是越权,因为没有任何东西阻止它。在只需要读取一个配置文件时,它去读了相隔两个目录的另一个配置文件;在 ls . 已经足够时,它运行了 find /
  2. 代理读取到的某些内容试图重定向它。网页、文件或 API 响应中可能含有看起来像指令的文字。如果代理无法区分“我正在处理的内容”和“我应该遵循的指令”,一份被投毒的文档就可能劫持它。这就是提示词注入问题,它在结构上不同于第一种失效:代理并不是自己越权,而是被自己的工具输出中的某些内容利用了。
  3. 代理执行的某个合法操作仍然泄露了不该泄露的内容。代理被允许读取那个文件,而文件碰巧包含一个 API 密钥。调用本身没有任何错误——只是内容很敏感,却没有人在它进入上下文窗口之前将其遮盖;而上下文窗口之后可能被记录、被截图,或者被交给另一个系统。

三种不同的失效模式需要三种不同的机制。这是 AgentGuard 背后的核心设计决策:它不是一个过滤器,而是三个相互独立的层,因为把三者混为一谈,只会得到一个三件事都做不好的工具。

执行点应该放在哪里

接下来的决定是,这套逻辑应该放在哪里。下面是几种选择,以及我没有选择它们的原因:

  • 放在代理的提示词里(“请不要读取 SSH 密钥”)。这不是执行机制,只是一项可以被模型说服而放弃的建议。如果安全边界存在于自然语言里,它就不是边界。
  • 放在 MCP 服务器内部。对于自己控制的服务器,这种做法是合理的;但它意味着你必须在每一个使用的服务器中重复实现同一套策略逻辑,而且对于你无法控制源码的第三方服务器,它不起作用。
  • 放在操作系统层(容器、seccomp、受限用户)。这确实很有价值,也能够形成互补——可以参阅威胁模型中关于沙箱化服务器进程的说明——但它处理的是系统调用,而不是工具调用的语义内容。它可以阻止一个进程打开文件;却没有“这条 JSON-RPC 消息是一次针对 read_filetools/call,其中有一个看起来像路径的参数”这样的概念,而我想在这个语义层面上推理策略。

我最终采用的方案是:位于代理与 MCP 服务器之间的通信线路上,在每一条 tools/call 消息被转发前检查它,也在每一个结果被返回前检查它。MCP 已经给出了一个清晰的协议边界——通过 stdio 传输、以换行分隔的 JSON-RPC——因此这种做法不需要修改代理或服务器。你只需要让代理指向这个中间代理,而不是直接指向服务器;中间代理便会安静地执行策略。代理与服务器仍然像之前一样使用 MCP 通信;双方都不知道 AgentGuard 位于中间。

三层机制,简要说明

  1. 策略引擎(输入侧):针对文件路径的 glob 拒绝列表、针对 shell 命令的正则表达式拒绝列表,以及针对网络调用的主机名允许列表。它根据键名(pathcommandurl……)匹配 tools/call 的参数,因为不同 MCP 服务器并不共享同一种参数模式。这对应第一种失效模式:在调用发生前阻止它。
  2. 注入检测器(输出侧,第一遍):扫描工具输出中具有指令形态的文字。一旦命中,就阻断整个结果,而不是试图精确地删除那个有问题的句子——下一篇文章会进一步解释原因。这对应第二种失效模式。
  3. 秘密脱敏器(输出侧,第二遍):在注入检测器没有阻断的输出中遮盖已知的秘密格式。这对应第三种失效模式——调用没有问题,但内容有问题。

来自这三层的每一个决定都会被写入审计日志。日志使用哈希链连接(每一条记录都承诺前一条记录的哈希),因此如果有人事后编辑过去的记录——例如试图掩盖自己的踪迹——就可以通过重新遍历哈希链检测出来。之所以加入这一部分,是因为如果日志本身不可信,“审计日志”就是一句空洞的承诺;攻击者能够悄悄编辑的日志,与没有日志没有实质区别。

我决定不承诺什么

完整的推理位于仓库的 THREAT_MODEL.md 中。简短地说:我宁愿发布一个边界诚实、范围更窄的工具,也不愿发布一个范围更广、却暗示了我无法支持之保证的工具。AgentGuard 不会沙箱化 MCP 服务器进程本身——如果服务器在操作系统层面对自己的行为撒谎,这超出了协议级代理能够观察的范围。它无法捕获每一种注入措辞——规则公开写在策略 YAML 中,阅读过规则的攻击者可以换一种表达绕过它;用 LLM 分类层补足规则遗漏之处,确实是一个真实存在的缺口,而不是被隐藏起来的缺口。审计日志是可检测篡改的,而不是不可篡改的——它可以证明某一次具体的编辑发生过,但无法阻止拥有完整文件系统权限的人删除日志,并从头开始建立一条全新的、内部自洽的哈希链。

构建安全工具的全部意义,在于让人们能够推理它究竟为自己带来了什么。一个悄悄夸大承诺的工具比没有工具更糟——它会改变人们认为自己不必亲自检查的事情。

这些都不是我藏在细则里的保留意见。它们写在 README 中,写在专门的威胁模型文档里,也写在这篇文章里。

下一篇文章会专门讨论注入检测器:为什么 v1 选择基于规则的检测,而不是 LLM 分类器;一次误报与一次漏报分别会付出什么代价;以及测试套件在默认规则继续发布之前发现的一个真实错误。