第 4 章:权限系统
你会给刚入职的实习生 root 权限吗?
显然不会。你会先给他们代码库的只读权限,或许还有某个特性分支的写入权限。你会仔细审查他们的前几次 Pull Request (PR)。随着他们证明了自己的能力,你会逐步扩大权限范围:提交权、部署权,最后才是生产环境的凭证。信任是循序渐进赢得的。
智能体(Agent)也应受到同样的审慎对待。不同的是,智能体的工作速度更快,决策毫无停顿,而且永远不会问:“等等,我真的该这么做吗?”
持续六小时的停机事故
一家初创公司赋予了其 AI 编程助手完整的仓库访问权限。这个智能体表现得非常出色——出色到令人惊叹。它重构了代码,修复了漏洞,还更新了依赖项。然而,在一个周五的傍晚,当开发者正在享用晚餐时,智能体决定“清理”一些它认为多余的配置文件。
不幸的是,生产数据库的连接字符串也在其中。
由此导致的停机持续了六个小时。事后复盘报告中指出,根本原因只有一句话:“智能体拥有修改任何文件的权限。我们原以为它懂得分寸。”
这种假设正是故障的根源。
程序与智能体的区别
当你编写传统程序时,它完全按照你编写的代码执行。不多,也不少。但智能体不同。你给它一个目标——“部署这个特性”——由它决定采取哪些行动。它可能会修改文件、运行命令、调用 API 或访问外部服务。
看看一个典型的编程智能体能做些什么:
- 文件操作:创建、编辑、删除进程可访问的任何文件。
- Shell 执行:以你的用户权限运行任意命令。
- API 调用:发起 HTTP 请求,可能会产生费用或泄露数据。
- 数据库访问:查询、修改或删除表。
- Git 操作:提交、推送、强制推送(force-push)、删除分支。
其中每一项都可能造成严重损害。拥有工具访问权的传统程序遵循可追踪的确定性逻辑。而拥有 these 权限的智能体则根据学习到的模式、上下文推理,以及偶尔产生的“幻觉”(对原本不该发生的事产生误导性的自信)做出决策。
这并不是拒绝智能体的理由——它们的灵活性正是其价值所在。但这要求我们建立一种不同的安全思维模型。
旧模型是:“我会在每次运行前审查每一项更改。”
在智能体的工作速度下,这几乎是不可能的。你无法针对每个任务都去审查五十次文件编辑、三十条 shell 命令和十几次 API 调用。
新模型必须是:“我会定义规则,让安全的操作自动发生,危险的操作始终被禁止,并在其他所有情况下征求我的意见。”
默认安全原则:失败即锁定
这是基本原则。不确定时,拒绝。规则不匹配时,询问。系统混淆时,停止。
过度拦截的代价是小小的不便。拦截不足的代价可能是灾难性的。
权限系统的每一个设计决策都应倾向于谨慎。智能体有时会被卡住,这没关系。备选方案是长达六小时的停机,或者更糟。
三级规则体系
每个权限系统都需要回答一个核心问题:是否允许此操作?
最简单且有效的模型将所有操作分为三类:
- 始终拒绝 (Always Deny):绝不应发生的操作。
- 始终允许 (Always Allow):预先批准的、安全的操作。
- 始终询问 (Always Ask):需要人工判断的操作。
关键点在于评估顺序。拒绝规则始终最先检查。
思考一下原因:
// 一个幼稚的实现(错误示例)
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
这类似于移动应用申请针对特定目录的权限。智能体在它的沙盒内可以执行任何操作。
人机协作:最后的安全网
没有自动化系统是完美的。人工判断始终是最终的防线。关键在于知道何时激活它。
何时中断
在以下情况中断并征求人工意见:
- 新动作类型的首次出现:即使规则允许,首次检查也能发现规则漏洞。
- 影响核心资源的操作:生产数据库、主分支、计费 API。即使有允许规则,对于高影响操作也应要求二次确认。
- 快可疑的动作链:单个动作可能没问题,但序列——“查询用户数据 -> 发起网络请求 -> 删除日志”——则非常值得审查。
- 触发阈值:超过 N 个文件删除,超过 M 次 API 调用,超过 X 元的花费。这些绝对限制需要人工确认后才能突破。
权限持久化
当人员批准或拒绝时,应捕获其范围:
- 仅允许一次:仅针对本次实例。
- 始终允许:加入永久允许规则。
- 当前会话允许:在智能体重启前有效。
- 对该模式允许:允许此类及类似操作。
“始终允许”虽然方便但有风险——权限会随着时间不断积累。建议进行定期审查。
审计日志
每一项权限决策都应被记录。这些日志有三个作用:
- 调试:出问题时,精准回溯发生了什么以及原因。
- 规则完善:人工决策中的模式揭示了应添加哪些规则。
- 合规性:在受监管的环境中,你需要以此证明所有操作均经过授权。审计日志就是你的证据。
权限系统的根基不是技术,而是认知的转变:从"智能体大概知道该怎么做"转变为"智能体只能做我明确允许的事"。一旦完成这种转变,正确的架构就会自然而然地浮现。