Read OSS

安全设计理念:策略引擎、沙箱机制与安全检查器

高级

前置知识

  • 第 3 篇:工具与调度器
  • 了解安全沙箱的基本概念
  • 熟悉 TOML 配置格式

安全设计理念:策略引擎、沙箱机制与安全检查器

一个能够在本机执行 shell 命令、编辑文件的智能编程助手,必须具备健壮的安全保障。Gemini CLI 为此构建了三层防御体系:基于规则的策略引擎(控制工具能做什么)、平台级沙箱(限制工具在哪里执行)、以及可插拔的安全检查器(引入上下文感知的判断逻辑)。本文将逐层剖析这套架构。

PolicyEngine:基于规则的访问控制

PolicyEngine 是第一道防线。它将工具调用与一组按优先级排序的规则逐一比对,每条规则会产生三种决策之一:ALLOWDENYASK_USER

规则从 TOML 策略文件加载,并按优先级从高到低排序。当工具调用到来时,引擎找到第一条匹配的规则并返回其决策。配置结构如下:

[[rules]]
toolName = "shell"
decision = "allow"
commandPrefix = "git status"
priority = 100

[[rules]]
toolName = "shell"
decision = "deny"
commandPrefix = "git push --force"
priority = 200

[[rules]]
toolName = "mcp_myserver_*"
decision = "ask_user"
priority = 50

入口方法 check() 位于第 488 行,处理以下几类复杂情况:

  • MCP 服务器名称解析 — 从工具注解中提取服务器名称,或解析 mcp_serverName_toolName 格式的完全限定名称(FQN)
  • 参数模式匹配 — 使用键名排序后的稳定 JSON 序列化结果来匹配 argsPattern 正则表达式
  • 工具别名解析 — 通过 getToolAliases() 同时检查当前名称和历史遗留名称
  • shell 命令特殊处理 — 识别 shell 工具调用,并将其转交给命令级解析逻辑处理
flowchart TD
    A[PolicyEngine.check(toolCall)] --> B[Resolve serverName]
    B --> C[Compute stringified args]
    C --> D{Is shell command?}
    D -- Yes --> E[Parse command for<br/>per-command policy]
    D -- No --> F[Match against rules]
    E --> F
    F --> G{First matching rule?}
    G -- Found --> H[Return rule.decision]
    G -- None --> I{Safety checkers match?}
    I -- Yes --> J[Run checker]
    I -- No --> K[Return default decision]
    J --> L[Return checker result]

ruleMatches() 方法位于第 79 行,支持以下匹配维度:

  • 审批模式过滤 — 规则可以指定适用的模式(DEFAULTYOLOAUTO_EDITPLAN
  • 子智能体作用域 — 规则可以针对特定的子智能体生效
  • MCP 服务器匹配 — 通过 mcpName 字段或通配符模式进行匹配
  • 注解匹配 — 规则可以要求工具具备特定注解
  • 交互模式过滤 — 针对管道输入和终端交互分别应用不同规则

针对 shell 命令的参数级策略解析

shell 命令需要特殊对待,因为 git statusgit push --force 虽然调用的是同一个工具(shell),但风险等级却天差地别。策略引擎会解析 shell 命令,将策略评估细化到参数层面。

当检测到 shell 工具调用时,引擎会:

  1. 从工具调用中提取 command 参数
  2. 通过 splitCommands() 拆分复合命令(如 cmd1 && cmd2
  3. 使用 shell-quote 库的 shellParse() 解析每条子命令
  4. 将每条子命令与带有 commandPrefix 模式的规则进行匹配

这样就能实现精细化控制,例如"允许 npm test,但禁止 npm publish",而无需对所有 shell 命令一刀切地放行或拦截。规则中的 commandPrefix 字段支持单个字符串或数组,可同时匹配多个前缀。

flowchart TD
    A["shell tool call:<br/>npm test && npm publish"] --> B[splitCommands]
    B --> C["Sub-command 1: npm test"]
    B --> D["Sub-command 2: npm publish"]
    C --> E{Match rules}
    D --> F{Match rules}
    E --> G["Rule: allow npm test → ALLOW"]
    F --> H["Rule: deny npm publish → DENY"]
    G --> I{Both sub-commands}
    H --> I
    I --> J["Final: DENY<br/>(most restrictive wins)"]

提示: 在为 shell 命令编写 TOML 策略规则时,请优先使用 commandPrefix 而非 argsPattern 进行命令匹配。引擎能够正确处理管道链、&&|| 运算符,但对序列化参数字符串直接使用原始正则表达式在面对复杂 shell 语法时会出错。

平台级沙箱机制

沙箱层在操作系统层面运行,限制整个进程可访问的资源范围。SandboxManager 接口定义了统一契约,由各平台提供具体实现。

MacOsSandboxManager 利用 macOS 的沙箱隔离机制(sandbox-exec 系统),限制文件系统访问、网络连接和系统调用。其核心方法包括:

  • isKnownSafeCommand(args) — 对已知无害命令(如 lscat)走快速放行通道,同时检查模式配置中的 approvedTools 列表。
  • isDangerousCommand(args) — 识别明显危险的操作。
  • prepareCommand(req) — 核心方法。负责清理环境变量、根据权限配置(只读模式、工作区写入权限、网络访问权限)构建沙箱配置文件,并用 sandbox-exec 包装命令。
graph TD
    subgraph "Platform Sandbox Managers"
        MAC[MacOsSandboxManager<br/>seatbelt profiles]
        DOCKER[Docker/Podman<br/>container isolation]
        WIN[WindowsSandboxManager<br/>Windows sandbox]
        NOOP[NoopSandboxManager<br/>no restrictions]
    end
    
    SI[SandboxManager Interface]
    SI --> MAC
    SI --> DOCKER
    SI --> WIN
    SI --> NOOP
    
    POLICY[SandboxPolicyManager<br/>persistent permissions]
    MAC --> POLICY
    DOCKER --> POLICY

macOS 实现还支持虚拟命令转换:__read 会被替换为 /bin/cat__write 则被替换为 cat > "$1" 管道,通过受控的原语实现沙箱内的文件访问。

在 Linux 上,Docker 或 Podman 容器提供隔离环境。沙箱决策发生在启动序列的早期(如第 1 篇所述):如果启用了沙箱且当前进程不在沙箱中,则进程会将自身重新启动到容器内部。

安全检查器:CheckerRunner 与 Conseca

在基于规则的策略之外,Gemini CLI 还支持可插拔的安全检查器,能够对工具调用作出更细致的判断。CheckerRunner 负责协调两类检查器:

进程内检查器 — 直接在 Node.js 进程中运行。CheckerRegistry 按名称解析检查器,ContextBuilder 组装所需上下文,检查器返回 ALLOWDENYASK_USER

外部检查器 — 作为独立进程运行,具有超时限制(默认 5 秒)。工具调用详情通过 stdin 以 JSON 格式传入,检查器的 stdout 输出通过 Zod schema 解析为结果。

const SafetyCheckResultSchema = z.discriminatedUnion('decision', [
  z.object({ decision: z.literal(SafetyCheckDecision.ALLOW), reason: z.string().optional() }),
  z.object({ decision: z.literal(SafetyCheckDecision.DENY), reason: z.string().min(1) }),
  z.object({ decision: z.literal(SafetyCheckDecision.ASK_USER), reason: z.string().min(1) }),
]);

安全检查器规则与策略规则一同定义在 TOML 配置中。它们有独立的优先级排序和匹配逻辑(包括工具名称通配符和参数模式),产生的 SafetyCheckDecision 值会回流至策略处理流水线。

ConsecaSafetyChecker 是一个进程内实现,通过启发式方法和对话上下文来评估工具调用。其启用标志(enableConseca)来自 Config 配置。

审批模式与确认流程

Gemini CLI 支持四种审批模式,控制工具自动放行的积极程度:

模式 行为
DEFAULT 所有破坏性操作均需确认
YOLO 自动放行所有操作(仅在受信任环境中使用)
AUTO_EDIT 自动放行文件编辑,shell 命令仍需确认
PLAN 只读模式,拦截所有写入和执行操作

这些模式通过规则中的 modes 字段与策略规则联动——规则可以指定仅在特定模式下生效。例如,YOLO 模式的规则可以设置 priority: PRIORITY_YOLO_ALLOW_ALL,覆盖所有其他规则。

确认流程整合了所有安全层:

stateDiagram-v2
    [*] --> PolicyCheck: Tool call arrives
    PolicyCheck --> AutoAllow: ALLOW
    PolicyCheck --> AutoDeny: DENY
    PolicyCheck --> CheckerPhase: ASK_USER
    
    CheckerPhase --> AutoAllow: Checker says ALLOW
    CheckerPhase --> AutoDeny: Checker says DENY
    CheckerPhase --> HookPhase: Checker says ASK_USER
    
    HookPhase --> AutoAllow: Hook says proceed
    HookPhase --> AutoDeny: Hook says block
    HookPhase --> UIConfirmation: Hook says ask user
    
    UIConfirmation --> Executed: User approves
    UIConfirmation --> Cancelled: User denies
    UIConfirmation --> PolicyUpdate: User says "always allow"
    
    PolicyUpdate --> Executed: Update rules, then execute
    
    AutoAllow --> Executed
    AutoDeny --> Cancelled

当用户对某个工具选择"始终允许"时,调度器中的 updatePolicy() 会以适当的精度创建一条新规则。对于 shell 命令,它会捕获 commandPrefix;对于 MCP 工具,则捕获 mcpName。用户还可以选择"始终允许并保存",将规则持久化到磁盘。

提示: PolicyEngine 上的 disableAlwaysAllow 标志可以阻止"始终允许"规则生效,适用于管理员希望保持集中管控的环境。更多安全配置项请参阅 PolicyEngineConfig

这套安全架构的优势在于其分层设计。策略规则提供粗粒度控制,沙箱约束执行环境,安全检查器引入上下文感知的判断能力。每一层都能独立拦截工具调用,而确认流程则确保在自动化决策不够充分时,用户始终掌握最终控制权。

下一篇文章将探讨构建在这套安全基础之上的可扩展性机制——hooks、skills、MCP 集成以及扩展打包系统。