任务队列解决了"Agent 做什么",但没有解决"Agent 在哪里做、能做什么、做完怎么交付"。本文设计一个生产级 Agent 执行器,用 Git Worktree 实现文件隔离,用命令代理实现权限分级,用 Artifact Store 把 diff、日志、测试结果结构化回传工作台,让 Agent 从"能写代码"变成"能安全地写代码并交付审查"。

Agent 执行器设计:沙箱、工作树、命令代理与结果回传

任务队列解决了"Agent 做什么",但没有解决"Agent 在哪里做、能做什么、做完怎么交付"。本文设计一个生产级 Agent 执行器,用 Git Worktree 实现文件隔离,用命令代理实现权限分级,用 Artifact Store 把 diff、日志、测试结果结构化回传工作台,让 Agent 从"能写代码"变成"能安全地写代码并交付审查"。

一、为什么不能直接让 Agent 跑在主进程里

把 Agent 执行器当成"开个子进程跑一下"是最常见的误解。先看一个典型事故:

text
2026-05-22 14:32  Agent 接到任务:"修复支付模块的金额计算精度问题"
2026-05-22 14:33  Agent 修改了 src/payment/calculate.js
2026-05-22 14:33  Agent 想跑测试,执行了 npm test
2026-05-22 14:34  测试脚本里有 postinstall:rm -rf /tmp/build && ./scripts/sync-prod-data.sh
2026-05-22 14:34  sync-prod-data.sh 把生产数据库的 10 万条订单拉到了本地
2026-05-22 14:35  Agent 把包含真实订单号的日志作为上下文发给了模型

这条事故链路暴露了 4 个执行器应该管住、但被放任的问题:

问题 直接运行的后果 执行器该做的事
文件系统无隔离 Agent 改到主分支正在开发的文件,互相覆盖 每个任务独占一个 Git Worktree
命令无分级 一条 rm -rfcurl prod-db 就能毁掉环境 命令分类 + 白名单 + 审批路由
网络无策略 Agent 能把代码、日志、密钥 POST 到任何地方 出站白名单 + 域名审计
结果无归档 改了什么、跑了什么测试、卡在哪里全靠人翻终端 Diff / 日志 / 测试结果结构化回传

把执行器独立出来的核心动机不是"性能",而是"可控"。一个失控的 Agent 比没有 Agent 更危险——它写得快、改得多、出错时已经把主分支污染了。

二、执行器总体架构

执行器由四个核心组件协作完成一次任务:

text
┌─────────────────────────────────────────────────────────────────┐
│                        Task Dispatcher                         │
│                 (领取任务、分配执行器、管理超时)                   │
└────────────────────────────┬────────────────────────────────────┘
                             │  dispatch(task_id, payload)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                         Agent Executor                         │
│                                                                 │
│  ┌──────────────┐  ┌───────────────┐  ┌────────────────────┐  │
│  │   Worktree   │  │   Sandbox     │  │  Command Proxy     │  │
│  │   Manager    │→ │   (fs, net,   │→ │  (classify → run   │  │
│  │              │  │    env)       │  │   or ask approval) │  │
│  └──────┬───────┘  └───────┬───────┘  └─────────┬──────────┘  │
│         │                  │                     │              │
│         │           ┌──────▼───────┐      ┌──────▼────────┐   │
│         │           │ Agent 进程    │      │ 命令审计日志   │   │
│         │           │ (Claude/...)  │      │               │   │
│         │           └──────┬───────┘      └───────────────┘   │
│         │                  │                                   │
│         ▼                  ▼                                   │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │                    Artifact Collector                    │ │
│  │  (git diff, test report, log tail, error trace)          │ │
│  └───────────────────────────┬──────────────────────────────┘ │
└──────────────────────────────┼─────────────────────────────────┘
                               │  upload(task_id, artifacts)
                               ▼
                    ┌─────────────────────┐
                    │   Artifact Store    │
                    │   (S3 / OSS / Minio) │
                    └─────────────────────┘

四个组件各自的职责边界很清晰:

  • Worktree Manager:给每个任务克隆或检出一个独立的工作目录,任务结束清理
  • Sandbox:限定 Agent 进程的文件访问范围、网络出站、环境变量
  • Command Proxy:拦截 Agent 发起的所有命令,按风险分级执行或拒绝
  • Artifact Collector:任务结束前,把改动、日志、测试结果打包上传到 Artifact Store

三、Worktree:让每个任务拥有独立的工作目录

Git Worktree 是 Agent 执行器的基础设施。它比"克隆整个仓库"快 10 倍,比"在同一目录并发修改"安全 100 倍。

3.1 Worktree 生命周期

text
任务入队
    │
    ▼
┌──────────────────────────┐
│ 1. git worktree add      │  → .worktrees/task-<id>-<branch>
│    --detach <base-commit> │    (基于目标分支最新 commit)
└──────────┬───────────────┘
           │
           ▼
┌──────────────────────────┐
│ 2. Agent 在此目录工作     │  → 所有文件改动只影响此 worktree
│    修改代码、跑测试、提交  │    主分支完全不受影响
└──────────┬───────────────┘
           │
           ▼
┌──────────────────────────┐
│ 3. 收集 artifact         │  → git diff, git log, test report
│    打包上传 Artifact Store │
└──────────┬───────────────┘
           │
           ▼
┌──────────────────────────┐
│ 4. git worktree remove   │  → 清理磁盘
│    (任务成功时保留分支)    │    失败任务立即清理
└──────────────────────────┘

3.2 Worktree 管理代码

python
import subprocess
import shutil
from pathlib import Path

class WorktreeManager:
    """Git Worktree 生命周期管理"""

    def __init__(self, repo_root: str, worktree_base: str = ".worktrees"):
        self.repo_root = Path(repo_root)
        self.worktree_base = Path(worktree_base)
        self.worktree_base.mkdir(exist_ok=True)

    def create(self, task_id: str, base_branch: str = "main") -> Path:
        """为任务创建独立 worktree,返回路径"""
        # 基于目标分支最新 commit,而不是当前 HEAD
        base_commit = self._run_git("rev-parse", base_branch).strip()
        branch_name = f"agent/task-{task_id}"
        worktree_path = self.worktree_base / f"task-{task_id}"

        # 创建新分支 + worktree
        self._run_git(
            "worktree", "add", "-b", branch_name,
            str(worktree_path), base_commit
        )
        return worktree_path

    def collect_diff(self, worktree_path: Path, base_branch: str = "main") -> str:
        """收集 worktree 相对 base_branch 的所有改动"""
        return self._run_git(
            "-C", str(worktree_path),
            "diff", f"origin/{base_branch}...HEAD"
        )

    def cleanup(self, worktree_path: Path, keep_branch: bool = False):
        """清理 worktree,可选保留分支(用于 PR)"""
        self._run_git("worktree", "remove", "--force", str(worktree_path))
        if not keep_branch:
            # 提取分支名
            branch = worktree_path.name.replace("task-", "agent/task-")
            self._run_git("branch", "-D", branch)

    def _run_git(self, *args) -> str:
        result = subprocess.run(
            ["git"] + list(args),
            cwd=self.repo_root,
            capture_output=True, text=True, timeout=30, check=True
        )
        return result.stdout

关键设计点:

  • 基于 base_branch 的 commit 而不是 HEAD:避免任务开始时拿到主分支上一个未合并的实验性 commit
  • 分支命名规范 agent/task-<id>:方便批量查询、清理、审计
  • cleanup 时可选保留分支:成功的任务保留分支,后续可以开 PR;失败任务直接删除

四、Sandbox:文件系统、网络与环境变量隔离

4.1 文件系统隔离策略

Agent 不应该能读到整个仓库以外的文件,更不应该能改 /etc~/.ssh~/.aws 这种目录。Sandbox 通过白名单方式控制:

yaml
# executor-sandbox.yaml
sandbox:
  # 文件系统:只允许访问 worktree 目录 + 显式声明的共享目录
  filesystem:
    allowed_paths:
      - path: "{{worktree}}"           # 当前任务的 worktree(动态替换)
        mode: readwrite
      - path: "/tmp/agent-{{task_id}}" # 任务专属临时目录
        mode: readwrite
      - path: "{{repo_root}}/node_modules"
        mode: readonly                  # 依赖目录只读
      - path: "{{repo_root}}/docs"
        mode: readonly                  # 文档目录只读
    denied_paths:
      - "/etc"
      - "/var"
      - "~/.ssh"
      - "~/.aws"
      - "~/.gnupg"
      - "/root"
      # 即使 worktree 在 /home 下,也不允许越权到父目录
      - "{{worktree}}/.."

  # 网络:默认拒绝所有出站,只放行显式域名
  network:
    default_policy: deny
    allowed_egress:
      - host: "registry.npmjs.org"     # npm 安装依赖
        ports: [443]
      - host: "pypi.org"               # pip 安装依赖
        ports: [443]
      - host: "github.com"             # 拉 tag、release
        ports: [443]
      - host: "api.openai.com"         # 模型 API(如果需要)
        ports: [443]
      - host: "api.anthropic.com"
        ports: [443]
    denied_egress:
      - "0.0.0.0/8"                   # 内网全禁
      - "10.0.0.0/8"
      - "172.16.0.0/12"
      - "192.168.0.0/16"
      - "169.254.0.0/16"              # 云元数据服务

  # 环境变量:脱敏,禁止把密钥传给 Agent 子进程
  env:
    deny_patterns:
      - "*PASSWORD*"
      - "*SECRET*"
      - "*TOKEN*"
      - "*KEY*"
      - "AWS_*"
      - "DATABASE_URL"
    allow_explicit:
      NODE_ENV: "test"
      CI: "true"

这套配置的核心原则是:默认拒绝,显式允许。任何 Agent 想访问的路径、域名、环境变量,都必须在配置里写明。

4.2 为什么禁止内网和元数据服务

云环境里最容易被忽略的攻击面是 169.254.169.254——AWS/GCP/Azure 的元数据服务。一个没被沙箱限制的 Agent 只需要一行 curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ 就能拿到实例的 IAM 临时凭证。内网其他服务(数据库、内部 API)同理:Agent 不应该有直接访问的能力,所有外部调用必须通过命令代理走审批。

五、命令代理:分级执行与审批路由

Agent 要执行命令时,不应该直接调用 shell,而必须经过 Command Proxy。Proxy 对每条命令做三件事:解析 → 分级 → 路由

5.1 命令分级函数

python
import re
from enum import Enum
from dataclasses import dataclass
from typing import Optional

class RiskLevel(Enum):
    SAFE = "safe"            # 只读、无副作用,直接执行
    MODERATE = "moderate"    # 有副作用但可逆,记录审计后执行
    HIGH = "high"            # 高风险,需人工审批
    FORBIDDEN = "forbidden"  # 绝对禁止,直接拒绝

@dataclass
class CommandVerdict:
    level: RiskLevel
    reason: str
    approver: Optional[str] = None  # 高风险命令的指定审批人

class CommandClassifier:
    """命令分级器:根据预定义规则对命令分类"""

    # 只读命令:查看状态、读文件、跑测试
    SAFE_PATTERNS = [
        r"^ls(\s|$)", r"^cat\s+", r"^head\s+", r"^tail\s+",
        r"^grep\s+", r"^find\s+", r"^wc\s+", r"^file\s+",
        r"^git\s+(status|log|diff|show|branch|tag)\b",
        r"^npm\s+test(\s|$)", r"^npm\s+run\s+(test|lint|build)(\s|$)",
        r"^pytest\b", r"^jest\b", r"^mocha\b",
        r"^echo\s+", r"^pwd$", r"^which\s+", r"^type\s+",
    ]

    # 中等风险:可逆的修改
    MODERATE_PATTERNS = [
        r"^git\s+(add|commit|checkout|reset|revert)\b",
        r"^npm\s+install\s+", r"^pip\s+install\s+",
        r"^mkdir\s+", r"^cp\s+", r"^mv\s+", r"^touch\s+",
        r"^chmod\s+", r"^chown\s+",
    ]

    # 高风险:不可逆或影响外部
    HIGH_PATTERNS = [
        r"^git\s+(push|merge|rebase)\b",
        r"^rm\s+-rf\s+/", r"^rm\s+-rf\s+~",
        r"curl\s+.*\s(-X\s*POST|-X\s*PUT|-X\s*DELETE)\s+",
        r"npm\s+publish\b", r"pip\s+upload\b",
        r"docker\s+(push|rm|stop)\b",
        r"kubectl\s+(apply|delete|rollout)\b",
        r"^ssh\s+", r"^scp\s+",
    ]

    # 绝对禁止
    FORBIDDEN_PATTERNS = [
        r"rm\s+-rf\s+/\s*$",          # 删根目录
        r":\(\)\s*\{\s*:\|:&\s*\};:",  # fork bomb
        r"mkfs\.",                      # 格式化磁盘
        r"dd\s+if=.+of=/dev/",         # 写设备
        r"chmod\s+-R\s+777\s+/",       # 全网开放权限
        r"curl.*169\.254\.169\.254",    # 元数据服务
        r"env\s*\|\s*.*PASSWORD",       # 暴露密钥
    ]

    def classify(self, command: str) -> CommandVerdict:
        """对命令分级,返回 verdict"""
        cmd_stripped = command.strip()

        # 先检查绝对禁止
        for pattern in self.FORBIDDEN_PATTERNS:
            if re.search(pattern, cmd_stripped):
                return CommandVerdict(
                    level=RiskLevel.FORBIDDEN,
                    reason=f"命中禁止规则:{pattern}"
                )

        # 再检查高风险
        for pattern in self.HIGH_PATTERNS:
            if re.search(pattern, cmd_stripped):
                return CommandVerdict(
                    level=RiskLevel.HIGH,
                    reason=f"高风险命令,需人工审批:{cmd_stripped[:80]}"
                )

        # 检查中等风险
        for pattern in self.MODERATE_PATTERNS:
            if re.search(pattern, cmd_stripped):
                return CommandVerdict(
                    level=RiskLevel.MODERATE,
                    reason=f"可逆修改,记录审计:{cmd_stripped[:80]}"
                )

        # 最后检查只读
        for pattern in self.SAFE_PATTERNS:
            if re.search(pattern, cmd_stripped):
                return CommandVerdict(
                    level=RiskLevel.SAFE,
                    reason=f"只读命令:{cmd_stripped[:80]}"
                )

        # 未知命令默认为高风险——宁可误拦不可放过
        return CommandVerdict(
            level=RiskLevel.HIGH,
            reason=f"未识别命令,默认高风险:{cmd_stripped[:80]}"
        )

5.2 分级处理策略

text
┌────────────────────────────────────────────────────────────┐
│                    Command Proxy                            │
└─────────────────────────────┬──────────────────────────────┘
                              │
            ┌─────────────────┼─────────────────┐
            │                 │                 │
            ▼                 ▼                 ▼
      ┌──────────┐      ┌──────────┐      ┌──────────┐
      │  SAFE    │      │ MODERATE │      │   HIGH   │
      │ 直接执行  │      │ 记录+执行 │      │  暂停等待 │
      │ 无审批    │      │ 无审批    │      │  人工审批 │
      └────┬─────┘      └────┬─────┘      └────┬─────┘
           │                 │                 │
           │                 │         ┌───────┴────────┐
           │                 │         │ 审批人收到通知   │
           │                 │         │ 点"同意""拒绝"│
           │                 │         │ 超时 5min 自动拒│
           │                 │         └───────┬────────┘
           │                 │                 │
           ▼                 ▼                 ▼
      ┌─────────────────────────────────────────────┐
      │        命令审计日志(所有分级都记录)         │
      │  task_id | command | level | verdict | ts   │
      └─────────────────────────────────────────────┘

FORBIDDEN 级别的命令在任何情况下都不会执行,连审批通道都不开放——Agent 收到拒绝响应后应该意识到这条路径不通,换一种方式完成任务。

六、Artifact 收集与回传

Agent 完成任务后,工作台需要看到三样东西:改了什么(diff)、跑得怎么样(测试结果)、卡在哪里(日志)。Artifact Collector 负责把这些结构化打包。

6.1 Artifact 结构

text
artifact-store/
└── tasks/
    └── task-<id>/
        ├── manifest.json           # 元数据:任务ID、开始/结束时间、使用模型
        ├── diff.patch              # git diff 完整输出
        ├── files-changed.json      # 改动文件列表 + 每文件增删行数
        ├── commands.jsonl          # 执行过的所有命令(按时间)
        ├── agent-log.txt           # Agent 对话 / 思考日志
        ├── test-report.xml         # 测试结果(JUnit 格式)
        ├── coverage.json           # 覆盖率报告(如果有)
        ├── errors.json             # 错误与异常汇总
        └── review-summary.md       # 给 reviewer 的摘要(Agent 自生成)

6.2 收集代码

python
import json
from datetime import datetime, timezone
from pathlib import Path

class ArtifactCollector:
    """任务结束后收集所有产物"""

    def __init__(self, worktree: Path, task_id: str, store_root: Path):
        self.worktree = worktree
        self.task_id = task_id
        self.store_dir = store_root / "tasks" / f"task-{task_id}"
        self.store_dir.mkdir(parents=True, exist_ok=True)
        self.wt_manager = WorktreeManager(str(worktree.parent))

    def collect_all(self, base_branch: str, started_at: str) -> dict:
        """收集全部 artifact,返回 manifest"""
        manifest = {
            "task_id": self.task_id,
            "base_branch": base_branch,
            "started_at": started_at,
            "finished_at": datetime.now(timezone.utc).isoformat(),
            "artifacts": {},
        }

        # 1. Diff
        diff = self.wt_manager.collect_diff(self.worktree, base_branch)
        (self.store_dir / "diff.patch").write_text(diff)
        manifest["artifacts"]["diff"] = {
            "path": "diff.patch",
            "size_bytes": len(diff.encode()),
            "files_changed": diff.count("\ndiff --git"),
        }

        # 2. 改动文件列表
        files_changed = self._collect_files_changed(base_branch)
        (self.store_dir / "files-changed.json").write_text(
            json.dumps(files_changed, indent=2)
        )
        manifest["artifacts"]["files_changed"] = {
            "path": "files-changed.json",
            "count": len(files_changed),
        }

        # 3. 测试结果(如果跑了的话)
        test_report = self.worktree / "test-results.xml"
        if test_report.exists():
            (self.store_dir / "test-report.xml").write_text(
                test_report.read_text()
            )
            manifest["artifacts"]["test_report"] = {
                "path": "test-report.xml",
                "passed": self._count_test_results(test_report, "passed"),
                "failed": self._count_test_results(test_report, "failed"),
            }

        # 4. 命令执行日志
        commands_log = self._collect_commands_log()
        (self.store_dir / "commands.jsonl").write_text(commands_log)
        manifest["artifacts"]["commands"] = {
            "path": "commands.jsonl",
            "count": len(commands_log.strip().splitlines()),
        }

        # 5. 自生成 review summary
        review = self._generate_review_summary(diff, files_changed)
        (self.store_dir / "review-summary.md").write_text(review)
        manifest["artifacts"]["review_summary"] = {
            "path": "review-summary.md",
        }

        # 写入 manifest
        (self.store_dir / "manifest.json").write_text(
            json.dumps(manifest, indent=2)
        )
        return manifest

    def _collect_files_changed(self, base_branch: str) -> list[dict]:
        """解析 diff 得到每个文件 + 增删行数"""
        stat = self.wt_manager._run_git(
            "-C", str(self.worktree),
            "diff", f"--numstat", f"origin/{base_branch}...HEAD"
        )
        result = []
        for line in stat.strip().splitlines():
            if not line.strip():
                continue
            added, deleted, path = line.split("\t")
            result.append({
                "path": path,
                "added": int(added) if added.isdigit() else 0,
                "deleted": int(deleted) if deleted.isdigit() else 0,
            })
        return result

    def _count_test_results(self, report_path: Path, status: str) -> int:
        """简易 JUnit 解析"""
        content = report_path.read_text()
        return content.count(f'<failure') if status == "failed" else content.count('testsuite')

    def _collect_commands_log(self) -> str:
        """从命令代理收集本次任务执行过的所有命令"""
        log_path = Path("/var/log/agent-proxy") / f"{self.task_id}.jsonl"
        return log_path.read_text() if log_path.exists() else ""

    def _generate_review_summary(self, diff: str, files: list[dict]) -> str:
        """给 reviewer 的摘要——让 reviewer 30 秒内知道改了啥"""
        total_added = sum(f["added"] for f in files)
        total_deleted = sum(f["deleted"] for f in files)
        file_list = "\n".join(
            f"- `{f['path']}` (+{f['added']}/-{f['deleted']})" for f in files
        )
        return (
            f"## 任务 {self.task_id} 改动摘要\n\n"
            f"**统计**:{len(files)} 个文件,+{total_added}/-{total_deleted} 行\n\n"
            f"**改动文件**:\n{file_list}\n\n"
            f"**完整 diff**:见 `diff.patch`\n"
        )

6.3 回传时机与失败处理

Artifact 收集必须在 worktree 删除之前完成。一个常见的 bug 是:清理 worktree 后再去 git diff,结果拿到空字符串。正确的顺序是:

  1. Agent 报告完成(或失败)
  2. Collector 收集 diff、文件列表、日志
  3. Collector 打包上传到 Artifact Store
  4. 确认上传成功(校验 checksum)
  5. 最后才清理 worktree

任何一步失败,worktree 都保留,由死信处理流程人工检查。

七、参数说明

参数 类型 默认值 说明
worktree_base Path .worktrees/ Worktree 根目录,所有任务的工作目录都创建在这里
base_branch String main 任务基于哪个分支创建 worktree,通常是目标发布分支
sandbox.allowed_paths List 文件系统白名单,支持 {{worktree}}{{task_id}} 模板变量
sandbox.denied_paths List 文件系统黑名单,优先级高于白名单
sandbox.network.default_policy Enum deny 网络默认策略:deny(默认拒绝)或 allow(默认允许)
sandbox.network.allowed_egress List 出站白名单,按 host + ports 配置
sandbox.env.deny_patterns List 环境变量过滤通配符,匹配的变量不会传给 Agent 子进程
proxy.safe_patterns List 见代码 只读命令正则列表,命中后直接执行
proxy.moderate_patterns List 见代码 可逆修改命令正则列表,记录审计后执行
proxy.high_patterns List 见代码 高风险命令正则列表,需人工审批后执行
proxy.forbidden_patterns List 见代码 绝对禁止命令正则列表,任何情况下都不执行
proxy.approval_timeout Integer 300 高风险命令审批超时秒数,超时视为拒绝
artifact.store_root Path ./artifacts/ Artifact 存储根目录
artifact.upload_timeout Integer 60 上传超时秒数
artifact.keep_on_failure Boolean true 收集失败时是否保留 worktree

八、落地检查清单

  • Worktree 隔离:每个任务有独立 worktree,任务之间文件系统互不干扰
  • 分支命名规范:所有 Agent 创建的分支以 agent/task-<id> 开头,便于审计和批量清理
  • 基础 commit 正确:Worktree 基于目标分支最新 commit 创建,而不是 main 的 HEAD(避免拉到未合并内容)
  • 文件系统白名单:Agent 只能读写 worktree 和显式声明的共享目录,无法访问 ~/.ssh/etc 等敏感路径
  • 网络白名单:默认拒绝所有出站,仅放行 npm/pypi/github/模型 API 等必要域名
  • 内网和元数据屏蔽169.254.0.0/1610.0.0.0/8 等内网段完全禁止访问
  • 环境变量脱敏*PASSWORD**SECRET**TOKEN*AWS_* 等变量不会传给 Agent 子进程
  • 命令分级完整:所有 Agent 发起的命令都经过 Command Proxy,按 SAFE/MODERATE/HIGH/FORBIDDEN 分类处理
  • 高风险命令审批git pushnpm publishkubectl apply 等必须人工审批,超时 5 分钟自动拒绝
  • 绝对禁止命令rm -rf /mkfsdd、元数据服务调用等命令在任何情况下都不会执行
  • 命令审计日志:所有命令(无论分级)都记录到 commands.jsonl,包含 task_id、command、level、verdict、timestamp
  • Artifact 完整:任务结束后能拿到 diff、文件改动列表、测试结果、命令日志、review 摘要五件套
  • Artifact 先于清理:worktree 只有在 artifact 上传成功后才能被清理
  • 失败保留现场:artifact 收集失败或任务失败时,worktree 保留供人工排查,不自动删除

九、真实经验与踩坑

9.1 不要用 Docker 容器替代 Git Worktree

第一版执行器我们用 Docker 容器做隔离:每次任务起一个容器,把代码挂载进去。听起来完美,实际踩了三个坑:

问题一:npm install 慢到无法接受。容器内没有 node_modules 缓存,每个任务要重新安装依赖,光 this 一步就要 2-5 分钟。Git Worktree 共享 .git 目录和全局 node_modules(如果项目配置了),安装时间降到秒级。

问题二:容器内的 Git 操作复杂度高。Agent 想在容器里跑 git rebase -igit cherry-pick,都要处理TTY、credential helper 等问题。而 Worktree 就是原生 Git 操作,Agent 不需要感知。

问题三:容器日志收集不便。容器的 stdout/stderr 要通过 Docker daemon 拿,跨主机更麻烦。Worktree 模式下 Agent 是普通子进程,日志直接写文件,收集简单。

现在的架构是:Worktree 管代码隔离 + seccomp/AppArmor 管系统调用隔离 + 网络命名空间管网络。Docker 留给长期运行的服务(比如 Artifact Store、审批服务),不给短任务用。

9.2 命令分级正则的陷阱

命令分级听起来简单,做起来坑很多。最典型的问题是 shell 转义和管道组合

bash
# 看起来安全,实际危险
ls -la | xargs -I {} rm {}          # ls 命中 SAFE,整体却是 rm
git log --oneline; rm -rf /tmp/data  # git log 命中 SAFE,分号后是 FORBIDDEN
echo $(cat /etc/shadow)              # echo 命中 SAFE,实际在读敏感文件

我们踩过的坑和解法:

  • 问题:只匹配命令开头 → 被管道、分号、子 shell 绕过
  • 解决:先做 shell 解析,把命令拆成语法树,对每个叶子命令单独分级,整体级别取最高
  • 兜底:无法解析的命令一律按 HIGH 处理,让人工审批
python
# 错误做法:只匹配整条字符串
if command.startswith("ls"):
    return RiskLevel.SAFE  # 危险!ls | xargs rm 也通过了

# 正确做法:解析管道和分号,对每段分级
def classify_pipeline(pipeline: str) -> RiskLevel:
    segments = split_pipeline(pipeline)  # 按 | ; && || 拆分
    levels = [classify_single(seg) for seg in segments]
    # 返回最高风险等级
    return max(levels, key=lambda l: RISK_ORDER.index(l))

9.3 Artifact 上传失败不要静默吞掉

早期版本里,如果 Artifact Store 暂时不可用,Collector 会 log 一条 warning 然后继续。结果就是:任务显示 "completed",但工作台上看不到任何 diff 和测试结果——reviewer 没法审查,等于白做。

现在的策略是:上传失败 = 任务未完成。Collector 会重试 3 次(间隔 1s/5s/30s),都失败就把任务标记为 artifact_failed,worktree 保留,人工处理。这条规则看起来严格,但它避免了"完成了但其实没完成"这种更糟的情况。

9.4 Worktree 一定要定期清理

Agent 任务跑飞了、进程被 kill 了、网络中断了……各种原因都会留下孤儿 worktree。一个月下来磁盘可能被占满。我们加了一个 daily cron:

bash
# 清理超过 7 天、且没有对应活跃任务的 worktree
0 3 * * * /opt/agent-platform/scripts/cleanup-orphan-worktrees.sh --max-age=7d

脚本逻辑:遍历 .worktrees/ 下所有目录,检查是否有对应状态为 executing 的任务,如果没有且创建时间超过 7 天,则 git worktree remove + 删除对应分支。