mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-22 17:53:04 -07:00
Address comments
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user