mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 12:34:38 -07:00
Agent Skills: Extension Support & Security Disclosure (#15834)
This commit is contained in:
@@ -5,15 +5,20 @@
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import {
|
||||
requestConsentNonInteractive,
|
||||
requestConsentInteractive,
|
||||
maybeRequestConsentOrFail,
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
SKILLS_WARNING_MESSAGE,
|
||||
} from './consent.js';
|
||||
import type { ConfirmationRequest } from '../../ui/types.js';
|
||||
import type { ExtensionConfig } from '../extension.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { debugLogger, type SkillDefinition } from '@google/gemini-cli-core';
|
||||
|
||||
const mockReadline = vi.hoisted(() => ({
|
||||
createInterface: vi.fn().mockReturnValue({
|
||||
@@ -40,11 +45,18 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
});
|
||||
|
||||
describe('consent', () => {
|
||||
beforeEach(() => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'consent-test-'));
|
||||
});
|
||||
afterEach(() => {
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
if (tempDir) {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('requestConsentNonInteractive', () => {
|
||||
@@ -250,6 +262,102 @@ describe('consent', () => {
|
||||
);
|
||||
expect(requestConsent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should request consent if skills change', async () => {
|
||||
const skill1Dir = path.join(tempDir, 'skill1');
|
||||
const skill2Dir = path.join(tempDir, 'skill2');
|
||||
await fs.mkdir(skill1Dir, { recursive: true });
|
||||
await fs.mkdir(skill2Dir, { recursive: true });
|
||||
await fs.writeFile(path.join(skill1Dir, 'SKILL.md'), 'body1');
|
||||
await fs.writeFile(path.join(skill1Dir, 'extra.txt'), 'extra');
|
||||
await fs.writeFile(path.join(skill2Dir, 'SKILL.md'), 'body2');
|
||||
|
||||
const skill1: SkillDefinition = {
|
||||
name: 'skill1',
|
||||
description: 'desc1',
|
||||
location: path.join(skill1Dir, 'SKILL.md'),
|
||||
body: 'body1',
|
||||
};
|
||||
const skill2: SkillDefinition = {
|
||||
name: 'skill2',
|
||||
description: 'desc2',
|
||||
location: path.join(skill2Dir, 'SKILL.md'),
|
||||
body: 'body2',
|
||||
};
|
||||
|
||||
const config: ExtensionConfig = {
|
||||
...baseConfig,
|
||||
mcpServers: {
|
||||
server1: { command: 'npm', args: ['start'] },
|
||||
server2: { httpUrl: 'https://remote.com' },
|
||||
},
|
||||
contextFileName: 'my-context.md',
|
||||
excludeTools: ['tool1', 'tool2'],
|
||||
};
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
await maybeRequestConsentOrFail(
|
||||
config,
|
||||
requestConsent,
|
||||
false,
|
||||
undefined,
|
||||
false,
|
||||
[skill1, skill2],
|
||||
);
|
||||
|
||||
const expectedConsentString = [
|
||||
'Installing extension "test-ext".',
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
'This extension will run the following MCP servers:',
|
||||
' * server1 (local): npm start',
|
||||
' * server2 (remote): https://remote.com',
|
||||
'This extension will append info to your gemini.md context using my-context.md',
|
||||
'This extension will exclude the following core tools: tool1,tool2',
|
||||
'',
|
||||
chalk.bold('Agent Skills:'),
|
||||
SKILLS_WARNING_MESSAGE,
|
||||
'This extension will install the following agent skills:',
|
||||
` * ${chalk.bold('skill1')}: desc1`,
|
||||
` (Location: ${skill1.location}) (2 items in directory)`,
|
||||
` * ${chalk.bold('skill2')}: desc2`,
|
||||
` (Location: ${skill2.location}) (1 items in directory)`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledWith(expectedConsentString);
|
||||
});
|
||||
|
||||
it('should show a warning if the skill directory cannot be read', async () => {
|
||||
const lockedDir = path.join(tempDir, 'locked');
|
||||
await fs.mkdir(lockedDir, { recursive: true, mode: 0o000 });
|
||||
|
||||
const skill: SkillDefinition = {
|
||||
name: 'locked-skill',
|
||||
description: 'A skill in a locked dir',
|
||||
location: path.join(lockedDir, 'SKILL.md'),
|
||||
body: 'body',
|
||||
};
|
||||
|
||||
const requestConsent = vi.fn().mockResolvedValue(true);
|
||||
try {
|
||||
await maybeRequestConsentOrFail(
|
||||
baseConfig,
|
||||
requestConsent,
|
||||
false,
|
||||
undefined,
|
||||
false,
|
||||
[skill],
|
||||
);
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
` (Location: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
// Restore permissions so cleanup works
|
||||
await fs.chmod(lockedDir, 0o700);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user