mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 14:44:29 +00:00
Add allowedExtensions setting (#17695)
This commit is contained in:
@@ -107,13 +107,14 @@ they appear in the UI.
|
|||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
| UI Label | Setting | Description | Default |
|
| UI Label | Setting | Description | Default |
|
||||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- | ------- |
|
| ------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||||
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
| Blocks extensions from Git | `security.blockGitExtensions` | Blocks installing and loading extensions from Git. | `false` |
|
||||||
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
|
| Extension Source Regex Allowlist | `security.allowedExtensions` | List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting. | `[]` |
|
||||||
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
|
| Folder Trust | `security.folderTrust.enabled` | Setting to track whether Folder trust is enabled. | `false` |
|
||||||
|
| Enable Environment Variable Redaction | `security.environmentVariableRedaction.enabled` | Enable redaction of environment variables that may contain secrets. | `false` |
|
||||||
|
|
||||||
### Experimental
|
### Experimental
|
||||||
|
|
||||||
|
|||||||
@@ -773,6 +773,13 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
- **Requires restart:** Yes
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
|
- **`security.allowedExtensions`** (array):
|
||||||
|
- **Description:** List of Regex patterns for allowed extensions. If nonempty,
|
||||||
|
only extensions that match the patterns in this list are allowed. Overrides
|
||||||
|
the blockGitExtensions setting.
|
||||||
|
- **Default:** `[]`
|
||||||
|
- **Requires restart:** Yes
|
||||||
|
|
||||||
- **`security.folderTrust.enabled`** (boolean):
|
- **`security.folderTrust.enabled`** (boolean):
|
||||||
- **Description:** Setting to track whether Folder trust is enabled.
|
- **Description:** Setting to track whether Folder trust is enabled.
|
||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
|
|||||||
@@ -144,6 +144,26 @@ export class ExtensionManager extends ExtensionLoader {
|
|||||||
previousExtensionConfig?: ExtensionConfig,
|
previousExtensionConfig?: ExtensionConfig,
|
||||||
): Promise<GeminiCLIExtension> {
|
): Promise<GeminiCLIExtension> {
|
||||||
if (
|
if (
|
||||||
|
this.settings.security?.allowedExtensions &&
|
||||||
|
this.settings.security?.allowedExtensions.length > 0
|
||||||
|
) {
|
||||||
|
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||||
|
(pattern) => {
|
||||||
|
try {
|
||||||
|
return new RegExp(pattern).test(installMetadata.source);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!extensionAllowed) {
|
||||||
|
throw new Error(
|
||||||
|
`Installing extension from source "${installMetadata.source}" is not allowed by the "allowedExtensions" security setting.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
(installMetadata.type === 'git' ||
|
(installMetadata.type === 'git' ||
|
||||||
installMetadata.type === 'github-release') &&
|
installMetadata.type === 'github-release') &&
|
||||||
this.settings.security.blockGitExtensions
|
this.settings.security.blockGitExtensions
|
||||||
@@ -152,6 +172,7 @@ export class ExtensionManager extends ExtensionLoader {
|
|||||||
'Installing extensions from remote sources is disallowed by your current settings.',
|
'Installing extensions from remote sources is disallowed by your current settings.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUpdate = !!previousExtensionConfig;
|
const isUpdate = !!previousExtensionConfig;
|
||||||
let newExtensionConfig: ExtensionConfig | null = null;
|
let newExtensionConfig: ExtensionConfig | null = null;
|
||||||
let localSourcePath: string | undefined;
|
let localSourcePath: string | undefined;
|
||||||
@@ -522,10 +543,39 @@ Would you like to attempt to install via "git clone" instead?`,
|
|||||||
const installMetadata = loadInstallMetadata(extensionDir);
|
const installMetadata = loadInstallMetadata(extensionDir);
|
||||||
let effectiveExtensionPath = extensionDir;
|
let effectiveExtensionPath = extensionDir;
|
||||||
if (
|
if (
|
||||||
|
this.settings.security?.allowedExtensions &&
|
||||||
|
this.settings.security?.allowedExtensions.length > 0
|
||||||
|
) {
|
||||||
|
if (!installMetadata?.source) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load extension ${extensionDir}. The ${INSTALL_METADATA_FILENAME} file is missing or misconfigured.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const extensionAllowed = this.settings.security?.allowedExtensions.some(
|
||||||
|
(pattern) => {
|
||||||
|
try {
|
||||||
|
return new RegExp(pattern).test(installMetadata?.source);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid regex pattern in allowedExtensions setting: "${pattern}. Error: ${getErrorMessage(e)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!extensionAllowed) {
|
||||||
|
debugLogger.warn(
|
||||||
|
`Failed to load extension ${extensionDir}. This extension is not allowed by the "allowedExtensions" security setting.`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
(installMetadata?.type === 'git' ||
|
(installMetadata?.type === 'git' ||
|
||||||
installMetadata?.type === 'github-release') &&
|
installMetadata?.type === 'github-release') &&
|
||||||
this.settings.security.blockGitExtensions
|
this.settings.security.blockGitExtensions
|
||||||
) {
|
) {
|
||||||
|
debugLogger.warn(
|
||||||
|
`Failed to load extension ${extensionDir}. Extensions from remote sources is disallowed by your current settings.`,
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -622,6 +622,7 @@ describe('extension tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not load github extensions if blockGitExtensions is set', async () => {
|
it('should not load github extensions if blockGitExtensions is set', async () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
createExtension({
|
createExtension({
|
||||||
extensionsDir: userExtensionsDir,
|
extensionsDir: userExtensionsDir,
|
||||||
name: 'my-ext',
|
name: 'my-ext',
|
||||||
@@ -645,6 +646,73 @@ describe('extension tests', () => {
|
|||||||
const extension = extensions.find((e) => e.name === 'my-ext');
|
const extension = extensions.find((e) => e.name === 'my-ext');
|
||||||
|
|
||||||
expect(extension).toBeUndefined();
|
expect(extension).toBeUndefined();
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
'Extensions from remote sources is disallowed by your current settings.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load allowed extensions if the allowlist is set.', async () => {
|
||||||
|
createExtension({
|
||||||
|
extensionsDir: userExtensionsDir,
|
||||||
|
name: 'my-ext',
|
||||||
|
version: '1.0.0',
|
||||||
|
installMetadata: {
|
||||||
|
type: 'git',
|
||||||
|
source: 'http://allowed.com/foo/bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const extensionAllowlistSetting = createTestMergedSettings({
|
||||||
|
security: {
|
||||||
|
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
extensionManager = new ExtensionManager({
|
||||||
|
workspaceDir: tempWorkspaceDir,
|
||||||
|
requestConsent: mockRequestConsent,
|
||||||
|
requestSetting: mockPromptForSettings,
|
||||||
|
settings: extensionAllowlistSetting,
|
||||||
|
});
|
||||||
|
const extensions = await extensionManager.loadExtensions();
|
||||||
|
|
||||||
|
expect(extensions).toHaveLength(1);
|
||||||
|
expect(extensions[0].name).toBe('my-ext');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load disallowed extensions if the allowlist is set.', async () => {
|
||||||
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
createExtension({
|
||||||
|
extensionsDir: userExtensionsDir,
|
||||||
|
name: 'my-ext',
|
||||||
|
version: '1.0.0',
|
||||||
|
installMetadata: {
|
||||||
|
type: 'git',
|
||||||
|
source: 'http://notallowed.com/foo/bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const extensionAllowlistSetting = createTestMergedSettings({
|
||||||
|
security: {
|
||||||
|
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
extensionManager = new ExtensionManager({
|
||||||
|
workspaceDir: tempWorkspaceDir,
|
||||||
|
requestConsent: mockRequestConsent,
|
||||||
|
requestSetting: mockPromptForSettings,
|
||||||
|
settings: extensionAllowlistSetting,
|
||||||
|
});
|
||||||
|
const extensions = await extensionManager.loadExtensions();
|
||||||
|
const extension = extensions.find((e) => e.name === 'my-ext');
|
||||||
|
|
||||||
|
expect(extension).toBeUndefined();
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
'This extension is not allowed by the "allowedExtensions" security setting',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not load any extensions if admin.extensions.enabled is false', async () => {
|
it('should not load any extensions if admin.extensions.enabled is false', async () => {
|
||||||
@@ -1116,6 +1184,30 @@ describe('extension tests', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not install a disallowed extension if the allowlist is set', async () => {
|
||||||
|
const gitUrl = 'https://somehost.com/somerepo.git';
|
||||||
|
const allowedExtensionsSetting = createTestMergedSettings({
|
||||||
|
security: {
|
||||||
|
allowedExtensions: ['\\b(https?:\\/\\/)?(www\\.)?allowed\\.com\\S*'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
extensionManager = new ExtensionManager({
|
||||||
|
workspaceDir: tempWorkspaceDir,
|
||||||
|
requestConsent: mockRequestConsent,
|
||||||
|
requestSetting: mockPromptForSettings,
|
||||||
|
settings: allowedExtensionsSetting,
|
||||||
|
});
|
||||||
|
await extensionManager.loadExtensions();
|
||||||
|
await expect(
|
||||||
|
extensionManager.installOrUpdateExtension({
|
||||||
|
source: gitUrl,
|
||||||
|
type: 'git',
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
`Installing extension from source "${gitUrl}" is not allowed by the "allowedExtensions" security setting.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should prompt for trust if workspace is not trusted', async () => {
|
it('should prompt for trust if workspace is not trusted', async () => {
|
||||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||||
isTrusted: false,
|
isTrusted: false,
|
||||||
|
|||||||
@@ -1268,6 +1268,17 @@ const SETTINGS_SCHEMA = {
|
|||||||
description: 'Blocks installing and loading extensions from Git.',
|
description: 'Blocks installing and loading extensions from Git.',
|
||||||
showInDialog: true,
|
showInDialog: true,
|
||||||
},
|
},
|
||||||
|
allowedExtensions: {
|
||||||
|
type: 'array',
|
||||||
|
label: 'Extension Source Regex Allowlist',
|
||||||
|
category: 'Security',
|
||||||
|
requiresRestart: true,
|
||||||
|
default: [] as string[],
|
||||||
|
description:
|
||||||
|
'List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.',
|
||||||
|
showInDialog: true,
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
folderTrust: {
|
folderTrust: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
label: 'Folder Trust',
|
label: 'Folder Trust',
|
||||||
|
|||||||
@@ -1273,6 +1273,16 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"allowedExtensions": {
|
||||||
|
"title": "Extension Source Regex Allowlist",
|
||||||
|
"description": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.",
|
||||||
|
"markdownDescription": "List of Regex patterns for allowed extensions. If nonempty, only extensions that match the patterns in this list are allowed. Overrides the blockGitExtensions setting.\n\n- Category: `Security`\n- Requires restart: `yes`\n- Default: `[]`",
|
||||||
|
"default": [],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"folderTrust": {
|
"folderTrust": {
|
||||||
"title": "Folder Trust",
|
"title": "Folder Trust",
|
||||||
"description": "Settings for folder trust.",
|
"description": "Settings for folder trust.",
|
||||||
|
|||||||
Reference in New Issue
Block a user