用残留测试检查 Agent 工作区是否清干净
文章通过金丝雀字符串追踪提示词、Shell 历史和日志中的数据残留,梳理开发服务器与模型端点之间的信任边界。测试包含用于验证扫描器有效性的阳性样本,以及用于检查清理结果的阴性样本。
AI 深度解读
把开发服务器关掉,不代表工作区已经忘记你的数据。作者在一次临时工作区测试后,仍在外发提示词记录、Shell 历史和一个没打开过的日志文件里找到了测试字符串。没有组件遭到入侵,数据只是按各自的默认行为留了下来。
文章给出的办法很具体:先放入假凭据,确认扫描器能找到它,再销毁工作区,检查它是否消失。这套测试值得借鉴,但它只能验证部分本地残留,不能证明模型服务商删除了提示词,而且文中的脚本只是模板,没有执行验证。
怎样证明临时工作区真的清理过
把仓库搬到免费服务器,再让 Agent 调用免费模型端点,会新增两道信任边界:服务器能接触哪些文件,模型请求能带走哪些内容。只检查最终提交的代码,覆盖不到工作目录、命令历史和日志。
作者建议用金丝雀字符串(canary)代替真实凭据。它是一段容易搜索的唯一标记,生成方式是 CANARY="CANARY-$(openssl rand -hex 8)"。模板把它导出为环境变量,再写入 $WS/.canary.env,内容为 DEMO_TOKEN。WS 默认指向 $HOME/work/agent-ws。
接下来,让 Agent 执行一个会读取 .canary.env 的小任务。任务有没有业务价值不重要,关键是让标记经过真实读取路径。
扫描必须先跑阳性样例:使用 python3 scan_residue.py,传入 --root "$WS" --root "$HOME/work" --canary "$CANARY" --expect present。如果找不到标记,应当先排查扫描路径、扫描器和任务执行情况。
先证明扫描器能发现已知残留,再相信它报告的零命中。
随后才进入销毁和阴性检查,验证预期清理范围内是否还有标记:
./teardown.sh "$WS" # 停止实例、删除卷、清理历史
python3 scan_residue.py \
--root "$WS" --root "$HOME/work" --root "$HOME" \
--canary "$CANARY" --expect absent
要关注最后一行的 --expect absent:同一个扫描器切换了断言,检查目标从「必须找到」变成「必须找不到」。
模板中,阳性检查有命中返回 0,没有命中返回 1;阴性检查没有命中返回 0,发现残留返回 2。CI 因而可以把清理要求变成明确的失败条件。不过,原文没有提供 teardown.sh 的实现,注释里的停止实例、删除卷和清理历史,都需要使用者自己落实。
请求发出去之前,也要留下可检查的证据
只扫描仓库目录,无法判断 Agent 是否把文件内容带进模型请求。作者因此要求在任务运行前加入本地请求记录器。
模板通过 MODEL_EGRESS_LOG="$HOME/work/egress.ndjson" 指定记录位置,建议代理追加 {ts, path, body},再原样转发请求。默认不记录响应体,也不记录请求头,因为请求头可能带有密钥。
这能回答一个直接的问题:测试标记有没有进入准备发给模型的请求正文?但原文只描述了代理行为,没有给出代理实现,也没说明如何强制所有请求经过它。设置环境变量本身并不等于完成拦截。
记录器还会制造新的敏感文件。egress.ndjson 可能保存完整提示词,既然测试要求清理后找不到标记,就必须明确它何时取证、何时删除。否则,观察数据流的工具也会成为残留来源。
扫描器的覆盖范围同样有限:它跳过 .git、node_modules、__pycache__,并试图排除 .cache/pip;大于 8 * 1024 * 1024 字节的文件也不会读取。这里还有个实现细节:目录过滤比较的是单个目录名,.cache/pip 这种带路径分隔符的条目,不能按预期排除对应目录。
免费额度为什么也需要硬性闸门
文章还处理了另一种失控:Agent 陷入重试循环,持续消耗免费额度。作者没有给出某家服务的固定配额,而是要求从配置读取 free_quota,因为额度可能调整。
下面的模板在请求前预留输入、最大输出和安全余量:
from dataclasses import dataclass
@dataclass
class Ledger:
free_quota: int # 从配置读取,由操作者提供,会随时间变化
spent: int = 0
max_completion: int = 2048
safety_margin: int = 50_000
def admit(prompt_tokens: int, ledger: Ledger) -> tuple[bool, str]:
reserve = prompt_tokens + ledger.max_completion + ledger.safety_margin
remaining = ledger.free_quota - ledger.spent
if reserve > remaining:
return False, f"refused: need {reserve}, have {remaining}"
return True, f"allowed: {remaining - reserve} left after this call"
要关注 return False 的拒绝分支:调用方必须据此停止 Agent 循环,只打印警告没有约束力。
这里的 2048 和 50_000 是模板参数,不是服务商承诺。代码也没有展示实际用量如何回写 spent,或并发请求如何共同扣减预算,因此它只是准入判断,还不是完整的额度管理器。
作者把这套测试放进了 MonkeyCode 免费模型和免费服务器的评估场景,同时披露文章属于产品推广。免费能力是否可用、配额和服务器规格如何,原文都要求以当时条款为准。
我的判断
这篇最有价值的地方,是把「临时环境应该会清理」改成可执行的断言。与只写一份清理检查表相比,退出码能让遗漏真正阻断流程。
但「阴性通过」的含义必须收窄。模板遇到不存在的根目录会跳过,遇到 OSError 也会继续。如果目录不可读,最终仍可能得到零命中。阳性测试只要求某处找到一次标记,也不能证明每个目录都扫描成功。
它还只匹配原始字节。Base64 编码、分块发送和部分读取可能绕过检测;本地文件消失,更不能证明远端模型端点忘记了提示词。作者对此有明确说明。
与请求前的外发允许列表相比,残留扫描更适合检查清理结果;允许列表更适合限制数据流向。两者解决的问题不同,不能互相替代。对于 Agent 能读取的文件,原文还建议在工具层增加允许列表,并单独测试。
文章没有给出对照实验,也没有展示模板的实际运行报告,因此不能据此认定方案覆盖充分,更不能把它当成 MonkeyCode 的安全背书。对于不带凭据和客户数据、只运行三十分钟的简单原型,作者也认为测试成本可能高于收益。
能拿来做什么
- 在前端或 Node 仓库接入云端 Agent 前,先用独立测试目录和假
DEMO_TOKEN跑通读取、请求记录和清理流程,再接触真实配置。 - 给临时工作区流水线加入先阳性、后阴性的扫描步骤,并把路径不可读、扫描失败与正常零命中区分开。
- 在日常 AI 编程工具中,把额度拒绝接到循环停止逻辑,同时补齐实际用量回写,避免重试持续消耗预算。
- 为请求记录文件规定清理时点,并分别验证文件读取允许列表和外发允许列表,避免把事后扫描当成全部防护。