法不净空,觉无性也。

第8章 长出新手脚——Capability 与执行环境

2026.07.29

Agent 写的代码谁来执行?在哪里执行?活多久?——这三个问题决定了 Capability 的架构。


模式层

8.1 Environment × Lifecycle 矩阵

Capability 执行时的两个核心维度是 Environment(在哪里执行)和 Lifecycle(活多久)。

Environment(执行环境):

  • Shell:agent 在宿主机上直接执行脚本或二进制。最大权限——能访问文件系统、网络、进程。破坏力等同于用户在终端按回车
  • Wasm:在 WebAssembly 沙箱中执行。编译时隔离——Wasm 模块只能访问通过 WASI(WebAssembly System Interface)接口暴露的资源。没有文件系统访问(除非显式挂载)
  • Docker:在容器中执行。文件系统隔离——通过挂载卷控制访问范围。网络隔离——通过 docker network 配置

三个环境的信任成本依次递增:Shell 最危险(需要最高信任成本)、Wasm 居中(编译时隔离)、Docker 最安全(完整容器隔离)。

Lifecycle(生命周期):

  • OneShot:执行一次,输出结果,进程退出。适合:编译脚本、数据迁移、一次性分析
  • OnDemand:按需调用,每次调用创建新的进程。适合:代码生成、定时任务、API 请求
  • Persistent:常驻服务,跨 turn 存活,通过网络/IPC(Inter-Process Communication)调用。适合:后台监听器、状态维护服务

3×3 矩阵:

OneShotOnDemandPersistent
Shell执行一次 shell 脚本按需启动 shell 进程常驻后台进程(如 http server)
Wasm执行一次 wasm 模块按需调用 wasm 函数Wasm 常驻实例(受限)
Docker运行一次容器按需创建容器常驻容器服务

实际常用的组合是:Shell/OneShot(日常脚本)、Shell/Persistent(后台服务)、Docker/OneShot(安全隔离的一次性任务)。Wasm 环境在 CodeCoder 中的使用频率较低——因为 Wasm 生态的工具链支持尚未成熟,且 Wasm 模块的调试体验不如 Shell 脚本。

8.2 三种沙箱的信任递进

Environment 的选择决定了 Capability 的信任成本。

Shell 环境的信任成本最高。Shell 环境能访问宿主机的一切:文件系统、网络、进程、环境变量、内核接口。一个 Shell 环境的 Capability 可以删除任意文件、读取任意环境变量、连接任意网络地址。

因此 Shell 环境的天花板规则最严格:最高只能到 SessionAllowlist,不能升到 ProjectAllowlist。 任何 Shell 环境的 Capability,即使经过多次执行验证,也不能被授予跨 session 的自动授权。

Wasm 环境的信任成本居中。Wasm 模块在编译时被沙箱化——它只能访问通过 WASI 接口显式暴露的资源。没有文件系统(除非挂载)、没有网络(除非代理)、没有进程控制。

但 Wasm 的局限性也在这里。许多常见的 agent 操作——读文件、跑 shell 命令——在 Wasm 中不可用,除非通过 WASI 扩展接口支持。Wasm 适合纯计算的 Capability(数据转换、格式检查、静态分析),不适合需要访问外部资源的 Capability。

Docker 环境的信任成本最低(即最安全)。Docker 容器提供完整的文件系统隔离——容器内的 /etc/passwd 不是宿主的 /etc/passwd。网络隔离——容器只能访问配置中允许的网络地址。进程隔离——容器内不能看到宿主的进程列表。

Docker 环境可以升到 ProjectAllowlist——因为即使 Capability 代码有恶意行为,它也被限制在容器内。容器可以挂载宿主机目录作为卷,但挂载的路径和权限由 manifest 声明,而不是由 Capability 的代码控制。

环境隔离级别最高信任级别典型用例
Shell无隔离SessionAllowlist日常脚本、编译、部署
Wasm编译时隔离ProjectAllowlist纯计算任务
Docker容器隔离ProjectAllowlist不可信代码、多租户任务

8.3 执行后端路由

run_capability 的核心逻辑是按 manifest 声明的 Environment 路由到对应的执行后端:

fn run_capability(name: &str, context: &Context) -> Result<Output> {
    // 1. 从 Registry 获取 Capability
    let cap = context.registry.capabilities.get(name)?;

    // 2. 检查权限
    let perm_key = format!("run_capability:{}", cap.environment());
    context.permission_check(&perm_key)?;

    // 3. 按 Environment 路由
    match cap.environment() {
        Environment::Shell => run_shell(cap, context),
        Environment::Wasm => run_wasm(cap, context),
        Environment::Docker => run_docker(cap, context),
    }
}

每个执行后端的实现:

fn run_shell(cap: &Capability, context: &Context) -> Result<Output> {
    let entrypoint = &cap.manifest.entry;
    let output = std::process::Command::new("sh")
        .arg(entrypoint)
        .current_dir(cap.dir())
        .output()?;
    Ok(Output::from_process(output))
}

fn run_wasm(cap: &Capability, context: &Context) -> Result<Output> {
    let wasm_path = cap.dir().join(&cap.manifest.entry);
    // Wasm 运行时
    let engine = wasmtime::Engine::default();
    let module = wasmtime::Module::from_file(&engine, &wasm_path)?;
    // … 设置 WASI 接口,执行模块
    // Wasm 执行尚未完全实现(详见 ADR 0021)
    return Err("Wasm execution not yet supported".into());
}

fn run_docker(cap: &Capability, context: &Context) -> Result<Output> {
    let image = &cap.manifest.docker_image;
    let entrypoint = &cap.manifest.entry;
    let mount_volumes = &cap.manifest.volumes;

    // 构建 docker run 命令
    let mut cmd = std::process::Command::new("docker");
    cmd.args(["run", "--rm"]);
    for vol in mount_volumes {
        cmd.args(["-v", &format!("{}:{}", vol.host, vol.container)]);
    }
    cmd.args([image, entrypoint]);
    let output = cmd.output()?;
    Ok(Output::from_process(output))
}

run_wasm 返回错误——Wasm 执行在 CodeCoder 中尚未完全实现。原因见 ADR 深度阅读。


案例层

8.4 Capability manifest 声明式设计

每个 Capability 在 capabilities/ 目录下有自己的子目录,包含一个 manifest.yaml 和一个或多个入口点文件。

# capabilities/daily-report/manifest.yaml
name: daily-report
description: "生成每日代码审查报告"
version: 1

environment: shell
lifecycle: oneshot

entry: report.sh

permissions:
  - run_command:git
  - write_file:daily-report-*

volumes: []    # 仅 Docker 环境使用
docker_image: ""  # 仅 Docker 环境使用

manifest 的核心字段:

  • environment / lifecycle:声明 Capability 在什么环境运行、活多久
  • entry:入口点文件路径,相对于 Capability 目录
  • permissions:执行这个 Capability 需要哪些额外的权限(可选,用于细化权限申请)
  • volumes / docker_image:Docker 环境的专用配置

manifest 的声明式设计使 run_capability 可以在不执行代码的情况下判断:这个 Capability 需要什么环境、是否需要额外权限、需要多久。它像一个"执行前声明"——在触发实际执行之前,系统已经知道了 Capability 的全部执行需求。

8.5 OneShot 示例:每日报告生成

# capabilities/daily-report/manifest.yaml
name: daily-report
environment: shell
lifecycle: oneshot
entry: report.sh

report.sh 的内容:

#!/bin/sh
# 收集当日 git 日志
git log --since="1 day ago" --format="%h %s" > /tmp/commits.txt
# 统计文件变更
git diff --stat $(git rev-list --max-parents=0 HEAD)..HEAD

agent 调用 run_capability daily-report 时,Shell 后端执行 report.sh,捕获 stdout 和 stderr,返回给 agent。执行完毕后进程退出,没有残留状态。

OneShot 的优点是简单——没有状态管理、没有进程监督、没有端口冲突。每次执行都是干净的起点。

8.6 Persistent 示例:HTTP 健康检查服务

# capabilities/health-check/manifest.yaml
name: health-check
environment: shell
lifecycle: persistent
entry: server.sh

server.sh 启动一个简单的 HTTP 服务,定期检查系统状态:

#!/bin/sh
PORT=${PORT:-8080}
while true; do
    echo -e "HTTP/1.1 200 OK\n\n$(date): system healthy" | nc -l -p $PORT
done

agent 调用 run_capability health-check 时,Shell 后端启动 server.sh 作为后台进程,将 PID 注册到 RunningServiceTable

struct RunningService {
    pid: u32,
    capability_name: String,
    started_at: Instant,
    port: Option<u16>,
}

struct RunningServiceTable {
    services: HashMap<String, RunningService>,
}

impl RunningServiceTable {
    fn register(&mut self, cap: &Capability, pid: u32) {
        self.services.insert(cap.name.clone(), RunningService {
            pid,
            capability_name: cap.name.clone(),
            started_at: Instant::now(),
            port: cap.manifest.port,
        });
    }

    fn unregister(&mut self, name: &str) {
        if let Some(service) = self.services.remove(name) {
            // 终止进程
            std::process::Command::new("kill")
                .arg(service.pid.to_string())
                .spawn().ok();
        }
    }
}

Persistent Capability 的关键设计点:

  • 进程监督:RunningServiceTable 记录每个 Persistent Capability 的 PID。当 agent 退出或 daemon 关闭时,通过 unregister 终止所有常驻服务
  • 崩溃恢复:Capability 崩溃后,系统不会自动重启。RunningServiceTable 将它的状态标记为 Failed,而不是自动 spawn 新进程。原因:Capability 的崩溃可能是永久性问题(代码 bug、配置错误),自动重启会导致循环崩溃
  • 跨 session 不持久:RunningServiceTable 在内存中,不持久化到磁盘。daemon 重启后,Persistent Capability 需要重新启动

ADR 深度阅读

Wasm Capability 的源代码→wasm 编译 deferred(ADR 0021)

ADR 0021 记录了 Wasm 执行端的状态:Capability 的 manifest 可以声明 environment: wasm,但 run_capability 的 Wasm 后端只接受预编译的 .wasm.wat 文件。从 Rust 源代码编译到 Wasm 模块的路径未实现。

为什么会 deferred?原因有两个。

第一,编译工具链的依赖问题。 从 Rust 源代码编译到 Wasm 模块需要 wasm-packwasm-gc 等工具链,这些工具链的安装和版本管理增加了 Capability 的运行环境复杂度。一个 Shell 脚本只需要 sh——几乎每个系统都有。一个 Wasm 模块需要 wasmtime 或类似的运行时——不是每个系统都有。

第二,Capability 的代码来源。 当前的 Capability 以 Shell 脚本为主,因为 agent 生成的代码最自然的形态就是 Shell 脚本。从 Shell 脚本到 Wasm 模块的转换不是直接的——需要重写为 Rust 或其他语言。这增加了 agent 生成 Capability 的认知成本。

Wasm 执行端的 deferred 状态意味着 environment: wasm 在当前版本中是"保留但不激活"的——manifest 可以这样写,但执行时会返回"Wasm 运行时未就绪"的错误。这与"隔离不静默降级"的原则一致:不会因为 Wasm 不可用就自动落到 Shell 环境执行。

Docker 不可用时为什么不静默降级

如果 Capability 声明了 environment: docker,但服务器上没有安装 Docker,系统应该怎么做?

选 A:报错,"Docker 不可用,Capability 无法执行" 选 B:在 Shell 环境中执行(因为脚本可能是跨环境的)

CodeCoder 选 A。不允许静默降级。 理由是:manifest 中声明 environment: docker 是 Capability 作者(agent)的意图。如果系统在 Docker 不可用时偷偷落到 Shell 环境,Capability 的信任模型就被破坏了——用户批准了"在 Docker 中执行",但实际是在 Shell 中执行的。

静默降级的后果是:Docker 环境可以升到 ProjectAllowlist,Shell 环境不能。如果系统静默降级到 Shell,一个本应只能到 SessionAllowlist 的 Capability 被授予了 ProjectAllowlist——安全闸门被绕过了。

这个设计决策与天花板规则一致:环境声明是安全承诺的一部分。 声明 environment: docker 意味着"我只需要容器级别的隔离"。系统有义务确保这个承诺成立,或者在它无法成立时拒绝执行。


下一章从安全角度重新审视三分架构——自撰安全回路如何防止 agent 在自我进化过程中越界。