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

@@ -2091,5 +2091,30 @@ describe('Config JIT Initialization', () => {
expect(skillManager.setDisabledSkills).toHaveBeenCalledWith([]);
});
it('should update admin settings from onReload', async () => {
const mockOnReload = vi.fn().mockResolvedValue({
adminSkillsEnabled: false,
});
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
skillsSupport: true,
onReload: mockOnReload,
};
config = new Config(params);
await config.initialize();
const skillManager = config.getSkillManager();
vi.spyOn(skillManager, 'setAdminSettings');
await config.reloadSkills();
expect(skillManager.setAdminSettings).toHaveBeenCalledWith(false);
});
});
});

View File

@@ -377,13 +377,17 @@ export interface ConfigParameters {
enableAgents?: boolean;
skillsSupport?: boolean;
disabledSkills?: string[];
adminSkillsEnabled?: boolean;
experimentalJitContext?: boolean;
disableLLMCorrection?: boolean;
onModelChange?: (model: string) => void;
mcpEnabled?: boolean;
extensionsEnabled?: boolean;
agents?: AgentSettings;
onReload?: () => Promise<{ disabledSkills?: string[] }>;
onReload?: () => Promise<{
disabledSkills?: string[];
adminSkillsEnabled?: boolean;
}>;
}
export class Config {
@@ -511,13 +515,17 @@ export class Config {
private hookSystem?: HookSystem;
private readonly onModelChange: ((model: string) => void) | undefined;
private readonly onReload:
| (() => Promise<{ disabledSkills?: string[] }>)
| (() => Promise<{
disabledSkills?: string[];
adminSkillsEnabled?: boolean;
}>)
| undefined;
private readonly enableAgents: boolean;
private readonly agents: AgentSettings;
private readonly skillsSupport: boolean;
private disabledSkills: string[];
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly disableLLMCorrection: boolean;
@@ -594,6 +602,7 @@ export class Config {
this.disableLLMCorrection = params.disableLLMCorrection ?? false;
this.skillsSupport = params.skillsSupport ?? false;
this.disabledSkills = params.disabledSkills ?? [];
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
this.modelAvailabilityService = new ModelAvailabilityService();
this.previewFeatures = params.previewFeatures ?? undefined;
this.experimentalJitContext = params.experimentalJitContext ?? false;
@@ -777,20 +786,22 @@ export class Config {
]);
initMcpHandle?.end();
// Discover skills if enabled
if (this.skillsSupport) {
await this.getSkillManager().discoverSkills(
this.storage,
this.getExtensions(),
);
this.getSkillManager().setDisabledSkills(this.disabledSkills);
// Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums
if (this.getSkillManager().getSkills().length > 0) {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
this.getToolRegistry().registerTool(
new ActivateSkillTool(this, this.messageBus),
this.getSkillManager().setAdminSettings(this.adminSkillsEnabled);
if (this.adminSkillsEnabled) {
await this.getSkillManager().discoverSkills(
this.storage,
this.getExtensions(),
);
this.getSkillManager().setDisabledSkills(this.disabledSkills);
// Re-register ActivateSkillTool to update its schema with the discovered enabled skill enums
if (this.getSkillManager().getSkills().length > 0) {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
this.getToolRegistry().registerTool(
new ActivateSkillTool(this, this.messageBus),
);
}
}
}
@@ -1593,21 +1604,29 @@ export class Config {
if (this.onReload) {
const refreshed = await this.onReload();
this.disabledSkills = refreshed.disabledSkills ?? [];
this.getSkillManager().setAdminSettings(
refreshed.adminSkillsEnabled ?? this.adminSkillsEnabled,
);
}
await this.getSkillManager().discoverSkills(
this.storage,
this.getExtensions(),
);
this.getSkillManager().setDisabledSkills(this.disabledSkills);
// Re-register ActivateSkillTool to update its schema with the newly discovered skills
if (this.getSkillManager().getSkills().length > 0) {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
this.getToolRegistry().registerTool(
new ActivateSkillTool(this, this.messageBus),
if (this.getSkillManager().isAdminEnabled()) {
await this.getSkillManager().discoverSkills(
this.storage,
this.getExtensions(),
);
this.getSkillManager().setDisabledSkills(this.disabledSkills);
// Re-register ActivateSkillTool to update its schema with the newly discovered skills
if (this.getSkillManager().getSkills().length > 0) {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
this.getToolRegistry().registerTool(
new ActivateSkillTool(this, this.messageBus),
);
} else {
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
}
} else {
this.getSkillManager().clearSkills();
this.getToolRegistry().unregisterTool(ActivateSkillTool.Name);
}

View File

@@ -11,8 +11,6 @@ import nodePath from 'node:path';
import type { PolicySettings } from './types.js';
import { ApprovalMode, PolicyDecision, InProcessCheckerType } from './types.js';
import { Storage } from '../config/storage.js';
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
@@ -20,7 +18,9 @@ afterEach(() => {
});
describe('createPolicyEngineConfig', () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
const { Storage } = await import('../config/storage.js');
// Mock Storage to avoid picking up real user/system policies from the host environment
vi.spyOn(Storage, 'getUserPoliciesDir').mockReturnValue(
'/non/existent/user/policies',

View File

@@ -189,7 +189,7 @@ description: project-desc
name: skill1
description: desc1
---
`,
body1`,
);
const storage = new Storage('/dummy');
@@ -247,4 +247,20 @@ description: desc1
expect(enabled).toHaveLength(2);
expect(enabled.map((s) => s.name)).toContain('builtin-skill');
});
it('should maintain admin settings state', async () => {
const service = new SkillManager();
// Case 1: Enabled by admin
service.setAdminSettings(true);
expect(service.isAdminEnabled()).toBe(true);
// Case 2: Disabled by admin
service.setAdminSettings(false);
expect(service.isAdminEnabled()).toBe(false);
});
});

View File

@@ -15,6 +15,7 @@ export { type SkillDefinition };
export class SkillManager {
private skills: SkillDefinition[] = [];
private activeSkillNames: Set<string> = new Set();
private adminSkillsEnabled = true;
/**
* Clears all discovered skills.
@@ -23,6 +24,20 @@ export class SkillManager {
this.skills = [];
}
/**
* Sets administrative settings for skills.
*/
setAdminSettings(enabled: boolean): void {
this.adminSkillsEnabled = enabled;
}
/**
* Returns true if skills are enabled by the admin.
*/
isAdminEnabled(): boolean {
return this.adminSkillsEnabled;
}
/**
* Discovers skills from standard user and project locations, as well as extensions.
* Precedence: Extensions (lowest) -> User -> Project (highest).

View File

@@ -278,7 +278,7 @@ describe('WorkspaceContext with real filesystem', () => {
// handle it gracefully and return false.
expect(workspaceContext.isPathWithinWorkspace(linkA)).toBe(false);
expect(workspaceContext.isPathWithinWorkspace(linkB)).toBe(false);
});
}, 30000);
});
});