From e599bf8ca34c343a85d21478576d183729edc7fa Mon Sep 17 00:00:00 2001 From: Christine Betts Date: Wed, 25 Feb 2026 12:35:59 -0500 Subject: [PATCH] Address comments --- docs/extensions/reference.md | 7 ++--- packages/cli/src/config/extension-manager.ts | 31 ++++++++++++++++--- packages/cli/src/config/extension.test.ts | 29 ++++++++++++++--- .../cli/src/test-utils/createExtension.ts | 2 -- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/docs/extensions/reference.md b/docs/extensions/reference.md index cebb675108..56db528e8c 100644 --- a/docs/extensions/reference.md +++ b/docs/extensions/reference.md @@ -122,8 +122,7 @@ The manifest file defines the extension's behavior and configuration. } }, "contextFileName": "GEMINI.md", - "excludeTools": ["run_shell_command"], - "policies": "policies.toml" + "excludeTools": ["run_shell_command"] } ``` @@ -231,8 +230,8 @@ agent definition files (`.md`) to an `agents/` directory in your extension root. ### Policy Engine Extensions can contribute policy rules and safety checkers to the Gemini CLI -[Policy Engine](../admin/policy-engine.md). These rules are defined in `.toml` -files and take effect when the extension is activated. +[Policy Engine](../reference/policy-engine.md). These rules are defined in +`.toml` files and take effect when the extension is activated. To add policies, create a `policies/` directory in your extension's root and place your `.toml` policy files inside it. Gemini CLI automatically loads all diff --git a/packages/cli/src/config/extension-manager.ts b/packages/cli/src/config/extension-manager.ts index aeca7b91bb..5218e332ac 100644 --- a/packages/cli/src/config/extension-manager.ts +++ b/packages/cli/src/config/extension-manager.ts @@ -55,6 +55,7 @@ import { loadPoliciesFromToml, PolicyDecision, ApprovalMode, + isSubpath, type PolicyRule, type SafetyCheckerRule, } from '@google/gemini-cli-core'; @@ -704,9 +705,18 @@ Would you like to attempt to install via "git clone" instead?`, } const contextFiles = getContextFileNames(config) - .map((contextFileName) => - path.join(effectiveExtensionPath, contextFileName), - ) + .map((contextFileName) => { + const contextFilePath = path.join( + effectiveExtensionPath, + contextFileName, + ); + if (!isSubpath(effectiveExtensionPath, contextFilePath)) { + throw new Error( + `Invalid context file path: "${contextFileName}". Context files must be within the extension directory.`, + ); + } + return contextFilePath; + }) .filter((contextFilePath) => fs.existsSync(contextFilePath)); const hydrationContext: VariableContext = { @@ -762,6 +772,11 @@ Would you like to attempt to install via "git clone" instead?`, let checkers: SafetyCheckerRule[] | undefined; const policyDir = path.join(effectiveExtensionPath, 'policies'); + if (!isSubpath(effectiveExtensionPath, policyDir)) { + throw new Error( + `Invalid policy directory path. Policies must be within the extension directory.`, + ); + } if (fs.existsSync(policyDir)) { const result = await loadPoliciesFromToml( [policyDir], @@ -790,7 +805,9 @@ Would you like to attempt to install via "git clone" instead?`, return false; } - rule.source = `Extension (${config.name}): ${rule.source}`; + rule.source = rule.source?.startsWith(`Extension (${config.name}):`) + ? rule.source + : `Extension (${config.name}): ${rule.source}`; return true; }); } @@ -805,7 +822,11 @@ Would you like to attempt to install via "git clone" instead?`, return false; } - checker.source = `Extension (${config.name}): ${checker.source}`; + checker.source = checker.source?.startsWith( + `Extension (${config.name}):`, + ) + ? checker.source + : `Extension (${config.name}): ${checker.source}`; return true; }); } diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index f2bd736e85..24e96c7107 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -238,6 +238,27 @@ describe('extension tests', () => { expect(extensions[0].name).toBe('test-extension'); }); + it('should throw an error if a context file path is outside the extension directory', async () => { + const consoleSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + createExtension({ + extensionsDir: userExtensionsDir, + name: 'traversal-extension', + version: '1.0.0', + contextFileName: '../secret.txt', + }); + + const extensions = await extensionManager.loadExtensions(); + expect(extensions).toHaveLength(0); + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'traversal-extension: Invalid context file path: "../secret.txt"', + ), + ); + consoleSpy.mockRestore(); + }); + it('should load context file path when GEMINI.md is present', async () => { createExtension({ extensionsDir: userExtensionsDir, @@ -640,7 +661,7 @@ name = "yolo-checker" // Bad extension const badExtDir = path.join(userExtensionsDir, 'bad-ext'); - fs.mkdirSync(badExtDir); + fs.mkdirSync(badExtDir, { recursive: true }); const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME); fs.writeFileSync(badConfigPath, '{ "name": "bad-ext"'); // Malformed @@ -648,7 +669,7 @@ name = "yolo-checker" expect(extensions).toHaveLength(1); expect(extensions[0].name).toBe('good-ext'); - expect(consoleSpy).toHaveBeenCalledExactlyOnceWith( + expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining( `Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`, ), @@ -671,7 +692,7 @@ name = "yolo-checker" // Bad extension const badExtDir = path.join(userExtensionsDir, 'bad-ext-no-name'); - fs.mkdirSync(badExtDir); + fs.mkdirSync(badExtDir, { recursive: true }); const badConfigPath = path.join(badExtDir, EXTENSIONS_CONFIG_FILENAME); fs.writeFileSync(badConfigPath, JSON.stringify({ version: '1.0.0' })); @@ -679,7 +700,7 @@ name = "yolo-checker" expect(extensions).toHaveLength(1); expect(extensions[0].name).toBe('good-ext'); - expect(consoleSpy).toHaveBeenCalledExactlyOnceWith( + expect(consoleSpy).toHaveBeenCalledWith( expect.stringContaining( `Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`, ), diff --git a/packages/cli/src/test-utils/createExtension.ts b/packages/cli/src/test-utils/createExtension.ts index 92c5c9a466..56d02e7053 100644 --- a/packages/cli/src/test-utils/createExtension.ts +++ b/packages/cli/src/test-utils/createExtension.ts @@ -27,7 +27,6 @@ export function createExtension({ installMetadata = undefined as ExtensionInstallMetadata | undefined, settings = undefined as ExtensionSetting[] | undefined, themes = undefined as CustomTheme[] | undefined, - policies = undefined as string | undefined, } = {}): string { const extDir = path.join(extensionsDir, name); fs.mkdirSync(extDir, { recursive: true }); @@ -40,7 +39,6 @@ export function createExtension({ mcpServers, settings, themes, - policies, }), );