Skip to content

第 20 章 权限治理 —— Trust Boundaries

Coding agent 的核心安全问题可以归结为一句话:你在多大程度上信任一个概率模型去操作你的文件系统和网络——这个信任度决定了整个权限架构的形态。

20.1 信任模型的光谱

不同产品在"完全信任 LLM"到"零信任"之间选择了不同的位置,形成了一个连续的信任光谱。理解这个光谱是理解所有权限设计的前提。

mermaid
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:#87ceeb

DeepAgents:"Trust the LLM" + 沙箱层限制

DeepAgents 的信任模型是"相信模型的意图,但限制其影响范围"。它不在工具调用层做细粒度审批,而是通过 sandbox backend 将整个执行环境隔离:

python
# 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

toml
# 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 命令做三级裁决:

text
Decision::Allow     → 直接执行,可选跳过沙箱
Decision::Prompt    → 需要用户审批
Decision::Forbidden → 直接拒绝,附带理由

规则存储在 rules/*.rules 文件中,按 config layer 的优先级堆叠(项目级 > 用户级 > 全局级)。系统还维护了一个 BANNED_PREFIX_SUGGESTIONS 列表,禁止对 python3bashgitsudo 等通用前缀生成"永久允许"规则——否则一条 allow prefix ["bash"] 就会使所有 bash 命令绕过审批。

OpenCode:Build / Plan 双 Agent 分权

OpenCode 采用了一种结构上更激进的信任模型——将权限边界嵌入到 Agent 的角色定义中,而非依赖外部沙箱:

Agent类型文件操作Bash适用场景
Buildprimary全部启用全部启用实际开发
Planprimaryask(需审批)ask(需审批)分析规划
Generalsubagent全部启用全部启用并行子任务
Exploresubagent只读禁用代码探索

权限系统支持三种粒度:

json
{
  "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 CodeAskForApproval 枚举定义了五种审批策略:Never(全自动,依赖沙箱保护)、OnFailure(失败时才请求审批)、OnRequest(仅当工具请求超出沙箱范围时审批)、UnlessTrusted(除已知安全命令外均审批)、Granular(分别控制沙箱审批和规则审批)
  • Codexsuggest(仅建议不执行)→ auto-edit(文件修改自动、shell 命令需审批)→ full-auto(全自动)
  • DeepAgents:沙箱内全自动,HITL(Human-in-the-Loop)审批仅在特定 middleware 触发时出现

20.2 审批工作流

审批不只是一个"是/否"弹窗——它是一个包含命令解析、策略匹配、用户交互、规则学习的完整工作流。

审批决策流

mermaid
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、可逆操作生产环境操作日常开发
所需信任基础沙箱 + 快照规则 + 白名单

混合模式是实践中最常见的选择,其核心机制是逐步建立信任

  1. 初始状态:所有未知命令需要审批
  2. 用户批准后:系统提议生成一条 prefix rule(如 allow prefix ["cargo", "test"]
  3. 写入规则文件:后续匹配该前缀的命令自动放行
  4. 规则优先级:更具体的规则覆盖更通用的规则

Codex 中的 ExecPolicyAmendment 就实现了这种学习机制——当用户批准一个命令时,系统尝试从该命令推导出一个合理的前缀规则,但会过滤掉过于宽泛的前缀(python3bashgit 等在 BANNED_PREFIX_SUGGESTIONS 中的条目)。

Codex 审批模式:suggest / auto-edit / full-auto

Codex 定义了三种递进的审批模式,对应 CLI 中不同的 --approval-mode 参数:

suggest 模式:模型只能提议改动,不能执行任何操作。所有工具调用的结果都是模拟的——模型看到的是"如果执行会怎样"的预览。适合代码审查场景。

auto-edit 模式:文件编辑(apply_patch)自动执行,但 shell 命令仍需人工审批。这是基于一个实际观察:文件修改是可逆的(git checkout),但 shell 命令的副作用可能不可逆(如 rmcurl POST、数据库操作)。

full-auto 模式:所有操作自动执行,完全依赖沙箱保护。此模式下 AskForApproval 设置为 Never,意味着即使 ExecPolicy 规则标记某命令为 Prompt,也会因审批策略冲突而被降级为 Forbidden(而非静默执行)。这是一个重要的安全设计——full-auto 不意味着"绕过所有检查",而是意味着"不能人工审批的命令直接拒绝"。

20.3 Hooks 系统

Hooks 是 coding agent 提供给用户的可编程扩展点——在关键生命周期事件前后执行用户自定义逻辑,实现安全检查、日志记录、工作流集成等功能。

三层架构:SessionStart / UserPromptSubmit / Stop

Codex 的 Hooks 系统定义了三个生命周期事件,每个事件有独立的输入 / 输出 schema:

text
SessionStart        ─── 会话启动时触发(startup / resume / clear)
UserPromptSubmit    ─── 用户提交 prompt 时触发
Stop                ─── Agent 停止时触发

每个事件支持三种 handler 类型:

json
{
  "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 对象,包含当前上下文信息:

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 的输出控制后续行为:

json
{
  "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 结果的合并:

text
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 模式:

python
#!/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 阶段根据项目类型注入特定的安全规则:

python
#!/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 阶段记录完整的会话操作日志用于事后审计:

bash
#!/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 的最大区别在于:

text
传统测试:  input → system → output → assert(output == expected)
Agent 评估: input → agent → output → judge_agent(output, criteria) → score

Judge Agent 的评判标准通常包括:

  • 功能正确性:生成的代码是否满足需求
  • 安全合规性:是否违反了权限边界
  • 效率:工具调用次数是否合理
  • 幂等性:重复执行是否产生相同结果

DeepAgents 的 eval 框架通过 deepagents_evals 实现了这套体系,支持 LangSmith 集成用于结果可视化和回归检测。

训练数据收集:每次 action-sequence 都是 RL 的训练信号

Agent 每次完整的任务执行过程产生一条 trajectory:

text
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 规则的持续迭代。

数据回流的架构遵循"采集-存储-分析"三步:

text
Agent 执行
    ├── transcript.jsonl     → 完整对话和工具调用记录
    ├── analytics events     → 结构化指标(耗时、token 数、工具调用频率)
    └── hook outputs         → 安全事件和审批记录


    Evaluator Pipeline
    ├── 自动化评估          → LLM-as-a-Judge 打分
    ├── 人工抽样审查        → 边界 case 标注
    └── 回归检测            → 与基线版本对比


    改进闭环
    ├── Prompt 优化         → system prompt 迭代
    ├── 规则更新            → ExecPolicy rules 调整
    └── 模型训练            → RLVR fine-tuning

Codex 的 analytics_client 模块实现了事件采集端,每个工具调用的元信息(工具名、耗时、是否在沙箱内执行、沙箱策略类型、输出预览)都被序列化为结构化事件。这些事件通过 Hook 系统的 AfterToolUse 事件类型暴露给外部系统:

json
{
  "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 字段标记该操作是否可能修改状态,sandboxsandbox_policy 字段记录实际使用的隔离级别——这两个字段的组合可以回溯分析"哪些危险操作在没有沙箱保护的情况下执行了",是安全审计的核心数据源。