mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-22 03:51:22 -07:00
feat: Support Extension Hooks with Security Warning (#14460)
This commit is contained in:
@@ -112,20 +112,31 @@ describe('consent', () => {
|
||||
|
||||
it('should request consent if there is no previous config', async () => {
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(baseConfig, requestConsent, undefined);
|
||||
await maybeRequestConsentOrFail(
|
||||
baseConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not request consent if configs are identical', async () => {
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(baseConfig, requestConsent, baseConfig);
|
||||
await maybeRequestConsentOrFail(
|
||||
baseConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
baseConfig,
|
||||
false,
|
||||
);
|
||||
expect(requestConsent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if consent is denied', async () => {
|
||||
const requestConsent = vi.fn().mockResolvedValue(false);
|
||||
await expect(
|
||||
maybeRequestConsentOrFail(baseConfig, requestConsent, undefined),
|
||||
maybeRequestConsentOrFail(baseConfig, requestConsent, false, undefined),
|
||||
).rejects.toThrow('Installation cancelled for "test-ext".');
|
||||
});
|
||||
|
||||
@@ -141,7 +152,12 @@ describe('consent', () => {
|
||||
excludeTools: ['tool1', 'tool2'],
|
||||
};
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(config, requestConsent, undefined);
|
||||
await maybeRequestConsentOrFail(
|
||||
config,
|
||||
requestConsent,
|
||||
false,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const expectedConsentString = [
|
||||
'Installing extension "test-ext".',
|
||||
@@ -163,7 +179,13 @@ describe('consent', () => {
|
||||
mcpServers: { server1: { command: 'npm', args: ['start'] } },
|
||||
};
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(newConfig, requestConsent, prevConfig);
|
||||
await maybeRequestConsentOrFail(
|
||||
newConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
prevConfig,
|
||||
false,
|
||||
);
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -174,7 +196,13 @@ describe('consent', () => {
|
||||
contextFileName: 'new-context.md',
|
||||
};
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(newConfig, requestConsent, prevConfig);
|
||||
await maybeRequestConsentOrFail(
|
||||
newConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
prevConfig,
|
||||
false,
|
||||
);
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -185,7 +213,41 @@ describe('consent', () => {
|
||||
excludeTools: ['new-tool'],
|
||||
};
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(newConfig, requestConsent, prevConfig);
|
||||
await maybeRequestConsentOrFail(
|
||||
newConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
prevConfig,
|
||||
false,
|
||||
);
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should include warning when hooks are present', async () => {
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(
|
||||
baseConfig,
|
||||
requestConsent,
|
||||
true,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'⚠️ This extension contains Hooks which can automatically execute commands.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should request consent if hooks status changes', async () => {
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(
|
||||
baseConfig,
|
||||
requestConsent,
|
||||
true,
|
||||
baseConfig,
|
||||
false,
|
||||
);
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,7 +103,10 @@ async function promptForConsentInteractive(
|
||||
* Builds a consent string for installing an extension based on it's
|
||||
* extensionConfig.
|
||||
*/
|
||||
function extensionConsentString(extensionConfig: ExtensionConfig): string {
|
||||
function extensionConsentString(
|
||||
extensionConfig: ExtensionConfig,
|
||||
hasHooks: boolean,
|
||||
): string {
|
||||
const sanitizedConfig = escapeAnsiCtrlCodes(extensionConfig);
|
||||
const output: string[] = [];
|
||||
const mcpServerEntries = Object.entries(sanitizedConfig.mcpServers || {});
|
||||
@@ -130,6 +133,11 @@ function extensionConsentString(extensionConfig: ExtensionConfig): string {
|
||||
`This extension will exclude the following core tools: ${sanitizedConfig.excludeTools}`,
|
||||
);
|
||||
}
|
||||
if (hasHooks) {
|
||||
output.push(
|
||||
'⚠️ This extension contains Hooks which can automatically execute commands.',
|
||||
);
|
||||
}
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
@@ -145,12 +153,15 @@ function extensionConsentString(extensionConfig: ExtensionConfig): string {
|
||||
export async function maybeRequestConsentOrFail(
|
||||
extensionConfig: ExtensionConfig,
|
||||
requestConsent: (consent: string) => Promise<boolean>,
|
||||
hasHooks: boolean,
|
||||
previousExtensionConfig?: ExtensionConfig,
|
||||
previousHasHooks?: boolean,
|
||||
) {
|
||||
const extensionConsent = extensionConsentString(extensionConfig);
|
||||
const extensionConsent = extensionConsentString(extensionConfig, hasHooks);
|
||||
if (previousExtensionConfig) {
|
||||
const previousExtensionConsent = extensionConsentString(
|
||||
previousExtensionConfig,
|
||||
previousHasHooks ?? false,
|
||||
);
|
||||
if (previousExtensionConsent === extensionConsent) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user