加载中...
  • 你敢让 AI 直接跑 rm -rf 吗:生产级权限的四层防线 loading

    吃透 AI Agent 开发 · 第 14 篇 · 第三章 · Tool System

    权限不是工具名称上的一个 readonly 标记,而是路径、参数、环境、审批和审计共同组成的纵深防线。

    权限不是工具名称上的一个 readonly 标记,而是路径、参数、环境、审批和审计共同组成的纵深防线。

    权限判断要回答四个问题

    用户说“清理构建产物”时,系统不能只看工具名是不是 shell。它还要知道谁发起、当前工作区在哪里、命令会触碰哪些真实路径、是否需要确认、动作是否可回滚。缺一层,剩下的层就会被迫承担不该承担的责任。

    flowchart TB
      Identity[发起者与会话] --> Scope[工作区与真实路径]
      Scope --> Arguments[命令和参数策略]
      Arguments --> Approval[用户确认或 Hook]
      Approval --> Execute[受控执行]
      Execute --> Audit[结果与审计]
    

    这是一条纵深防线,不是一扇弹窗。弹窗只能表达一次确认,不能替代 symlink 解析、命令分解、后台任务和恢复证据。

    路径检查最容易被“看起来安全”骗过

    interface PathDecision {
      allowed: boolean
      requested: string
      resolved: string
      reason: 'inside-workspace' | 'outside-workspace' | 'symlink-escape' | 'absolute-disabled'
    }
    
    function explain(decision: PathDecision): string {
      return decision.allowed
        ? `允许访问 ${decision.resolved}`
        : `拒绝访问 ${decision.requested}:${decision.reason}`
    }
    

    检查字符串前缀不够。C:\workspace\project-old 可能不是 C:\workspace\project,符号链接也可能让解析后的真实路径跳出工作区。策略层需要同时保存请求路径和解析路径,审计里只显示脱敏摘要,但恢复时必须能定位目标。

    四层防线各自挡什么

    防线 能解决的问题 解决不了的问题
    身份与会话 谁提出了请求 请求目标是否安全
    路径与参数 是否越界、是否命中危险模式 用户是否真的理解副作用
    审批与 Hook 是否需要人工确认 外部动作是否已经成功
    执行与审计 取消、超时和记录 事前判断是否足够细

    把这些层写成一套函数并不代表安全。真正的要求是所有入口都经过它们,后台 Shell、Slash 命令、MCP 工具和自定义工具不能各自另开一条后门。

    q-code 的安全阅读顺序

    步骤 路径 需要确认
    1. 路径策略 src/tools/path-policy.ts 绝对路径、symlink 和 cwd 如何比较
    2. Shell 保护 src/tools/shell-tools.ts 危险命令、交互和后台任务如何处理
    3. Hook matcher src/hooks/matcher.ts 哪些事件会触发决策
    4. Hook runner src/hooks/runner.ts block、warn、modify 如何传回工具管线

    失败反例:只在 UI 弹一个确认框

    只在 UI 弹一个确认框,后台入口仍可以绕过;或只检查字符串前缀导致 symlink 穿透。这样的系统在演示中显得“很安全”,一旦通过 API、恢复任务或另一个工具入口执行,确认框就不再是边界。

    修复时把授权放到执行入口之前,并让拒绝原因成为结构化结果。用户需要知道是目录越界、参数危险还是缺少确认;模型也只能根据这个原因修正意图,不能把拒绝当成普通工具异常后无限重试。

    用攻击者视角做五个测试

    练习:为同一个删除请求准备五种输入:相对路径、绝对路径、父目录跳转、指向外部目录的 symlink、包含管道和重定向的命令。分别记录解析后的目标、命中的规则、是否需要确认以及最终审计事件。

    再测试权限变化:用户确认后切换工作区、取消会话、让外部文件先被修改。安全边界不仅要挡住坏请求,也要在状态变化时拒绝过期授权。

    先写威胁模型,再写“危险命令列表”

    安全规则若从几个字符串开始,很快会变成无穷正则。先列资产、入口和攻击路径,才知道该在哪层挡。

    对本地代码 Agent,资产至少包括项目文件、项目外个人文件、环境变量与凭据、Git 历史、网络身份、后台进程。入口不仅是用户 Prompt,还包括仓库里的恶意文档、工具输出、MCP 返回值、Skill 和自定义命令。攻击者可能诱导模型读取密钥、把内容发往外部、覆盖文件或执行下载脚本。

    一条典型链路是:README 中藏提示注入,模型照着调用 Shell 读取 .env,再把内容作为查询参数请求外部 URL。只禁 rm -rf 完全挡不住。需要文件读取范围、敏感模式检测、网络目标策略和工具结果来源标记共同工作。

    威胁模型不要求预测所有命令,而是让团队知道哪类资产必须有硬边界,哪类风险只能靠隔离环境降低。

    审批必须绑定动作摘要

    用户确认“允许写入 src/config.ts”,模型稍后却把参数换成 src/auth.ts。如果审批只保存一个布尔值 approved = true,后续任何写入都可能借用这次确认。

    审批 receipt 应绑定工具名、规范化参数 hash、解析后的资源、当前工作区版本和过期时间:

    interface ApprovalReceipt {
      requestId: string
      tool: string
      normalizedInputHash: string
      resourceVersion?: string
      cwdRealPathHash: string
      approvedAt: string
      expiresAt: string
      approvedBy: 'user' | 'policy'
    }
    

    执行前重新计算并逐项比较。参数被 Hook 修改、文件已被别人更新、cwd 切换或 receipt 过期,都需要重新确认。这是在处理 TOCTOU:检查时和使用时之间,世界可能已经变化。

    界面展示给用户的也应是规范化动作,不只是模型原文。例如“删除缓存”要展开为三个真实目录、预计文件数、是否可恢复。确认文字越模糊,receipt 的技术绑定越无法弥补理解偏差。

    Plan Mode 是工具可见性的硬边界

    用户说“先给方案”,最稳做法不是仅在 system prompt 写“不要执行”,而是在本轮工具集中移除写入、Shell 和外部修改能力。模型即使受提示注入,也没有可调用路径。

    只读工具仍可能泄露敏感信息,因此 Plan Mode 不等于无权限。它只是把副作用面缩小。绝对路径、用户目录和信任目录仍执行各自策略。

    计划获批后,系统生成新的执行上下文和工具集合,不复用一个全局 isPlan 布尔值。会话里记录审批了哪份计划,但每个具体高风险动作是否还需确认,由策略根据资源和参数决定。

    读取秘密和写出秘密是两道门

    很多系统只限制读取 .env,却允许 Shell 输出环境变量,或允许 HTTP 工具把任意文本发向外部。数据防泄漏要同时看 source 与 sink。

    source 包括凭据文件、进程环境、SSH 配置和工具原始响应;sink 包括网络请求、Git 提交、外部 MCP、审计平台和最终回答。策略可以阻止高敏数据进入普通模型上下文,或在发送前检测已知 secret pattern。

    检测不是万能的,未知密钥格式和编码都可能绕过。更强边界是最小环境变量传递、网络 allowlist、隔离凭据代理,以及根本不把生产 token 暴露给 Agent 进程。Prompt 里的“不要泄露”只能当补充。

    网络工具要防的不只是域名

    允许访问一个 URL 时,要考虑重定向、DNS 变化、私网地址和云 metadata。看起来是公开域名,解析后可能指向 127.0.0.1 或内网;首次请求允许,302 又跳到未批准目标。

    每次重定向都重新应用策略,禁止 link-local、loopback 和私网范围,除非该工具明确为受控内网连接。代理环境也要记录最终目标,不能因为流量经过可信代理就默认目的地可信。

    下载内容不应自动执行。curl ... | sh 或 PowerShell 下载后 Invoke-Expression 属于组合风险,命令策略需要识别;更稳方式是分成下载、校验 hash、展示来源、审批执行几个结构化动作。

    Windows Shell 需要自己的规则

    rm -rf 只是类 Unix 示例。Windows 还要关注 Remove-Item -Recurse -Forcerd /s /q、PowerShell 子表达式、重定向、管道和调用运算符。实际运行 PowerShell 7,就按 PowerShell 语义解析,不能把字符串交给 Bash 规则判断。

    命令可能通过变量和脚本文件隐藏真实动作,静态分析无法完全展开。因此高风险 Shell 永远不应被宣传成“正则检查后安全”。本地工作区限制、最小 OS 权限、可恢复快照、人工审批和审计共同降低风险。

    对常见动作优先提供结构化工具:读取 Git 状态、运行固定测试、应用 patch。Shell 保留为逃生舱,权限自然比专用只读工具高。

    回滚能力不能替代授权

    有文件快照并不意味着可以放松写权限。快照可能写失败,外部命令可能修改未追踪文件,数据库和网络动作也未必可逆。回滚是事故后的恢复层,不是执行前的放行依据。

    写工具只有在快照确认成功后才执行,回滚前再比较写后 hash,避免覆盖用户随后做的新修改。若发生冲突,展示 diff 和恢复选项,不自动用旧版本覆盖。

    用“拒绝是否一致”验收纵深防线

    同一个越界读取从 CLI、SubAgent、用户命令、自定义 Tool 和 MCP 适配层发起,都应得到相同策略状态和可理解原因。若只有 UI 入口弹框,后台入口成功,说明授权还不是执行边界。

    测试也要包含允许案例,避免规则把工具全部封死。安全目标不是“什么都不能做”,而是低风险动作流畅,高风险动作可见、可确认,越界动作稳定拒绝,状态变化后旧授权失效。用户能预测系统什么时候会停下来,才会愿意授予必要能力。

    临时授权应像租约,而不是角色升级

    用户允许本轮读取一个工作区外日志,不代表以后所有绝对路径都可读。系统生成只绑定 normalized realpath、能力 read、session/turn、过期时间和内容 hash 的 lease;不开放全局 ALLOW_OUTSIDE_CWD

    文件变化、路径重新解析到别处、会话结束或用户撤销时 lease 失效。SubAgent 不自动继承主 Agent 的临时授权,任务合同明确传递且只能更窄。外部附件授权也不能转成 Shell cwd 权限。

    审批界面展示“读取该文件一次”与“允许当前项目目录”的差别,默认推荐最小范围。Audit 记录 leaseId、批准者、资源摘要和消费结果,不记录正文。

    用测试证明租约不能复用:第一次读取成功;同路径内容变化后要求重新确认;改为 sibling 路径被拒;恢复旧会话后 lease 过期;主 Agent 成功不代表子 Agent 成功。这样的权限模型既允许真实工作,又不会用一个方便开关拆掉全部边界。

    本文目录
    本文目录