第 20 章 权限治理 —— Trust Boundaries
Coding agent 的核心安全问题可以归结为一句话:你在多大程度上信任一个概率模型去操作你的文件系统和网络——这个信任度决定了整个权限架构的形态。
20.1 信任模型的光谱
不同产品在"完全信任 LLM"到"零信任"之间选择了不同的位置,形成了一个连续的信任光谱。理解这个光谱是理解所有权限设计的前提。
graph LR
A["完全信任<br>(无沙箱)"] --> B["Trust the LLM<br>+ 沙箱隔离<br>DeepAgents"]
B --> C["精细化策略<br>+ Approval Profile<br>Codex"]
C --> D["双 Agent 分权<br>Build / Plan<br>OpenCode"]
D --> E["人工逐条审批<br>Manual Approval<br>Claude Code"]
style A fill:#ff6b6b
style B fill:#ffa07a
style C fill:#ffd700
style D fill:#90ee90
style E fill:#87ceebDeepAgents:"Trust the LLM" + 沙箱层限制
DeepAgents 的信任模型是"相信模型的意图,但限制其影响范围"。它不在工具调用层做细粒度审批,而是通过 sandbox backend 将整个执行环境隔离:
# DeepAgents sandbox backend 的核心抽象
class SandboxBackendProtocol:
"""所有操作通过 execute() 在隔离环境中运行"""
async def execute(self, command: str, stdin: str = "") -> ExecuteResponse: ...
async def write(self, path: str, content: str) -> WriteResult: ...
async def read(self, path: str) -> ReadResult: ...
async def edit(self, path: str, old: str, new: str) -> EditResult: ...关键设计:所有文件操作(write / read / edit / glob / grep)都不是直接调用 OS API,而是通过 execute() 方法在沙箱内执行一段 Python 脚本。这意味着即使 LLM 生成了恶意命令,其影响范围被限制在沙箱进程内。具体的沙箱后端可以是 Docker 容器、Modal 远程沙箱、Daytona 云端环境、甚至 QuickJS 引擎——Agent 代码完全不感知底层隔离机制。
这种架构的优势是开发体验极佳——模型拥有完整的工具集,不需要反复请求权限,执行效率高。代价是安全边界粗粒度——沙箱内的一切操作等价于完全信任。
Codex:精细的 SandboxPolicy + 权限 Profile
Codex 的信任模型居于光谱中间,通过多层机制实现精细控制。其核心是 SandboxPolicy 枚举和 PermissionProfile:
# Codex permissions.toml 示例
[default]
[default.filesystem]
":project_roots" = "read-write"
":tmpdir" = "read-write"
"~/.config" = "read-only"
"/usr" = "read-only"
":minimal" = "read-only"
[default.network]
enabled = true
mode = "limited"
allowed_domains = ["api.openai.com", "registry.npmjs.org"]这里有三个层次的控制:
文件系统层:通过 FileSystemSandboxPolicy 定义每个路径的访问模式(read-only / read-write)。特殊路径标记如 :project_roots(项目根目录)、:tmpdir(临时目录)、:minimal(运行时最小必需路径)提供了语义化的路径引用,避免硬编码绝对路径。路径解析支持 ~ 展开、Windows 设备路径(\\?\)规范化等跨平台场景。
网络层:NetworkSandboxPolicy 分为 Restricted(默认禁止所有网络)和 Enabled(允许网络,但可通过 allowed_domains / denied_domains 做域名白名单 / 黑名单过滤)。网络隔离通过 MITM 代理(codex-network-proxy)实现,支持 HTTP/HTTPS 和 SOCKS5。
执行策略层:ExecPolicy 基于 Starlark 规则引擎,对每个 shell 命令做三级裁决:
Decision::Allow → 直接执行,可选跳过沙箱
Decision::Prompt → 需要用户审批
Decision::Forbidden → 直接拒绝,附带理由规则存储在 rules/*.rules 文件中,按 config layer 的优先级堆叠(项目级 > 用户级 > 全局级)。系统还维护了一个 BANNED_PREFIX_SUGGESTIONS 列表,禁止对 python3、bash、git、sudo 等通用前缀生成"永久允许"规则——否则一条 allow prefix ["bash"] 就会使所有 bash 命令绕过审批。
OpenCode:Build / Plan 双 Agent 分权
OpenCode 采用了一种结构上更激进的信任模型——将权限边界嵌入到 Agent 的角色定义中,而非依赖外部沙箱:
| Agent | 类型 | 文件操作 | Bash | 适用场景 |
|---|---|---|---|---|
| Build | primary | 全部启用 | 全部启用 | 实际开发 |
| Plan | primary | ask(需审批) | ask(需审批) | 分析规划 |
| General | subagent | 全部启用 | 全部启用 | 并行子任务 |
| Explore | subagent | 只读 | 禁用 | 代码探索 |
权限系统支持三种粒度:
{
"permission": {
"edit": "deny",
"bash": {
"*": "ask",
"git status *": "allow",
"git push": "ask",
"rm -rf *": "deny"
},
"webfetch": "deny"
}
}allow:静默执行,不提示用户ask:弹出审批菜单,用户逐条确认deny:完全禁用该工具,不出现在模型的工具列表中
Bash 权限额外支持 glob 模式匹配,且最后匹配的规则优先(last-match-wins),这意味着应该把 * 通配符放在前面作为默认策略,具体命令模式放在后面做例外。
双 Agent 模型的核心洞察:Plan Agent 存在的意义不是"只读版的 Build",而是让模型在分析阶段使用不同的推理策略。当 Plan Agent 知道自己不能修改文件时,它会产出更完整的分析报告和更详细的改动方案。这是一种通过约束工具集来引导模型行为的间接 prompt engineering。
Manual Approval + --auto
大多数 coding agent 的默认模式是逐条人工审批——每个工具调用都暂停执行、展示给用户、等待确认。这是最安全但最慢的模式。
--auto 标志翻转了这个默认值,允许 Agent 自主执行所有(或部分)工具调用。实际产品中,auto 模式的粒度各不相同:
- Claude Code:
AskForApproval枚举定义了五种审批策略:Never(全自动,依赖沙箱保护)、OnFailure(失败时才请求审批)、OnRequest(仅当工具请求超出沙箱范围时审批)、UnlessTrusted(除已知安全命令外均审批)、Granular(分别控制沙箱审批和规则审批) - Codex:
suggest(仅建议不执行)→auto-edit(文件修改自动、shell 命令需审批)→full-auto(全自动) - DeepAgents:沙箱内全自动,HITL(Human-in-the-Loop)审批仅在特定 middleware 触发时出现
20.2 审批工作流
审批不只是一个"是/否"弹窗——它是一个包含命令解析、策略匹配、用户交互、规则学习的完整工作流。
审批决策流
flowchart TD
A["Agent 请求执行命令"] --> B{ExecPolicy<br>规则匹配}
B -->|"规则: Allow"| C["直接执行<br>可选跳过沙箱"]
B -->|"规则: Forbidden"| D["拒绝执行<br>返回理由"]
B -->|"规则: Prompt"| E{审批策略<br>AskForApproval}
B -->|"无匹配规则"| F{命令危险性<br>检测}
F -->|"is_known_safe"| C
F -->|"might_be_dangerous"| E
F -->|"其他"| G{沙箱类型}
G -->|"Restricted"| H{是否请求<br>沙箱越权}
G -->|"Unrestricted"| C
H -->|"是"| E
H -->|"否"| C
E -->|"Never"| I{沙箱是否<br>显式禁用}
I -->|"是"| C
I -->|"否"| D
E -->|"OnRequest<br>OnFailure"| J["提示用户审批"]
E -->|"UnlessTrusted"| J
E -->|"Granular"| K{区分 rule<br>vs sandbox}
K -->|"rule 审批关闭"| D
K -->|"sandbox 审批关闭"| D
K -->|"审批开启"| J
J --> L{用户决策}
L -->|"允许"| C
L -->|"拒绝"| D
L -->|"永久允许"| M["写入 rules 文件<br>更新内存策略"]
M --> C自动执行 vs 人工审批 vs 混合模式
三种模式的对比涉及根本性的权衡:
| 维度 | 全自动 | 全人工 | 混合模式 |
|---|---|---|---|
| 执行速度 | 最快 | 最慢 | 中等 |
| 安全性 | 依赖沙箱 | 最高 | 高 |
| 用户体验 | 无打断 | 频繁打断 | 偶尔打断 |
| 适用场景 | CI/CD、可逆操作 | 生产环境操作 | 日常开发 |
| 所需信任基础 | 沙箱 + 快照 | 无 | 规则 + 白名单 |
混合模式是实践中最常见的选择,其核心机制是逐步建立信任:
- 初始状态:所有未知命令需要审批
- 用户批准后:系统提议生成一条 prefix rule(如
allow prefix ["cargo", "test"]) - 写入规则文件:后续匹配该前缀的命令自动放行
- 规则优先级:更具体的规则覆盖更通用的规则
Codex 中的 ExecPolicyAmendment 就实现了这种学习机制——当用户批准一个命令时,系统尝试从该命令推导出一个合理的前缀规则,但会过滤掉过于宽泛的前缀(python3、bash、git 等在 BANNED_PREFIX_SUGGESTIONS 中的条目)。
Codex 审批模式:suggest / auto-edit / full-auto
Codex 定义了三种递进的审批模式,对应 CLI 中不同的 --approval-mode 参数:
suggest 模式:模型只能提议改动,不能执行任何操作。所有工具调用的结果都是模拟的——模型看到的是"如果执行会怎样"的预览。适合代码审查场景。
auto-edit 模式:文件编辑(apply_patch)自动执行,但 shell 命令仍需人工审批。这是基于一个实际观察:文件修改是可逆的(git checkout),但 shell 命令的副作用可能不可逆(如 rm、curl POST、数据库操作)。
full-auto 模式:所有操作自动执行,完全依赖沙箱保护。此模式下 AskForApproval 设置为 Never,意味着即使 ExecPolicy 规则标记某命令为 Prompt,也会因审批策略冲突而被降级为 Forbidden(而非静默执行)。这是一个重要的安全设计——full-auto 不意味着"绕过所有检查",而是意味着"不能人工审批的命令直接拒绝"。
20.3 Hooks 系统
Hooks 是 coding agent 提供给用户的可编程扩展点——在关键生命周期事件前后执行用户自定义逻辑,实现安全检查、日志记录、工作流集成等功能。
三层架构:SessionStart / UserPromptSubmit / Stop
Codex 的 Hooks 系统定义了三个生命周期事件,每个事件有独立的输入 / 输出 schema:
SessionStart ─── 会话启动时触发(startup / resume / clear)
UserPromptSubmit ─── 用户提交 prompt 时触发
Stop ─── Agent 停止时触发每个事件支持三种 handler 类型:
{
"hooks": {
"SessionStart": [{
"matcher": "^startup$",
"hooks": [
{"type": "command", "command": "echo 'session started'", "timeout": 5},
{"type": "prompt"},
{"type": "agent"}
]
}],
"UserPromptSubmit": [{
"hooks": [
{"type": "command", "command": "python3 safety_check.py"}
]
}],
"Stop": [{
"hooks": [
{"type": "command", "command": "git stash"}
]
}]
}
}- command:执行 shell 命令,通过 stdin 接收 JSON 格式的事件数据,通过 stdout 输出 JSON 格式的决策结果
- prompt:注入额外的 system message(用于动态修改 Agent 的行为指令)
- agent:调用另一个 Agent 来处理事件
Hook 的输入是一个标准化的 JSON 对象,包含当前上下文信息:
{
"session_id": "...",
"turn_id": "...",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl",
"hook_event_name": "UserPromptSubmit",
"model": "claude-sonnet-4-20250514",
"permission_mode": "default",
"prompt": "用户输入的原始内容"
}Hook 的输出控制后续行为:
{
"continue": true,
"stopReason": null,
"suppressOutput": false,
"systemMessage": "额外注入的系统指令",
"decision": "block",
"reason": "检测到敏感操作"
}continue: false:中断后续 hook 执行和 Agent 流程decision: "block":阻止当前操作(仅 UserPromptSubmit 和 Stop 支持)systemMessage:向 Agent 注入额外上下文(以 developer message 角色插入对话历史)
Handler 执行模型
多个 handler 并行执行,但事件的最终结果是所有 handler 结果的合并:
SessionStart:
- scope: Thread(整个会话生命周期)
- matcher: 支持正则匹配 source 字段(startup / resume / clear)
- 输出: 可注入 additional_context
UserPromptSubmit:
- scope: Turn(单轮对话)
- matcher: 无(所有 prompt 都触发)
- 输出: 可 block prompt、注入 additional_context
Stop:
- scope: Turn
- matcher: 无
- 输出: 可 block 停止行为Handler 的发现通过 ConfigLayerStack 实现——从项目目录到用户全局目录的每一层配置中收集 hooks.json 文件,低优先级的 handler 先执行,高优先级的后执行。
安全检查作为 Hook 的典型用例
Hook 的核心价值是让安全策略可编程化。以下是几个典型的安全 hook 实现模式:
Prompt 注入检测:在 UserPromptSubmit 阶段检查用户输入是否包含可疑的 prompt injection 模式:
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
prompt = data.get("prompt", "")
# 检测常见的 prompt injection 模式
suspicious_patterns = [
"ignore previous instructions",
"you are now",
"system prompt:",
"{{", # 模板注入
]
for pattern in suspicious_patterns:
if pattern.lower() in prompt.lower():
json.dump({
"continue": False,
"decision": "block",
"reason": f"Potential prompt injection detected: '{pattern}'"
}, sys.stdout)
sys.exit(0)
json.dump({"continue": True}, sys.stdout)会话初始化注入:在 SessionStart 阶段根据项目类型注入特定的安全规则:
#!/usr/bin/env python3
import json, sys, os
data = json.load(sys.stdin)
cwd = data.get("cwd", "")
# 根据项目类型注入不同的安全上下文
context = []
if os.path.exists(os.path.join(cwd, "package.json")):
context.append("This is a Node.js project. Never run npm scripts without reviewing package.json first.")
if os.path.exists(os.path.join(cwd, ".env")):
context.append("CRITICAL: .env file detected. Never read, display, or commit .env files.")
output = {"continue": True}
if context:
output["hookSpecificOutput"] = {
"hookEventName": "SessionStart",
"additionalContext": "\n".join(context)
}
json.dump(output, sys.stdout)审计日志:在 Stop 阶段记录完整的会话操作日志用于事后审计:
#!/bin/bash
# Stop hook: 将 transcript 复制到审计目录
INPUT=$(cat)
TRANSCRIPT=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('transcript_path',''))")
if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
AUDIT_DIR="$HOME/.audit/codex/$(date +%Y%m%d)"
mkdir -p "$AUDIT_DIR"
cp "$TRANSCRIPT" "$AUDIT_DIR/"
fi
echo '{"continue": true}'20.4 数据回流与评估
Agent 系统的权限治理不能只看单次执行是否安全——还需要持续监控 Agent 的行为质量,并将执行数据转化为改进模型和策略的信号。
关键指标
Agent 开发本身难度不高(架构特性和扩展性明确后),但验证难度极大。保持对线上 Agent 数据的采集是持续优化的基础。需要关注六个核心指标:
| 指标 | 含义 | 测量方法 |
|---|---|---|
| Success Rate | 任务执行成功率 | 端到端验证:代码能否编译、测试是否通过、需求是否满足 |
| LLM Sensitivity | 不同模型下的执行结果差异 | 同一任务 + 同一 prompt,切换底层模型(GPT-4o / Claude / Gemini),比较输出 |
| Prompt Sensitivity | 同义表述下的结果一致性 | 对同一需求做同义词替换,观察工具调用序列是否稳定 |
| Hallucination | 幻觉率 | 当系统明确告知"某信息不可知"后,模型是否仍在执行器参数中编造数据 |
| Scalability | 工具数量增长后的性能退化 | 固定任务,逐步增加可用工具数量(10 → 50 → 100),观察成功率和选择准确率 |
| Autonomy | 模型的主动工具调用倾向 | 观察模型是否在没有明确指令时主动使用工具探索,而非仅回答"我不知道" |
这些指标之间存在张力:提高 Autonomy 通常会降低 Success Rate(更多主动尝试 = 更多失败),降低 Prompt Sensitivity 需要更强的 instruction following 能力但可能增加 Hallucination。权限系统的调优本质上是在这些指标之间寻找帕累托最优。
Evaluator 系统:LLM-as-a-Judge 本身就是 Agent
Agent 的评估系统与传统软件测试有本质区别——由于 LLM 的输出是非确定性的,传统的 assert output == expected 无法工作。取而代之的是 LLM-as-a-Judge:用另一个(通常更强的)模型来评判被测 Agent 的输出质量。
这创造了一个递归结构:Evaluator 本身也是一个 Agent。它有自己的 prompt、工具集、判断标准。这与传统 test suite 的最大区别在于:
传统测试: input → system → output → assert(output == expected)
Agent 评估: input → agent → output → judge_agent(output, criteria) → scoreJudge Agent 的评判标准通常包括:
- 功能正确性:生成的代码是否满足需求
- 安全合规性:是否违反了权限边界
- 效率:工具调用次数是否合理
- 幂等性:重复执行是否产生相同结果
DeepAgents 的 eval 框架通过 deepagents_evals 实现了这套体系,支持 LangSmith 集成用于结果可视化和回归检测。
训练数据收集:每次 action-sequence 都是 RL 的训练信号
Agent 每次完整的任务执行过程产生一条 trajectory:
trajectory = [
(state_0, action_0, observation_0), # 读取需求文件
(state_1, action_1, observation_1), # 搜索代码库
(state_2, action_2, observation_2), # 编辑文件
(state_3, action_3, observation_3), # 运行测试
...
(state_n, action_n, reward) # 最终结果 + 奖励信号
]这条 trajectory 同时是三种用途的数据:
1. Prompt Engineering 的 A/B 测试数据:比较不同 system prompt 下的 trajectory 长度、成功率、工具选择分布,用于全自动 PE(Prompt Engineering)优化。
2. RLVR(RL from Verifiable Rewards)的训练集:当任务有可验证的结果(测试通过 / 编译成功 / 输出匹配),reward 信号可以自动生成,无需人工标注。每条 trajectory 都是一个 (prompt, action_sequence, reward) 三元组,可直接用于 PPO / DPO 训练。
3. 安全策略的改进信号:统计哪些命令被用户拒绝、哪些 hook 触发了 block、哪些场景下模型的权限请求是合理的——这些数据驱动 ExecPolicy 规则的持续迭代。
数据回流的架构遵循"采集-存储-分析"三步:
Agent 执行
├── transcript.jsonl → 完整对话和工具调用记录
├── analytics events → 结构化指标(耗时、token 数、工具调用频率)
└── hook outputs → 安全事件和审批记录
│
▼
Evaluator Pipeline
├── 自动化评估 → LLM-as-a-Judge 打分
├── 人工抽样审查 → 边界 case 标注
└── 回归检测 → 与基线版本对比
│
▼
改进闭环
├── Prompt 优化 → system prompt 迭代
├── 规则更新 → ExecPolicy rules 调整
└── 模型训练 → RLVR fine-tuningCodex 的 analytics_client 模块实现了事件采集端,每个工具调用的元信息(工具名、耗时、是否在沙箱内执行、沙箱策略类型、输出预览)都被序列化为结构化事件。这些事件通过 Hook 系统的 AfterToolUse 事件类型暴露给外部系统:
{
"event_type": "after_tool_use",
"tool_name": "local_shell",
"tool_kind": "local_shell",
"executed": true,
"success": true,
"duration_ms": 42,
"mutating": true,
"sandbox": "none",
"sandbox_policy": "danger-full-access",
"output_preview": "ok"
}mutating 字段标记该操作是否可能修改状态,sandbox 和 sandbox_policy 字段记录实际使用的隔离级别——这两个字段的组合可以回溯分析"哪些危险操作在没有沙箱保护的情况下执行了",是安全审计的核心数据源。