mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 11:04:42 -07:00
feat(cli): add security consent prompts for skill installation (#16549)
This commit is contained in:
@@ -1298,10 +1298,11 @@ describe('extension tests', () => {
|
||||
|
||||
expect(mockRequestConsent).toHaveBeenCalledWith(
|
||||
`Installing extension "my-local-extension".
|
||||
${INSTALL_WARNING_MESSAGE}
|
||||
This extension will run the following MCP servers:
|
||||
* test-server (local): node dobadthing \\u001b[12D\\u001b[K server.js
|
||||
* test-server-2 (remote): https://google.com`,
|
||||
* test-server-2 (remote): https://google.com
|
||||
|
||||
${INSTALL_WARNING_MESSAGE}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -191,12 +191,13 @@ describe('consent', () => {
|
||||
|
||||
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',
|
||||
'',
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
].join('\n');
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledWith(expectedConsentString);
|
||||
@@ -324,7 +325,6 @@ describe('consent', () => {
|
||||
|
||||
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',
|
||||
@@ -332,13 +332,17 @@ describe('consent', () => {
|
||||
'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:',
|
||||
'\nThis extension will install the following agent skills:\n',
|
||||
` * ${chalk.bold('skill1')}: desc1`,
|
||||
` (Location: ${skill1.location}) (2 items in directory)`,
|
||||
` * ${chalk.bold('skill2')}: desc2`,
|
||||
` (Location: ${skill2.location}) (1 items in directory)`,
|
||||
chalk.dim(` (Source: ${skill1.location}) (2 items in directory)`),
|
||||
'',
|
||||
` * ${chalk.bold('skill2')}: desc2`,
|
||||
chalk.dim(` (Source: ${skill2.location}) (1 items in directory)`),
|
||||
'',
|
||||
'',
|
||||
INSTALL_WARNING_MESSAGE,
|
||||
'',
|
||||
SKILLS_WARNING_MESSAGE,
|
||||
].join('\n');
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledWith(expectedConsentString);
|
||||
@@ -375,10 +379,42 @@ describe('consent', () => {
|
||||
|
||||
expect(requestConsent).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
` (Location: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`,
|
||||
` (Source: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('skillsConsentString', () => {
|
||||
it('should generate a consent string for skills', async () => {
|
||||
const skill1Dir = path.join(tempDir, 'skill1');
|
||||
await fs.mkdir(skill1Dir, { recursive: true });
|
||||
await fs.writeFile(path.join(skill1Dir, 'SKILL.md'), 'body1');
|
||||
|
||||
const skill1: SkillDefinition = {
|
||||
name: 'skill1',
|
||||
description: 'desc1',
|
||||
location: path.join(skill1Dir, 'SKILL.md'),
|
||||
body: 'body1',
|
||||
};
|
||||
|
||||
const { skillsConsentString } = await import('./consent.js');
|
||||
const consentString = await skillsConsentString(
|
||||
[skill1],
|
||||
'https://example.com/repo.git',
|
||||
'/mock/target/dir',
|
||||
);
|
||||
|
||||
expect(consentString).toContain(
|
||||
'Installing agent skill(s) from "https://example.com/repo.git".',
|
||||
);
|
||||
expect(consentString).toContain('Install Destination: /mock/target/dir');
|
||||
expect(consentString).toContain('\n' + SKILLS_WARNING_MESSAGE);
|
||||
expect(consentString).toContain(` * ${chalk.bold('skill1')}: desc1`);
|
||||
expect(consentString).toContain(
|
||||
chalk.dim(`(Source: ${skill1.location}) (1 items in directory)`),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,27 @@ export const SKILLS_WARNING_MESSAGE = chalk.yellow(
|
||||
"Agent skills inject specialized instructions and domain-specific knowledge into the agent's system prompt. This can change how the agent interprets your requests and interacts with your environment. Review the skill definitions at the location(s) provided below to ensure they meet your security standards.",
|
||||
);
|
||||
|
||||
/**
|
||||
* Builds a consent string for installing agent skills.
|
||||
*/
|
||||
export async function skillsConsentString(
|
||||
skills: SkillDefinition[],
|
||||
source: string,
|
||||
targetDir?: string,
|
||||
): Promise<string> {
|
||||
const output: string[] = [];
|
||||
output.push(`Installing agent skill(s) from "${source}".`);
|
||||
output.push('\nThe following agent skill(s) will be installed:\n');
|
||||
output.push(...(await renderSkillsList(skills)));
|
||||
|
||||
if (targetDir) {
|
||||
output.push(`Install Destination: ${targetDir}`);
|
||||
}
|
||||
output.push('\n' + SKILLS_WARNING_MESSAGE);
|
||||
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests consent from the user to perform an action, by reading a Y/n
|
||||
* character from stdin.
|
||||
@@ -120,7 +141,6 @@ async function extensionConsentString(
|
||||
const output: string[] = [];
|
||||
const mcpServerEntries = Object.entries(sanitizedConfig.mcpServers || {});
|
||||
output.push(`Installing extension "${sanitizedConfig.name}".`);
|
||||
output.push(INSTALL_WARNING_MESSAGE);
|
||||
|
||||
if (mcpServerEntries.length) {
|
||||
output.push('This extension will run the following MCP servers:');
|
||||
@@ -149,23 +169,37 @@ async function extensionConsentString(
|
||||
}
|
||||
if (skills.length > 0) {
|
||||
output.push(`\n${chalk.bold('Agent Skills:')}`);
|
||||
output.push(SKILLS_WARNING_MESSAGE);
|
||||
output.push('This extension will install the following agent skills:');
|
||||
for (const skill of skills) {
|
||||
output.push(` * ${chalk.bold(skill.name)}: ${skill.description}`);
|
||||
const skillDir = path.dirname(skill.location);
|
||||
let fileCountStr = '';
|
||||
try {
|
||||
const skillDirItems = await fs.readdir(skillDir);
|
||||
fileCountStr = ` (${skillDirItems.length} items in directory)`;
|
||||
} catch {
|
||||
fileCountStr = ` ${chalk.red('⚠️ (Could not count items in directory)')}`;
|
||||
}
|
||||
output.push(` (Location: ${skill.location})${fileCountStr}`);
|
||||
output.push('\nThis extension will install the following agent skills:\n');
|
||||
output.push(...(await renderSkillsList(skills)));
|
||||
}
|
||||
|
||||
output.push('\n' + INSTALL_WARNING_MESSAGE);
|
||||
if (skills.length > 0) {
|
||||
output.push('\n' + SKILLS_WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared logic for formatting a list of agent skills for a consent prompt.
|
||||
*/
|
||||
async function renderSkillsList(skills: SkillDefinition[]): Promise<string[]> {
|
||||
const output: string[] = [];
|
||||
for (const skill of skills) {
|
||||
output.push(` * ${chalk.bold(skill.name)}: ${skill.description}`);
|
||||
const skillDir = path.dirname(skill.location);
|
||||
let fileCountStr = '';
|
||||
try {
|
||||
const skillDirItems = await fs.readdir(skillDir);
|
||||
fileCountStr = ` (${skillDirItems.length} items in directory)`;
|
||||
} catch {
|
||||
fileCountStr = ` ${chalk.red('⚠️ (Could not count items in directory)')}`;
|
||||
}
|
||||
output.push(chalk.dim(` (Source: ${skill.location})${fileCountStr}`));
|
||||
output.push('');
|
||||
}
|
||||
return output.join('\n');
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user