第 3 卷
复制代码 1{
2"type": "rule.update", 3"id": "rule-7a9546c1",
4"state": "active", 5"at": 1750000000001,
6"by": "人(/firewall approve)", 7"patch": {}
8}
两条都带 by 与 at,标注这次变更来自谁、发生在什么时候。by 的来源有三类:人的命令、Agent 的工具,以及人绕过账本直接改文件的带外修改。少了这两个字段,事后就分不出
“这条规则是 Agent 在级别 2 下自己转正的还是人批的”,也说不清几笔改动的先后;有了它们,逐条读事件就能复原一次规则变更的经过,这正是“人掌握决定权”能被审计的关键。
上面两条分属其中两类。本讲一共四类事件:rule.add 新增、rule.update 修改、 rule.reject 驳回或下线、rule.remove 删除。
再说折叠:拿一个初始状态,把事件按顺序逐个作用上去,走完得到最终状态。本讲是从一个空 Map 出发,事件依次作用,最后剩下什么规则,就是当前规则集。实现就是一个 Map 从
头走到尾:
复制代码
| 1 | exportfunction fold(events: RuleEvent[]): Rule[] { |
|---|---|
| 2 | constmap =newMap<string, rule>() |
| 3 | for(conste of events) { |
| 4 | if(e.type ==='rule.add') { |
| 5 | map.set(e.id, { ...e.input, id: e.id, state: e.state ??'candidate'}) // |
| 6 | }elseif(e.type ==='rule.update') { |
| 7 | constr = map.get(e.id) |
| 8 | if(r) map.set(e.id, { ...r, ...e.patch, state: e.state ?? r.state }) |
| 9 | }elseif(e.type ==='rule.reject') { |
16return [...map.values()] 17}</string, rule>
最后看落盘。内存中折叠出的规则集还要落成数据面读的文件(rules.ledger.json),写的时候排队执行,避免并发互相覆盖;先写临时文件再 rename,不留半截。数据面判定请
求时读的就是这个文件,所以文件一落地,这条规则就算实际生效了。至于它加载之后怎么匹配请求,本讲不展开,如果你感兴趣可以看第 18 讲的引擎代码。
以下是删掉 rules.ledger.json 后首次运行 npm run start:ch19 的真实输出。草稿 id 随机,“Agent 起草候选”与“Agent 测试”两步走真实 DeepSeek 模型,规则名、条件与
token 用量每次运行都可能不同。这个示例每次运行都换一个新的落盘目录,所以权威不会累积上一次的事实;但那个规则文件留在磁盘上不会自己消失,再跑一次时,它里面的规则会在
启动时被补记回权威,事件数与规则数因此越跑越多。想复现这组数字,先删掉那个文件。
复制代码 1=== /firewall level 1(默认)===
2自主级别已设为 1:起草自动;批准与驳回要人确认 3
| 4 | === Agent起草候选->人审批->发布(真实模型)=== |
|---|---|
| 5 | 模型原始回复: {"name":"block ip 1.2.3.4","action":"deny","when":{"field":"ip","op": |
| 6 | 模型token用量:in=175out=40 |
| 7 | 草稿rule-7a9546c1起草为候选: {"name":"block ip 1.2.3.4","when":{"field":"ip","op" |
| 8 | 人审批后rule-7a9546c1已active |
9 10=== 带外修改:人直接改文件,补记对账 ===
11带外新增 rule-outofband 已补记 12
13=== facts 校验 === 14一致=true,事件数=3,当前规则数=2
15
| 16 | ===重启恢复(新进程从磁盘上的JSONL恢复权威)=== |
|---|---|
| 17 | 恢复后规则数=2 |
| 19 | ===账本事实写进dsh会话日志(会话日志是权威,文件只是产物)=== |
| 20 | 权威:会话日志内事实=3条,折叠出规则数=2 |
| 26 | Agent起草候选rule-99b8429d(state=candidate) |
|---|---|
| 27 | 人审批后rule-99b8429d state=active |
| 28 | facts一致=true,事件数=5 |
29
本讲小结
这一讲把规则从“纯手工操作”推进到“可生成、可审批、可生效”。
受约束生成:模型按 prompt 写明的字段约定起草,代码侧 isValidRuleWhen 把关, field / op 只能取约定值、value 非空,吐出的规则一定合法;第一稿一律是候选,不
直接生效。
审批闸与自主级别:审批闸装在工具流水线前两站,tools/pre-execute 按级别拦写操作、tools.guard 用白名单限工具范围;自主级别分为三档:0 / manual / off、1
(默认)、2,决定闸开多大,人用 /firewall 命令来掌控。
账本:变更记成 rule.add、rule.update、rule.reject、rule.remove 四类事件,追加进会话日志,折叠出规则集再原子写落成数据面读的文件,文件只是产物,权威在
会话日志。
这背后真正的变化是规则从“一个配置文件”变成“有变更记录、能审计、能回放”的账本。
Agent 的权限和能力由人来掌控。下一讲我们看 Agent 怎么从异常请求里长出候选规则。
注:本讲 GitHub 链接
思考题
动手实操:示例在 src/firewall/ch19/,直接改代码、重跑验证。
- 跑 approval-e2e.test.ts,它走真实的 tools.execute 流水线,你会看到 0 级别
的写工具被 tools/pre-execute 拦下;再跑 approval.test.ts,它只断言