Agent 执行器设计:沙箱、工作树、命令代理与结果回传
任务队列解决了"Agent 做什么",但没有解决"Agent 在哪里做、能做什么、做完怎么交付"。本文设计一个生产级 Agent 执行器,用 Git Worktree 实现文件隔离,用命令代理实现权限分级,用 Artifact Store 把 diff、日志、测试结果结构化回传工作台,让 Agent 从"能写代码"变成"能安全地写代码并交付审查"。
一、为什么不能直接让 Agent 跑在主进程里
把 Agent 执行器当成"开个子进程跑一下"是最常见的误解。先看一个典型事故:
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 -rf 或 curl prod-db 就能毁掉环境 |
命令分类 + 白名单 + 审批路由 |
| 网络无策略 | Agent 能把代码、日志、密钥 POST 到任何地方 | 出站白名单 + 域名审计 |
| 结果无归档 | 改了什么、跑了什么测试、卡在哪里全靠人翻终端 | Diff / 日志 / 测试结果结构化回传 |
把执行器独立出来的核心动机不是"性能",而是"可控"。一个失控的 Agent 比没有 Agent 更危险——它写得快、改得多、出错时已经把主分支污染了。
二、执行器总体架构
执行器由四个核心组件协作完成一次任务:
┌─────────────────────────────────────────────────────────────────┐
│ 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 生命周期
任务入队
│
▼
┌──────────────────────────┐
│ 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 管理代码
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 通过白名单方式控制:
# 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 命令分级函数
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 分级处理策略
┌────────────────────────────────────────────────────────────┐
│ Command Proxy │
└─────────────────────────────┬──────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SAFE │ │ MODERATE │ │ HIGH │
│ 直接执行 │ │ 记录+执行 │ │ 暂停等待 │
│ 无审批 │ │ 无审批 │ │ 人工审批 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ │ ┌───────┴────────┐
│ │ │ 审批人收到通知 │
│ │ │ 点"同意"或"拒绝"│
│ │ │ 超时 5min 自动拒│
│ │ └───────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────┐
│ 命令审计日志(所有分级都记录) │
│ task_id | command | level | verdict | ts │
└─────────────────────────────────────────────┘FORBIDDEN 级别的命令在任何情况下都不会执行,连审批通道都不开放——Agent 收到拒绝响应后应该意识到这条路径不通,换一种方式完成任务。
六、Artifact 收集与回传
Agent 完成任务后,工作台需要看到三样东西:改了什么(diff)、跑得怎么样(测试结果)、卡在哪里(日志)。Artifact Collector 负责把这些结构化打包。
6.1 Artifact 结构
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 收集代码
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,结果拿到空字符串。正确的顺序是:
- Agent 报告完成(或失败)
- Collector 收集 diff、文件列表、日志
- Collector 打包上传到 Artifact Store
- 确认上传成功(校验 checksum)
- 最后才清理 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/16、10.0.0.0/8等内网段完全禁止访问 - 环境变量脱敏:
*PASSWORD*、*SECRET*、*TOKEN*、AWS_*等变量不会传给 Agent 子进程 - 命令分级完整:所有 Agent 发起的命令都经过 Command Proxy,按 SAFE/MODERATE/HIGH/FORBIDDEN 分类处理
- 高风险命令审批:
git push、npm publish、kubectl apply等必须人工审批,超时 5 分钟自动拒绝 - 绝对禁止命令:
rm -rf /、mkfs、dd、元数据服务调用等命令在任何情况下都不会执行 - 命令审计日志:所有命令(无论分级)都记录到
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 -i、git cherry-pick,都要处理TTY、credential helper 等问题。而 Worktree 就是原生 Git 操作,Agent 不需要感知。
问题三:容器日志收集不便。容器的 stdout/stderr 要通过 Docker daemon 拿,跨主机更麻烦。Worktree 模式下 Agent 是普通子进程,日志直接写文件,收集简单。
现在的架构是:Worktree 管代码隔离 + seccomp/AppArmor 管系统调用隔离 + 网络命名空间管网络。Docker 留给长期运行的服务(比如 Artifact Store、审批服务),不给短任务用。
9.2 命令分级正则的陷阱
命令分级听起来简单,做起来坑很多。最典型的问题是 shell 转义和管道组合:
# 看起来安全,实际危险
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 处理,让人工审批
# 错误做法:只匹配整条字符串
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:
# 清理超过 7 天、且没有对应活跃任务的 worktree
0 3 * * * /opt/agent-platform/scripts/cleanup-orphan-worktrees.sh --max-age=7d脚本逻辑:遍历 .worktrees/ 下所有目录,检查是否有对应状态为 executing 的任务,如果没有且创建时间超过 7 天,则 git worktree remove + 删除对应分支。