Address comments

This commit is contained in:
Christine Betts
2026-02-25 12:35:59 -05:00
parent 5ddaccde67
commit e599bf8ca3
4 changed files with 54 additions and 15 deletions
+3 -4
View File
@@ -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.
### <a id="policy-engine"></a>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
+26 -5
View File
@@ -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;
});
}
+25 -4
View File
@@ -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"`,
),
@@ -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,
}),
);