文章提出让模型只提交结构化操作意图,由能力代理解析路径、主机和接收方,再按清单决定允许、拒绝或请求审批。所有决策写入审计记录,从而把Agent安全转化为可测试的精确权限策略。
Agent 事故很少像电影里的反派那样张扬。它们更像是一个乐于助人的助手,把两项你从未打算组合起来的权限拼到了一起:一个可以汇总文件夹内容的笔记读取器,加上一个可以向 webhook 发送消息的工具,再加上文档中一段被投毒的文字,告诉它应该把笔记发送到哪里。
在写过将 prompt 变更视为迁移之后,我开始用同样的方式对待工具权限:把它当作一份需要评审的契约,而不是 system prompt 里某种模糊的氛围。我反复采用的模式是 capability broker(能力代理器)。模型可以请求执行某项操作,但无权亲自执行。
整个流程被刻意设计得非常朴素:
Agent 返回一个结构化意图,例如 {'tool': 'fs.read', 'args': {'path': 'notes/trip.md'}}。
在任何操作触及操作系统或网络之前,broker 会解析路径、主机、收件人和参数标志。
manifest 决定允许、拒绝,还是需要审批。
模型收到的要么是经过清理的数据,要么是一个看起来很普通的工具错误。
每一次决策都会追加到本地审计表中。
这样一来,安全问题就从“模型会一直保持规矩吗?”变成了“策略是否准许了这一次具体调用?”后一个问题是可以测试的。
# capabilities.toml
schema = 1
[tools.fs_read]
roots = ['./workspace', './public_data']
never = ['*.pem', '.env', './private']
[tools.http_fetch]
hosts = ['api.github.com', 'raw.githubusercontent.com']
max_bytes = 200000
[tools.notify]
webhooks = ['https://hooks.internal.example/*']
approval = 'human'
# broker.py
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
import fnmatch, json, sqlite3, tomllib
class Verdict(Enum):
ALLOW = 'allow'
DENY = 'deny'
APPROVAL = 'approval'
@dataclass
class Decision:
verdict: Verdict
reason: str = ''
class Broker:
def __init__(self, manifest_path='capabilities.toml', db='audit.sqlite3'):
self.cfg = tomllib.loads(Path(manifest_path).read_text())
self.db = sqlite3.connect(db)
self.db.execute('create table if not exists audit(tool text, args text, verdict text, reason text)')
def _record(self, tool, args, decision):
self.db.execute('insert into audit values (?,?,?,?)', (tool, json.dumps(args), decision.verdict.value, decision.reason))
self.db.commit()
def _path_ok(self, raw):
p = Path(raw).expanduser().resolve()
root_hits = [Path(r).expanduser().resolve() for r in self.cfg['tools']['fs_read']['roots']]
if not any(p == r or r in p.parents for r in root_hits):
return Decision(Verdict.DENY, 'outside approved roots')
s = str(p)
if any(fnmatch.fnmatch(s, pat) or p.name == pat for pat in self.cfg['tools']['fs_read']['never']):
return Decision(Verdict.DENY, 'matches never list')
return Decision(Verdict.ALLOW)
def review(self, tool, args):
table = self.cfg['tools'].get(tool)
if table is None:
d = Decision(Verdict.DENY, 'tool is not declared')
elif tool == 'fs_read':
d = self._path_ok(args.get('path', ''))
elif tool == 'http_fetch':
host = args.get('host', '')
d = Decision(Verdict.ALLOW) if host in table['hosts'] else Decision(Verdict.DENY, 'host not declared')
elif tool == 'notify':
url = args.get('url', '')
ok = any(fnmatch.fnmatch(url, pat) for pat in table['webhooks'])
d = Decision(Verdict.APPROVAL, 'human signoff required') if ok and table.get('approval') == 'human' else Decision(Verdict.DENY, 'webhook not declared')
else:
d = Decision(Verdict.DENY, 'no reviewer implemented')
self._record(tool, args, d)
return d
def invoke(self, tool, args):
d = self.review(tool, args)
if d.verdict is not Verdict.ALLOW:
return {'ok': False, 'error': 'tool unavailable'} # do not leak policy detail
return run_real_tool(tool, args) # inject implementations here
这里最重要的行为是:拒绝必须平淡无奇。如果 Agent 请求读取 ./workspace/../../.env,它得到的仍然是同一种平淡的 tool unavailable 响应形式,看起来与临时故障没有区别。它可以选择其他操作,但无法得知究竟触发了哪条规则,也无法与执行规则的组件讨价还价。
一个小型的 pytest 测试矩阵,比一段聪明的 prompt 更有用:
import pytest
from broker import Broker, Verdict
@pytest.mark.parametrize('path,verdict', [
('workspace/report.md', Verdict.ALLOW),
('workspace/../.env', Verdict.DENY),
('public_data/../private/payroll.csv', Verdict.DENY),
('workspace/key.pem', Verdict.DENY),
])
def test_fs_boundaries(tmp_path, path, verdict):
b = Broker()
assert b.review('fs_read', {'path': path}).verdict is verdict
def test_undeclared_tool_is_closed():
assert Broker().review('shell.exec', {'cmd': 'ls'}).verdict is Verdict.DENY
def test_approval_is_not_silent_allow():
b = Broker()
d = b.review('notify', {'url': 'https://hooks.internal.example/alerts'})
assert d.verdict is Verdict.APPROVAL
然后再加入一套对抗性语料:恳求模型的文档、冒充用户的文档、声称策略已经变更的文档,或是在 markdown 注释中编码指令的文档。断言永远不应该是“模型拒绝了”。正确的断言是:如果模型输出了一个被禁止的意图,审计表中就会出现一条拒绝记录,并且没有发生任何真实的副作用。表现良好的模型当然很好,但真正的控制措施,是一个依然会说“不”的 broker。
要对这种架构进行 red team 测试,并不需要投入巨额预算。你需要的是一个可以反复输入各种奇怪内容的 endpoint,以便在 manifest 不断演进的过程中持续测试。披露声明:本文是 MonkeyCode 产品推广活动的一部分。我曾在开发期间使用 MonkeyCode 提供的免费模型访问权限和免费服务器选项,把它作为一个低门槛环境来运行这些对抗性测试闭环;请把这视为一条可用性说明,而不是 benchmark、配额承诺或生产环境建议。在围绕它构建系统之前,请先查看其当前文档。
如果它适合你的场景,可以把它用于演练:植入恶意文档、重新生成意图、对比审计输出,再收紧 manifest。对于任何面向客户的系统,都应保留同一个 broker,然后根据延迟、数据处理方式和合规需求替换背后的模型。broker 不应该关心意图由哪个模型生成,因为无论由谁生成,意图都是不可信的。
它并不是 sandbox。安全的 allowlist 无法挽救存在漏洞的 fs_read 实现、工具内部泄露的 token 或 parser bug。对于严肃的工作负载,还应将其置于容器、独立用户、seccomp/AppArmor 和网络出口规则的保护之下。
路径检查很容易定义得不够严谨。匹配前应先完成路径解析,并考虑符号链接和大小写不敏感的文件系统,还要对路径分隔符、Unicode、.. 和末尾斜杠进行 fuzz 测试。
审批疲劳确实存在。如果每一次 notify 操作都需要人工点击确认,人们最终会开始无脑批准。应该把审批保留给不可逆或外部可见的操作。
固定流水线从中获益不大。如果你的工作流从不允许模型自由选择工具,那么静态路由可能已经能以更少的机制提供同等保障。
受监管的数据删除、支付、生产环境部署,以及医疗或法律相关操作,所需的保护措施远不止 glob:它们还需要双人控制、速率限制、防篡改日志,有时甚至需要正式评审。
这个理念经得起时间考验,而且非常简单:让高度依赖判断的文本生成远离高度依赖权限的操作执行。让模型提出建议,让平淡无奇的代码作出处理。对策略进行版本化,重放审计记录,并让拒绝看起来与工具普通的倒霉一天没有任何区别。
对于后续操作,你可以考虑屏蔽此人和/或举报滥用行为。