Compare commits

...

7 Commits

Author SHA1 Message Date
AK 17b06f7f69 Merge branch 'main' into akkr/subagents 2026-03-03 11:16:46 -08:00
AK fb918380d0 Merge branch 'main' into akkr/subagents 2026-03-02 19:16:47 -08:00
Akhilesh Kumar 4ef38c966c fix(core): properly store and expose checkerRunner in Config 2026-03-02 23:14:05 +00:00
Akhilesh Kumar 56a3ef94e4 Merge remote-tracking branch 'origin/main' into akkr/subagents 2026-03-02 23:11:15 +00:00
Akhilesh Kumar e74bfa1040 docs: update subagent policy plan to match implementation 2026-03-02 23:08:38 +00:00
Akhilesh Kumar 7554679d15 fix(core): support snake_case in policy settings schema 2026-03-02 23:07:58 +00:00
Akhilesh Kumar 03c6714c6d Implementation for the policy changes for subagents 2026-03-02 21:33:18 +00:00
12 changed files with 587 additions and 41 deletions
+2
View File
@@ -264,6 +264,8 @@ it yourself; just report it.
| `temperature` | number | No | Model temperature (0.0 - 2.0). |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
| `policy` | object | No | Scoped policy settings for the agent (see [Policy Engine](../reference/policy-engine.md)). |
| `mcp_servers` | object | No | MCP servers private to this agent. Key is server name, value is server configuration. |
### Optimizing your subagent
+47 -30
View File
@@ -733,37 +733,47 @@ Would you like to attempt to install via "git clone" instead?`,
}
}
let publicMcpServers: Record<string, MCPServerConfig> | undefined;
let privateMcpServers: Record<string, MCPServerConfig> | undefined;
if (config.mcpServers) {
if (this.settings.admin.mcp.enabled === false) {
config.mcpServers = undefined;
} else {
if (this.settings.admin.mcp.enabled !== false) {
// Apply admin allowlist if configured
const adminAllowlist = this.settings.admin.mcp.config;
if (adminAllowlist && Object.keys(adminAllowlist).length > 0) {
const result = applyAdminAllowlist(
config.mcpServers,
adminAllowlist,
);
config.mcpServers = result.mcpServers;
const filteredMcpServers =
adminAllowlist && Object.keys(adminAllowlist).length > 0
? (() => {
const result = applyAdminAllowlist(
config.mcpServers,
adminAllowlist,
);
if (result.blockedServerNames.length > 0) {
const message = getAdminBlockedMcpServersMessage(
result.blockedServerNames,
undefined,
);
coreEvents.emitConsoleLog('warn', message);
}
return result.mcpServers ?? {};
})()
: config.mcpServers;
if (result.blockedServerNames.length > 0) {
const message = getAdminBlockedMcpServersMessage(
result.blockedServerNames,
undefined,
);
coreEvents.emitConsoleLog('warn', message);
}
}
const entries = Object.entries(filteredMcpServers).map(
([key, value]) =>
[key, filterMcpConfig(value)] as [string, MCPServerConfig],
);
// Then apply local filtering/sanitization
if (config.mcpServers) {
config.mcpServers = Object.fromEntries(
Object.entries(config.mcpServers).map(([key, value]) => [
key,
filterMcpConfig(value),
]),
);
}
publicMcpServers = Object.fromEntries(
entries.filter(([, v]) => v.visibility !== 'private'),
);
privateMcpServers = Object.fromEntries(
entries.filter(([, v]) => v.visibility === 'private'),
);
if (Object.keys(publicMcpServers).length === 0)
publicMcpServers = undefined;
if (Object.keys(privateMcpServers).length === 0)
privateMcpServers = undefined;
}
}
@@ -854,9 +864,16 @@ Would you like to attempt to install via "git clone" instead?`,
const agentLoadResult = await loadAgentsFromDirectory(
path.join(effectiveExtensionPath, 'agents'),
);
agentLoadResult.agents = agentLoadResult.agents.map((agent) =>
recursivelyHydrateStrings(agent, hydrationContext),
);
agentLoadResult.agents = agentLoadResult.agents.map((agent) => {
const hydrated = recursivelyHydrateStrings(agent, hydrationContext);
if (privateMcpServers && hydrated.kind === 'local') {
hydrated.mcpServers = {
...privateMcpServers,
...hydrated.mcpServers,
};
}
return hydrated;
});
// Log errors but don't fail the entire extension load
for (const error of agentLoadResult.errors) {
@@ -871,7 +888,7 @@ Would you like to attempt to install via "git clone" instead?`,
path: effectiveExtensionPath,
contextFiles,
installMetadata,
mcpServers: config.mcpServers,
mcpServers: publicMcpServers,
excludeTools: config.excludeTools,
hooks,
isActive: this.extensionEnablementManager.isEnabled(
+12
View File
@@ -16,6 +16,12 @@ import {
DEFAULT_MAX_TIME_MINUTES,
} from './types.js';
import type { A2AAuthConfig } from './auth-provider/types.js';
import { type MCPServerConfig } from '../config/config.js';
import { type PolicySettings } from '../policy/types.js';
import {
PolicySettingsSchema,
MCPServersConfigSchema,
} from '../policy/schemas.js';
import { isValidToolName } from '../tools/tool-names.js';
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
import { getErrorMessage } from '../utils/errors.js';
@@ -38,6 +44,8 @@ interface FrontmatterLocalAgentDefinition
temperature?: number;
max_turns?: number;
timeout_mins?: number;
policy?: PolicySettings;
mcp_servers?: Record<string, MCPServerConfig>;
}
/**
@@ -111,6 +119,8 @@ const localAgentSchema = z
temperature: z.number().optional(),
max_turns: z.number().int().positive().optional(),
timeout_mins: z.number().int().positive().optional(),
policy: PolicySettingsSchema.optional(),
mcp_servers: MCPServersConfigSchema.optional(),
})
.strict();
@@ -470,6 +480,8 @@ export function markdownToAgentDefinition(
tools: markdown.tools,
}
: undefined,
policy: markdown.policy,
mcpServers: markdown.mcp_servers,
inputConfig,
metadata,
};
@@ -554,6 +554,104 @@ describe('LocalAgentExecutor', () => {
getToolSpy.mockRestore();
});
it('should support scoped policy and unique toolsets', async () => {
const toolToAllow = 'ls';
const toolToBlock = READ_FILE_TOOL_NAME;
const definition = createTestDefinition([toolToAllow, toolToBlock]);
definition.policy = {
tools: {
exclude: [toolToBlock],
},
};
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
const agentRegistry = executor['toolRegistry'];
// Tool explicitly allowed in definition but BLOCKED by policy should NOT be active
expect(agentRegistry.getTool(toolToAllow)).toBeDefined();
expect(agentRegistry.getTool(toolToBlock)).toBeUndefined();
});
it('should incorporate ADMIN policies from parent context into scoped policy engine', async () => {
const adminToolToBlock = 'admin_blocked_tool';
const userToolToBlock = 'user_blocked_tool';
// Mock parent policy engine with admin and user rules
const parentPolicyEngine = mockConfig.getPolicyEngine();
vi.spyOn(parentPolicyEngine, 'getRules').mockReturnValue([
{
toolName: adminToolToBlock,
decision: ApprovalMode.DEFAULT, // Use actual enum value if possible, or cast appropriately
priority: 5.1, // Admin tier
source: 'Admin Policy',
},
{
toolName: userToolToBlock,
decision: ApprovalMode.DEFAULT,
priority: 4.1, // User tier
source: 'User Policy',
},
] as unknown as PolicyRule[]);
vi.spyOn(parentPolicyEngine, 'getCheckers').mockReturnValue([]);
// Create a subagent definition that has its own policy
const definition = createTestDefinition([
adminToolToBlock,
userToolToBlock,
LS_TOOL_NAME,
]);
definition.policy = {
tools: {
allowed: [userToolToBlock], // Subagent tries to allow what user blocked
},
};
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
const scopedPolicyEngine = executor['runtimeContext'].getPolicyEngine();
const rules = scopedPolicyEngine.getRules();
// Should have incorporated the ADMIN rule
expect(rules).toContainEqual(
expect.objectContaining({
toolName: adminToolToBlock,
priority: 5.1,
}),
);
// Should NOT have incorporated the USER rule
expect(rules).not.toContainEqual(
expect.objectContaining({
toolName: userToolToBlock,
priority: 4.1,
}),
);
// Verify effective decisions
const adminCheck = await scopedPolicyEngine.check(
{ name: adminToolToBlock, args: {} },
undefined,
);
expect(adminCheck.decision).toBe('deny');
const userCheck = await scopedPolicyEngine.check(
{ name: userToolToBlock, args: {} },
undefined,
);
// User block was NOT inherited, and subagent policy allowed it.
expect(userCheck.decision).toBe('allow');
});
});
describe('run (Execution Loop and Logic)', () => {
@@ -2458,4 +2556,59 @@ describe('LocalAgentExecutor', () => {
expect(mockSetHistory).toHaveBeenCalledWith(compressedHistory);
});
});
describe('Isolated Tool Discovery and Shadowing', () => {
it('should allow subagent-specific MCP tools to shadow global tools in the isolated registry', async () => {
const toolName = 'shadowed_tool';
const globalTool = new MockTool({ name: toolName, description: 'Global Version' });
parentToolRegistry.registerTool(globalTool);
const subagentMcpTool = {
tool: vi.fn(),
callTool: vi.fn(),
} as unknown as CallableTool;
const mcpTool = new DiscoveredMCPTool(
subagentMcpTool,
'private-server',
toolName,
'Subagent Version',
{},
mockConfig.getMessageBus(),
);
// Mock McpClientManager to register our shadowing tool
const definition = createTestDefinition();
definition.mcpServers = {
'private-server': { command: 'node', args: ['server.js'] }
};
// We need to mock the McpClientManager's behavior
const McpClientManagerModule = await import('../tools/mcp-client-manager.js');
vi.spyOn(McpClientManagerModule.McpClientManager.prototype, 'maybeDiscoverMcpServer')
.mockImplementation(async function(this: unknown) {
// Manually register the tool into the registry provided to the manager
const manager = this as unknown as { toolRegistry: ToolRegistry };
manager.toolRegistry.registerTool(mcpTool);
});
const executor = await LocalAgentExecutor.create(
definition,
mockConfig,
onActivity,
);
const agentRegistry = executor['toolRegistry'];
// The tool in the agent's registry should be the subagent's version
const tool = agentRegistry.getTool(toolName);
expect(tool).toBeDefined();
expect(tool?.description).toContain('Subagent Version');
expect(tool).not.toBe(globalTool);
// The global registry should remain unchanged
expect(parentToolRegistry.getTool(toolName)).toBe(globalTool);
expect(parentToolRegistry.getTool(toolName)?.description).toContain('Global Version');
});
});
});
+78 -3
View File
@@ -16,7 +16,13 @@ import type {
Schema,
} from '@google/genai';
import { ToolRegistry } from '../tools/tool-registry.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import {
DiscoveredMCPTool,
MCP_QUALIFIED_NAME_SEPARATOR,
} from '../tools/mcp-tool.js';
import { PolicyEngine } from '../policy/policy-engine.js';
import { createPolicyEngineConfig } from '../policy/config.js';
import { McpClientManager } from '../tools/mcp-client-manager.js';
import { CompressionStatus } from '../core/turn.js';
import { type ToolCallRequestInfo } from '../scheduler/types.js';
import { ChatCompressionService } from '../services/chatCompressionService.js';
@@ -115,12 +121,81 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
runtimeContext: Config,
onActivity?: ActivityCallback,
): Promise<LocalAgentExecutor<TOutput>> {
const parentToolRegistry = runtimeContext.getToolRegistry();
// Create an isolated tool registry for this agent instance.
const agentToolRegistry = new ToolRegistry(
runtimeContext,
runtimeContext.getMessageBus(),
);
const parentToolRegistry = runtimeContext.getToolRegistry();
// Create a scoped configuration if subagent has private policies or MCP servers.
let agentContext = runtimeContext;
if (definition.policy || definition.mcpServers) {
const parentPolicyEngine = runtimeContext.getPolicyEngine();
// Admin policies have priority >= ADMIN_POLICY_TIER (5).
const adminRules = parentPolicyEngine
.getRules()
.filter((r) => (r.priority ?? 0) >= 5);
const adminCheckers = parentPolicyEngine
.getCheckers()
.filter((c) => (c.priority ?? 0) >= 5);
const policyConfig = definition.policy
? await createPolicyEngineConfig(
definition.policy,
runtimeContext.getApprovalMode(),
undefined, // defaultPoliciesDir
adminRules,
adminCheckers,
)
: {
rules: adminRules,
checkers: adminCheckers,
approvalMode: runtimeContext.getApprovalMode(),
};
const scopedPolicyEngine = new PolicyEngine(
policyConfig,
runtimeContext.getCheckerRunner(),
);
// Populate scoped registry with parent's tools (including built-ins).
// If a subagent has private MCP servers, tools from those servers will
// be registered during discovery and will overwrite any global tools
// with the same name in this isolated registry.
for (const tool of parentToolRegistry.getAllKnownTools()) {
agentToolRegistry.registerTool(tool);
}
let scopedMcpManager: McpClientManager | undefined;
if (definition.mcpServers) {
scopedMcpManager = new McpClientManager(
runtimeContext.getClientVersion(),
agentToolRegistry,
runtimeContext, // Use parent context for some services, but we will scope it soon
);
}
agentContext = runtimeContext.createScopedConfig({
policyEngine: scopedPolicyEngine,
toolRegistry: agentToolRegistry,
mcpClientManager: scopedMcpManager,
});
// Update registry and mcp manager to use the scoped context
agentToolRegistry.setConfig(agentContext);
if (scopedMcpManager) {
scopedMcpManager.setConfig(agentContext);
// Discover and register subagent-specific MCP tools
for (const [name, config] of Object.entries(definition.mcpServers!)) {
await scopedMcpManager.maybeDiscoverMcpServer(name, config);
}
}
}
const allAgentNames = new Set(
runtimeContext.getAgentRegistry().getAllAgentNames(),
);
@@ -182,7 +257,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
return new LocalAgentExecutor(
definition,
runtimeContext,
agentContext,
agentToolRegistry,
parentPromptId,
parentCallId,
+12
View File
@@ -14,6 +14,8 @@ import { type z } from 'zod';
import type { ModelConfig } from '../services/modelConfigService.js';
import type { AnySchema } from 'ajv';
import type { A2AAuthConfig } from './auth-provider/types.js';
import type { PolicySettings } from '../policy/types.js';
import type { MCPServerConfig } from '../config/config.js';
/**
* Describes the possible termination modes for an agent.
@@ -130,6 +132,16 @@ export interface LocalAgentDefinition<
// Optional configs
toolConfig?: ToolConfig;
/**
* Scoped policy settings for this agent.
*/
policy?: PolicySettings;
/**
* MCP servers private to this agent.
*/
mcpServers?: Record<string, MCPServerConfig>;
/**
* An optional function to process the raw output from the agent's final tool
* call into a string format.
+65 -6
View File
@@ -424,6 +424,12 @@ export class MCPServerConfig {
readonly targetAudience?: string,
/* targetServiceAccount format: <service-account-name>@<project-num>.iam.gserviceaccount.com */
readonly targetServiceAccount?: string,
/**
* Visibility of the MCP server.
* 'public' (default): available to all agents.
* 'private': hidden from the primary agent, available only to specific subagents.
*/
readonly visibility?: 'public' | 'private',
) {}
}
@@ -588,7 +594,9 @@ export interface ConfigParameters {
export class Config implements McpContext {
private toolRegistry!: ToolRegistry;
private _scopedToolRegistry?: ToolRegistry;
private mcpClientManager?: McpClientManager;
private _scopedMcpClientManager?: McpClientManager;
private allowedMcpServers: string[];
private blockedMcpServers: string[];
private allowedEnvironmentVariables: string[];
@@ -728,6 +736,8 @@ export class Config implements McpContext {
private readonly useWriteTodos: boolean;
private readonly messageBus: MessageBus;
private readonly policyEngine: PolicyEngine;
private readonly checkerRunner: CheckerRunner;
private _scopedPolicyEngine?: PolicyEngine;
private policyUpdateConfirmationRequest:
| PolicyUpdateConfirmationRequest
| undefined;
@@ -964,6 +974,7 @@ export class Config implements McpContext {
checkersPath,
timeout: 30000, // 30 seconds to allow for LLM-based checkers
});
this.checkerRunner = checkerRunner;
this.policyUpdateConfirmationRequest =
params.policyUpdateConfirmationRequest;
@@ -1325,10 +1336,18 @@ export class Config implements McpContext {
return this.sessionId;
}
getClientVersion(): string {
return this.clientVersion;
}
setSessionId(sessionId: string): void {
this.sessionId = sessionId;
}
getCheckerRunner(): CheckerRunner | undefined {
return this.checkerRunner;
}
setTerminalBackground(terminalBackground: string | undefined): void {
this.terminalBackground = terminalBackground;
}
@@ -1578,7 +1597,15 @@ export class Config implements McpContext {
}
getToolRegistry(): ToolRegistry {
return this.toolRegistry;
return this._scopedToolRegistry ?? this.toolRegistry;
}
getMcpClientManager(): McpClientManager | undefined {
return this._scopedMcpClientManager ?? this.mcpClientManager;
}
setMcpClientManager(manager: McpClientManager): void {
this.mcpClientManager = manager;
}
getPromptRegistry(): PromptRegistry {
@@ -1734,7 +1761,7 @@ export class Config implements McpContext {
}
}
const policyExclusions = this.policyEngine.getExcludedTools(
const policyExclusions = this.getPolicyEngine().getExcludedTools(
toolMetadata,
allToolNames,
);
@@ -1778,9 +1805,6 @@ export class Config implements McpContext {
return this.extensionsEnabled;
}
getMcpClientManager(): McpClientManager | undefined {
return this.mcpClientManager;
}
setUserInteractedWithMcp(): void {
this.mcpClientManager?.setUserInteractedWithMcp();
@@ -2669,7 +2693,7 @@ export class Config implements McpContext {
}
getPolicyEngine(): PolicyEngine {
return this.policyEngine;
return this._scopedPolicyEngine ?? this.policyEngine;
}
getEnableHooks(): boolean {
@@ -2960,6 +2984,41 @@ export class Config implements McpContext {
}
};
/**
* Creates a scoped copy of this configuration with overrides for policy and tools.
* Scoped configs are used by subagents to maintain isolation from the primary agent.
* This uses prototype-based shadowing for overrides while maintaining access to global state.
*/
createScopedConfig(overrides: {
policyEngine?: PolicyEngine;
toolRegistry?: ToolRegistry;
mcpClientManager?: McpClientManager;
}): Config {
const scoped = Object.create(this) as unknown as Config;
// Define properties explicitly to ensure they shadow the base class properties
Object.defineProperties(scoped, {
_scopedPolicyEngine: {
value: overrides.policyEngine,
writable: true,
configurable: true,
enumerable: true,
},
_scopedToolRegistry: {
value: overrides.toolRegistry,
writable: true,
configurable: true,
enumerable: true,
},
_scopedMcpClientManager: {
value: overrides.mcpClientManager,
writable: true,
configurable: true,
enumerable: true,
},
});
return scoped;
}
/**
* Disposes of resources and removes event listeners.
*/
+4 -2
View File
@@ -250,6 +250,8 @@ export async function createPolicyEngineConfig(
settings: PolicySettings,
approvalMode: ApprovalMode,
defaultPoliciesDir?: string,
baseRules: PolicyRule[] = [],
baseCheckers: SafetyCheckerRule[] = [],
): Promise<PolicyEngineConfig> {
const policyDirs = getPolicyDirectories(
defaultPoliciesDir,
@@ -301,8 +303,8 @@ export async function createPolicyEngineConfig(
}
}
const rules: PolicyRule[] = [...tomlRules];
const checkers = [...tomlCheckers];
const rules: PolicyRule[] = [...baseRules, ...tomlRules];
const checkers = [...baseCheckers, ...tomlCheckers];
// Priority system for policy rules:
+66
View File
@@ -0,0 +1,66 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { z } from 'zod';
/**
* Zod schema for MCPServerConfig.
*/
export const MCPServerConfigSchema = z.object({
command: z.string().optional(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
trust: z.boolean().optional(),
alwaysAllowTools: z.array(z.string()).optional(),
includeTools: z.array(z.string()).optional(),
excludeTools: z.array(z.string()).optional(),
targetAudience: z.string().optional(),
targetServiceAccount: z.string().optional(),
visibility: z.enum(['public', 'private']).optional(),
});
/**
* Zod schema for PolicySettings.
*/
export const PolicySettingsSchema = z.preprocess(
(val) => {
if (typeof val === 'object' && val !== null) {
const v = val as Record<string, unknown>;
// Map snake_case to camelCase
if (v.policy_paths && !v.policyPaths) {
v.policyPaths = v.policy_paths;
}
if (v.workspace_policies_dir && !v.workspacePoliciesDir) {
v.workspacePoliciesDir = v.workspace_policies_dir;
}
if (v.mcp_servers && !v.mcpServers) {
v.mcpServers = v.mcp_servers;
}
}
return val;
},
z.object({
mcp: z
.object({
excluded: z.array(z.string()).optional(),
allowed: z.array(z.string()).optional(),
})
.optional(),
tools: z
.object({
exclude: z.array(z.string()).optional(),
allowed: z.array(z.string()).optional(),
})
.optional(),
mcpServers: z
.record(z.object({ trust: z.boolean().optional() }))
.optional(),
policyPaths: z.array(z.string()).optional(),
workspacePoliciesDir: z.string().optional(),
}),
);
export const MCPServersConfigSchema = z.record(MCPServerConfigSchema);
@@ -76,6 +76,14 @@ export class McpClientManager {
this.eventEmitter = eventEmitter;
}
/**
* Updates the configuration used by the MCP client manager.
* This is used when creating scoped managers for subagents.
*/
setConfig(config: Config): void {
this.cliConfig = config;
}
setUserInteractedWithMcp() {
this.userInteractedWithMcp = true;
}
+15
View File
@@ -206,6 +206,14 @@ export class ToolRegistry {
this.messageBus = messageBus;
}
/**
* Updates the configuration used by the tool registry.
* This is used when creating scoped registries for subagents.
*/
setConfig(config: Config): void {
this.config = config;
}
getMessageBus(): MessageBus {
return this.messageBus;
}
@@ -232,6 +240,13 @@ export class ToolRegistry {
this.allKnownTools.set(tool.name, tool);
}
/**
* Returns all known tools, including inactive ones.
*/
getAllKnownTools(): AnyDeclarativeTool[] {
return Array.from(this.allKnownTools.values());
}
/**
* Unregisters a tool definition by name.
*
+125
View File
@@ -0,0 +1,125 @@
# Technical Specification: Subagent Policy Isolation and Unique Toolsets
## Overview
This specification details the implementation of independent policies and unique, isolated toolsets for subagents. Currently, subagents inherit the primary agent's policy and toolset, which limits isolation and security. The proposed mechanism allows subagents to have private tools (built-in and MCP) that are hidden from the primary agent and subject to their own scoped policies.
## 1. YAML Frontmatter Schema
The subagent Markdown configuration (`.md` files) will be updated to support a `policy` block and an `mcp_servers` block in the YAML frontmatter.
### Schema Definition
```yaml
---
name: security-specialist
display_name: Security Specialist
description: Expert in vulnerability scanning and security audits.
# Explicitly listed tools available to the subagent
tools:
- builtin:read_file
- mcp:security-scanner:scan_vulnerabilities
# Scoped policy for the subagent
policy:
tools:
allowed:
- "builtin:read_file"
- "mcp:security-scanner:*"
exclude:
- "builtin:shell"
- "builtin:write_file"
mcp:
allowed:
- "security-scanner"
excluded:
- "*"
# Trust configuration for MCP servers
mcp_servers:
security-scanner:
trust: true
# MCP servers private to this subagent
mcp_servers:
security-scanner:
command: "npx"
args: ["@security/mcp-server"]
env:
API_KEY: "${SECURITY_API_KEY}"
---
```
## 2. Orchestration and Scoping Logic
The isolation is enforced by creating a scoped execution context for each subagent.
### Scoped Config and Policy Engine
1. **`LocalAgentExecutor.create`**: When a subagent is instantiated, it will no longer share the global `Config` directly.
2. **Config Scoping**: A new method `Config.createScopedConfig()` will be implemented to create a lightweight fork of the configuration.
3. **Policy Isolation**: If the subagent definition includes a `policy` block, a new `PolicyEngine` instance will be created using these settings. This engine will be used by the subagent's `ToolRegistry`.
4. **Tool Registry Scoping**: The subagent's `ToolRegistry` will be initialized with the scoped `PolicyEngine`. It will prioritize tools explicitly defined in the subagent's `tools` list and `mcp_servers` block.
## 3. Extension Authors and Private MCP Tools
Extension authors can now define MCP servers that are only available to specific agents within that extension, hiding them from the primary agent.
### `gemini-extension.json` Updates
MCP servers in extensions can now specify a `visibility` field.
```json
{
"name": "security-extension",
"mcpServers": {
"private-audit-tool": {
"command": "...",
"visibility": "private"
}
},
"agents": [
{
"name": "auditor",
"path": "agents/auditor.md"
}
]
}
```
- **`visibility: "public"` (default)**: Registered globally and available to the primary agent.
- **`visibility: "private"`**: Not registered globally. The `ExtensionManager` will attach these server configurations to the agents loaded from the same extension.
## 4. Component Updates
### Agent Registry (`packages/core/src/agents/registry.ts`)
- Update `AgentDefinition` and its internal storage to hold the scoped `policy` and private `mcpServers`.
- Ensure `registerAgent` handles the ingestion of these new fields.
### Tool Registry / Dispatcher (`packages/core/src/tools/tool-registry.ts`)
- The registry will now correctly filter tools based on the scoped `PolicyEngine` provided during construction.
- It will support dynamic registration of subagent-specific MCP servers without affecting the global registry.
### Configuration Parser (`packages/core/src/agents/agentLoader.ts`)
- Update `FrontmatterLocalAgentDefinition` and `localAgentSchema` (Zod) to include the `policy` and `mcp_servers` fields.
- Update `markdownToAgentDefinition` to map these fields to the internal `AgentDefinition`.
## 5. Runtime Enforcement
Boundaries are enforced at multiple levels:
1. **Discovery Boundary**: The primary agent's `ToolRegistry` never sees "private" or "internal" MCP tools.
2. **Policy Boundary**: The subagent's `PolicyEngine` independently evaluates every tool call against the subagent's specific `allow`/`exclude` rules.
3. **Execution Boundary**: `LocalAgentExecutor` validates all incoming `function_call` requests against the subagent's scoped `ToolRegistry` before scheduling execution.
---
## Implementation Roadmap
### Phase 1: Core Types and Parsing
- [ ] Update `AgentDefinition` and `LocalAgentDefinition` types in `core/agents/types.ts`.
- [ ] Update Zod schemas and parsing logic in `core/agents/agentLoader.ts`.
- [ ] Add `visibility` field to `MCPServerConfig` in `core/config/config.ts`.
### Phase 2: Configuration Scoping
- [ ] Implement `Config.createScopedConfig()` in `core/config/config.ts`.
- [ ] Update `PolicyEngine` to support initialization from `PolicySettings`.
### Phase 3: Isolated Execution
- [ ] Modify `LocalAgentExecutor.create` to use scoped config and policy.
- [ ] Implement logic to start and register subagent-specific MCP servers in the isolated `ToolRegistry`.
- [ ] Update `ExtensionManager` in `cli/config/extension-manager.ts` to handle private MCP server visibility.
### Phase 4: Validation and Testing
- [ ] Add unit tests for scoped policy enforcement.
- [ ] Add integration tests for subagent-specific MCP tools.
- [ ] Verify that private tools are not leaked to the primary agent's `ToolRegistry`.