权限系统
Agent 能动手改你的电脑,凭什么信它?权限系统就是那道闸。这一页贴 permissions.ts 的真实判定代码,讲清 5 种模式、过闸顺序、以及"哪些操作再高的权限也拦不住"。Day 07/08 讲工具"能干什么",今天讲工具"动手前要不要先经你同意",Day 10 收官第 2 周。
plan(只准看图纸不准动)→ default(动手前都问你)→ acceptEdits(在自家范围内改可以不问)→ bypassPermissions(基本随他干)。但关键在于——哪怕你签了"随便干",动"承重墙 / 燃气总阀"(.git/.bashrc 这类危险文件)仍然必须再签一次字。今天最该记住的一句:最大的放行档位,也保留最小的安全底线。为什么需要权限
rm -rf / 就直接执行——你的电脑就没了。模型会判断错、也可能被网页/文件里的恶意文字诱导(提示注入)。只要它能不经你同意就动手,一次失误就是灾难。所以必须在"真动手前"设一道闸。模型会自主决定调 Bash 跑命令、调 Edit 改文件。但模型可能判断错、也可能被恶意输入诱导。权限系统就是"在工具真执行前踩一脚刹车"——高风险操作停下来问你,低风险的放行,还能按规则批量允许/禁止。
Day 07 L08 讲过:执行管线里"权限检查"排在 tool.call() 之前(第 6 关)。今天钻进那关。核心文件 src/utils/permissions/permissions.ts,文件系统相关在 filesystem.ts(Day 08 见过危险清单),Bash 在 bashSecurity.ts(Day 08 的 tree-sitter)。
5 种权限模式
你用 Shift+Tab 切换的就是这些模式(src/types/permissions.ts:15):
default
正常询问:高风险操作弹窗确认
acceptEdits
工作目录内的编辑自动放行,其它照常问
plan
只读规划,不动手;ExitPlanMode 提交方案后才开工
bypassPermissions
几乎全放行——但 deny 规则、ask 规则、safetyCheck 仍免疫(L05)
dontAsk
把所有 ask 直接转成 deny(宁可不做也不问)
plan(只看不动)→ default(动手要问)→ acceptEdits(改文件不用问)→ bypassPermissions(基本全放)。越往右越省心但越危险。dontAsk 是另类——不是"更信任",而是"别烦我,拿不准的就别做"。实际最常在 default 和 acceptEdits 之间切。另有内部模式 auto(不弹窗、用 AI 分类器判,L07)。权限规则从哪来
除了模式,还有规则——三种行为 allow/deny/ask,可精确到工具甚至参数。配在 settings.json:
{
"permissions": {
"allow": ["Read", "Bash(git status)"], // 整工具 或 参数级
"deny": ["Bash(rm *)", "Edit(/etc/**)"],
"ask": ["Bash(git push*)"]
}
}
Bash = "整个 Bash 工具";Bash(git status) = "只有跑 git status 时"。参数级让你精细控制——"允许 git status,但 git push 要问"。你在弹窗里点"始终允许 Bash(git status)",就是往规则里加一条参数级 allow。allow: ["Bash(git status)"]、ask: ["Bash(git push*)"]、deny: ["Bash(rm *)"]。模型想跑
git status → 命中 allow → 静默放行;模型想跑
git push origin main → 命中 ask → 弹窗问你;模型想跑
rm foo.txt → 命中 deny → 直接拒绝,连问都不问。同一个 Bash 工具,靠参数级规则精细分流。规则来源有优先级(permissions.ts):userSettings / projectSettings / localSettings / flagSettings / policySettings / cliArg / command / session——企业策略(policy)最高。它们最终落到权限上下文的 alwaysAllowRules/alwaysDenyRules/alwaysAskRules。
判定顺序:一步步过闸(真实代码)
入口 hasPermissionsToUseTool(permissions.ts:473)→ 内部 hasPermissionsToUseToolInner(:1179)。源码里用注释标了 step 编号,逐条过。看开头两步的真实代码(:1190):
// 1a. Entire tool is denied
const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
if (denyRule) {
return { behavior: 'deny', ... } // ← 命中 deny 规则,直接拒绝(最先、最硬)
}
// 1b. Check if the entire tool should always ask for permission
const askRule = getAskRuleForTool(appState.toolPermissionContext, tool)
if (askRule) {
// ...(sandbox 自动放行是例外)
return { behavior: 'ask', ... } // ← 命中 ask 规则,弹窗询问
}
完整过闸顺序(源码 step 编号):
整工具 deny 规则 → deny(最先,最硬)
整工具 ask 规则 → ask
调工具自己的 checkPermissions(Day 07/08,如 Read 的"工作目录内放行"、Bash 的 AST)
工具返回 deny → deny
需要用户交互 / 内容级 ask / safetyCheck(.git/.claude 等,Day 08)→ ask(bypass 免疫)
模式放行:bypassPermissions → allow
整工具 allow 规则 → allow
passthrough 转 ask(工具没明确表态,交给用户)
:1083)明说:"bypassPermissions mode respects everything that fires before step 2a"(bypass 模式尊重 step 2a 之前触发的一切)。这个"顺序编排"就是安全的核心:把"不该做的"和"要确认的"放在"放行"之前判,且让它们凌驾于放行模式。哪些连 bypass 都拦得住
很多人以为 bypassPermissions = "全放行"。不对。由 L04 的顺序(deny/ask/safetyCheck 都在 2a 之前)可知,即使 bypass 模式,这几类仍然拦得住:
- deny 规则(1a/1d):你明说"绝不许"的红线,永远禁止。
- ask 规则(1b/1f):你明说"要确认"的,仍会问。
- safetyCheck(1g):动
.git/.claude/.bashrc/.mcp.json等危险文件仍会问(Day 08 那份清单)。 - 需要用户交互的工具(1e):如 AskUserQuestion。
审批弹窗 = await 一个 Promise(真实代码)
判定出 ask 后,怎么变成你看到的那张权限卡、并"等你点"?hasPermissionsToUseTool 只产出决策,真正弹 UI + 等待的是 useCanUseTool(src/hooks/useCanUseTool.tsx:52)。真实代码:
return new Promise(resolve => { // ← 创建一个 Promise,先不 resolve
const ctx = createPermissionContext(tool, input, ...);
if (ctx.resolveIfAborted(resolve)) return;
const decisionPromise = hasPermissionsToUseTool(tool, input, ...); // 算出 allow/deny/ask
return decisionPromise.then(async result => {
if (result.behavior === 'allow') { ... resolve(...) } // 直接放行
// 若是 ask:把一个 {tool, input, onAllow, onReject} 压进确认队列,
// 等用户点"允许/拒绝"时才 resolve 这个 Promise
});
});
await canUseTool(...))。当需要用户确认时,代码创建一个 Promise 但先不 resolve,把它的 resolve 函数塞进 UI 的确认队列。界面渲染出权限卡(Day 04/18 讲的队列渲染),你点"允许"就调 onAllow() → resolve('allow'),那个 await 才继续往下。于是"等一个人类点按钮"被表达成了"await 一个 Promise"——工具执行天然地在这一步挂起,不用轮询、不用回调地狱。await 一个 Promise = "在这等它有结果再往下"。resolve 是"给这个 Promise 填上结果"的函数——一调用,所有 await 它的地方就继续。这里的巧思:创建 Promise 时把 resolve 存起来(塞进队列),等用户点按钮那一刻才调它。本质就是:"用户的点击"当成"未来的结果",用 Promise 把等待表达出来。权限卡组件是 PermissionRequest(components/permissions/),内部按工具类型分派到 BashPermissionRequest / FileEditPermissionRequest 等具体样式。
👶 小白:程序在"等我点按钮"的时候,它到底卡在哪一行、又没卡死整个界面,怎么做到的?
👨🏫 老师:卡在 await canUseTool(...) 那一行——但"卡"的只是工具执行这条异步链,界面(另一条 React 渲染线)照样能动、能接收你的点击。诀窍是:创建了一个 Promise 却先不给它结果,把它的 resolve 函数存进确认队列;你点"允许"那一刻才调 resolve('allow'),那行 await 立刻续上。等一个人点按钮 = await 一个"由按钮来 resolve 的 Promise",既不轮询也不回调地狱。
auto 模式:用 AI 分类器代替弹窗
auto 模式(permissions.ts:519 附近)不弹窗,改用一个 AI 分类器自动判断这次操作安不安全。判定路径:
- 先试 acceptEdits 快速路径(工作目录内编辑直接过)。
- 查"安全工具白名单"(如只读工具直接放)。
- 都不满足 → 调分类器(一个小模型)判"这个操作危险吗",据此 allow/deny。
default 太烦(每步都问),bypass 太险(全放)。auto 是折中——用一个便宜的 AI 分类器替你做"这操作要不要拦"的判断:只读/编辑这类明显安全的直接放,可疑的(删文件、改系统配置)才拦。相当于配了个"自动审批助理"。这是本 fork 的特色能力。无人值守时(headless/后台代理,没人能点弹窗)走另一条:拿不准就默认拒绝——"没人确认 = 不做",安全优先。checkRuleBasedPermissions(:1088):即使 hook 或分类器说"放行",仍会用它复核一遍 deny/ask 规则——hook/分类器不能绕过你明确设的 deny 规则和 ask 规则。又一处"底线不可绕过"。今日小结 + 动手
🧠 今天你应该能回答
- 5 种权限模式各是什么?(用"信任滑杆"记)
- 权限规则三种行为 + 整工具 vs 参数级?
- 判定顺序为什么"deny/ask 在 allow 前"?(禁止>询问>允许)
- bypassPermissions 拦不住哪几类?(deny/ask 规则、safetyCheck、交互工具)为什么留这些底线?
- 审批弹窗怎么用"await 一个 Promise"实现?(resolve 存进队列,点按钮才调)
- auto 模式怎么工作?为什么无人值守时默认拒绝?
✋ 动手:对着真实代码读一遍
# 1. 权限模式定义
sed -n '15,45p' src/types/permissions.ts
# 2. 判定顺序 step 1a/1b(L04,真实代码)
sed -n '1179,1228p' src/utils/permissions/permissions.ts
# 3. bypass 免疫的注释(L05)
sed -n '1083,1090p' src/utils/permissions/permissions.ts
# 4. "await 一个 Promise" 的审批(L06)
sed -n '46,104p' src/hooks/useCanUseTool.tsx
# 5. 亲身体验:起 dev,Shift+Tab 切模式,让它改个文件看弹窗
bun run dev
query.ts 里"每轮开头分级瘦身"的真实代码:microcompact 微压缩、autocompact 大摘要、以及 Stop hook 续命 / token 预算这些"何时才算真结束"的协商机制。