首页
/ gemini-cli 策略引擎扩展解析:通过 policies 目录为 CLI 贡献安全规则与安全校验器

gemini-cli 策略引擎扩展解析:通过 policies 目录为 CLI 贡献安全规则与安全校验器

2026-09-06 14:44:22作者:昌雅子Ethen

本文以 gemini-cli 仓库中自带的策略引擎示例扩展(packages/cli/src/commands/extensions/examples/policies)为主体,完整拆解“一个扩展如何向 Policy Engine 贡献规则”的全流程:policies/ 目录的加载机制、policies.toml 中每条规则与安全校验器的字段含义、Tier 2 优先级语义,以及“扩展只能收紧、不能放松”的安全边界。读完本文,你可以独立编写一个带策略规则的扩展,理解其被加载、校验、脱敏(sanitization)的底层链路,并验证规则在真实会话中的拦截效果。

示例扩展概览

示例扩展位于 packages/cli/src/commands/extensions/examples/policiesREADME 对其定位的描述是:演示如何向 Gemini CLI 的 Policy Engine 贡献安全规则(rules)和安全校验器(safety checkers)。

它的目录结构极简,只有两个文件:

policies/
├── gemini-extension.json   # 扩展清单(manifest)
└── policies/
    └── policies.toml       # 策略规则文件

其中 gemini-extension.json 是标准的扩展清单,仅包含三项元数据:

{
  "name": "policy-example",
  "version": "1.0.0",
  "description": "An example extension demonstrating Policy Engine support."
}

而全部策略逻辑都集中在 policies/policies.toml。Gemini CLI 的约定是:只要扩展根目录下存在 policies/ 目录,加载扩展时就会自动读取其中所有 .toml 文件——这一点由扩展管理器 extension-manager.ts 中的代码直接证实:

const policyDir = path.join(effectiveExtensionPath, 'policies');
if (fs.existsSync(policyDir)) {
  const result = await loadExtensionPolicies(config.name, policyDir);
  rules = result.rules;
  checkers = result.checkers;
  // 加载出错时仅记录警告,不会让整个扩展加载失败
}

也就是说,示例扩展无需任何运行时代码(MCP server、hooks 等),纯粹靠声明式 TOML 文件就能改变 CLI 的工具调用审批行为。这也是官方 extensions 参考文档 推荐的扩展策略贡献方式。

逐行解读 policies.toml 的三类策略

示例文件 policies.toml 完整定义了三种典型策略,下面逐条讲解其字段与语义。

规则一:rm -rf 命令强制询问用户

# Rule: Always ask the user before running a specific dangerous shell command.
[[rule]]
toolName = "run_shell_command"
commandPrefix = "rm -rf"
decision = "ask_user"
priority = 100

这条规则命中所有 run_shell_command 工具调用中、命令行以 rm -rf 开头的情况,决策为 ask_user——模型发起此类删除时,Policy Engine 会弹出确认提示而非直接执行或静默拒绝。

各字段的含义与约束(依据 toml-loader.ts 中的 Zod 校验 Schema):

  • toolName:规则作用的目标工具,可以是字符串或字符串数组;也可以填 "*" 匹配所有工具。
  • commandPrefix:shell 命令的便捷匹配语法,要求命令行以该前缀开头。源码 validateShellCommandSyntax 中强制了两条约束:commandPrefix / commandRegex 只能搭配 toolName = "run_shell_command"(且必须是字符串而非数组),且二者互斥,也不能与 argsPattern 同时使用。
  • decision:取值为 PolicyDecision 枚举的 allow / deny / ask_user 之一。
  • priority:规则在同 Tier 内的排序权重。Schema 中明确限制其必须为 [0, 999] 的整数(Schema 定义),原因见下文的 Tier 转换公式——>= 1000 的优先级会“越级”到下一个 Tier,造成优先级混乱。

规则二:拒绝用 grep 搜索敏感文件

# Rule: Deny access to sensitive files using the grep tool.
[[rule]]
toolName = "grep_search"
argsPattern = "(\.env|id_rsa|passwd)"
decision = "deny"
priority = 200
denyMessage = "Access to sensitive credentials or system files is restricted by the policy-example extension."

这条规则与规则一使用不同的匹配方式:argsPattern 是对工具任意参数做正则匹配。当 grep_search 的参数中出现 .envid_rsapasswd 任一关键词时,直接返回 deny,并向用户展示自定义的 denyMessage,明确告知这是 policy-example 扩展的策略所致。

几个值得注意的底层细节:

  • argsPattern 相对的还有 commandRegex,但如前所述它只服务于 run_shell_commandargsPattern 则适用于任何工具。
  • 正则并非想写什么都能写。加载时 toml-loader.ts 会先尝试编译 new RegExp(argsPattern),再调用 isSafeRegExp 做 ReDoS 安全检查;不安全的嵌套量词模式会被直接丢弃并报错,避免一条恶意/失误的正则拖垮整个会话。
  • toolName 有拼写纠错能力:若名称与内置工具名的 Levenshtein 距离在 3 以内(如 re__ad 之于 read_file),加载器会给出警告提示;距离过远的名称则被当作动态注册的 agent 工具,静默放行(validateToolName)。

安全校验器:写操作的路径白名单校验

# Safety Checker: Apply path validation to all write operations.
[[safety_checker]]
toolName = ["write_file", "replace"]
priority = 300
[safety_checker.checker]
type = "in-process"
name = "allowed-path"
required_context = ["environment"]

safety_checkerrule 是两类不同的策略原语:规则输出一个决策(allow/deny/ask_user),而校验器在执行前对工具调用的参数本身做结构化校验,校验不通过即阻断。

这条校验器把所有 write_filereplace 写操作接入内置的 allowed-path 校验器——它会校验目标文件路径是否处于允许写入的范围内。相关类型定义在 types.ts

export enum InProcessCheckerType {
  ALLOWED_PATH = 'allowed-path',
  CONSECA = 'conseca',
}

export interface InProcessCheckerConfig {
  type: 'in-process';
  name: InProcessCheckerType;
  config?: AllowedPathConfig;
  required_context?: Array<keyof SafetyCheckInput['context']>;
}

字段说明:

  • toolName:校验器同样支持数组写法,示例中一条校验器覆盖了两个写工具。
  • type = "in-process":校验器在 CLI 进程内运行(内置实现);TOML Schema 还支持 type = "external" 声明外部校验进程(Schema 定义)。
  • name = "allowed-path":即 InProcessCheckerType.ALLOWED_PATH,是进程内内置校验器之一。
  • required_context = ["environment"]:声明该校验器运行时需要的上下文键(如环境信息),引擎会把这些上下文注入校验输入。
  • 可选的 configallowed-path 校验器还支持 AllowedPathConfig 配置项——included_args 显式指定哪些参数键按路径校验、excluded_args 显式排除某些参数键,示例中未使用即采用默认推断。

扩展策略的 Tier 2 优先级模型

TOML 里写的 priority 并不是最终生效值。toml-loader.ts 中的转换公式为:

// Formula: tier + priority/1000
function transformPriority(priority: number, tier: number): number {
  return tier + priority / 1000;
}

即最终优先级 = 所在 Tier 编号 + TOML 优先级除以 1000。这解释了为什么 priority 上限是 999:它保证规则只会落在本 Tier 的小数区间内,不会侵入其他 Tier。getTierName 中的分层映射为:

Tier 层级名 转换后优先级区间
1 default(内置默认策略) 1.000 ~ 1.999
2 extension(扩展策略) 2.000 ~ 2.999
3 workspace(工作区策略) 3.000 ~ 3.999
4 user(用户策略) 4.000 ~ 4.999
5 admin(管理员策略) 5.000 ~ 5.999

示例扩展的三条策略加载后,生效优先级分别为 2.1、2.2、2.3:整体高于内置默认规则,但低于工作区、用户和管理员层的策略。这与 extensions 参考文档 的描述一致——扩展贡献的规则运行在独立的扩展层(tier 2),可以收紧默认行为,但不能覆盖更高权限层级的策略。

安全边界:扩展只能收紧,不能放松

README 末尾的 Security note 是本示例最重要的一点:Gemini CLI 会忽略扩展贡献的任何 allow 决策与 yolo 模式配置。这不是文档约定,而是加载器里的硬编码过滤。config.ts 中的 loadExtensionPolicies 在拿到解析结果后做了两轮筛选:

// Security: Extensions are not allowed to automatically approve tool calls.
if (rule.decision === PolicyDecision.ALLOW) {
  debugLogger.warn(
    `[PolicyConfig] Extension "${extensionName}" attempted to contribute an ALLOW rule ... Ignoring this rule for security.`
  );
  return false;
}
// Security: Extensions are not allowed to contribute YOLO mode rules.
if (rule.modes?.includes(ApprovalMode.YOLO)) { ... return false; }

checkers 数组同样会被过滤掉任何带 yolo 模式标记的校验器。被过滤的规则只会留下调试日志,不会报错,扩展其余规则照常生效。

这个设计确立了清晰的安全语义:第三方扩展可以往“更严格”方向修改 CLI 行为(拒绝某类命令、强制用户确认、追加路径校验),但永远不能替用户批准工具调用,也不能绕过用户确认机制。对扩展作者而言,这意味着写策略时要避开 decision = "allow"modes = ["yolo"],它们会被静默丢弃;对使用者而言,即使安装了不规范的扩展,也不会出现“扩展自动放行危险操作”的越权风险。

此外还有一个细节:过滤后的每条规则 source 字段会被改写为 Extension (policy-example): policies.toml 形式,用于在审批提示中溯源规则出处,避免多个同名扩展文件互相混淆。

安装与验证:从 link 到观察拦截效果

README 给出的使用步骤可直接复制运行。在 gemini-cli 仓库根目录(或任意已安装 CLI 的环境)执行:

gemini extensions link packages/cli/src/commands/extensions/examples/policies

然后重启 Gemini CLI 会话,即可按 README 描述的三条路径逐一验证策略生效:

  1. 触发 ask_user:让模型执行删除目录的操作。当它生成 rm -rf ... 命令时,Policy Engine 不再按默认策略处理,而是弹出用户确认提示——对应规则一的 commandPrefix = "rm -rf" 匹配。
  2. 触发 deny:让模型搜索敏感文件(例如让它 grep 查找 .env 中的密钥)。argsPattern 命中后调用被直接拒绝,并显示自定义文案 “Access to sensitive credentials or system files is restricted by the policy-example extension.”——这正是规则二 denyMessage 的落地效果。
  3. 观察安全校验器:此后任何文件写操作(write_file / replace)都会经过 allowed-path 校验器的路径校验,可通过会话日志确认校验器被实际调用。

验证时可以关注的排查点:

  • 若规则未生效,先确认 policies/ 目录名是否拼写正确(加载器只认扩展根目录下的 policies 目录,见 extension-manager.ts)。
  • 若某个 .toml 文件存在语法或 Schema 错误,扩展管理器会逐条打印 Error loading policies from <扩展名> 的警告日志(extension-manager.ts),但不会中断扩展加载——其余正确规则依然生效。
  • 策略文件解析本身的完整错误分类(文件读取失败、TOML 解析失败、Schema 校验失败、正则不安全、工具名拼写警告等)定义在 toml-loader.tsPolicyFileErrorType 中,出错时按类型给出针对性修复建议。

小结

这份位于 packages/cli/src/commands/extensions/examples/policies 的示例扩展,用最少的代码(一份清单 + 一份 TOML)展示了 gemini-cli 策略扩展的完整能力面:

  • 声明式安全规则[[rule]] 通过 commandPrefix / argsPattern 精确匹配工具调用,输出 ask_userdeny(含自定义 denyMessage);
  • 进程内安全校验器[[safety_checker]]allowed-path 等内置校验器挂到指定工具的参数校验链路上,支持 required_contextconfig 微调;
  • 分层优先级:Tier 2 扩展层与 [0, 999] 的 TOML 优先级共同决定规则排序,天然低于工作区/用户/管理员层;
  • 单向安全边界:加载器强制丢弃扩展贡献的 allow 决策与 yolo 模式,确保扩展只能收紧策略。

如需编写自己的策略扩展,可以直接复制该示例目录作为骨架,替换 policies/ 下的 TOML 内容,再配合 extensions 参考文档 与策略引擎的完整规则参考 docs/reference/policy-engine.md 查阅全部可用字段。

登录后查看全文
热门项目推荐
相关项目推荐