mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 19:52:02 +00:00
Merge branch 'main' into refactor/task-to-agent-tool
This commit is contained in:
commit
9d6aca8efe
160 changed files with 24692 additions and 1942 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue