第9章:权限系统 —— 四级裁决与细粒度授权状态机
赋予智能体读写文件与终端执行的权限,就像把家里的钥匙和保险箱密码交给一个不知疲倦但可能偶尔产生幻觉的助手。权限系统是端侧编码智能体的生死防线。在 Zed 中,tool_permissions.rs (2,419 行)与配套模块构筑了一套严密的立体裁决体系 —— 从 5 组绝对不可绕过的硬编码防御规则,到四级权限裁决模式,再到严格局限于内存生命周期的会话级授权缓存。本章将全面解密这套工业级安全中枢。
一、开场:为什么简单的“允许/拒绝”是危险的?
很多初涉 Agent 开发的团队,对权限控制的理解往往停留在两种极端:
过于宽松(YOLO 模式) :完全不拦截,模型要执行什么命令就直接裸跑。结果开发者在一次代码重构中,眼睁睁看着模型执行了一条错误的清理脚本,清空了整个工作区乃至用户主目录;
过于严苛(弹窗地狱) :读取一个文件弹窗一次,执行一个 ls 弹窗一次,用户每分钟要点击数十次“批准”,最终导致“警报疲劳(Alert Fatigue)”——用户甚至不再阅读命令内容,麻木地盲点“同意”,安全防线彻底沦为摆设。
Zed 的权限系统致力于在绝对底线安全 与极致开发流畅度 之间找到精密的平衡点:
对于毁灭性破坏 :哪怕用户在配置文件中写了“允许所有操作”,系统也绝对硬性拒绝,连弹窗都不提供 ;
对于常规可信操作 :提供会话期记忆(Thread-scoped Grants),允许用户在当前会话中“对此目录永久信任”,但关闭窗口后自动失效;
对于路径穿越与符号链接逃逸 :执行严格的物理规范化解析,杜绝恶意代码通过软链接偷窥私钥。
二、不可绕过的硬编码防线:HARDCODED_SECURITY_RULES 在 crates/agent/src/tool_permissions.rs:26 中,驻扎着系统最高优先级的绝对防御规则:
rust
// crates/agent/src/tool_permissions.rs:14-25
const HARDCODED_SECURITY_DENIAL_MESSAGE: &str = "Blocked by built-in security rule. This operation is considered too \
harmful to be allowed, and cannot be overridden by settings.";
/// Security rules that are always enforced and cannot be overridden by any setting.
/// These protect against catastrophic operations like wiping filesystems.
pub struct HardcodedSecurityRules {
pub terminal_deny: Vec<CompiledRegex>,
}
【来源:crates/agent/src/tool_permissions.rs:14-25】
2.1 阻断的五大灾难性命令正则 Zed 通过编译期 LazyLock 初始化了 5 组专门防御毁灭性递归删除的正则表达式:
rust
// crates/agent/src/tool_permissions.rs:28-66
pub static HARDCODED_SECURITY_RULES: LazyLock<HardcodedSecurityRules> = LazyLock::new(|| {
// 匹配前置短标志位 (-rf, -rfv, -v 等) 或长标志位 (--recursive, --force 等)
const FLAGS: &str = r"(--[a-zA-Z0-9][-a-zA-Z0-9_]*(=[^\s]*)?\s+|-[a-zA-Z]+\s+)*";
// 匹配操作数后面的后置标志位(GNU rm 允许选项出现在路径之后)
const TRAILING_FLAGS: &str = r"(\s+--[a-zA-Z0-9][-a-zA-Z0-9_]*(=[^\s]*)?|\s+-[a-zA-Z]+)*\s*";
HardcodedSecurityRules {
terminal_deny: vec![
// 1. 递归删除根目录: "rm -rf /", "rm -rfv /", "rm -rf /*", "rm / -rf"
CompiledRegex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?/\*?{TRAILING_FLAGS}$"), false).unwrap(),
// 2. 递归删除用户家目录: "rm -rf ~", "rm -rf ~/", "rm -rf ~/*", "rm ~ -rf"
CompiledRegex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?~/?\*?{TRAILING_FLAGS}$"), false).unwrap(),
// 3. 递归删除 $HOME: "rm -rf $HOME", "rm -rf ${HOME}", "rm $HOME -rf"
CompiledRegex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?(\$HOME|\$\{{HOME\}})/?(\*)?{TRAILING_FLAGS}$"), false).unwrap(),
// 4. 递归删除当前工程根目录: "rm -rf .", "rm -rf ./", "rm -rf ./*", "rm . -rf"
CompiledRegex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?\./?\*?{TRAILING_FLAGS}$"), false).unwrap(),
// 5. 递归删除父级目录: "rm -rf ..", "rm -rf ../", "rm -rf ../*", "rm .. -rf"
CompiledRegex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?\.\./?\*?{TRAILING_FLAGS}$"), false).unwrap(),
],
}
});
【来源:crates/agent/src/tool_permissions.rs:28-66】
2.2 正则背后的对抗性工程细节 很多人写拦截规则只写一句 ^rm -rf / ,这种规则在真实的黑客或狡猾模型面前不堪一击。注意 Zed 正则中的几处极深考量:
参数乱序防御(Flags Permutation) :
GNU 版本的 rm 命令允许参数后置!例如 rm / -rf 或 rm -v / -rf 。Zed 定义了 FLAGS 与 TRAILING_FLAGS 两套模式,无论标志位出现在路径的前面、中间还是末尾,全被捕获;
长参数与组合参数防御 :
同时匹配 --recursive --force 与组合短参数 -rfv ;
双横杠防御(POSIX End-of-Options) :
考虑到了类似 rm -rf -- / 这种试图用 -- 截断参数解析器的变体;
绝对不可覆写性(Non-Bypassable) :
在权限判定的第一行代码中,优先调用 check_hardcoded_security_rules 。如果命中,直接返回 ToolPermissionDecision::Deny ,底层直接跳过任何用户设置与会话授权 !
三、四级权限模式(Four-Tier Permission Modes) 在硬编码禁区之外,用户的合法指令按照清晰的四级模式进行裁决:
判定顺序不可调换:硬编码安全禁令 → Shell 动态参数插值检查 → 本会话是否已授权(ThreadSandboxGrants )→ settings.json 静态规则;四道都没有结论时才挂起任务、向用户弹出交互授权卡片。前两道是绝对拒绝,用户配置改不动它们。
模式名称 裁决结果 适用典型场景 是否向用户弹窗? AllowAlways 自动放行执行 安全的纯读取操作(如 read_file , grep , find_path ) 否 AllowOnce 仅批准本次单次调用 用户临时批准某条一次性危险命令(如 cargo clean ) 首次弹出,批准后消费 Deny 拒绝执行并上报错误 命中安全规则、用户在弹窗中点击“拒绝”或全局禁写 否 Ask 挂起当前 Step,等待人机交互确认 首次修改生产代码、运行未知的编译或网络命令 是
四、细粒度模式匹配与符号链接逃逸防御 在 crates/agent_settings/src/agent_settings.rs 中,用户可以配置高度细粒度的规则。例如:
json
{
"tool_permissions": {
"default": "ask",
"tools": {
"terminal": {
"default": "ask",
"rules": [
{ "pattern": "^cargo (check|test)", "decision": "allow" },
{ "pattern": "^git --no-pager log", "decision": "allow" },
{ "pattern": "^rm", "decision": "deny" }
]
}
}
}
}
符号链接穿透防御(Symlink Traversal Defense) 除了终端指令,文件读写工具同样存在越权风险。
最经典的攻击路径是:恶意仓库在工程内部建立一个软链接:
bash
ln -s /etc/passwd ./project/config.json
如果 Agent 盲目调用 read_file("project/config.json") ,权限系统若仅检查字符串前缀 project/ ,就会误以为该文件在工作区内部而放行读取,造成主机机密文件外泄!
Zed 在 crates/agent/src/tool_permissions.rs:940 中引入了 symlink_target 检查:
在裁决文件路径前,必须先调用 resolve_canonical 将路径展开为其在文件系统中的物理真实绝对路径 ;
检查该绝对物理路径是否仍然完整包裹在工作区(Worktree)的根目录范围之内;
一旦发现符号链接指向了工作区外部的敏感位置,立即将权限范围升级为 ToolPermissionScope::SymlinkTarget ,强行弹出高危警告阻断!
五、内存作用域授权缓存:sandbox_grants 在前端弹窗中,当用户看到一条可信任的高频命令时,通常会勾选:“在当前会话中总是允许(Allow for this thread)” 。
Zed 是如何实现这一特性的?来看 Thread 结构体中的核心字段:
rust
// crates/agent/src/thread.rs:1305-1310
/// Sandbox permissions the user approved "for the rest of the thread".
/// Shared with each tool call's event stream so repeated requests for
/// already-granted permissions skip the approval prompt.
/// Never persisted — lives and dies with this thread.
sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
【来源:crates/agent/src/thread.rs:1305-1310】
为什么必须“Never persisted”(绝不落盘持久化)?
防止权限污染扩散 :
开发者在处理临时 Debug 任务时,可能会放宽权限允许 Agent 写入某个系统临时目录。如果这个授权被持久化保存到全局配置文件或本地数据库中,当一周后开发者在同一个项目里开启一个全新任务时,这个高危的宽松授权依然生效,极易引发误操作;
线程隔离性 :
sandbox_grants 由 Rc<RefCell<...>> 包装,其生命周期与当前的 Entity<Thread> 严格绑定。
主线程授权的目录,不会盲目同步给不相关的其他并行线程;
用户一旦关闭当前 Tab 页面,内存回收,所有在此会话中授予的特权瞬间清零!
六、人机交互闭环:弹窗、打断与自动清理 当一个工具调用被判定为 ToolCallStatus::WaitingForConfirmation 时,系统的异步通道进入挂起状态:
rust
// crates/acp_thread/src/acp_thread.rs:1270-1277
WaitingForConfirmation {
current_status: acp::ToolCallStatus,
options: PermissionOptions,
respond_tx: oneshot::Sender<RequestPermissionOutcome>,
kind: AuthorizationKind,
}
此时,UI 面板会渲染出带有鲜艳标识的确认卡片,清晰呈现:
工具名称(如 terminal 或 edit_file );
涉及的物理路径或完整命令字符串;
授权选项集(AllowOnce / AllowAlwaysThisThread / Deny )。
异常打断自动清理(Interrupted by Follow-up) 如果在用户犹豫是否批准该命令时,用户突然在输入框敲入了一句:“算了我改主意了,不要删这个文件”:
上层 UI 会捕获这一新的输入事件;
acp_thread 立即向挂起的 respond_tx 通道发送 RequestPermissionOutcome::InterruptedByFollowUp ;
后台正在等待审批的工具立即以 RejectOnce 快速失败退出;
状态机迅速转入消费用户的最新指令。
这种设计彻底杜绝了后台留存“僵尸授权等待任务”的隐患。
七、动手复核 你可以在本地源码检出中执行以下命令,直接复核本章提到的安全规则与关键代码:
bash
# 1. 验证 5 组硬编码绝对阻断正则表达式
sed -n '28,68p' crates/agent/src/tool_permissions.rs
# 2. 检查硬编码拒绝提示词常量
grep -n "HARDCODED_SECURITY_DENIAL_MESSAGE" crates/agent/src/tool_permissions.rs
# 3. 检查符号链接目标路径检查逻辑
grep -n -C 5 "symlink_target" crates/agent/src/thread.rs
# 4. 查看 sandbox_grants 的内存声明与不落盘注释
sed -n '1304,1312p' crates/agent/src/thread.rs
# 5. 检查打断权限审批的回传选项定义
grep -n -C 5 "FOLLOW_UP_PERMISSION_DENIED_OPTION_ID" crates/agent/src/agent.rs
八、总结 Zed 的权限系统体现了一款工业级产品对“代码生命线”的敬畏:
硬编码规则构筑了不可跨越的红线 :即便用户失误,系统也誓死捍卫根目录与主目录;
四级裁决消除了警报疲劳 :让安全检查恰到好处地出现在真正危险的时刻;
内存授权与符号链接防御封堵了逃逸缝隙 :杜绝持久化权限泄漏与软链接穿透;
优雅打断保障了人机主权 :让用户随时能够夺回对系统的绝对控制。
在下一章中,我们将进一步深入操作系统的底层,揭开 Zed 是如何调用 Bubblewrap、Seatbelt 与 WSL,构筑三层跨平台 OS 隔离沙箱 的。