mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 11:34:44 -07:00
feat(cli): add security consent prompts for skill installation (#16549)
This commit is contained in:
@@ -7,11 +7,18 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const mockInstallSkill = vi.hoisted(() => vi.fn());
|
||||
const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn());
|
||||
const mockSkillsConsentString = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../utils/skillUtils.js', () => ({
|
||||
installSkill: mockInstallSkill,
|
||||
}));
|
||||
|
||||
vi.mock('../../config/extensions/consent.js', () => ({
|
||||
requestConsentNonInteractive: mockRequestConsentNonInteractive,
|
||||
skillsConsentString: mockSkillsConsentString,
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
debugLogger: { log: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
@@ -23,6 +30,8 @@ describe('skill install command', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
||||
mockSkillsConsentString.mockResolvedValue('Mock Consent String');
|
||||
mockRequestConsentNonInteractive.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe('installCommand', () => {
|
||||
@@ -37,9 +46,10 @@ describe('skill install command', () => {
|
||||
});
|
||||
|
||||
it('should call installSkill with correct arguments for user scope', async () => {
|
||||
mockInstallSkill.mockResolvedValue([
|
||||
{ name: 'test-skill', location: '/mock/user/skills/test-skill' },
|
||||
]);
|
||||
mockInstallSkill.mockImplementation(async (_s, _sc, _p, _ol, rc) => {
|
||||
await rc([]);
|
||||
return [{ name: 'test-skill', location: '/mock/user/skills/test-skill' }];
|
||||
});
|
||||
|
||||
await handleInstall({
|
||||
source: 'https://example.com/repo.git',
|
||||
@@ -51,6 +61,7 @@ describe('skill install command', () => {
|
||||
'user',
|
||||
undefined,
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Successfully installed skill: test-skill'),
|
||||
@@ -58,6 +69,47 @@ describe('skill install command', () => {
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining('location: /mock/user/skills/test-skill'),
|
||||
);
|
||||
expect(mockRequestConsentNonInteractive).toHaveBeenCalledWith(
|
||||
'Mock Consent String',
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip prompt and log consent when --consent is provided', async () => {
|
||||
mockInstallSkill.mockImplementation(async (_s, _sc, _p, _ol, rc) => {
|
||||
await rc([]);
|
||||
return [{ name: 'test-skill', location: '/mock/user/skills/test-skill' }];
|
||||
});
|
||||
|
||||
await handleInstall({
|
||||
source: 'https://example.com/repo.git',
|
||||
consent: true,
|
||||
});
|
||||
|
||||
expect(mockRequestConsentNonInteractive).not.toHaveBeenCalled();
|
||||
expect(debugLogger.log).toHaveBeenCalledWith(
|
||||
'You have consented to the following:',
|
||||
);
|
||||
expect(debugLogger.log).toHaveBeenCalledWith('Mock Consent String');
|
||||
expect(mockInstallSkill).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should abort installation if consent is denied', async () => {
|
||||
mockRequestConsentNonInteractive.mockResolvedValue(false);
|
||||
mockInstallSkill.mockImplementation(async (_s, _sc, _p, _ol, rc) => {
|
||||
if (!(await rc([]))) {
|
||||
throw new Error('Skill installation cancelled by user.');
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
await handleInstall({
|
||||
source: 'https://example.com/repo.git',
|
||||
});
|
||||
|
||||
expect(debugLogger.error).toHaveBeenCalledWith(
|
||||
'Skill installation cancelled by user.',
|
||||
);
|
||||
expect(process.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('should call installSkill with correct arguments for workspace scope and subpath', async () => {
|
||||
@@ -76,6 +128,7 @@ describe('skill install command', () => {
|
||||
'workspace',
|
||||
'my-skills-dir',
|
||||
expect.any(Function),
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,24 +5,43 @@
|
||||
*/
|
||||
|
||||
import type { CommandModule } from 'yargs';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { debugLogger, type SkillDefinition } from '@google/gemini-cli-core';
|
||||
import { getErrorMessage } from '../../utils/errors.js';
|
||||
import { exitCli } from '../utils.js';
|
||||
import { installSkill } from '../../utils/skillUtils.js';
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
requestConsentNonInteractive,
|
||||
skillsConsentString,
|
||||
} from '../../config/extensions/consent.js';
|
||||
|
||||
interface InstallArgs {
|
||||
source: string;
|
||||
scope?: 'user' | 'workspace';
|
||||
path?: string;
|
||||
consent?: boolean;
|
||||
}
|
||||
|
||||
export async function handleInstall(args: InstallArgs) {
|
||||
try {
|
||||
const { source } = args;
|
||||
const { source, consent } = args;
|
||||
const scope = args.scope ?? 'user';
|
||||
const subpath = args.path;
|
||||
|
||||
const requestConsent = async (
|
||||
skills: SkillDefinition[],
|
||||
targetDir: string,
|
||||
) => {
|
||||
if (consent) {
|
||||
debugLogger.log('You have consented to the following:');
|
||||
debugLogger.log(await skillsConsentString(skills, source, targetDir));
|
||||
return true;
|
||||
}
|
||||
return requestConsentNonInteractive(
|
||||
await skillsConsentString(skills, source, targetDir),
|
||||
);
|
||||
};
|
||||
|
||||
const installedSkills = await installSkill(
|
||||
source,
|
||||
scope,
|
||||
@@ -30,6 +49,7 @@ export async function handleInstall(args: InstallArgs) {
|
||||
(msg) => {
|
||||
debugLogger.log(msg);
|
||||
},
|
||||
requestConsent,
|
||||
);
|
||||
|
||||
for (const skill of installedSkills) {
|
||||
@@ -68,6 +88,12 @@ export const installCommand: CommandModule = {
|
||||
'Sub-path within the repository to install from (only used for git repository sources).',
|
||||
type: 'string',
|
||||
})
|
||||
.option('consent', {
|
||||
describe:
|
||||
'Acknowledge the security risks of installing a skill and skip the confirmation prompt.',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.check((argv) => {
|
||||
if (!argv.source) {
|
||||
throw new Error('The source argument must be provided.');
|
||||
@@ -79,6 +105,7 @@ export const installCommand: CommandModule = {
|
||||
source: argv['source'] as string,
|
||||
scope: argv['scope'] as 'user' | 'workspace',
|
||||
path: argv['path'] as string | undefined,
|
||||
consent: argv['consent'] as boolean | undefined,
|
||||
});
|
||||
await exitCli();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user