Read OSS

扩展 Gemini CLI:Hooks、Skills、MCP 与扩展系统

高级

前置知识

  • 本系列第 1-4 篇文章
  • 了解发布/订阅模式与策略设计模式
  • 熟悉 MCP(模型上下文协议)的基本概念

扩展 Gemini CLI:Hooks、Skills、MCP 与扩展系统

Gemini CLI 从设计之初就考虑了高度可定制性。无论是拦截 Agent 执行轮次的 shell 命令钩子,还是为系统引入全新工具能力的 MCP 服务器,代码库提供了多个可扩展接口。本文将逐一梳理这些接口——由五个组件构成的 Hook 系统、四级优先级的 Skill 机制、支持 OAuth 的 MCP 集成、带 HMAC 完整性校验的扩展包系统、作为策略模式实现的模型路由,以及子 Agent 系统。

Hook 系统架构

packages/core/src/hooks/ 中的 Hook 系统由五个组件协同工作,负责在关键生命周期节点执行用户定义的 shell 命令。

flowchart TD
    EVENT[Lifecycle Event] --> EH[HookEventHandler<br/>dispatches events]
    EH --> HP[HookPlanner<br/>determines which hooks fire]
    HP --> HR[HookRunner<br/>executes as shell commands]
    HR --> HA[HookAggregator<br/>combines results]
    HA --> RESULT[Aggregated Result]
    
    REG[HookRegistry<br/>stores hook configs] --> HP

HookSystem 类负责将这些组件串联起来:

constructor(config: Config) {
    this.hookRegistry = new HookRegistry(config);
    this.hookRunner = new HookRunner(config);
    this.hookAggregator = new HookAggregator();
    this.hookPlanner = new HookPlanner(this.hookRegistry);
    this.hookEventHandler = new HookEventHandler(
        config, this.hookPlanner, this.hookRunner, this.hookAggregator,
    );
}

HookRegistry 从多个来源(用户配置、工作区配置、扩展包)收集并存储 Hook 配置。每个 Hook 指定了监听的事件类型、可选的匹配规则,以及是否串行执行。

HookPlanner 负责判断哪些已注册的 Hook 应当响应当前事件,它会查询 Registry 并对匹配表达式进行求值。

HookRunner 将 Hook 作为 shell 命令执行——Hook 不是 JavaScript 函数,而是外部进程。这种设计实现了与语言无关的扩展能力,同时提供了一道安全边界。Hook 脚本通过环境变量和标准输入接收上下文信息。

HookAggregator 汇总同一事件触发的多个 Hook 的执行结果。结果中可包含系统消息、待注入的额外上下文,以及流程控制决策(停止、拦截、继续)。

HookEventHandler 是整体的调度器,提供了 fireSessionStartEventfireBeforeAgentEventfireAfterAgentEvent 等具有明确类型的方法。

八个生命周期事件如下:

事件 触发时机
SessionStart 会话开始或恢复时
SessionEnd 会话结束时
BeforeAgent 针对某个提示词发起第一次模型调用之前
AfterAgent 模型响应完成且无待处理工具时
BeforeModel 每次 API 调用之前
AfterModel 每次 API 响应之后
BeforeToolSelection 工具列表发送给模型之前
PreCompress 对话历史压缩之前

正如第 2 篇文章所介绍的,BeforeAgentAfterAgent 可以停止执行、附带原因地拦截流程,或注入上下文。BeforeModel 可以修改请求配置或返回合成响应。BeforeToolSelection 可以控制模型实际看到哪些工具。

提示: Hook 以 shell 命令方式执行,因此可以用任意语言编写——Python 脚本、Go 二进制文件,甚至单行 bash 命令都可以。Hook 通过标准输入接收 JSON 格式的上下文,并将 JSON 结果输出到标准输出。

Skills:发现机制与优先级

Skills 是用户可在会话中激活的专用提示词/工具配置。SkillManager 会从四个位置按优先级由低到高加载 Skill:

flowchart BT
    B[1. Built-in skills<br/>Lowest precedence] --> E[2. Extension skills]
    E --> U[3. User skills<br/>~/.gemini/skills/]
    U --> W[4. Workspace skills<br/>.gemini/skills/<br/>Highest precedence]

第 47 行discoverSkills() 方法按以下顺序执行:

  1. 内置 Skills:来自 skills/builtin/ 目录(标记有 isBuiltin = true
  2. 扩展 Skills:来自已激活的扩展包(extension.skills
  3. 用户 Skills:来自 ~/.gemini/skills/~/.gemini/.agents/skills/
  4. 工作区 Skills:来自 .gemini/skills/.gemini/.agents/skills/(仅在目录受信任时加载)

当多个 Skill 同名时,addSkillsWithPrecedence() 会让高优先级来源的版本覆盖低优先级来源的版本。这意味着工作区 Skill 可以覆盖内置 Skill,从而实现项目级定制。

值得注意的安全考量是:工作区 Skills 只有在目录受信任的情况下才会加载,不受信任的项目无法注入可能影响 Agent 行为的 Skill。

MCP 服务器集成

MCP(模型上下文协议)集成让 Gemini CLI 能够通过 stdio 或 HTTP 连接外部工具服务器,涵盖 OAuth 认证、服务器发现和动态工具注册等功能。

MCPOAuthProvider 负责处理需要认证的 MCP 服务器的 OAuth/PKCE 流程,管理授权服务器元数据发现、令牌获取、刷新与存储。

sequenceDiagram
    participant Config as Config.initialize()
    participant MCM as McpClientManager
    participant OAuth as MCPOAuthProvider
    participant MCP as MCP Server
    participant TR as ToolRegistry
    participant PE as PolicyEngine
    
    Config->>MCM: Connect to configured servers
    MCM->>OAuth: Authenticate (if needed)
    OAuth->>MCP: PKCE auth flow
    MCP-->>OAuth: Access token
    MCM->>MCP: listTools()
    MCP-->>MCM: Tool schemas
    MCM->>TR: registerTool(DiscoveredMCPTool)
    Note over TR: mcp_serverName_toolName
    
    Note over PE: Policy rules with<br/>mcp_serverName_* wildcards<br/>control access

MCP 工具由 DiscoveredMCPTool 包装,该类继承自 BaseDeclarativeTool,并添加了用于策略匹配的 MCP 元数据(服务器名称、工具注解)。mcp_ 前缀约定(由 MCP_TOOL_PREFIX 定义)确保了与内置工具的命名不会冲突,同时支持 mcp_myserver_* 这样的通配符策略规则。

策略引擎的通配符匹配(详见第 4 篇文章)支持三种 MCP 匹配模式:

  • mcp_* — 匹配来自任意服务器的所有 MCP 工具
  • mcp_serverName_* — 匹配特定服务器的所有工具
  • mcp_serverName_toolName — 匹配某个特定工具

扩展系统与完整性校验

扩展包将多种扩展能力打包成可安装的模块,一个扩展包可以提供 Hooks、Skills、MCP 服务器配置、斜杠命令、主题以及策略规则。

系统的一项关键安全特性是 HMAC 完整性校验机制。IntegrityKeyManager 管理一个 256 位的密钥,用于对扩展元数据进行签名:

class IntegrityKeyManager {
    private readonly fallbackKeyPath: string;
    private readonly keychainService: KeychainService;
    private cachedSecretKey: string | null = null;
    
    async getSecretKey(): Promise<string> {
        if (this.cachedSecretKey) return this.cachedSecretKey;
        
        if (await this.keychainService.isAvailable()) {
            try {
                this.cachedSecretKey = await this.getSecretKeyFromKeychain();
                return this.cachedSecretKey;
            } catch (e) {
                // Fall back to file-based storage
            }
        }
        
        this.cachedSecretKey = await this.getSecretKeyFromFile();
        return this.cachedSecretKey;
    }
}

密钥优先存储在操作系统密钥链(通过 KeychainService)中,若不可用则回退到权限为 0o600 的文件存储。安装扩展时,系统会用该密钥对元数据进行签名;加载时则验证签名,以检测是否遭到篡改。

graph TD
    subgraph "Extension Package"
        H[Hooks]
        S[Skills]
        MCP[MCP Servers]
        CMD[Commands]
        TH[Themes]
        POL[Policies]
    end
    
    INST[Extension Install] --> SIGN[HMAC Sign with secret key]
    SIGN --> STORE[Store signed metadata]
    
    LOAD[Extension Load] --> VERIFY[Verify HMAC signature]
    VERIFY --> ACTIVE[Activate extension]
    VERIFY --> REJECT[Reject tampered extension]
    
    subgraph "Key Storage"
        KC[OS Keychain<br/>preferred]
        FILE[File ~/.gemini/<br/>fallback, 0600]
    end

提示: 如果在系统迁移后扩展校验莫名失败,请检查密钥链是否已随之迁移。~/.gemini/ 中的备用密钥文件可能与原系统签名扩展时所用的密钥不一致。

作为扩展点的模型路由

模型路由——即决定每轮对话由哪个模型处理——采用了组合策略模式(Composite Strategy Pattern)。ModelRouterService 按优先级顺序串联七个策略:

flowchart LR
    REQ[Routing Request] --> F[FallbackStrategy]
    F --> O[OverrideStrategy]
    O --> A[ApprovalModeStrategy]
    A --> GC[GemmaClassifierStrategy]
    GC --> C[ClassifierStrategy]
    C --> N[NumericalClassifierStrategy]
    N --> D[DefaultStrategy<br/>terminal]

每个策略接收一个 RoutingContext(对话历史、当前请求、请求的模型),要么返回 RoutingDecision,要么将决策传递给下一个策略:

  • FallbackStrategy — 主模型不可用时激活
  • OverrideStrategy — 处理显式的模型切换请求(如 /model flash
  • ApprovalModeStrategy — 为 PLAN 模式选择合适的模型
  • GemmaClassifierStrategy — 使用本地 Gemma 模型进行路由分类
  • ClassifierStrategy — 通用的基于 LLM 的分类策略
  • NumericalClassifierStrategy — 使用数值评分启发式方法
  • DefaultStrategy — 终止策略,始终返回已配置的模型

CompositeStrategy 包装所有策略,并保证终止策略一定能产生结果。这是经典的责任链模式(Chain of Responsibility)——每个策略要么处理请求,要么将其传递下去。

子 Agent 与派生 MessageBus

子 Agent 系统让 Gemini CLI 能够为特定任务(如浏览器自动化)派生子 Agent。子 Agent 需要拥有独立的工具注册表和消息总线,以避免与父 Agent 相互干扰。

正如第 1 篇文章所介绍的,第 46–72 行MessageBus.derive() 会创建一个作用域隔离的子总线:

derive(subagentName: string): MessageBus {
    const bus = new MessageBus(this.policyEngine, this.debug);
    bus.publish = async (message: Message) => {
        if (message.type === MessageBusType.TOOL_CONFIRMATION_REQUEST) {
            return this.publish({
                ...message,
                subagent: message.subagent
                    ? `${subagentName}/${message.subagent}`
                    : subagentName,
            });
        }
        return this.publish(message);
    };
    // Delegate subscriptions to parent
    bus.subscribe = this.subscribe.bind(this);
    // ...
}

派生总线重写了 publish 方法,为确认请求添加子 Agent 名称前缀。其余操作(subscribe、unsubscribe、on、off)则全部委托给父总线。这样一来,子 Agent 的工具确认请求会在父级 UI 中以清晰的归属信息展示(如 "browser/navigate"),而常规事件处理则照常流转。

子 Agent 系统还与 AgentLoopContext 紧密协作。每个子 Agent 会获得一个派生的上下文,拥有独立的工具注册表和消息总线,同时共享相同的 Config 和沙箱管理器。这正是第 1 篇文章中介绍的 AgentLoopContext 接口设计的价值所在——接受该接口的组件对父 Agent 上下文和子 Agent 上下文的处理方式完全一致。

在本系列的最后一篇文章中,我们将深入探讨构建在上述所有系统之上的两个主要交互层——基于 React/Ink 的终端 UI 与可编程 SDK。