系列《给 AI 上缰绳:从周末 MVP 到多路指挥台》第 3 篇。
上一篇交完底:v2 的信任层——授权合同、围栏、四段链——在测试环境里全绿,但它有个尴尬的处境:独立存在。心脏再健康,没装进身体,就不会跳。信任层要证明自己被用起来了,就得跑在一个真实的 Agent 平台上,被真实任务连续捶打。
10 月 1 日,v3 开工。这篇讲的就是这具身体:怎么从零搭起来、工程量多大、信任层怎么嵌进去,以及——这部分篇幅最大——实战里踩出来的每一个坑。
先交代口径
一句话定位:v3 是一个受治理的渗透测试 Agent 平台。对话式 Agent 在 Kali 沙箱里执行攻击动作,治理层约束每一次越界,最终产出可复核的 PTES 报告。v2 那套信任层没有重造一遍,它变成了这个平台的一个器官。
体量先摆出来:自有代码 382 件、88,712 行,占总量 85.2%(总量 465 件 / 104,118 行,vendored 第三方资产单列、不计入自有)。测试基线:后端 904 passed + 24 skipped(928 项),双前端合计 124 个用例,全绿。
出处:v3 仓 README(核心指标表 + 平台进化节,归属盘点 2026-10-12 实跑口径)。
时间账:10 月 1 日开工。头三天立地基——分层骨架、沙箱、治理接入、测试基线、记忆系统;10 月 4 日定工程路线,5 日五批连打(四批同日批结),6 日终验收官、git 建仓;10 月 7 日起进入真实任务。从开工到 git 首个 commit,六天。
搭建:这具身体怎么立起来
这次的工程纪律比前两代都重——每个工程批次动工前,先落一份批内细案:这批建什么面、验收线是什么、动哪些文件。细案我批了才动工,收口拿数据说话。
动工前的选型,四个决策。
后端骨架。Flask,五层:api 管协议、services 管业务、models 管持久化、agents 管 Agent 的工具与权限、core 管横切配置。分层的理由很俗:平台代码要长期演化,层不清楚的地方就是将来出事故的地方。Agent 相关的东西单独一层,因为它动得最凶——全平台动得最凶的三件活代码里,工具面、权限面在这层,会话运行时在 services 层。
数据面。四个中间件各管一摊:MySQL 管业务与审计——关系数据加事务保证,审计链的落链点在这里;Redis 管实时事件——Agent 执行过程的事件流由它承载;Qdrant 管向量检索——记忆与经验卡的语义层;Neo4j 管图——资产与发现之间的攻击图关系。为什么不用一个库包打天下:审计要事务、事件要低延迟、检索要向量、攻击图要图查询,四种负载挤一个库,每一种都别扭。v2 的黑板没有消失,拆进了这套结构里。
执行面。这个决策是 v1 的教训直接换来的。v1 用 SSH 连一台远程 Kali:交互式工具最别扭——nmap 扫到一半要等、sqlmap 跑起来要盯输出,全在 SSH 通道里硬模拟;环境还不隔离,每次作业在同一台机器上叠状态。v3 换成 Docker 容器沙箱:每个交战独立容器、独立作战网络,Agent 在 tmux 会话里跑交互式工具——交互式工具本来就是 tmux 的主场。隔离从"约定"变成硬保证:沙箱网络里只有授权目标可达,越界在网络上就不通。
前端双端加 BFF。用平台作战的人和管平台的人是两类角色,两种界面:用户端是作战工作台——对话流、沙箱、审批;管理端是治理面——审计、用户、配置。中间一层 BFF 做实时推送网关:Agent 的执行过程以事件流推到前端,作战的人看得见它每一步在干什么——不是事后翻日志,是直播。
| v2:独立信任层 | v3:完整平台 |
|---|---|
| 授权合同(令牌·四元组) | client-web 用户端 · admin-web 管理端 |
| 黑板 / 网关 / 工具层 | BFF 实时推送网关(WS 事件流) |
| 熔断 / 审计哈希链 | Flask 后端(API·服务·模型·智能体·核心) |
| 87+25 测试集 | MySQL · Redis · Qdrant · Neo4j |
| (独立库,测试环境里跑) | Kali 沙箱容器(tmux · 独立作战网络) |
| — | 信任层=后端治理面,嵌在三条链路上 |
出处:v2 侧见 fence 仓(第 2 篇已引);v3 侧见 v3 仓 README 架构节。
选型定完,v2 信任层的接入点也定了。它是本篇的主角,嵌在三条链路上:
交战创建时。授权文件(RoE)上传、边界固化。固化那一刻铸造签名令牌,客户合同哈希锚定进令牌;同时往审计哈希链上落一条 roe_frozen 事件,载荷里带边界原文的 SHA-256——事后改写边界原文,链上的哈希对不上,一眼假。链写失败呢?整个固化事务回滚。fail-closed:治理动作不落链,就不许成立。
动作执行时。沙箱网络执法,v2 的 87 例越界拦截测试口径原样搬进平台——同一套执法面换了宿主。中间还有一道人工闸:高危动作在执行前拦下来等人放行(HITL 审批),Critical 级的放行必须填理由,空理由的放行请求直接 422 拒回,审计状态不动。
交战收尾时。run 终局捕获证据包:审批记录、审计链、决策轨迹、交战配置等七个条目打成包,manifest 加签,报告中心里一键复算验证。最终产出双签的 PTES 报告——37.6KB 的实物文件,不是模板截图。
地基期立的还有两样。一是记忆系统:三层记忆(会话内 local / 项目 project / 跨项目 user)加经验卡蒸馏——Agent 打完一仗,值得留下的东西自动沉淀成卡片,喂给语义检索,下一仗能查。二是测试基线:动工头一批交付的不是功能,是回归测试——之后每一批工程都在这条基线上收口,只增不减。当代码增速远超人工能读完的速度,"先有尺子再动工"不是洁癖,是必需品。
五批工程:88,000 行怎么验收
骨架和地基立起来之后的问题:功能面怎么建、建完怎么证明每一面都过硬?
88,000 行的量级带来一个前两代没有的问题:我读不完每一行。读不完,验收就不能靠读,得靠门禁——每个工程批次的收口标准是机械的,门禁过了才算过。
我的做法是五批工程,逐面建、逐面过堂:鉴权面、管理端、用户端、运行时核心、最后收口终验。一批一个面,每批同一条验收纪律,三条:
- 全量回归零失败——批前锚定基线,只增不减,到收官锚在 928 项;
- 功能矩阵逐行打钩——动工前把全部功能面登记成矩阵,每批完成后逐行核对,任何功能面不得丢失或降级;
- 更强证明点——每个过堂的面至少拿出一个可测试的更强证明,凭测试或实测数据呈报,不接受"感觉更好了"。
鉴权批。全平台统一 JWT 签发、验签、刷新,三角色 RBAC。更强证明点是那张矩阵:111 个端点 × 7 种身份态(admin / operator / auditor / 未登录 / 过期 / 伪造签名 / 已拉黑)全组合测试,越权请求 403 且留审计痕。另加一道结构性守门:启动时对账鉴权矩阵与代码装饰器,对不上直接拒启。新端点忘了登记权限,服务起不来——防的是"漏配"这种最无聊也最常见的事故。
运行时核心批。主战场,三巨头:会话运行时(1,940 行)、工具面(1,491 行)、权限面(1,084 行)。这批的更强证明点是可证明性本身——决策轨迹全程留痕,每个"决策→工具→参数→结果"进 decision_trace;预算熔断从"会话中断了"升级为可审计遥测,stop_reason 和用量落库。顺带一个架构决策:平台最初有个独立的 kill-chain 编排器,判定为与运行时职责重叠后整体退役,2,006 行删除。删功能也是工程——删错比写错难挽回,所以删除走留档加回归全绿。
最后一批收口做的是开源就绪:脱敏三判据(凭据 / 敏感路径 / 邮箱占位公开面零命中)、.env 全键占位(缺省值全是 CHANGE_ME,启动告警)、git 历史干净——先脱敏、后建仓,敏感信息零进历史。v3 仓至今保持"planned but not yet done"的开源状态:就绪了,还没开。这是如实声明,不是欲擒故纵。
出处:内部五批复执档案(2026-10-05/06);v3 git 首 commit edb5be2(2026-10-06,开源就绪态闭包)。
88,000 行里我逐行读过的可能不到一成,但每批收口时,矩阵、回归、证明点三道门是机械的。人不读代码,人定门禁。门禁错了全错——这个风险的交代放在失效边界。
淬火:实战里踩出来的坑
10 月 7 日起平台跑真实任务。头几天是功能增强(检索优化、模型解析链路),从 12 日开始进入修复高峰:12 到 14 日,三天,40 多个 commit 落库。这一节的故事全部来自这段时间。
毒记忆:它记住了"工具坏了"
平台有 Task 系列工具(建任务、报进度),跑在宿主环境上。某天宿主环境出问题——缺了执行文件,Task 工具调用报错。这是个普通故障,我修了:平台接管 Task 工具,宿主侧短路。
修完发现没用。Agent 起跑后照样声明"TaskCreate 面板因宿主故障不可用(与既往一致)",直接绕开规划工具。查它的记忆库,找到了:那次故障被它写进了跨 run 的持久记忆层,五条,是之前几个 run 攒下的。系统提示里写得明明白白的规划教导,被这几条旧记忆压着——经验性记忆压过指令,这是模型的通病,不是它不听话。
我第一反应是再改提示词:加粗、置顶、说三遍。改到一半想明白了——没用。提示词是读取时的对抗,记忆是存储时的污染;源头还在写,读取侧怎么对冲都是打地鼠。最后落了四层:
- 写入侧:记忆分层纪律——环境与工具的临时状态(工具报错、宿主故障、瞬时网络异常)只许写 local 层(会话结束即弃),严禁写持久层。环境故障会被平台修复,持久化的环境状态必然过期,过期就是毒;
- 存储侧:五条毒记忆软删(留删除时间戳,不物理删,保审计口径);
- 读取侧:系统提示权威声明——Task 工具由平台接管、始终可用,历史记忆里的"宿主故障"均为过时记录,禁止据此绕开;
- 检测侧:清毒纪律入册,部署时执行。
0fe4eda fix(prompt): 记忆分层纪律——环境状态只进 local 层,堵毒记忆写入源头
bdba07f fix(prompt): 计划工具平台接管权威声明——对冲旧记忆毒知识
出处:v3 git log(2026-10-14,两 commit body 含五条毒记忆实证)。
这个坑的教训比事故本身值钱:Agent 的记忆是资产,也是污染面。它会学习是好事,但它学到的东西里有一类是"对当时环境的正确观察"——这类知识的有效期跟环境绑定,环境一变就成了毒。你在修机器,还得管它的记忆卫生。
僵尸 run 与死循环:三种死法
run 的死亡形态,实战里集齐了三种。
第一种,"没声了"。线程退出了,没有任何终局事件,界面停在中途,刷新也没用。用户能发现的死亡不是最糟的,最糟的是死亡本身不可观测。
第二种,僵尸。交战中断后 run 的注册表状态还是 running,平台重启,它们变成僵尸——状态在内存里,重启即丢,沙箱容器会话也跟着消失。
第三种,声明死循环。Agent 想结束任务,调 task_complete——引导文件里写的是裸名,调不通;它换成纯文本说"我做完了",纯文本轮没有出口判定,对话继续;它再说一遍"我做完了"……实证现场:48 轮对话。
三种死法的修复各自落位:中断路径收尸 + 启动时扫尸(重启后把孤儿 run 状态收敛);收尸必补终局事件(界面刷新就能看到真实终态);run 线程退出必查终局,没有终局的退出本身就是审计事件;纯文本轮补出口、修正引导。
8b1adba fix(runtime): 僵尸 run 双兜底——中断收尸 + 启动扫尸
2220b68 fix(runtime): 事件流闭合——收尸必补终局事件,根治刷新无效的死亡定格
5bef90c fix(runtime): 静默退出审计——run 线程退出必查终局,消灭「没声了」类死亡
4319c7f 根治完成声明死循环(conv=48 实证:task_complete 裸名引导调不通 + 纯文本轮无出口)
出处:v3 git log(2026-10-14)。
死循环这条的教训单独说一句:Agent 的"完成"是个协议,不是一句话。它说"做完了"不算完,得有机器认的出口——而出口本身也是会坏的,坏了的表现形式是无限礼貌地重复告别。
清单:其余的坑不展开,但都在案
三天 40 多个 commit,拣有普适性的列几条:
| 坑 | 一句话 |
|---|---|
| 令做 pwn,它自作主张刷 crypto | 计划里声明了题型,执行时换了顺手的——计划声明题型强校验,声明什么就必须执行什么 |
| 模型全程英文输出 | 用户看不懂自己的 Agent 在说什么——中文输出硬规则进系统提示 |
| 时间显示偏早 8 小时 ×3 处 | 三处绕过集中解析、各算各的时区——收敛到单点 |
| 重命名输入框输一个字母被全选覆盖 | focus+select 放在 nextTick 里,手快的用户抢在 select 前敲了字——改同步执行,竞态窗口消除 |
| 问答被扩张为执行 | 用户问个问题,Agent 当成任务开干——白名单制任务边界 |
| 模型幻觉弃枪 | 它"以为"预算不够提前放弃——系统提示注入战役预算真数 |
出处:v3 git log(2026-10-12~14 各 commit);内部《实战反馈修复批次技术总结报告》(2026-10-13)。
选题边界那条多说半句:它和 v2 的越界是同一个思路——"想做什么"和"被允许做什么"之间必须有机器执法的缝,提示词糊不上。
保命钱:两段熔断
预算的故事值得单独讲,因为它是"代价"最直观的样本。
单轮预算熔断上线后,实战里发现一个尴尬:熔断掐死的常常是收尾。一个 run 的预算烧到线,熔断——但收尾动作(写记忆、沉淀经验卡、生成报告、声明完成)也要花钱,而且这些动作恰恰发生在最贵的总结阶段之后。掐在线上,Agent 死在"马上就能写遗书"的地方,前面烧的钱等于白扔。
方案是两段熔断:进攻段给预算的 85%,烧到线熔断;熔断后开收尾段,给剩下 15%——保命钱。收尾段里攻击类工具一律拒绝(审计留痕),只准收尾四件套:写记忆、写经验卡、生成报告、声明完成。
进攻段 85% ——烧到线——→ 熔断报错 ——一键收尾——→ 收尾段 15%(攻击工具全禁)
定价侧还有个意外发现:Agent 运行时对兼容端点上的第三方模型(非原生模型 ID),按最高档牌价计价。实测一个 run:输入 121K + 缓存读 546K + 输出 3.3K,按那个档估 3.09 美元,恰好压过预算 3 美元的 85% 线(2.55)触发熔断——按便宜档算只有 0.57,解释不通。所以预警估算按保守档(宁可早提醒),精确熔断以实测为准;显示层加了折算系数,日后换端点只改一处常量。这个发现本身写进了回归测试,成了实证锚点。
出处:内部《实战反馈修复批次技术总结报告》第九章(run 66 实证,含 $3.09 > $2.55 回归锚点)。
断头管线:功能存在 ≠ 功能生效
最后一批坑最扎心:三条功能面全部存在、全部完好——但从未被触发过。
杀伤链泳道恒空、审计链"链完整(0 条)"、知识三库全 0。我第一反应是前端 bug。查完发现前端忠实呈现了一个从未发生过的动作:规划生成器全仓零调用方,没有任何代码路径会调它;审计链只接了审批和熔断两条路径,日常主路径(边界固化、状态变更)压根不落链;知识三库的写入工具功能齐全,但系统提示从没引导 Agent 用它——Agent 边做边只写记忆和经验卡,结构化战果对平台不可见。
另一个同类:证据包捕获函数里有个 except Exception: pass,失败被静默吞掉。实战 10 个 run 全部走完了终局链(沉淀出的 157 张经验卡覆盖全部 run),但证据包一张没有。失败原因不可考——因为失败本身无日志。修复不是"修好捕获",是让失败可观测:吞异常处补 warning、堆栈、上下文。吞掉的语义保留(捕获失败绝不影响 run 收敛),但从无声变有声。
这两类问题的共性比单个 bug 重要:机制存在 ≠ 机制生效。功能写了、测试绿了、页面在,没有任何东西触发它,它就等于不存在。而"没被触发"不报错,它是静默的。实战是唯一的照妖镜——这也是"信任层必须装进真实平台"的另一个理由:在测试环境里,断头管线和接通的管线长得一模一样。
这套方案的失效边界
单焦点是硬上限。整个平台是单交战、单焦点设计:一个作战室、一条对话流、一个沙箱。它把"一次交战"做成了闭环,代价是人只能盯一路。实战里我很快撞到这条墙:Agent 在跑,我得盯着;切出去干别的,回来它可能已经死循环了 48 轮。这不是实现缺陷,是架构定位——一个人盯一路,人的注意力就是吞吐量上限。
淬火修的是踩到的坑。40 多个修复 commit 是"发现了的"问题数,不是"存在过的"问题数。没踩到的坑照常存在,下一个死循环可能换了形态在路上。
门禁模式有个单点:定门禁的人。88,000 行我逐行读不过一成,验收全靠矩阵、回归、证明点三道机械门。这套体系防得住"实现骗验收",防不住"门禁本身定错"——矩阵漏登记一个功能面,零回退核对就漏一列。门禁的设计错误是我的人为错误,会被机械地放大。
治理的数字是测试口径。87 例越界拦截来自测试集实测;实战期间没有真实越界事件被拦下——这既说明 Agent 守规矩,也意味着执法面没被真刀真枪地验过。拦得住测试里的越界,和拦得住实战中的越界,是两个命题。
系列导航(按已发列表滚动):
- F0《我开源了一个能证明 AI 干过什么的工具》
- 第 1 篇《一个周末,我让大模型帮我造出了第一个安全 Agent》
- 第 2 篇《缰绳:一句提示词防不住一个 Agent》
- 本篇:第 3 篇《身体:把信任层装进一个真实平台——88,000 行怎么验收》
项目入口:GitHub TLotU-lab/cyberseccat-fence——v2 信任层的公开工件仓(授权合同模型、87+25 测试集、M3/M4 门记录)。v3 平台本体处于开源就绪态、尚未公开,本篇 v3 数字待开仓后可对账。
下一篇:事故单元。机制存在不等于机制生效——三起真实事故,三套写过了、测过了的机制,在真实路径上接到的值是错的。授权额度凭空变无限、七分钟零 token 僵死、48 轮停不下来——同一套形状,三种死法。