MusePi

Gemini Manifest Extensions (gemini-extension.json)

This document covers how the coding-agent discovers and parses Gemini-style manifest extensions (gemini-extension.json) into the extensions capability.

It does not cover TypeScript/JavaScript extension module loading (extensions/*.ts, index.ts, package.json omp.extensions), which is documented in extension-loading.md.

Implementation files


What gets discovered

The Gemini provider (id: gemini, priority 60) registers an extensions loader that scans two fixed roots:

Path resolution is direct from ctx.home and ctx.cwd via getUserPath() / getProjectPath().

Important scope rule: project lookup is cwd-only. It does not walk parent directories.


Directory scan rules

For each root (~/.gemini/extensions and <cwd>/.gemini/extensions), discovery does:

  1. readDirEntries(root)
  2. keep only direct child directories (entry.isDirectory())
  3. for each child <name>, attempt to read exactly:
    • <root>/<name>/gemini-extension.json

There is no recursive scan beyond one directory level.

Hidden directories

Gemini manifest discovery does not filter out dot-prefixed directory names. If a hidden child directory exists and contains gemini-extension.json, it is considered.

Missing/unreadable files

If gemini-extension.json is missing or unreadable, that directory is skipped silently (no warning).


Manifest shape (as implemented)

The capability type defines this manifest shape:

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

Discovery-time behavior is intentionally loose:

Name normalization

Extension.name is set to:

  1. manifest.name if it is not null/undefined
  2. otherwise the extension directory name

No string-type enforcement is applied here.


Materialization into capability items

A valid parsed manifest creates one Extension capability item:

{
	name: manifest.name ?? <directory-name>,
	path: <extension-directory>,
	manifest: <parsed-json>,
	level: "user" | "project",
	_source: {
		provider: "gemini",
		providerName: "Gemini CLI" // attached by capability registry
		path: <absolute-manifest-path>,
		level: "user" | "project"
	}
}

Notes:


Error handling and warning semantics

Warned

Not warned (silent skip)

This means partial validity is accepted: only syntactic JSON failure emits a warning.


Precedence and deduplication with other sources

extensions capability is aggregated across providers by the capability registry.

Current providers for this capability:

Dedup key is ext.name (extensionCapability.key = ext => ext.name).

Cross-provider precedence

Higher-priority provider wins on duplicate extension names.

Intra-provider order effects

Because dedup is “first seen wins”, provider-local item order matters.

By contrast, native provider builds config dir order differently (project then user in getConfigDirs()), so native intra-provider shadowing is the opposite direction.


User vs project behavior summary

For Gemini manifests specifically:


Boundary: discovery metadata vs runtime extension loading

gemini-extension.json discovery currently feeds capability metadata (Extension items). It does not directly load runnable TS/JS extension modules.

Runtime module loading (discoverAndLoadExtensions() / loadExtensions()) uses the extension-module capability and explicit paths, and currently filters auto-discovered modules to provider native only.

Practical implication:

This boundary is intentional in current implementation and explains why manifest discovery and executable module loading can diverge.