第8章 长出新手脚——Capability 与执行环境
2026.07.29Agent 写的代码谁来执行?在哪里执行?活多久?——这三个问题决定了 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 矩阵:
| OneShot | OnDemand | Persistent | |
|---|---|---|---|
| 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-pack 或 wasm-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 在自我进化过程中越界。