跳转到内容

Gemini 清单扩展(`gemini-extension.json`)

本文档介绍编码代理如何发现并将 Gemini 风格的清单扩展(gemini-extension.json)解析为 extensions 能力。

本文档涵盖 TypeScript/JavaScript 扩展模块加载(extensions/*.tsindex.tspackage.json xcsh.extensions),相关内容记录于 extension-loading.md


Gemini 提供者(id: gemini,优先级 60)注册一个 extensions 加载器,扫描两个固定根目录:

  • 用户级:~/.gemini/extensions
  • 项目级:<cwd>/.gemini/extensions

路径解析通过 getUserPath() / getProjectPath() 直接从 ctx.homectx.cwd 获取。

重要的作用域规则:项目查找仅限于当前工作目录,不会向上遍历父目录。


对于每个根目录(~/.gemini/extensions<cwd>/.gemini/extensions),发现过程执行以下操作:

  1. readDirEntries(root)
  2. 仅保留直接子目录(entry.isDirectory()
  3. 对于每个子目录 <name>,尝试精确读取:
    • <root>/<name>/gemini-extension.json

不会在一级目录以外进行递归扫描。

Gemini 清单发现过滤以点号开头的目录名。如果某个隐藏子目录存在且包含 gemini-extension.json,则该目录会被纳入考虑。

如果 gemini-extension.json 缺失或不可读,该目录会被静默跳过(不发出警告)。


能力类型定义了以下清单结构:

interface ExtensionManifest {
name?: string;
description?: string;
mcpServers?: Record<string, Omit<MCPServer, "name" | "_source">>;
tools?: unknown[];
context?: unknown;
}

发现阶段的行为有意保持宽松:

  • 需要 JSON 解析成功。
  • 除 JSON 语法外,不对字段类型/内容进行运行时模式验证。
  • 解析后的对象作为 manifest 存储于能力条目上。

Extension.name 设置为:

  1. manifest.name 不为 null/undefined,则使用该值
  2. 否则使用扩展目录名

此处不强制执行字符串类型检查。


经过有效解析的清单会创建一个 Extension 能力条目:

{
name: manifest.name ?? <directory-name>,
path: <extension-directory>,
manifest: <parsed-json>,
level: "user" | "project",
_source: {
provider: "gemini",
providerName: "Gemini CLI" // 由能力注册表附加
path: <absolute-manifest-path>,
level: "user" | "project"
}
}

说明:

  • _source.pathcreateSourceMeta() 规范化为绝对路径。
  • extensions 的注册表级能力验证仅检查 namepath 是否存在。
  • 清单内部字段(mcpServerstoolscontext)在发现阶段不进行验证。

  • 清单文件中存在无效 JSON:
    • 警告格式:Invalid JSON in <manifestPath>
  • extensions 目录缺失
  • 子目录中不存在 gemini-extension.json
  • 清单文件不可读
  • 清单 JSON 语法有效但语义异常/不完整

这意味着接受部分有效性:仅语法 JSON 失败才会发出警告。


extensions 能力由能力注册表跨提供者聚合。

当前支持此能力的提供者:

  • nativepackages/coding-agent/src/discovery/builtin.ts)优先级 100
  • geminipackages/coding-agent/src/discovery/gemini.ts)优先级 60

去重键为 ext.nameextensionCapability.key = ext => ext.name)。

重复扩展名时,优先级更高的提供者获胜。

  • nativegemini 都发出扩展名 foo,则保留 native 条目。
  • 低优先级的重复条目仅保留在 result.all 中,并标记 _shadowed = true

由于去重策略为”先出现者获胜”,提供者内部的条目顺序至关重要。

  • Gemini 加载器先追加用户级,再追加项目级
  • 因此,~/.gemini/extensions<cwd>/.gemini/extensions 之间重复的名称会保留用户级条目,项目级条目被遮蔽。

相比之下,native 提供者通过 getConfigDirs() 以不同顺序构建配置目录(project 先于 user),因此 native 提供者内部的遮蔽方向相反。


对于 Gemini 清单:

  • 每次加载时都会扫描用户级和项目级根目录。
  • 项目级根目录固定为 <cwd>/.gemini/extensions(不进行祖先目录遍历)。
  • Gemini 来源内部的重复名称以用户级优先解析。
  • 与更高优先级提供者(尤其是 native)的重复名称会因优先级较低而落败。

边界:发现元数据与运行时扩展加载

Section titled “边界:发现元数据与运行时扩展加载”

gemini-extension.json 发现当前为能力元数据(Extension 条目)提供数据,直接加载可运行的 TS/JS 扩展模块。

运行时模块加载(discoverAndLoadExtensions() / loadExtensions())使用 extension-modules 和显式路径,并且当前仅将自动发现的模块过滤为提供者 native

实际影响:

  • Gemini 清单扩展可作为能力记录被发现。
  • 它们本身不会被扩展加载器管道作为运行时扩展模块执行。

这一边界在当前实现中是有意为之的,解释了为何清单发现与可执行模块加载可能存在差异。