diff --git a/docs/core/subagents.md b/docs/core/subagents.md index 659ed6d640..6d863f489e 100644 --- a/docs/core/subagents.md +++ b/docs/core/subagents.md @@ -7,20 +7,14 @@ the main agent's context or toolset. > **Note: Subagents are currently an experimental feature.** > -> To use custom subagents, you must explicitly enable them in your -> `settings.json`: +> To use custom subagents, you must ensure they are enabled in your +> `settings.json` (enabled by default): > > ```json > { > "experimental": { "enableAgents": true } > } > ``` -> -> **Warning:** Subagents currently operate in -> ["YOLO mode"](../reference/configuration.md#command-line-arguments), meaning -> they may execute tools without individual user confirmation for each step. -> Proceed with caution when defining agents with powerful tools like -> `run_shell_command` or `write_file`. ## What are subagents? diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 01aaea676f..8845b6dd69 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1158,9 +1158,8 @@ their corresponding top-level category object in your `settings.json` file. - **Requires restart:** Yes - **`experimental.enableAgents`** (boolean): - - **Description:** Enable local and remote subagents. Warning: Experimental - feature, uses YOLO mode for subagents - - **Default:** `false` + - **Description:** Enable local and remote subagents. + - **Default:** `true` - **Requires restart:** Yes - **`experimental.extensionManagement`** (boolean): diff --git a/packages/cli/src/commands/extensions/install.test.ts b/packages/cli/src/commands/extensions/install.test.ts index b0fd20d311..417e750651 100644 --- a/packages/cli/src/commands/extensions/install.test.ts +++ b/packages/cli/src/commands/extensions/install.test.ts @@ -137,6 +137,7 @@ describe('handleInstall', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], securityWarnings: [], discoveryErrors: [], @@ -379,6 +380,7 @@ describe('handleInstall', () => { mcps: [], hooks: [], skills: ['cool-skill'], + agents: ['cool-agent'], settings: [], securityWarnings: ['Security risk!'], discoveryErrors: ['Read error'], @@ -408,6 +410,10 @@ describe('handleInstall', () => { expect.stringContaining('cool-skill'), false, ); + expect(mockPromptForConsentNonInteractive).toHaveBeenCalledWith( + expect.stringContaining('cool-agent'), + false, + ); expect(mockPromptForConsentNonInteractive).toHaveBeenCalledWith( expect.stringContaining('Security Warnings:'), false, diff --git a/packages/cli/src/commands/extensions/install.ts b/packages/cli/src/commands/extensions/install.ts index eea7679c00..542d1240be 100644 --- a/packages/cli/src/commands/extensions/install.ts +++ b/packages/cli/src/commands/extensions/install.ts @@ -99,11 +99,15 @@ export async function handleInstall(args: InstallArgs) { if (hasDiscovery) { promptLines.push(chalk.bold('This folder contains:')); const groups = [ - { label: 'Commands', items: discoveryResults.commands }, - { label: 'MCP Servers', items: discoveryResults.mcps }, - { label: 'Hooks', items: discoveryResults.hooks }, - { label: 'Skills', items: discoveryResults.skills }, - { label: 'Setting overrides', items: discoveryResults.settings }, + { label: 'Commands', items: discoveryResults.commands ?? [] }, + { label: 'MCP Servers', items: discoveryResults.mcps ?? [] }, + { label: 'Hooks', items: discoveryResults.hooks ?? [] }, + { label: 'Skills', items: discoveryResults.skills ?? [] }, + { label: 'Agents', items: discoveryResults.agents ?? [] }, + { + label: 'Setting overrides', + items: discoveryResults.settings ?? [], + }, ].filter((g) => g.items.length > 0); for (const group of groups) { diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index 53d75bd436..37ddf87642 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -400,12 +400,10 @@ describe('SettingsSchema', () => { expect(setting).toBeDefined(); expect(setting.type).toBe('boolean'); expect(setting.category).toBe('Experimental'); - expect(setting.default).toBe(false); + expect(setting.default).toBe(true); expect(setting.requiresRestart).toBe(true); expect(setting.showInDialog).toBe(false); - expect(setting.description).toBe( - 'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents', - ); + expect(setting.description).toBe('Enable local and remote subagents.'); }); it('should have skills setting enabled by default', () => { diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 87fbe98fc3..04db402f07 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1838,9 +1838,8 @@ const SETTINGS_SCHEMA = { label: 'Enable Agents', category: 'Experimental', requiresRestart: true, - default: false, - description: - 'Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents', + default: true, + description: 'Enable local and remote subagents.', showInDialog: false, }, extensionManagement: { diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index 012b2aab2f..e68417fc55 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -66,6 +66,7 @@ describe('FolderTrustDialog', () => { mcps: Array.from({ length: 10 }, (_, i) => `mcp${i}`), hooks: Array.from({ length: 10 }, (_, i) => `hook${i}`), skills: Array.from({ length: 10 }, (_, i) => `skill${i}`), + agents: [], settings: Array.from({ length: 10 }, (_, i) => `setting${i}`), discoveryErrors: [], securityWarnings: [], @@ -95,6 +96,7 @@ describe('FolderTrustDialog', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], discoveryErrors: [], securityWarnings: [], @@ -125,6 +127,7 @@ describe('FolderTrustDialog', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], discoveryErrors: [], securityWarnings: [], @@ -152,6 +155,7 @@ describe('FolderTrustDialog', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], discoveryErrors: [], securityWarnings: [], @@ -332,6 +336,7 @@ describe('FolderTrustDialog', () => { mcps: ['mcp1'], hooks: ['hook1'], skills: ['skill1'], + agents: ['agent1'], settings: ['general', 'ui'], discoveryErrors: [], securityWarnings: [], @@ -355,6 +360,8 @@ describe('FolderTrustDialog', () => { expect(lastFrame()).toContain('- hook1'); expect(lastFrame()).toContain('• Skills (1):'); expect(lastFrame()).toContain('- skill1'); + expect(lastFrame()).toContain('• Agents (1):'); + expect(lastFrame()).toContain('- agent1'); expect(lastFrame()).toContain('• Setting overrides (2):'); expect(lastFrame()).toContain('- general'); expect(lastFrame()).toContain('- ui'); @@ -367,6 +374,7 @@ describe('FolderTrustDialog', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], discoveryErrors: [], securityWarnings: ['Dangerous setting detected!'], @@ -390,6 +398,7 @@ describe('FolderTrustDialog', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], discoveryErrors: ['Failed to load custom commands'], securityWarnings: [], @@ -413,6 +422,7 @@ describe('FolderTrustDialog', () => { mcps: [], hooks: [], skills: [], + agents: [], settings: [], discoveryErrors: [], securityWarnings: [], @@ -446,6 +456,7 @@ describe('FolderTrustDialog', () => { mcps: [`${ansiRed}mcp-with-ansi${ansiReset}`], hooks: [`${ansiRed}hook-with-ansi${ansiReset}`], skills: [`${ansiRed}skill-with-ansi${ansiReset}`], + agents: [], settings: [`${ansiRed}setting-with-ansi${ansiReset}`], discoveryErrors: [`${ansiRed}error-with-ansi${ansiReset}`], securityWarnings: [`${ansiRed}warning-with-ansi${ansiReset}`], diff --git a/packages/cli/src/ui/components/FolderTrustDialog.tsx b/packages/cli/src/ui/components/FolderTrustDialog.tsx index 6c1c0d9e8c..5f226b7d15 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.tsx @@ -135,6 +135,7 @@ export const FolderTrustDialog: React.FC = ({ { label: 'MCP Servers', items: discoveryResults?.mcps ?? [] }, { label: 'Hooks', items: discoveryResults?.hooks ?? [] }, { label: 'Skills', items: discoveryResults?.skills ?? [] }, + { label: 'Agents', items: discoveryResults?.agents ?? [] }, { label: 'Setting overrides', items: discoveryResults?.settings ?? [] }, ].filter((g) => g.items.length > 0); diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index fd478bba40..573a6bedde 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1246,7 +1246,7 @@ describe('Server Config (config.ts)', () => { const config = new Config(params); const mockAgentDefinition = { - name: 'codebase-investigator', + name: 'codebase_investigator', description: 'Agent 1', instructions: 'Inst 1', }; @@ -1294,7 +1294,7 @@ describe('Server Config (config.ts)', () => { it('should register subagents as tools even when they are not in allowedTools', async () => { const params: ConfigParameters = { ...baseParams, - allowedTools: ['read_file'], // codebase-investigator is NOT here + allowedTools: ['read_file'], // codebase_investigator is NOT here agents: { overrides: { codebase_investigator: { enabled: true }, @@ -1304,7 +1304,7 @@ describe('Server Config (config.ts)', () => { const config = new Config(params); const mockAgentDefinition = { - name: 'codebase-investigator', + name: 'codebase_investigator', description: 'Agent 1', instructions: 'Inst 1', }; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 32c7f067f3..1b09d59125 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -948,7 +948,7 @@ export class Config implements McpContext, AgentLoopContext { this.model = params.model; this.disableLoopDetection = params.disableLoopDetection ?? false; this._activeModel = params.model; - this.enableAgents = params.enableAgents ?? false; + this.enableAgents = params.enableAgents ?? true; this.agents = params.agents ?? {}; this.disableLLMCorrection = params.disableLLMCorrection ?? true; this.planEnabled = params.plan ?? true; @@ -3147,22 +3147,23 @@ export class Config implements McpContext, AgentLoopContext { */ private registerSubAgentTools(registry: ToolRegistry): void { const agentsOverrides = this.getAgentsSettings().overrides ?? {}; - if ( - this.isAgentsEnabled() || - agentsOverrides['codebase_investigator']?.enabled !== false || - agentsOverrides['cli_help']?.enabled !== false - ) { - const definitions = this.agentRegistry.getAllDefinitions(); + const definitions = this.agentRegistry.getAllDefinitions(); - for (const definition of definitions) { - try { - const tool = new SubagentTool(definition, this, this.messageBus); - registry.registerTool(tool); - } catch (e: unknown) { - debugLogger.warn( - `Failed to register tool for agent ${definition.name}: ${getErrorMessage(e)}`, - ); + for (const definition of definitions) { + try { + if ( + !this.isAgentsEnabled() || + agentsOverrides[definition.name]?.enabled === false + ) { + continue; } + + const tool = new SubagentTool(definition, this, this.messageBus); + registry.registerTool(tool); + } catch (e: unknown) { + debugLogger.warn( + `Failed to register tool for agent ${definition.name}: ${getErrorMessage(e)}`, + ); } } } diff --git a/packages/core/src/services/FolderTrustDiscoveryService.test.ts b/packages/core/src/services/FolderTrustDiscoveryService.test.ts index b6d7d7734a..ad23b027c0 100644 --- a/packages/core/src/services/FolderTrustDiscoveryService.test.ts +++ b/packages/core/src/services/FolderTrustDiscoveryService.test.ts @@ -42,6 +42,11 @@ describe('FolderTrustDiscoveryService', () => { await fs.mkdir(path.join(skillsDir, 'test-skill'), { recursive: true }); await fs.writeFile(path.join(skillsDir, 'test-skill', 'SKILL.md'), 'body'); + // Mock agents + const agentsDir = path.join(geminiDir, 'agents'); + await fs.mkdir(agentsDir); + await fs.writeFile(path.join(agentsDir, 'test-agent.md'), 'body'); + // Mock settings (MCPs, Hooks, and general settings) const settings = { mcpServers: { @@ -62,6 +67,7 @@ describe('FolderTrustDiscoveryService', () => { expect(results.commands).toContain('test-cmd'); expect(results.skills).toContain('test-skill'); + expect(results.agents).toContain('test-agent'); expect(results.mcps).toContain('test-mcp'); expect(results.hooks).toContain('test-hook'); expect(results.settings).toContain('general'); @@ -79,9 +85,6 @@ describe('FolderTrustDiscoveryService', () => { allowed: ['git'], sandbox: false, }, - experimental: { - enableAgents: true, - }, security: { folderTrust: { enabled: false, @@ -98,9 +101,6 @@ describe('FolderTrustDiscoveryService', () => { expect(results.securityWarnings).toContain( 'This project auto-approves certain tools (tools.allowed).', ); - expect(results.securityWarnings).toContain( - 'This project enables autonomous agents (enableAgents).', - ); expect(results.securityWarnings).toContain( 'This project attempts to disable folder trust (security.folderTrust.enabled).', ); @@ -158,4 +158,20 @@ describe('FolderTrustDiscoveryService', () => { expect(results.discoveryErrors).toHaveLength(0); expect(results.settings).toHaveLength(0); }); + + it('should flag security warning for custom agents', async () => { + const geminiDir = path.join(tempDir, GEMINI_DIR); + await fs.mkdir(geminiDir, { recursive: true }); + + const agentsDir = path.join(geminiDir, 'agents'); + await fs.mkdir(agentsDir); + await fs.writeFile(path.join(agentsDir, 'test-agent.md'), 'body'); + + const results = await FolderTrustDiscoveryService.discover(tempDir); + + expect(results.agents).toContain('test-agent'); + expect(results.securityWarnings).toContain( + 'This project contains custom agents.', + ); + }); }); diff --git a/packages/core/src/services/FolderTrustDiscoveryService.ts b/packages/core/src/services/FolderTrustDiscoveryService.ts index bdf5d76297..09e32210a8 100644 --- a/packages/core/src/services/FolderTrustDiscoveryService.ts +++ b/packages/core/src/services/FolderTrustDiscoveryService.ts @@ -16,6 +16,7 @@ export interface FolderDiscoveryResults { mcps: string[]; hooks: string[]; skills: string[]; + agents: string[]; settings: string[]; securityWarnings: string[]; discoveryErrors: string[]; @@ -37,6 +38,7 @@ export class FolderTrustDiscoveryService { mcps: [], hooks: [], skills: [], + agents: [], settings: [], securityWarnings: [], discoveryErrors: [], @@ -50,6 +52,7 @@ export class FolderTrustDiscoveryService { await Promise.all([ this.discoverCommands(geminiDir, results), this.discoverSkills(geminiDir, results), + this.discoverAgents(geminiDir, results), this.discoverSettings(geminiDir, results), ]); @@ -99,6 +102,34 @@ export class FolderTrustDiscoveryService { } } + private static async discoverAgents( + geminiDir: string, + results: FolderDiscoveryResults, + ) { + const agentsDir = path.join(geminiDir, 'agents'); + if (await this.exists(agentsDir)) { + try { + const entries = await fs.readdir(agentsDir, { withFileTypes: true }); + for (const entry of entries) { + if ( + entry.isFile() && + entry.name.endsWith('.md') && + !entry.name.startsWith('_') + ) { + results.agents.push(path.basename(entry.name, '.md')); + } + } + if (results.agents.length > 0) { + results.securityWarnings.push('This project contains custom agents.'); + } + } catch (e) { + results.discoveryErrors.push( + `Failed to discover agents: ${e instanceof Error ? e.message : String(e)}`, + ); + } + } + } + private static async discoverSettings( geminiDir: string, results: FolderDiscoveryResults, @@ -119,7 +150,7 @@ export class FolderTrustDiscoveryService { (key) => !['mcpServers', 'hooks', '$schema'].includes(key), ); - results.securityWarnings = this.collectSecurityWarnings(settings); + results.securityWarnings.push(...this.collectSecurityWarnings(settings)); const mcpServers = settings['mcpServers']; if (this.isRecord(mcpServers)) { @@ -159,10 +190,6 @@ export class FolderTrustDiscoveryService { ? settings['tools'] : undefined; - const experimental = this.isRecord(settings['experimental']) - ? settings['experimental'] - : undefined; - const security = this.isRecord(settings['security']) ? settings['security'] : undefined; @@ -179,10 +206,6 @@ export class FolderTrustDiscoveryService { condition: Array.isArray(allowedTools) && allowedTools.length > 0, message: 'This project auto-approves certain tools (tools.allowed).', }, - { - condition: experimental?.['enableAgents'] === true, - message: 'This project enables autonomous agents (enableAgents).', - }, { condition: folderTrust?.['enabled'] === false, message: diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index f482053d9f..df802f97a9 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -1970,9 +1970,9 @@ }, "enableAgents": { "title": "Enable Agents", - "description": "Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents", - "markdownDescription": "Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", - "default": false, + "description": "Enable local and remote subagents.", + "markdownDescription": "Enable local and remote subagents.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, "type": "boolean" }, "extensionManagement": {