第 4 章

权限系统

第 4 章:权限系统

你会给刚入职的实习生 root 权限吗?

显然不会。你会先给他们代码库的只读权限,或许还有某个特性分支的写入权限。你会仔细审查他们的前几次 Pull Request (PR)。随着他们证明了自己的能力,你会逐步扩大权限范围:提交权、部署权,最后才是生产环境的凭证。信任是循序渐进赢得的。

智能体(Agent)也应受到同样的审慎对待。不同的是,智能体的工作速度更快,决策毫无停顿,而且永远不会问:“等等,我真的该这么做吗?”


持续六小时的停机事故

一家初创公司赋予了其 AI 编程助手完整的仓库访问权限。这个智能体表现得非常出色——出色到令人惊叹。它重构了代码,修复了漏洞,还更新了依赖项。然而,在一个周五的傍晚,当开发者正在享用晚餐时,智能体决定“清理”一些它认为多余的配置文件。

不幸的是,生产数据库的连接字符串也在其中。

由此导致的停机持续了六个小时。事后复盘报告中指出,根本原因只有一句话:“智能体拥有修改任何文件的权限。我们原以为它懂得分寸。”

这种假设正是故障的根源。


程序与智能体的区别

当你编写传统程序时,它完全按照你编写的代码执行。不多,也不少。但智能体不同。你给它一个目标——“部署这个特性”——由它决定采取哪些行动。它可能会修改文件、运行命令、调用 API 或访问外部服务。

看看一个典型的编程智能体能做些什么:

其中每一项都可能造成严重损害。拥有工具访问权的传统程序遵循可追踪的确定性逻辑。而拥有 these 权限的智能体则根据学习到的模式、上下文推理,以及偶尔产生的“幻觉”(对原本不该发生的事产生误导性的自信)做出决策。

这并不是拒绝智能体的理由——它们的灵活性正是其价值所在。但这要求我们建立一种不同的安全思维模型。

旧模型是:“我会在每次运行前审查每一项更改。”

在智能体的工作速度下,这几乎是不可能的。你无法针对每个任务都去审查五十次文件编辑、三十条 shell 命令和十几次 API 调用。

新模型必须是:“我会定义规则,让安全的操作自动发生,危险的操作始终被禁止,并在其他所有情况下征求我的意见。”


默认安全原则:失败即锁定

这是基本原则。不确定时,拒绝。规则不匹配时,询问。系统混淆时,停止。

过度拦截的代价是小小的不便。拦截不足的代价可能是灾难性的。

权限系统的每一个设计决策都应倾向于谨慎。智能体有时会被卡住,这没关系。备选方案是长达六小时的停机,或者更糟。


三级规则体系

每个权限系统都需要回答一个核心问题:是否允许此操作?

最简单且有效的模型将所有操作分为三类:

关键点在于评估顺序。拒绝规则始终最先检查。

思考一下原因:

// 一个幼稚的实现(错误示例)
function checkPermission(action) {
  if (allowRules.match(action)) return ALLOW;
  if (denyRules.match(action)) return DENY;
  return ASK;
}

这看起来很合理,直到你遇到冲突。如果一条允许规则说“可以修改 /src 中的文件”,而一条拒绝规则说“不能修改 .env 文件”怎么办?在上述实现中,修改 /src/.env 将被允许——因为允许规则首先匹配成功。

// 正确的实现
function checkPermission(action) {
  if (denyRules.match(action)) return DENY;   // 安全优先
  if (allowRules.match(action)) return ALLOW;
  return ASK;                                  // 不确定 = 询问
}

这种“拒绝优先”模式在安全领域无处不在:防火墙规则、OAuth 作用域、Unix 权限。它的存在是因为“允许列表”比“黑名单”更容易推理。相比于定义“什么是危险的”,你可以更准确地枚举“什么是安全的”。

规则的模式匹配

规则需要灵活地匹配操作。硬编码的精确匹配无法扩展:

# 脆弱:精确匹配
allow:
  - "read /src/index.ts"
  - "read /src/utils.ts"
  - "read /src/helpers.ts"
  # ... 还有几百个

应改用 Glob 模式:

# 灵活:带通配符的模式
allow:
  - "read /src/**/*.ts"      # /src 下的任何 TypeScript 文件
  - "write /src/generated/*" # 仅限生成的工程文件
  
deny:
  - "write **/.env*"         # 任何地方的 .env 文件
  - "execute rm -rf *"       # 灾难性的删除操作
  - "write /etc/**"          # 系统配置

** 匹配任意层级的目录;* 匹配单层目录内的内容。这与开发者熟悉的模式一致——如 gitignore 或 shell globs。

但模式也有局限。你如何表达“可以运行 git 命令,但不能强制推送”?你需要语义化的规则:

deny:
  - tool: "bash"
    pattern: "git push --force*"
    reason: "强制推送可能会破坏历史记录"
    
  - tool: "bash"  
    pattern: "curl*|wget*"
    unless_path: "*.test.*"
    reason: "仅在测试中允许网络访问"

unless_path 修饰符展示了规则是如何组合的。这条规则的意思是:“拒绝网络命令,除非我们正在运行测试。” 随着你发现更多边缘情况,这些规则会不断演进——这是预料之中的。


多阶段验证管道

单一的权限检查是不够的。操作需要在多个阶段进行验证,每个阶段捕获不同的失败模式:

工具请求到达
        │
        ▼
┌─────────────────────────┐
│  1. 输入验证             │ ─── 请求格式是否正确?
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│  2. 执行前钩子 (Hooks)   │ ─── 自定义代码检查/修改/拒绝
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│  3. 规则匹配             │ ─── 拒绝 → 允许 → 询问
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│  4. 权限模式             │ ─── 自动 / 交互 / 严格?
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│  5. 工具特有逻辑         │ ─── 工具自身的安全检查
└───────────┬─────────────┘
            │
            ▼
       执行或拒绝

第 1 阶段:输入验证确保请求在结构上是有效的。如果智能体要求写入文件但未提供路径,这就是一个格式错误的请求——在进入昂贵的检查流程前就应直接拒绝。

第 2 阶段:执行前钩子允许你注入自定义逻辑。也许你需要记录每一次数据库查询,或者核实文件路径没有逃逸出沙盒。钩子提供了在不修改核心逻辑的情况下增强系统的能力。

第 3 阶段:规则匹配应用三级规则体系。大多数请求会在这里以明确的“允许”或“拒绝”结束。

第 4 阶段:权限模式决定了当规则不匹配时会发生什么。稍后会详细展开。

第 5 阶段:工具特有逻辑处理通用规则无法表达的细微差别。例如,文件写入工具可能会检查磁盘空间;Shell 工具可能会验证命令中是否存在注入攻击。每个工具都最清楚自己的风险点。

这种分层方法遵循了安全领域的“纵深防御 (Defense in Depth)”原则。单层防御可能有漏洞,但组合在一起后会非常严密。


权限模式

并非所有场景都需要相同的安全级别。在有人监控的情况下运行交互式智能体,与凌晨三点在 CI 流水线中运行智能体是完全不同的。

交互模式 (Interactive Mode)

这是有人监督会话的默认模式。当没有规则明确匹配时,智能体会暂停并询问:

智能体想要运行:rm -rf node_modules/
这将删除 847 个文件。

[a]仅允许一次  [A]始终允许  [d]拒绝  [D]始终拒绝  [?] 帮助

这是最安全的模式。不确定的操作需要人工判断。缺点是由于频繁交互带来的阻力——在你完善规则之前,你需要回答许多此类提示。

自动模式 (Auto Mode)

适用于需要减少干扰的工作流。当没有规则匹配时,AI 分类器会评估风险:

async function autoModeDecision(action: Action): Promise<Decision> {
  const riskScore = await classifyRisk(action);
  
  if (riskScore > RISK_THRESHOLD) {
    return DENY;  // 风险过高,无法自动批准
  }
  
  return ALLOW;   // 低风险,继续执行
}

自动模式速度更快,但带有风险。分类器可能会误判。为了缓解这一问题,自动模式通常配有“护栏”:操作预算、拒绝追踪,以及在某些特定类别下自动退回到交互模式。

你可以将自动模式想象成带超时机制的 sudo。一旦你通过了一次认证,后续命令就不再提示——但仅限在有限的时间内,且绝不包括某些极其危险的操作。

严格模式 (Strict Mode)

适用于高安全要求的场景。如果没有规则明确允许某项操作,它就会被拒绝。没有询问,也没有 AI 分类。

严格模式:操作需要明确的允许规则。
已拒绝:write /var/log/app.log

严格模式反转了默认逻辑。它不再是“除非危险,否则允许”,而是“除非明确允许,否则拒绝”。这非常适合生产部署、敏感代码库,或任何无法接受意外的场景。

旁路模式 (Bypass Mode)

适用于没有人工响应的 CI/CD 流水线. 所有未被明确拒绝的操作都被允许。

# CI 配置
agent:
  permission_mode: bypass
  deny_rules:
    - "write /etc/**"
    - "execute rm -rf *"
    - "api * --method DELETE"

基于钩子的可扩展性

静态规则无法涵盖所有场景。如果你需要执行以下操作该怎么办:

钩子允许你在管道的关键环节注入代码。

执行前钩子 (PreToolUse Hooks)

在工具运行前执行。可以修改请求、批准、拒绝或透传:

const auditHook: PreToolUseHook = async (request, context) => {
  await auditLog.record({
    tool: request.tool,
    input: request.input,
    user: context.user,
    timestamp: Date.now()
  });
  
  return { decision: 'passthrough' };  // 让后续检查继续
};

const sandboxHook: PreToolUseHook = async (request, context) => {
  if (request.tool === 'file_write') {
    const path = request.input.path;
    if (!path.startsWith(context.sandboxRoot)) {
      return { 
        decision: 'deny',
        reason: '路径逃逸沙盒范围'
      };
    }
  }
  return { decision: 'passthrough' };
};

执行后钩子 (PostToolUse Hooks)

执行完成后运行。对于记录结果、触发后续工作流或检测异常非常有用:

const resultMonitor: PostToolUseHook = async (request, result) => {
  if (result.status === 'error' && result.error.includes('permission denied')) {
    await alerting.notify('escalation', {
      message: '工具触碰到权限边界',
      request,
      result
    });
  }
};

危险操作检测

某些操作本身就带有高风险。你的权限系统需要启发式地识别它们。

风险命令的模式匹配

某些命令模式应始终被标记:

const DANGEROUS_PATTERNS = [
  /rm\s+(-rf?|--recursive).*\//,     // 递归删除
  />\s*\/dev\/(sda|null)/,           // 直接写入设备
  /chmod\s+777/,                      // 全开权限
  /curl.*\|\s*bash/,                  // 管道执行
  /DROP\s+(TABLE|DATABASE)/i,         // 数据库清理
  /:(){:|:&};:/,                       // Fork 炸弹
];

语义分析

模式可以捕获明显的情况,但绕过它们非常简单:

# 这些都会删除一切,但只有第一个匹配 "rm -rf"
rm -rf /
find / -delete
perl -e 'unlink glob "/*"'

语义分析会问:这到底做了什么? 你可以通过另一次 AI 调用(成本高但严密)或理解命令语义的静态分析工具来实现。

路径约束

如果将危险操作限制在特定目录中,许多操作会变得安全:

sandbox:
  root: /home/user/project
  writable:
    - /home/user/project/src

这类似于移动应用申请针对特定目录的权限。智能体在它的沙盒内可以执行任何操作。


人机协作:最后的安全网

没有自动化系统是完美的。人工判断始终是最终的防线。关键在于知道何时激活它。

何时中断

在以下情况中断并征求人工意见:

  1. 新动作类型的首次出现:即使规则允许,首次检查也能发现规则漏洞。
  2. 影响核心资源的操作:生产数据库、主分支、计费 API。即使有允许规则,对于高影响操作也应要求二次确认。
  3. 快可疑的动作链:单个动作可能没问题,但序列——“查询用户数据 -> 发起网络请求 -> 删除日志”——则非常值得审查。
  4. 触发阈值:超过 N 个文件删除,超过 M 次 API 调用,超过 X 元的花费。这些绝对限制需要人工确认后才能突破。

权限持久化

当人员批准或拒绝时,应捕获其范围:

“始终允许”虽然方便但有风险——权限会随着时间不断积累。建议进行定期审查。

审计日志

每一项权限决策都应被记录。这些日志有三个作用:

  1. 调试:出问题时,精准回溯发生了什么以及原因。
  2. 规则完善:人工决策中的模式揭示了应添加哪些规则。
  3. 合规性:在受监管的环境中,你需要以此证明所有操作均经过授权。审计日志就是你的证据。

权限系统的根基不是技术,而是认知的转变:从"智能体大概知道该怎么做"转变为"智能体只能做我明确允许的事"。一旦完成这种转变,正确的架构就会自然而然地浮现。