命令执行安全机制

深入理解 zapmyco 如何保障 AI 执行命令的安全性

~ 2 min read

AI Agent 可以执行任意 shell 命令,这是一把双刃剑——能力越强,安全责任越大。 zapmyco 设计了三层安全模型,在安全性和交互流畅度之间找到平衡。

三层安全模型

flowchart TD
    C["命令请求"] --> L1{"第一层<br/>内置安全列表"}
    L1 -->|"✅ 匹配"| ALLOW["自动放行"]
    L1 -->|"❌ 未命中"| L2{"第二层<br/>用户白名单 / 黑名单"}
    L2 -->|"deny 匹配"| DENY["拒绝"]
    L2 -->|"allow 匹配"| ALLOW
    L2 -->|"均未命中"| L3{"第三层<br/>运行时逐子命令审批"}
    L3 -->|"全部已准入"| ALLOW
    L3 -->|"部分未准入"| CONFIRM{"弹框审批"}
    CONFIRM -->|"允许"| ALLOW
    CONFIRM -->|"始终允许"| ADD["加入白名单"] --> ALLOW
    CONFIRM -->|"拒绝"| CANCEL["取消"]

    style ALLOW fill:#d4edda,color:#155724
    style DENY fill:#f8d7da,color:#721c24
    style CANCEL fill:#f8d7da,color:#721c24
    style CONFIRM fill:#fff3cd,color:#856404

每一层都是累加检查的:命中了上层就直接放行或拒绝,不命中才流到下一层。


第一层:内置安全列表

编译期定义的绝对安全命令列表,只包含纯读取系统状态的命令。

类别命令说明
pwd打印工作目录
whoami打印当前用户名
通用true无操作,返回 0
false无操作,返回 1
echo输出文本(需带参数,裸 echo 不放行)
printf格式化输出(需带参数)
cd改变工作目录
uname系统信息
hostname主机名
uptime系统运行时间
arch硬件架构
系统信息which定位命令路径(需带参数)
id用户身份信息
logname登录用户名
tty终端设备名
cal显示日历
seq生成数字序列(需带参数)
系统配置getconf系统配置变量(需带参数)
pathchk路径名检查(需带参数)
basename从路径中提取文件名(需带参数)
路径操作dirname从路径中提取目录名(需带参数)
realpath解析为绝对路径(需带参数)
目录列表ls列出目录内容
日期时间date查看当前日期时间

Windows 额外支持的命令:

命令说明
ver显示 Windows 版本
systeminfo显示系统信息
dir列出目录
date /t显示日期(/t 表示只查看不设置)
time /t显示时间(/t 表示只查看不设置)
vol显示卷标

匹配规则: 命令以列表中某个条目的前缀匹配。例如 ls 放行 lsls -la,但不放行 lsblk

内置列表的设计原则是”宁可漏放(回到确认流程)也不错放”。 如需扩展,请在第二层配置用户白名单。


第二层:用户白名单 / 黑名单

~/.zapmyco/settings.toml 中配置:

toml
[permissions.commands]
# 白名单:匹配的命令自动放行
allow = [
    "git status",
    "cargo check",
    "cargo clippy",
]

# 黑名单:匹配的命令自动拒绝(优先于白名单)
deny = [
    "rm -rf",
    "sudo",
]
  • allow 中的命令匹配后跳过所有审批
  • deny 中的命令匹配后直接拒绝,即使也在 allow
  • 匹配规则与内置列表相同(前缀匹配)

第三层:运行时审批(核心)

这是最值得关注的一层——它解决了传统审批系统的核心痛点。

复合命令拆分

AI 经常生成链式命令,例如 git status && cargo check && cargo test。 传统系统要么整条拒绝,要么整条弹框。

zapmyco 的命令拆分器split_commands)使用状态机逐字符扫描, 在顶层控制运算符处将命令拆分为独立子命令:

flowchart LR
    INPUT["git status && cargo check && cargo test"]
    SPLIT["命令拆分器"]
    P1["git status"]
    P2["cargo check"]
    P3["cargo test"]

    INPUT --> SPLIT
    SPLIT --> P1
    SPLIT --> P2
    SPLIT --> P3

拆分器正确跳过以下语法内的运算符:

语法示例是否拆分
单引号 '...'echo 'a && b'❌ 不拆分
双引号 "..."echo "a && b"❌ 不拆分
命令替换 $(...)echo $(foo && bar)❌ 不拆分
参数展开 ${...}echo ${VAR:-foo}❌ 不拆分
转义字符 \echo a \& b❌ 不拆分

重定向运算符的识别

> < >> >& &> << <<< <> 等复合重定向符号被正确识别为一个整体, 不会错误拆分:

flowchart LR
    A["echo 2>&1"]
    B["echo foo &> /dev/null"]
    C["exec 3<> /dev/tcp"]
    A1["1 part<br/>has_redirect=true"]
    B1["1 part<br/>has_redirect=true"]
    C1["1 part<br/>has_redirect=true"]

    A --> A1
    B --> B1
    C --> C1

审批决策流程

拆分后的每个子命令独立走审批决策,决策逻辑如下:

flowchart TD
    SUB["子命令"] --> FLAGS{"危险特征判断"}
    FLAGS -->|"含命令替换 $()"| SUB_PROMPT["⚠️ 始终弹框"]
    FLAGS -->|"含重定向 > <"| REDIR{"用户白名单?"}
    REDIR -->|"✅ 命中"| ALLOW["自动放行"]
    REDIR -->|"❌ 未命中"| SUB_PROMPT
    FLAGS -->|"干净命令"| CLEAN{"内置或用户白名单?"}
    CLEAN -->|"✅ 命中"| ALLOW
    CLEAN -->|"❌ 未命中"| SUB_PROMPT

    style ALLOW fill:#d4edda,color:#155724
    style SUB_PROMPT fill:#fff3cd,color:#856404

为什么这么分级:

特征示例风险内置列表用户白名单
干净cargo check不放行可放行
重定向 > <echo hello > file不放行可放行
命令替换 $() `echo $(whoami)不放行不放行
  • 重定向只是修改 I/O 流向,命令本身的危险性不变。 如果你信任 cargo buildcargo build > /tmp/log 也应该值得信任。
  • 命令替换会执行嵌入代码。echo $(rm -rf /)echo hello 完全不同, 所以即使命令在白名单中也必须弹框。

混合审批

这是最终的用户体验效果:

flowchart TD
    C["cargo build > /tmp/log && echo done"]
    S["split_commands()"]
    P1["cargo build > /tmp/log<br/>has_redirect=true"]
    P2["echo done<br/>clean"]

    C --> S
    S --> P1
    S --> P2

    P1 --> R1{"用户白名单?"}
    P2 --> R2{"内置列表?"}

    R1 -->|"❌ 未命中"| P1P["⚠️ 弹框"]
    R2 -->|"✅ echo 在列表中"| P2A["✅ 自动放行"]

    style P2A fill:#d4edda,color:#155724
    style P1P fill:#fff3cd,color:#856404

部分命令已准入时,只弹未准入的部分

⚠️ 准备执行命令:
  完整命令: cargo build > /tmp/log && echo done

以下命令需要授权:
  ▢ cargo build > /tmp/log  (包含文件重定向)

以下命令已在白名单中(自动放行):
  ✓ echo done

[允许] [始终允许] [拒绝]

全部未准入时,显示完整命令:

⚠️ 准备执行命令:
  完整命令: git status && cargo check
[允许] [始终允许] [拒绝]

全部已准入时,不弹任何框,静默执行。


三种交互模式

flowchart LR
    subgraph Terminal["终端模式"]
        T1["crossterm 选择菜单"]
        T2["允许 / 始终允许 / 拒绝"]
    end
    subgraph Web["Web 模式"]
        W1["SSE 推送审批事件"]
        W2["前端审批卡片"]
        W3["HTTP 异步返回结果"]
    end
    subgraph NonTTY["非交互环境"]
        N1["非 TTY(管道/CI)"]
        N2["默认拒绝"]
    end

    Terminal --> T1 --> T2
    Web --> W1 --> W2 --> W3
    NonTTY --> N1 --> N2

终端模式(TTY)

交互式终端中弹出三个选项的选择菜单:

  • 允许 — 单次执行
  • 始终允许 — 加入白名单后执行(危险命令被阻止加入)
  • 拒绝 — 取消执行

Web 模式

通过 SSE(Server-Sent Events)向后端发送 tool_approval_required 事件, 前端展示审批卡片,结果通过 POST /api/tool/approve 异步返回。

非交互环境

管道、CI、重定向等非 TTY 环境:默认拒绝所有需要审批的命令。这是安全设计。


执行路径

安全审批只影响是否执行,不影响如何执行。 命令原样透传给系统 shell:

flowchart LR
    subgraph Before["审批系统改造前"]
        CMD1["foo && bar"]
        CHECK1["contains_shell_control()"]
        REJECT1["检测到 & 整条拒绝 ❌"]
        CMD1 --> CHECK1 --> REJECT1
    end
    subgraph After["审批系统改造后"]
        CMD2["foo && bar"]
        SPLIT2["split_commands()"]
        P1["foo ✅ 白名单"]
        P2["bar ⚠️ 审批"]
        EXEC["sh -c 'foo && bar'"]
        CMD2 --> SPLIT2
        SPLIT2 --> P1
        SPLIT2 --> P2
        P1 --> EXEC
        P2 -->|允许| EXEC
    end
Unix:     sh -c "<原命令>; _ZMD_RC=$?; pwd -P > ...; exit $_ZMD_RC"
Windows:  cmd.exe /d /c "<写入临时 bat 文件>"

这就是为什么我们说**“只改审批路径,不改执行路径”**。


安全最佳实践

  1. 从小权限开始 默认只有内置安全列表放行,其他全审批。先这样用一段时间,观察 AI 的行为模式。

  2. 常用只读命令用内置列表就够了 pwd ls echo date 等已在列表中,无需重复配置。

  3. 白名单只放行你信任的命令 git status cargo check 这类只读检查命令是好的候选。 rm sudo 等危险命令被系统阻止加入白名单。

  4. 有重定向的命令谨慎放行 虽然用户白名单可以放行重定向命令(如 echo > file), 但建议只在明确信任该命令时配置。

  5. 定期审查白名单

    bash
    cat ~/.zapmyco/settings.toml | grep -A 20 "\[permissions"

技术原理(可选)

如果关心实现细节:

  • 命令拆分器 split_commands() 使用状态机逐字符扫描,O(n) 复杂度
  • 6 种扫描状态:Normal / SingleQuote / DoubleQuote / CommandSubst / ParamSubst / Backtick / Escape
  • 括号计数器和花括号计数器处理 $()${} 嵌套
  • 拆分器在任何异常输入下都不 panic(未闭合引号、未闭合 $(、空字符串等)
  • 拆分器出错的最坏情况:子命令未正确拆分 → 白名单不匹配 → 触发审批 → 安全降级为”需要用户确认” → fail-safe:永远不会绕过安全检查
Last updated Edit this page

Type to search…

↑↓ navigate open esc close