feat(admin): support admin-enforced settings for Agent Skills (#16406)

This commit is contained in:
N. Taylor Mullen
2026-01-13 23:40:23 -08:00
committed by GitHub
parent 66e7b479ae
commit bb6c574144
20 changed files with 350 additions and 52 deletions

View File

@@ -16,6 +16,9 @@ import {
refreshServerHierarchicalMemory,
SimpleExtensionLoader,
type FileDiscoveryService,
showMemory,
addMemory,
listMemoryFiles,
} from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -44,6 +47,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
content: 'Memory refreshed successfully.',
};
}),
showMemory: vi.fn(),
addMemory: vi.fn(),
listMemoryFiles: vi.fn(),
refreshServerHierarchicalMemory: vi.fn(),
};
});
@@ -78,6 +84,22 @@ describe('memoryCommand', () => {
mockGetUserMemory = vi.fn();
mockGetGeminiMdFileCount = vi.fn();
vi.mocked(showMemory).mockImplementation((config) => {
const memoryContent = config.getUserMemory() || '';
const fileCount = config.getGeminiMdFileCount() || 0;
let content;
if (memoryContent.length > 0) {
content = `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---`;
} else {
content = 'Memory is currently empty.';
}
return {
type: 'message',
messageType: 'info',
content,
};
});
mockContext = createMockCommandContext({
services: {
config: {
@@ -131,6 +153,20 @@ describe('memoryCommand', () => {
beforeEach(() => {
addCommand = getSubCommand('add');
vi.mocked(addMemory).mockImplementation((args) => {
if (!args || args.trim() === '') {
return {
type: 'message',
messageType: 'error',
content: 'Usage: /memory add <text to remember>',
};
}
return {
type: 'tool',
toolName: 'save_memory',
toolArgs: { fact: args.trim() },
};
});
mockContext = createMockCommandContext();
});
@@ -360,6 +396,21 @@ describe('memoryCommand', () => {
beforeEach(() => {
listCommand = getSubCommand('list');
mockGetGeminiMdfilePaths = vi.fn();
vi.mocked(listMemoryFiles).mockImplementation((config) => {
const filePaths = config.getGeminiMdFilePaths() || [];
const fileCount = filePaths.length;
let content;
if (fileCount > 0) {
content = `There are ${fileCount} GEMINI.md file(s) in use:\n\n${filePaths.join('\n')}`;
} else {
content = 'No GEMINI.md files in use.';
}
return {
type: 'message',
messageType: 'info',
content,
};
});
mockContext = createMockCommandContext({
services: {
config: {

View File

@@ -46,6 +46,7 @@ describe('skillsCommand', () => {
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue(skills),
getSkills: vi.fn().mockReturnValue(skills),
isAdminEnabled: vi.fn().mockReturnValue(true),
getSkill: vi
.fn()
.mockImplementation(
@@ -307,6 +308,43 @@ describe('skillsCommand', () => {
type: MessageType.ERROR,
text: 'Skill "non-existent" not found.',
}),
expect.any(Number),
);
});
it('should show error if skills are disabled by admin during disable', async () => {
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);
const disableCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'disable',
)!;
await disableCmd.action!(context, 'skill1');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
}),
expect.any(Number),
);
});
it('should show error if skills are disabled by admin during enable', async () => {
const skillManager = context.services.config!.getSkillManager();
vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);
const enableCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'enable',
)!;
await enableCmd.action!(context, 'skill1');
expect(context.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
}),
expect.any(Number),
);
});
});

View File

@@ -79,12 +79,26 @@ async function disableAction(
return;
}
const skillManager = context.services.config?.getSkillManager();
if (skillManager?.isAdminEnabled() === false) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
},
Date.now(),
);
return;
}
const skill = skillManager?.getSkill(skillName);
if (!skill) {
context.ui.addItem({
type: MessageType.ERROR,
text: `Skill "${skillName}" not found.`,
});
context.ui.addItem(
{
type: MessageType.ERROR,
text: `Skill "${skillName}" not found.`,
},
Date.now(),
);
return;
}
@@ -121,6 +135,18 @@ async function enableAction(
return;
}
const skillManager = context.services.config?.getSkillManager();
if (skillManager?.isAdminEnabled() === false) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: 'Agent skills are disabled by your admin.',
},
Date.now(),
);
return;
}
const result = enableSkill(context.services.settings, skillName);
let feedback = renderSkillActionFeedback(

View File

@@ -4,13 +4,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { createExtension } from '../../test-utils/createExtension.js';
import { useExtensionUpdates } from './useExtensionUpdates.js';
import { GEMINI_DIR } from '@google/gemini-cli-core';
import {
GEMINI_DIR,
loadAgentsFromDirectory,
loadSkillsFromDir,
} from '@google/gemini-cli-core';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { MessageType } from '../types.js';
@@ -36,6 +40,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...actual,
homedir: () => os.homedir(),
loadAgentsFromDirectory: vi
.fn()
.mockResolvedValue({ agents: [], errors: [] }),
loadSkillsFromDir: vi.fn().mockResolvedValue([]),
};
});
@@ -51,6 +59,11 @@ describe('useExtensionUpdates', () => {
let extensionManager: ExtensionManager;
beforeEach(() => {
vi.mocked(loadAgentsFromDirectory).mockResolvedValue({
agents: [],
errors: [],
});
vi.mocked(loadSkillsFromDir).mockResolvedValue([]);
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);