gemini-cli 策略引擎扩展解析:通过 policies 目录为 CLI 贡献安全规则与安全校验器
本文以 gemini-cli 仓库中自带的策略引擎示例扩展(packages/cli/src/commands/extensions/examples/policies)为主体,完整拆解“一个扩展如何向 Policy Engine 贡献规则”的全流程:policies/ 目录的加载机制、policies.toml 中每条规则与安全校验器的字段含义、Tier 2 优先级语义,以及“扩展只能收紧、不能放松”的安全边界。读完本文,你可以独立编写一个带策略规则的扩展,理解其被加载、校验、脱敏(sanitization)的底层链路,并验证规则在真实会话中的拦截效果。
示例扩展概览
示例扩展位于 packages/cli/src/commands/extensions/examples/policies,README 对其定位的描述是:演示如何向 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 的参数中出现 .env、id_rsa、passwd 任一关键词时,直接返回 deny,并向用户展示自定义的 denyMessage,明确告知这是 policy-example 扩展的策略所致。
几个值得注意的底层细节:
- 与
argsPattern相对的还有commandRegex,但如前所述它只服务于run_shell_command;argsPattern则适用于任何工具。 - 正则并非想写什么都能写。加载时 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_checker 与 rule 是两类不同的策略原语:规则输出一个决策(allow/deny/ask_user),而校验器在执行前对工具调用的参数本身做结构化校验,校验不通过即阻断。
这条校验器把所有 write_file 与 replace 写操作接入内置的 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"]:声明该校验器运行时需要的上下文键(如环境信息),引擎会把这些上下文注入校验输入。- 可选的
config:allowed-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 描述的三条路径逐一验证策略生效:
- 触发
ask_user:让模型执行删除目录的操作。当它生成rm -rf ...命令时,Policy Engine 不再按默认策略处理,而是弹出用户确认提示——对应规则一的commandPrefix = "rm -rf"匹配。 - 触发
deny:让模型搜索敏感文件(例如让它grep查找.env中的密钥)。argsPattern命中后调用被直接拒绝,并显示自定义文案 “Access to sensitive credentials or system files is restricted by the policy-example extension.”——这正是规则二denyMessage的落地效果。 - 观察安全校验器:此后任何文件写操作(
write_file/replace)都会经过allowed-path校验器的路径校验,可通过会话日志确认校验器被实际调用。
验证时可以关注的排查点:
- 若规则未生效,先确认
policies/目录名是否拼写正确(加载器只认扩展根目录下的policies目录,见 extension-manager.ts)。 - 若某个
.toml文件存在语法或 Schema 错误,扩展管理器会逐条打印Error loading policies from <扩展名>的警告日志(extension-manager.ts),但不会中断扩展加载——其余正确规则依然生效。 - 策略文件解析本身的完整错误分类(文件读取失败、TOML 解析失败、Schema 校验失败、正则不安全、工具名拼写警告等)定义在 toml-loader.ts 的
PolicyFileErrorType中,出错时按类型给出针对性修复建议。
小结
这份位于 packages/cli/src/commands/extensions/examples/policies 的示例扩展,用最少的代码(一份清单 + 一份 TOML)展示了 gemini-cli 策略扩展的完整能力面:
- 声明式安全规则:
[[rule]]通过commandPrefix/argsPattern精确匹配工具调用,输出ask_user或deny(含自定义denyMessage); - 进程内安全校验器:
[[safety_checker]]把allowed-path等内置校验器挂到指定工具的参数校验链路上,支持required_context与config微调; - 分层优先级:Tier 2 扩展层与
[0, 999]的 TOML 优先级共同决定规则排序,天然低于工作区/用户/管理员层; - 单向安全边界:加载器强制丢弃扩展贡献的
allow决策与yolo模式,确保扩展只能收紧策略。
如需编写自己的策略扩展,可以直接复制该示例目录作为骨架,替换 policies/ 下的 TOML 内容,再配合 extensions 参考文档 与策略引擎的完整规则参考 docs/reference/policy-engine.md 查阅全部可用字段。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00