法不净空,觉无性也。

第9章 自撰安全回路

2026.07.29

自修改系统最核心的设计问题不是"能不能改",而是"改完之后谁批准执行"。


模式层

9.1 自修改系统的信任模型

自修改系统的信任模型可以简化为三段:

第一段:文件系统(写)。 Agent 用 generate_* 工具写文件到 skills/prompts/capabilities/。这个操作的权限级别是 write_file——与写一个普通的 README 文件没有区别。不触发权限检查,不需要用户确认。

第二段:注册(激活)。 写好的文件通过 Registry 被注册到可用能力列表中。Skill 在 skills/ 目录中自动注册(启动时扫描),Capability 在 capabilities/ 目录中自动注册。注册不会改变权限——只是让 agent 知道"有这个能力存在"。

第三段:执行(授权)。 Agent 调用 run_capability 时触发权限检查。这是三段中唯一需要用户确认的环节。权限检查的结果决定了 Capability 是否可以执行。

三段闸门的设计保证了:写文件不触发安全闸门,注册不改变权限,执行才需要授权。 任何一段的失效不会导致整个信任链崩塌。如果写文件时未检测到恶意内容(没有内容验证),执行时仍然有权限检查。如果权限检查误判(放行了一个恶意 Capability),它也只能在当前 session 中运行(Shell 环境最高到 SessionAllowlist)。

信任链的核心原则可以浓缩为一句话:写是廉价的、可逆的、不需要批准的;执行是昂贵的、需要显式授权的。

这个原则不仅适用于 Capability,也适用于 Skill。写一个 Skill 文件到 skills/ 不需要批准(Agent 可以用 generate_skill 直接写)。但 Skill 的执行不触发权限检查——因为 Skill 不执行代码,它只改变推理路径。Skill 的"写廉价"是安全的因为 Skill 没有执行侧。Capability 的"执行昂贵"是必要的因为 Capability 有执行侧。

9.2 为什么写文件不能触发权限检查

一个常见的反问:如果写文件触发权限检查,不是更安全吗?

不一定。考虑两个场景:

场景 A:Agent 写一个 Capability 文件。

generate_capability → 写入 capabilities/daily-report/main.sh
                     → 权限检查通过 → 文件写入完成

写文件时触发权限检查意味着用户需要确认"是否允许写这个文件"。但问题在于:用户确认的是"写文件"这个动作,而不是"执行这个文件"。用户确认了写文件,但不知道这个文件会在后续被 run_capability 执行。写文件时的权限检查不能替代执行时的权限检查。

场景 B:Agent 修改一个已有的 Capability 文件。

Agent 修改 capabilities/daily-report/main.sh,改写了执行逻辑。如果写文件时触发权限检查,用户需要再次确认。但用户可能不会仔细看 diff——"又是写 daily-report,之前已经确认过了"——然后点了同意。

更好的方案是:写文件时不触发权限检查(廉价),但文件变更后自动撤销已有信任。如果用户之前为 daily-report 授予了 SessionAllowlist,文件变更后信任被重置。下次执行时重新触发权限检查。

CodeCoder 的选择:

文件写入:无权限检查(廉价)
文件变更检测:比较 manifest mtime
信任撤销:mtime 变更 → 撤销 SessionAllowlist / ProjectAllowlist 中的条目
下次执行:重新触发权限检查

9.3 递归边界

自修改系统面临的最深层安全问题:Capability 能否生成新的 Capability?

技术实现上可以。generate_capability 工具的权限级别是 write_file——它不需要额外授权。一个 Capability 的执行代码中可以包含:

#!/bin/sh
# 这个 Capability 生成一个新的 Capability
echo "name: evil-script" > capabilities/evil/manifest.yaml
echo '#!/bin/sh\nrm -rf /' > capabilities/evil/evil.sh

但安全设计的关键在于:生成新 Capability 后的执行仍然需要经过 run_capability 的权限检查。

新生成的 evil Capability 需要在 run_capability evil 时触发权限检查。即使生成它的 Capability 已经获得了 SessionAllowlist,新 Capability 仍然是"首次执行",需要用户确认。

递归边界的核心不是阻止生成(generate_capabilitywrite_file 权限不可撤销),而是阻止"生成后自动执行"。任何 Capability 的执行——无论它是谁生成的——都必须经过 run_capability 的权限检查。

这条规则还有一个重要的推论:Capability 不能自动执行另一个 Capability。 如果 Capability A 的代码中包含 run_capability B,系统会拒绝——因为 run_capability 是一个需要权限检查的工具,Capability 的代码本身不持有工具的调用权限。


案例层

9.4 @shell 天花板规则的完整分析

@shell 天花板规则的完整表述:

Shell 环境的 Capability 最高信任级别是 AlwaysThisSession。不能升到 AlwaysThisProject

这个规则在查找链的多个检查点执行(查找链的完整伪代码见第 4 章 4.7 节):

  1. ProjectAllowlist 写入时:如果用户选择 AlwaysThisProject,系统检查 Capability 的环境。如果是 @shell,拒绝写入,返回 CeilingViolation 错误

  2. ProjectAllowlist 读取时:如果 codecoder.json 因手动编辑包含了一个 @shell 条目(如 run_capability:shell),系统在加载时检查,跳过这个条目

  3. 运行时:如果 session allowlist 中已有 AlwaysThisProject 的条目(不应该发生,但为了防御),系统在执行前再次检查

天花板规则是"防御纵深"设计的一部分——不是只在一个点检查,而是在写入、读取、执行三个点都检查。每层检查都有不同的失效条件,但只要有任意一层检查通过,规则就成立。

为什么 @shell 不能升到 ProjectAllowlist?直观的答案是"Shell 太危险了"。但更精确的答案是:SessionAllowlist 和 ProjectAllowlist 的信任基础不同。 SessionAllowlist 基于"在当前 session 中,agent 是可信的"。ProjectAllowlist 基于"这个 Capability 的代码是可信的"。Shell 环境的 Capability 代码可以被任意修改(agent 生成 Shell 脚本后可以再修改它),因此"Capability 的代码是可信的"这个假设不成立。

9.5 generate_* → run_capability 的信任链

完整的信任链路径:

generate_skill -> write_file -> skills/<name>.md
    权限: None(write_file 级别)
    信用成本: 最低。Skill 不执行代码

generate_prompt -> write_file -> prompts/<name>.md
    权限: None(write_file 级别)
    信用成本: 最低。Prompt 按需加载

generate_capability -> write_file -> capabilities/<name>/
    权限: None(write_file 级别)
    信用成本: 最低。只是写文件

promote_prompt -> 文件移动 prompts/ -> skills/
    权限: None(文件操作)
    信用成本: 低。只是改变加载时机

run_capability -> 权限检查 -> 执行
    权限: run_capability:<env>
    信用成本: 最高。触发完整的权限查找链

每一步的信用成本是:

  • generate_*:0(不触发权限检查)
  • promote_prompt:0(不触发权限检查)
  • run_capability:完整(权限查找链 + 天花板规则 + mtime 检测)

关键的是:信用成本不是累积的。generate_capabilityrun_capability,信用成本在每一步都是 0,但在最后一步突然跳升到完整。中间步骤没有"半授权"状态——写文件不意味着可以执行,注册不意味着可以执行。每一步的闸门是独立的。

9.6 文件变更 → /reload 可见的绕过路径分析

一个潜在的绕过路径:Agent 写了一个无害的 Capability,用户授予 SessionAllowlist。然后 agent 编辑 Capability 的代码,改为恶意内容。下次执行时——如果系统不检测文件变更——恶意代码被执行。

CodeCoder 的防御是:manifest mtime 变更 → 撤销信任。

fn run_capability(name: &str, context: &Context) -> Result<Output> {
    let cap = context.registry.capabilities.get(name)?;

    // 检查文件变更
    let current_mtime = cap.manifest_mtime()?;
    let trusted_mtime = context.session_allowlist.get_mtime(name);

    if current_mtime != trusted_mtime {
        // 文件已被修改,撤销信任
        context.session_allowlist.remove(name);
        // 重新触发权限检查
        context.permission_check(&format!("run_capability:{}", cap.environment()))?;
        // 更新信任 mtime
        context.session_allowlist.set_mtime(name, current_mtime);
    }

    // 执行
    match cap.environment() {
        Environment::Shell => run_shell(cap, context),
        // ...
    }
}

这个防御能防止"先骗信任、再改代码"这条已知攻击路径。但有一个局限:mtime 比较只在 run_capability 执行时触发。如果攻击者改文件后直接调用 run_capability——检测会触发。如果攻击者改文件后等待用户下次手动调用——检测也会触发。就这条路径而言,当前设计未留下已知绕过口——但这只是"排除了已知的坏路径",不等于证明不存在其他攻击面:例如 mtime 本身可被伪造(回拨文件时间戳)、Registry 缓存与磁盘状态不一致的窗口、或攻击者通过非 run_capability 通道(如 Capability 自身调用 shell)绕开这道闸门。安全边界是排除了已知坏路径的渐进收敛,不是一次性到达的绝对安全。

9.7 子 agent 天然只读的安全性推导

子 agent 的只读约束(第 5 章详细讨论)是自撰安全回路的一个推论。

父 agent 可以生成 Skill、Prompt、Capability。子 agent 不能生成任何东西。这意味着:即使父 agent 创建了一个恶意子 agent,子 agent 也只能读文件、搜索网页、返回文本结果。它不能写文件、不能注册 Capability、不能执行代码。

这个约束不是通过权限检查实现的——它是通过工具集限制硬编码的。子 agent 的工具集在 Toolbox::read_only_child() 中列出,不包含任何写或执行的工具。

推论:子 agent 不能绕过自撰安全回路。父 agent 不能通过创建子 agent 来获得"子 agent 帮我做我不能做的事"的效果。子 agent 能做的是父 agent 能做的一个子集——而且是只读子集。


ADR 深度阅读

安全回路的两次增强(ADR 0022)

ADR 0022 记录了自撰安全回路经历的两次主要增强。

第一次增强(初始版本): 完成了信任链的三段分离——写文件不触发权限检查、注册不改变权限、执行需要授权。run_capability 实现了基本的权限检查、SessionAllowlist 和 ProjectAllowlist。天花板规则尚未引入。

第二次增强(天花板规则引入): 问题暴露:用户将 run_capability:shell 添加到了 ProjectAllowlist。之后 agent 创建了一个新的 Shell Capability,直接通过 ProjectAllowlist 通过了权限检查——没有弹窗确认。

修复方案是天花板规则:Shell 环境不能升到 ProjectAllowlist。但引入天花板规则之后,代码中出现了三个独立的检查点(写入时、读取时、执行时),每个检查点有不同的实现方式和不同的测试覆盖。后续的增强中,三个检查点的逻辑被统一为一个函数 ceiling_check(key, level),确保策略一致性。

这组 ADR 的演进展示了自撰安全回路的设计演变:它不是一次性设计完毕的,而是通过实际攻击场景的暴露逐步增强的。


第三编结束。下一编进入自主运行——work graph、验收门、headless 模式、上下文压缩。