Compare commits

...

9 Commits

Author SHA1 Message Date
AK e5dd9f321d Merge branch 'main' into akkr/subagents-policy 2026-03-06 08:46:14 -08:00
Akhilesh Kumar 456dadca85 feat(core): move subagent policy to declarative TOML and add tool annotations 2026-03-05 22:32:31 +00:00
AK dea509cca3 Merge branch 'main' into akkr/subagents-policy 2026-03-05 10:41:42 -08:00
Akhilesh Kumar 2c11a9263a fix: resolve preflight test timeouts and warnings
Mock shouldPromptForTerminalSetup in test environments to prevent action-required UI interruptions. Remove unnecessary dependency in useGeminiStream hook.
2026-03-05 18:37:16 +00:00
Akhilesh Kumar 058c93b9e3 fix: resolve PR comment by removing unsafe ApprovalMode type assertion 2026-03-05 18:27:59 +00:00
AK 5b19819529 Merge branch 'main' into akkr/subagents-policy 2026-03-05 10:15:33 -08:00
AK e7cc399a4f Merge branch 'main' into akkr/subagents-policy 2026-03-04 12:19:11 -08:00
Akhilesh Kumar 37606d27a8 fix: default subagents to global approval mode instead of yolo 2026-03-04 18:41:18 +00:00
Akhilesh Kumar 26f8ff5a7d feat: add explicit policy override for subagents 2026-03-04 18:07:14 +00:00
14 changed files with 67 additions and 56 deletions
+5
View File
@@ -131,6 +131,11 @@ vi.mock('../ui/components/GeminiRespondingSpinner.js', async () => {
};
});
vi.mock('../ui/utils/terminalSetup.js', () => ({
shouldPromptForTerminalSetup: vi.fn().mockResolvedValue(false),
useTerminalSetupPrompt: vi.fn(),
}));
export interface AppRigOptions {
fakeResponsesPath?: string;
terminalWidth?: number;
@@ -203,6 +203,10 @@ vi.mock('../utils/events.js');
vi.mock('../utils/handleAutoUpdate.js');
vi.mock('./utils/ConsolePatcher.js');
vi.mock('../utils/cleanup.js');
vi.mock('./utils/terminalSetup.js', () => ({
shouldPromptForTerminalSetup: vi.fn().mockResolvedValue(false),
useTerminalSetupPrompt: vi.fn(),
}));
import { useHistory } from './hooks/useHistoryManager.js';
import { useThemeCommand } from './hooks/useThemeCommand.js';
@@ -31,6 +31,7 @@ describe('agent-scheduler', () => {
mockConfig = {
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getAgentsSettings: vi.fn().mockReturnValue({}),
} as unknown as Mocked<Config>;
});
@@ -47,6 +48,7 @@ describe('agent-scheduler', () => {
const options = {
schedulerId: 'subagent-1',
agentName: 'mock-agent',
parentCallId: 'parent-1',
toolRegistry: mockToolRegistry as unknown as ToolRegistry,
signal: new AbortController().signal,
@@ -19,6 +19,8 @@ import type { EditorType } from '../utils/editor.js';
export interface AgentSchedulingOptions {
/** The unique ID for this agent's scheduler. */
schedulerId: string;
/** The name of the agent executing the tools. */
agentName: string;
/** The ID of the tool call that invoked this agent. */
parentCallId?: string;
/** The tool registry specific to this agent. */
@@ -46,6 +48,7 @@ export async function scheduleAgentTools(
): Promise<CompletedToolCall[]> {
const {
schedulerId,
agentName,
parentCallId,
toolRegistry,
signal,
@@ -58,6 +61,16 @@ export async function scheduleAgentTools(
const agentConfig: Config = Object.create(config);
agentConfig.getToolRegistry = () => toolRegistry;
// Apply any subagent-specific approval mode override, fallback to the main config mode.
const overrideMode =
config.getAgentsSettings()?.overrides?.[agentName]?.approvalMode;
if (overrideMode) {
agentConfig.getApprovalMode = () => overrideMode;
} else {
// Subagents follow the global approval mode by default.
agentConfig.getApprovalMode = () => config.getApprovalMode();
}
const scheduler = new Scheduler({
config: agentConfig,
messageBus: config.getMessageBus(),
@@ -1073,6 +1073,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
toolRequests,
{
schedulerId: this.agentId,
agentName: this.definition.name,
parentCallId: this.parentCallId,
toolRegistry: this.toolRegistry,
signal,
-35
View File
@@ -23,7 +23,6 @@ import {
type ModelConfig,
ModelConfigService,
} from '../services/modelConfigService.js';
import { PolicyDecision, PRIORITY_SUBAGENT_TOOL } from '../policy/types.js';
/**
* Returns the model config alias for a given agent definition.
@@ -315,39 +314,6 @@ export class AgentRegistry {
this.agents.set(mergedDefinition.name, mergedDefinition);
this.registerModelConfigs(mergedDefinition);
this.addAgentPolicy(mergedDefinition);
}
private addAgentPolicy(definition: AgentDefinition<z.ZodTypeAny>): void {
const policyEngine = this.config.getPolicyEngine();
if (!policyEngine) {
return;
}
// If the user has explicitly defined a policy for this tool, respect it.
// ignoreDynamic=true means we only check for rules NOT added by this registry.
if (policyEngine.hasRuleForTool(definition.name, true)) {
if (this.config.getDebugMode()) {
debugLogger.log(
`[AgentRegistry] User policy exists for '${definition.name}', skipping dynamic registration.`,
);
}
return;
}
// Clean up any old dynamic policy for this tool (e.g. if we are overwriting an agent)
policyEngine.removeRulesForTool(definition.name, 'AgentRegistry (Dynamic)');
// Add the new dynamic policy
policyEngine.addRule({
toolName: definition.name,
decision:
definition.kind === 'local'
? PolicyDecision.ALLOW
: PolicyDecision.ASK_USER,
priority: PRIORITY_SUBAGENT_TOOL,
source: 'AgentRegistry (Dynamic)',
});
}
private isAgentEnabled<TOutput extends z.ZodTypeAny>(
@@ -461,7 +427,6 @@ export class AgentRegistry {
);
}
this.agents.set(definition.name, definition);
this.addAgentPolicy(definition);
} catch (e) {
const errorMessage = `Error loading A2A agent "${definition.name}": ${e instanceof Error ? e.message : String(e)}`;
debugLogger.warn(`[AgentRegistry] ${errorMessage}`, e);
@@ -52,6 +52,7 @@ export class SubagentTool extends BaseDeclarativeTool<AgentInputs, ToolResult> {
messageBus,
/* isOutputMarkdown */ true,
/* canUpdateOutput */ true,
/* toolAnnotations */ { isSubagent: true, agentKind: definition.kind },
);
}
+1
View File
@@ -234,6 +234,7 @@ export interface AgentOverride {
modelConfig?: ModelConfig;
runConfig?: AgentRunConfig;
enabled?: boolean;
approvalMode?: ApprovalMode;
}
export interface AgentSettings {
@@ -0,0 +1,11 @@
# Default policy for dynamically registered local subagents
[[rule]]
toolAnnotations = { isSubagent = true, agentKind = "local" }
decision = "allow"
priority = 50
# Default policy for dynamically registered remote subagents
[[rule]]
toolAnnotations = { isSubagent = true, agentKind = "remote" }
decision = "ask_user"
priority = 50
@@ -13,7 +13,6 @@ import {
type SafetyCheckerRule,
InProcessCheckerType,
ApprovalMode,
PRIORITY_SUBAGENT_TOOL,
} from './types.js';
import type { FunctionCall } from '@google/genai';
import { SafetyCheckDecision } from '../safety/protocol.js';
@@ -1595,7 +1594,7 @@ describe('PolicyEngine', () => {
{
toolName: 'unknown_subagent',
decision: PolicyDecision.ALLOW,
priority: PRIORITY_SUBAGENT_TOOL,
priority: 1.05,
},
];
+22 -14
View File
@@ -8,7 +8,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
PolicyDecision,
ApprovalMode,
PRIORITY_SUBAGENT_TOOL,
} from './types.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
@@ -911,12 +910,27 @@ priority = 100
it('should override default subagent rules when in Plan Mode for unknown subagents', async () => {
const planTomlPath = path.resolve(__dirname, 'policies', 'plan.toml');
const fileContent = await fs.readFile(planTomlPath, 'utf-8');
const planFileContent = await fs.readFile(planTomlPath, 'utf-8');
const subagentsTomlPath = path.resolve(
__dirname,
'policies',
'subagents.toml',
);
const subagentsFileContent = await fs.readFile(subagentsTomlPath, 'utf-8');
const tempPolicyDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'plan-policy-test-'),
);
try {
await fs.writeFile(path.join(tempPolicyDir, 'plan.toml'), fileContent);
await fs.writeFile(
path.join(tempPolicyDir, 'plan.toml'),
planFileContent,
);
await fs.writeFile(
path.join(tempPolicyDir, 'subagents.toml'),
subagentsFileContent,
);
const getPolicyTier = () => 1; // Default tier
// 1. Load the actual Plan Mode policies
@@ -932,20 +946,14 @@ priority = 100
});
// 3. Simulate an unknown Subagent being registered (Dynamic Rule)
engine.addRule({
toolName: 'unknown_subagent',
decision: PolicyDecision.ALLOW,
priority: PRIORITY_SUBAGENT_TOOL,
source: 'AgentRegistry (Dynamic)',
});
// Now handled by subagents.toml policy using toolAnnotations
const checkResult = await engine.check(
{ name: 'unknown_subagent' },
{ isSubagent: true, agentKind: 'local' },
);
// 4. Verify Behavior:
// The Plan Mode "Catch-All Deny" (from plan.toml) should override the Subagent Allow
const checkResult = await engine.check(
{ name: 'unknown_subagent' },
undefined,
);
expect(
checkResult.decision,
'Unknown subagent should be DENIED in Plan Mode',
-5
View File
@@ -301,8 +301,3 @@ export interface CheckResult {
rule?: PolicyRule;
}
/**
* Priority for subagent tools (registered dynamically).
* Effective priority matching Tier 1 (Default) read-only tools.
*/
export const PRIORITY_SUBAGENT_TOOL = 1.05;
+1
View File
@@ -254,6 +254,7 @@ export class GeminiCliSession {
toolCallsToSchedule,
{
schedulerId: sessionId,
agentName: 'sdk-agent',
toolRegistry: scopedRegistry,
signal: abortSignal,
},
+5
View File
@@ -2241,6 +2241,11 @@
"enabled": {
"type": "boolean",
"description": "Whether to enable the agent."
},
"approvalMode": {
"type": "string",
"description": "Approval mode for this subagent's internal tool calls.",
"enum": ["default", "yolo", "plan", "autoEdit"]
}
}
},