四、命令风险分级与审批策略

1. 为什么需要风险分级?

想象你是门卫,有个人要进大楼。你不认识他,怎么决定放不放?

最简单的办法:看他长什么样

  • 穿着工服、戴着工牌的 → 可能是员工(L0,放行)
  • 穿着便装但看起来无害的 → 可能是访客(L2,登记一下)
  • 拿着撬棍的 → 可能是小偷(L4,直接拦住)

policy.ts 就是这个门卫。它通过正则匹配命令文本来判断风险等级。

2. 五级风险等级

回顾一下:

等级 含义 典型命令
L0 只读,无害 lscatgit status
L1 低风险本地执行 pnpm testnode script.js
L2 会写工作区 rmcpgit addnpm run
L3 网络/依赖/外部交互 curlnpm installgit push
L4 高危破坏性 rm -rfsudogit push --force

3. 正则模式匹配

每种等级对应一组正则表达式。匹配时从高到低------先检查 L4,再检查 L3......如果都没命中,默认是 L2。

3.1 L4------高危

typescript 复制代码
const L4_PATTERNS = [
  /\brm\s+-rf\b/i,        // rm -rf:递归强制删除
  /\bsudo\b/i,            // sudo:提权执行
  /\bmkfs\b/i,           // mkfs:格式化磁盘
  /\bdd\s+if=/i,          // dd:磁盘级读写
  /\bshutdown\b/i,        // shutdown:关机
  /\breboot\b/i,          // reboot:重启
  /\bgit\s+push\s+--force\b/i,  // git push --force:强推远端
  /\bgit\s+reset\s+--hard\b/i,  // git reset --hard:硬重置
] as const;

3.2 L3------网络/依赖

typescript 复制代码
const L3_PATTERNS = [
  /\bcurl\b/i,            // curl:HTTP 请求
  /\bwget\b/i,            // wget:下载文件
  /\bssh\b/i,             // ssh:远程登录
  /\bscp\b/i,             // scp:远程拷贝
  /\bgit\s+push\b/i,      // git push:推送到远端
  /\bgit\s+clone\b/i,     // git clone:从远端克隆
  /\bnpm\s+publish\b/i,   // npm publish:发布包
  /\bpnpm\s+publish\b/i,
  /\bpip\s+install\b/i,
  /\bpnpm\s+install\b/i,
  /\bnpm\s+install\b/i,
  /\byarn\s+install\b/i,
] as const;

3.3 L2------工作区写入

typescript 复制代码
const L2_PATTERNS = [
  /\brm\s+/i,      // rm(不带 -rf 的普通删除也是 L2)
  /\bmv\s+/i,      // mv:移动/重命名
  /\bcp\s+/i,      // cp:拷贝
  /\btouch\s+/i,   // touch:创建文件
  /\bmkdir\s+/i,   // mkdir:创建目录
  /\bchmod\b/i,    // chmod:改权限
  /\bchown\b/i,    // chown:改所有者
  /\bgit\s+add\b/i,
  /\bgit\s+commit\b/i,
  /\bgit\s+checkout\b/i,
  /\bgit\s+merge\b/i,
  /\bpnpm\b/i,     // pnpm(子命令很多,整体视为 L2)
  /\bnpm\s+run\b/i,
  /\bnpx\b/i,
  /\bvitest\b/i,
  /\bjest\b/i,
  /\beslint\b/i,
  /\bprettier\b/i,
] as const;

3.4 L1------低风险本地

typescript 复制代码
const L1_PATTERNS = [
  /^pnpm\s+test\b/i,
  /^npm\s+test\b/i,
  /^vitest\s+run\b/i,
  /^jest\b/i,
  /^node\s+/i,
  /^python3?\s+/i,
] as const;

注意 L1 用的是 ^(行首锚定),意思是"命令必须以这些开头"才算 L1。比如 echo hello && node script.js 不会命中 L1(因为有 echo 在前面),但会被 SHELL_META_PATTERN 升级到 L3。

3.5 L0------只读

typescript 复制代码
const L0_PATTERNS = [
  /^pwd$/i,
  /^ls(\s|$)/i,
  /^cat\s+/i,
  /^head\s+/i,
  /^tail\s+/i,
  /^grep\s+/i,
  /^rg\s+/i,
  /^find\s+\./i,
  /^git\s+status$/i,
  /^git\s+diff(\s|$)/i,
  /^git\s+log(\s|$)/i,
  /^git\s+show(\s|$)/i,
  /^echo\s+/i,
] as const;

L0 也用了 ^ 锚定------只有以这些命令开头的才是只读。ls && rm -rf / 不会被认为是 L0。

4. classifyCommandRisk------命令风险分级

typescript 复制代码
export function classifyCommandRisk(command: string): CommandRiskLevel {
  const line = command.trim();

  if (L4_PATTERNS.some((rx) => rx.test(line))) return 'L4';
  if (L3_PATTERNS.some((rx) => rx.test(line))) return 'L3';
  if (L2_PATTERNS.some((rx) => rx.test(line))) return 'L2';
  if (L1_PATTERNS.some((rx) => rx.test(line))) return 'L1';
  if (L0_PATTERNS.some((rx) => rx.test(line))) return 'L0';

  // 未知命令:不假设只读,按「可能写工作区」处理
  return 'L2';
}

核心逻辑:从高到低匹配,先命中高危即返回。如果都不命中,默认 L2(保守策略)。

5. analyzeCommandSpec------结构化分析

classifyCommandRisk 只看命令字符串,但 analyzeCommandSpec 能区分 shellexec 两种模式,产出更丰富的 SandboxPermissionIntent

5.1 shell 模式

typescript 复制代码
const SHELL_META_PATTERN = /(\|\||&&|[|;&<>`$()])/;



if (spec.kind === 'shell') {
  let riskLevel = classifyCommandRisk(spec.displayCommand);

  // shell 元字符(管道、重定向等)→ 至少 L3
  if (SHELL_META_PATTERN.test(spec.shellCommand)) {
    riskLevel = maxRiskLevel(riskLevel, 'L3');
  }

  // 第一个 token 是解释器(node/python/bash 等)?
  const firstToken = spec.displayCommand.trim().split(/\s+/)[0]?.toLowerCase();
  const usesInterpreter = firstToken ? SCRIPT_INTERPRETERS.has(firstToken) : false;

  return {
    kind: 'shell',
    riskLevel,
    usesShell: true,
    usesInterpreter,
    interpreter: usesInterpreter ? firstToken : undefined,
    executableBase: firstToken,
    usesNetwork: riskLevel === 'L3',
    writesWorkspace: riskLevel === 'L2' || riskLevel === 'L4' || usesInterpreter,
    touchesGit: /\bgit\b/i.test(spec.displayCommand),
    highImpact: riskLevel === 'L4',
  };
}

三个关键提升点

  1. Shell 元字符 :如果命令包含 |&&;> 等,说明实际不止一条简单命令,至少 L3
  2. 解释器检测nodepythonbash 等可以执行任意代码,风险更高
  3. Git 检测 :命令里出现 git 就标记 touchesGit: true

5.2 exec 模式

typescript 复制代码
/** 解释器类可执行文件:即使 argv 看起来无害,也可能执行任意脚本 */
const SCRIPT_INTERPRETERS = new Set([
  'node',
  'nodejs',
  'python',
  'python3',
  'bash',
  'sh',
  'zsh',
  'ruby',
  'perl',
  'osascript',
]);


const executableBase = path.basename(spec.executable).toLowerCase();
const normalized = [executableBase, ...spec.args].join(' ').trim();
let level = classifyCommandRisk(normalized);
const usesInterpreter = SCRIPT_INTERPRETERS.has(executableBase);

if (usesInterpreter) {
  level = maxRiskLevel(level, 'L2'); // 解释器本身视为可能写工作区
  if (spec.args.some((arg) => arg === '-e' || arg === '-c')) {
    level = maxRiskLevel(level, 'L3'); // node -e / bash -c → 至少 L3
  }
}

exec 模式的特殊处理

  1. executable 的 basename 和 args 拼成字符串,复用 classifyCommandRisk------意思是这样:比如用户传入 { kind: 'exec', executable: '/usr/bin/git', args: ['push', '--force'] },exec 模式下可执行文件路径和参数是分开的数组,但 classifyCommandRisk 只接收一个字符串。所以我们先取路径的最后一段(git),再和参数拼成 "git push --force",这样就能直接丢进 classifyCommandRisk 用正则去匹配了
  2. 如果是解释器,至少 L2(因为解释器能执行任意代码)
  3. node -ebash -c 这种内联代码,至少 L3(因为代码内容不可预知)

6. 审批策略映射

6.1 isBlockedByDefault

typescript 复制代码
export function isBlockedByDefault(level: CommandRiskLevel): boolean {
  return level === 'L4';
}

L4 命令在默认策略下直接拦截,不走审批流程。只有调用方显式同意后才能执行。

6.2 riskLevelNeedsApproval

typescript 复制代码
export function riskLevelNeedsApproval(level: CommandRiskLevel, policy: ApprovalPolicy): boolean {
  if (policy === 'autonomous') return false;  // 自主模式:一律不审
  if (policy === 'untrusted') return level !== 'L0';  // 不信任模式:只有 L0 自动过
  if (level === 'L0' || level === 'L1') return false;  // on-request:L0/L1 自动
  return true;  // L2+ 要审
}
策略 L0 L1 L2 L3 L4
autonomous 不审 不审 不审 不审 拦截+审
untrusted 不审 要审 要审 要审 拦截+审
on-request 不审 不审 要审 要审 拦截+审

6.3 needsApproval

typescript 复制代码
export function needsApproval(commandLine: string, policy: ApprovalPolicy) {
  return riskLevelNeedsApproval(classifyCommandRisk(commandLine), policy);
}

这是一个便捷函数,把分级和策略判断合二为一。

7. describeCommandRisk------给人看的说明

typescript 复制代码
export function describeCommandRisk(level: CommandRiskLevel, command: string): string {
  const messages: Record<CommandRiskLevel, string> = {
    L0: `只读命令,通常可自动执行:${command}`,
    L1: `本地测试/脚本命令,可能影响进程状态:${command}`,
    L2: `会写入工作区或变更仓库状态:${command}`,
    L3: `涉及网络、依赖下载或外部系统交互:${command}`,
    L4: `高危命令,默认强审批或拒绝:${command}`,
  };
  return messages[level];
}

这个函数产出的文本会显示在审批提示里,让用户知道"为什么要问我"。

8. 局限性

这个风险分级方案有一些天生的局限:

  1. 基于正则,不做 AST 解析echo hello && rm -rf / 中的 rm -rf 可能被识别为 L4,但如果命令更复杂,正则可能漏掉
  2. 不看命令上下文node -e "require('fs').readFileSync('/etc/passwd')" 命中了 node -e 升级为 L3,但实际读了什么只有执行后才知道
  3. 可被绕过bash -c "rm -rf /" 如果用 exec 模式传入,可能不会命中 L4 的正则(因为 displayCommand 是 bash -c rm -rf /,没有 rm -rf 的精确匹配)

所以------风险分级只是第一道防线,不能替代 OS 级沙箱和审计

9. 小结

函数 作用
classifyCommandRisk 对命令字符串打 L0~L4 标签
analyzeCommandSpec 对结构化命令规格做更丰富的权限意图分析
isBlockedByDefault L4 是否默认拦截
riskLevelNeedsApproval 给定策略,该等级是否要审批
needsApproval 便捷封装:分级 + 策略判断
describeCommandRisk 给人看的风险说明文案
isClearlySafeReadOnlyCommand 快速判断是否 L0 只读

核心原则:从高到低匹配,未知默认 L2,L4 默认拦截

相关推荐
阿乔外贸日记1 小时前
埃塞俄比亚出口全流程注意事项
大数据·人工智能·智能手机·云计算·汽车
程序员cxuan1 小时前
Agents.md 是什么
人工智能·后端·程序员
人工小情绪1 小时前
Windows 安装 Codex 桌面版,并用 CC Switch 管理配置
人工智能·windows·codex·cc switch
godspeed_lucip1 小时前
LLM和Agent——专题6:Multi Agent 入门(5)
人工智能·python
网安情报局1 小时前
告别排队与高延迟:直连GPT全系列,解锁低门槛、高稳定的AI生产力
人工智能·gpt·api·ai大模型
Hali_Botebie1 小时前
非共轭先验(Non-conjugate Prior)和共轭先验(Conjugate Prior)
人工智能·机器学习
没事别瞎琢磨2 小时前
三、配置系统——默认值与解析
人工智能·node.js
拓朗工控2 小时前
视觉检测行业工控机选型指南:核心要素与避坑策略
人工智能·数码相机·视觉检测·工控机·工业电脑
Urbano2 小时前
工装制作全流程科普:从面料到自动化生产
网络·人工智能