Merge branch 'main' into refactor/task-to-agent-tool

This commit is contained in:
tanzhenxin 2026-03-20 10:23:25 +08:00
commit 9d6aca8efe
160 changed files with 24692 additions and 1942 deletions

View file

@ -70,6 +70,7 @@ import { ideContextStore } from '../ide/ideContext.js';
import { InputFormat, OutputFormat } from '../output/types.js';
import { PromptRegistry } from '../prompts/prompt-registry.js';
import { SkillManager } from '../skills/skill-manager.js';
import { PermissionManager } from '../permissions/permission-manager.js';
import { SubagentManager } from '../subagents/subagent-manager.js';
import type { SubagentConfig } from '../subagents/types.js';
import {
@ -93,13 +94,19 @@ import {
type HookExecutionRequest,
type HookExecutionResponse,
} from '../confirmation-bus/types.js';
import {
PermissionMode,
NotificationType,
type PermissionSuggestion,
} from '../hooks/types.js';
import { fireNotificationHook } from '../core/toolHookTriggers.js';
// Utils
import { shouldAttemptBrowserLaunch } from '../utils/browser.js';
import { FileExclusions } from '../utils/ignorePatterns.js';
import { shouldDefaultToNodePty } from '../utils/shell-utils.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import { isToolEnabled, type ToolName } from '../utils/tool-utils.js';
import { type ToolName } from '../utils/tool-utils.js';
import { getErrorMessage } from '../utils/errors.js';
// Local config modules
@ -320,6 +327,12 @@ export interface ConfigParameters {
coreTools?: string[];
allowedTools?: string[];
excludeTools?: string[];
/** Merged permission rules from all sources (settings + CLI args). */
permissions?: {
allow?: string[];
ask?: string[];
deny?: string[];
};
toolDiscoveryCommand?: string;
toolCallCommand?: string;
mcpServerCommand?: string;
@ -413,6 +426,20 @@ export interface ConfigParameters {
hooksConfig?: Record<string, unknown>;
/** Warnings generated during configuration resolution */
warnings?: string[];
/**
* Callback for persisting a permission rule to settings.
* Injected by the CLI layer; core uses this to write allow/ask/deny rules
* to project or user settings when the user clicks "Always Allow".
*
* @param scope - 'project' for workspace settings, 'user' for user settings.
* @param ruleType - 'allow' | 'ask' | 'deny'.
* @param rule - The raw rule string, e.g. "Bash(git *)" or "Edit".
*/
onPersistPermissionRule?: (
scope: 'project' | 'user',
ruleType: 'allow' | 'ask' | 'deny',
rule: string,
) => Promise<void>;
}
function normalizeConfigOutputFormat(
@ -454,6 +481,7 @@ export class Config {
private subagentManager!: SubagentManager;
private extensionManager!: ExtensionManager;
private skillManager: SkillManager | null = null;
private permissionManager: PermissionManager | null = null;
private fileSystemService: FileSystemService;
private contentGeneratorConfig!: ContentGeneratorConfig;
private contentGeneratorConfigSources: ContentGeneratorConfigSources = {};
@ -475,6 +503,9 @@ export class Config {
private readonly coreTools: string[] | undefined;
private readonly allowedTools: string[] | undefined;
private readonly excludeTools: string[] | undefined;
private readonly permissionsAllow: string[];
private readonly permissionsAsk: string[];
private readonly permissionsDeny: string[];
private readonly toolDiscoveryCommand: string | undefined;
private readonly toolCallCommand: string | undefined;
private readonly mcpServerCommand: string | undefined;
@ -549,6 +580,11 @@ export class Config {
private readonly skipLoopDetection: boolean;
private readonly skipStartupContext: boolean;
private readonly warnings: string[];
private readonly onPersistPermissionRuleCallback?: (
scope: 'project' | 'user',
ruleType: 'allow' | 'ask' | 'deny',
rule: string,
) => Promise<void>;
private initialized: boolean = false;
readonly storage: Storage;
private readonly fileExclusions: FileExclusions;
@ -589,6 +625,9 @@ export class Config {
this.coreTools = params.coreTools;
this.allowedTools = params.allowedTools;
this.excludeTools = params.excludeTools;
this.permissionsAllow = params.permissions?.allow || [];
this.permissionsAsk = params.permissions?.ask || [];
this.permissionsDeny = params.permissions?.deny || [];
this.toolDiscoveryCommand = params.toolDiscoveryCommand;
this.toolCallCommand = params.toolCallCommand;
this.mcpServerCommand = params.mcpServerCommand;
@ -656,6 +695,7 @@ export class Config {
this.skipLoopDetection = params.skipLoopDetection ?? false;
this.skipStartupContext = params.skipStartupContext ?? false;
this.warnings = params.warnings ?? [];
this.onPersistPermissionRuleCallback = params.onPersistPermissionRule;
// Web search
this.webSearch = params.webSearch;
@ -785,6 +825,73 @@ export class Config {
(input['last_assistant_message'] as string) || '',
);
break;
case 'PreToolUse': {
result = await hookSystem.firePreToolUseEvent(
(input['tool_name'] as string) || '',
(input['tool_input'] as Record<string, unknown>) || {},
(input['tool_use_id'] as string) || '',
(input['permission_mode'] as PermissionMode | undefined) ??
PermissionMode.Default,
);
break;
}
case 'PostToolUse':
result = await hookSystem.firePostToolUseEvent(
(input['tool_name'] as string) || '',
(input['tool_input'] as Record<string, unknown>) || {},
(input['tool_response'] as Record<string, unknown>) || {},
(input['tool_use_id'] as string) || '',
(input['permission_mode'] as PermissionMode) || 'default',
);
break;
case 'PostToolUseFailure':
result = await hookSystem.firePostToolUseFailureEvent(
(input['tool_use_id'] as string) || '',
(input['tool_name'] as string) || '',
(input['tool_input'] as Record<string, unknown>) || {},
(input['error'] as string) || '',
input['is_interrupt'] as boolean | undefined,
(input['permission_mode'] as PermissionMode) || 'default',
);
break;
case 'Notification':
result = await hookSystem.fireNotificationEvent(
(input['message'] as string) || '',
(input['notification_type'] as NotificationType) ||
'permission_prompt',
(input['title'] as string) || undefined,
);
break;
case 'PermissionRequest':
result = await hookSystem.firePermissionRequestEvent(
(input['tool_name'] as string) || '',
(input['tool_input'] as Record<string, unknown>) || {},
(input['permission_mode'] as PermissionMode) ||
PermissionMode.Default,
(input['permission_suggestions'] as
| PermissionSuggestion[]
| undefined) || undefined,
);
break;
case 'SubagentStart':
result = await hookSystem.fireSubagentStartEvent(
(input['agent_id'] as string) || '',
(input['agent_type'] as string) || '',
(input['permission_mode'] as PermissionMode) ||
PermissionMode.Default,
);
break;
case 'SubagentStop':
result = await hookSystem.fireSubagentStopEvent(
(input['agent_id'] as string) || '',
(input['agent_type'] as string) || '',
(input['agent_transcript_path'] as string) || '',
(input['last_assistant_message'] as string) || '',
(input['stop_hook_active'] as boolean) || false,
(input['permission_mode'] as PermissionMode) ||
PermissionMode.Default,
);
break;
default:
this.debugLogger.warn(
`Unknown hook event: ${request.eventName}`,
@ -812,6 +919,8 @@ export class Config {
);
this.debugLogger.debug('MessageBus initialized with hook subscription');
} else {
this.debugLogger.debug('Hook system disabled, skipping initialization');
}
this.subagentManager = new SubagentManager(this);
@ -819,6 +928,10 @@ export class Config {
await this.skillManager.startWatching();
this.debugLogger.debug('Skill manager initialized');
this.permissionManager = new PermissionManager(this);
this.permissionManager.initialize();
this.debugLogger.debug('Permission manager initialized');
// Load session subagents if they were provided before initialization
if (this.sessionSubagents.length > 0) {
this.subagentManager.loadSessionSubagents(this.sessionSubagents);
@ -935,6 +1048,21 @@ export class Config {
// Initialize BaseLlmClient now that the ContentGenerator is available
this.baseLlmClient = new BaseLlmClient(this.contentGenerator, this);
// Fire auth_success notification hook (supports both interactive & non-interactive)
const messageBus = this.getMessageBus();
const hooksEnabled = this.getEnableHooks();
if (hooksEnabled && messageBus) {
fireNotificationHook(
messageBus,
`Successfully authenticated with ${authMethod}`,
NotificationType.AuthSuccess,
'Authentication successful',
).catch(() => {
// Silently ignore errors - fireNotificationHook has internal error handling
// and notification hooks should not block the auth flow
});
}
}
/**
@ -1191,6 +1319,10 @@ export class Config {
return this.targetDir;
}
getCwd(): string {
return this.targetDir;
}
getWorkspaceContext(): WorkspaceContext {
return this.workspaceContext;
}
@ -1243,16 +1375,60 @@ export class Config {
return this.appendSystemPrompt;
}
/** @deprecated Use getPermissionsAllow() instead. */
getCoreTools(): string[] | undefined {
return this.coreTools;
}
getAllowedTools(): string[] | undefined {
return this.allowedTools;
/**
* Returns the merged allow-rules for PermissionManager.
*
* This merges all sources so that PermissionManager receives a single,
* authoritative list:
* - settings.permissions.allow (persistent rules from all scopes)
* - allowedTools param (SDK / argv auto-approve list)
*
* Note: coreTools is intentionally excluded here it has whitelist semantics
* (only listed tools are registered), not auto-approve semantics. It is
* handled separately via PermissionManager.coreToolsAllowList.
*
* CLI callers (loadCliConfig) already pre-merge argv into permissionsAllow
* before constructing Config, so those fields will be empty for CLI usage.
* SDK callers construct Config directly and rely on allowedTools.
*/
getPermissionsAllow(): string[] {
const base = this.permissionsAllow ?? [];
const sdkAllow = [...(this.allowedTools ?? [])];
if (sdkAllow.length === 0) return base.length > 0 ? base : [];
const merged = [...base];
for (const t of sdkAllow) {
if (t && !merged.includes(t)) merged.push(t);
}
return merged;
}
getExcludeTools(): string[] | undefined {
return this.excludeTools;
getPermissionsAsk(): string[] {
return this.permissionsAsk;
}
/**
* Returns the merged deny-rules for PermissionManager.
*
* Merges:
* - settings.permissions.deny (persistent rules from all scopes)
* - excludeTools param (SDK / argv blocklist)
*
* CLI callers pre-merge argv.excludeTools into permissionsDeny.
*/
getPermissionsDeny(): string[] {
const base = this.permissionsDeny ?? [];
const sdkDeny = this.excludeTools ?? [];
if (sdkDeny.length === 0) return base.length > 0 ? base : [];
const merged = [...base];
for (const t of sdkDeny) {
if (t && !merged.includes(t)) merged.push(t);
}
return merged;
}
getToolDiscoveryCommand(): string | undefined {
@ -1884,6 +2060,24 @@ export class Config {
return this.skillManager;
}
getPermissionManager(): PermissionManager | null {
return this.permissionManager;
}
/**
* Returns the callback for persisting permission rules to settings files.
* Returns undefined if no callback was provided (e.g. SDK mode).
*/
getOnPersistPermissionRule():
| ((
scope: 'project' | 'user',
ruleType: 'allow' | 'ask' | 'deny',
rule: string,
) => Promise<void>)
| undefined {
return this.onPersistPermissionRuleCallback;
}
async createToolRegistry(
sendSdkMcpMessage?: SendSdkMcpMessage,
options?: { skipDiscovery?: boolean },
@ -1894,9 +2088,6 @@ export class Config {
sendSdkMcpMessage,
);
const coreToolsConfig = this.getCoreTools();
const excludeToolsConfig = this.getExcludeTools();
// Helper to create & register core tools that are enabled
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registerCoreTool = (ToolClass: any, ...args: unknown[]) => {
@ -1912,7 +2103,13 @@ export class Config {
return;
}
if (isToolEnabled(toolName, coreToolsConfig, excludeToolsConfig)) {
// PermissionManager handles both the coreTools allowlist (registry-level)
// and deny rules (runtime-level) in a single check.
const pmEnabled = this.permissionManager
? this.permissionManager.isToolEnabled(toolName)
: true; // Should never reach here after initialize(), but safe default.
if (pmEnabled) {
try {
registry.registerTool(new ToolClass(...args));
} catch (error) {