diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts index 3aaf0375..01ade407 100644 --- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts +++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts @@ -13,11 +13,12 @@ import type { ServerGeminiStreamEvent, TaskResultDisplay, } from '@qwen-code/qwen-code-core'; -import { GeminiEventType } from '@qwen-code/qwen-code-core'; +import { GeminiEventType, ToolErrorType } from '@qwen-code/qwen-code-core'; import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai'; import type { CLIAssistantMessage, CLIMessage, + CLIPermissionDenial, CLIResultMessage, CLIResultMessageError, CLIResultMessageSuccess, @@ -124,6 +125,9 @@ export abstract class BaseJsonOutputAdapter { // Last assistant message for result generation protected lastAssistantMessage: CLIAssistantMessage | null = null; + // Track permission denials (execution denied tool calls) + protected permissionDenials: CLIPermissionDenial[] = []; + constructor(config: Config) { this.config = config; this.mainAgentMessageState = this.createMessageState(); @@ -936,6 +940,7 @@ export abstract class BaseJsonOutputAdapter { /** * Emits a tool result message. + * Collects execution denied tool calls for inclusion in result messages. * @param request - Tool call request info * @param response - Tool call response info * @param parentToolUseId - Parent tool use ID (null for main agent) @@ -945,6 +950,19 @@ export abstract class BaseJsonOutputAdapter { response: ToolCallResponseInfo, parentToolUseId: string | null = null, ): void { + // Track permission denials (execution denied errors) + if ( + response.error && + response.errorType === ToolErrorType.EXECUTION_DENIED + ) { + const denial: CLIPermissionDenial = { + tool_name: request.name, + tool_use_id: request.callId, + tool_input: request.args, + }; + this.permissionDenials.push(denial); + } + const block: ToolResultBlock = { type: 'tool_result', tool_use_id: request.callId, @@ -988,6 +1006,7 @@ export abstract class BaseJsonOutputAdapter { /** * Builds a result message from options. * Helper method used by both emitResult implementations. + * Includes permission denials collected from execution denied tool calls. * @param options - Result options * @param lastAssistantMessage - Last assistant message for text extraction * @returns CLIResultMessage @@ -1020,7 +1039,7 @@ export abstract class BaseJsonOutputAdapter { duration_api_ms: options.apiDurationMs, num_turns: options.numTurns, usage, - permission_denials: [], + permission_denials: [...this.permissionDenials], error: { message: errorMessage }, }; } else { @@ -1036,7 +1055,7 @@ export abstract class BaseJsonOutputAdapter { num_turns: options.numTurns, result: resultText, usage, - permission_denials: [], + permission_denials: [...this.permissionDenials], }; if (options.stats) { @@ -1050,6 +1069,8 @@ export abstract class BaseJsonOutputAdapter { /** * Builds a subagent error result message. * Helper method used by both emitSubagentErrorResult implementations. + * Note: Subagent permission denials are not included here as they are tracked + * separately and would be included in the main agent's result message. * @param errorMessage - Error message * @param numTurns - Number of turns * @returns CLIResultMessageError @@ -1108,6 +1129,9 @@ export function partsToString(parts: Part[]): string { export function toolResultContent( response: ToolCallResponseInfo, ): string | undefined { + if (response.error) { + return response.error.message; + } if ( typeof response.resultDisplay === 'string' && response.resultDisplay.trim().length > 0 @@ -1119,9 +1143,6 @@ export function toolResultContent( // functionResponse parts that contain output content return functionResponsePartsToString(response.responseParts); } - if (response.error) { - return response.error.message; - } return undefined; } diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index 2fc35b0a..9ab57f2c 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -696,7 +696,7 @@ export class CoreToolScheduler { response: createErrorResponse( reqInfo, new Error(permissionErrorMessage), - ToolErrorType.TOOL_NOT_REGISTERED, + ToolErrorType.EXECUTION_DENIED, ), durationMs: 0, }; @@ -811,6 +811,32 @@ export class CoreToolScheduler { ); this.setStatusInternal(reqInfo.callId, 'scheduled'); } else { + /** + * In non-interactive mode where no user will respond to approval prompts, + * and not running as IDE companion or Zed integration, automatically deny approval. + * This is intended to create an explicit denial of the tool call, + * rather than silently waiting for approval and hanging forever. + */ + const shouldAutoDeny = + !this.config.isInteractive() && + !this.config.getIdeMode() && + !this.config.getExperimentalZedIntegration(); + + if (shouldAutoDeny) { + // Treat as execution denied error, similar to excluded tools + const errorMessage = `Qwen Code requires permission to use "${reqInfo.name}", but that permission was declined.`; + this.setStatusInternal( + reqInfo.callId, + 'error', + createErrorResponse( + reqInfo, + new Error(errorMessage), + ToolErrorType.EXECUTION_DENIED, + ), + ); + continue; + } + // Allow IDE to resolve confirmation if ( confirmationDetails.type === 'edit' && diff --git a/packages/core/src/tools/tool-error.ts b/packages/core/src/tools/tool-error.ts index 3c1c9a8a..27dc4285 100644 --- a/packages/core/src/tools/tool-error.ts +++ b/packages/core/src/tools/tool-error.ts @@ -14,6 +14,8 @@ export enum ToolErrorType { UNHANDLED_EXCEPTION = 'unhandled_exception', TOOL_NOT_REGISTERED = 'tool_not_registered', EXECUTION_FAILED = 'execution_failed', + // Try to execute a tool that is excluded due to the approval mode + EXECUTION_DENIED = 'execution_denied', // File System Errors FILE_NOT_FOUND = 'file_not_found',