mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: implement permission denial tracking for tool calls
This commit is contained in:
@@ -13,11 +13,12 @@ import type {
|
|||||||
ServerGeminiStreamEvent,
|
ServerGeminiStreamEvent,
|
||||||
TaskResultDisplay,
|
TaskResultDisplay,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} 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 { Part, GenerateContentResponseUsageMetadata } from '@google/genai';
|
||||||
import type {
|
import type {
|
||||||
CLIAssistantMessage,
|
CLIAssistantMessage,
|
||||||
CLIMessage,
|
CLIMessage,
|
||||||
|
CLIPermissionDenial,
|
||||||
CLIResultMessage,
|
CLIResultMessage,
|
||||||
CLIResultMessageError,
|
CLIResultMessageError,
|
||||||
CLIResultMessageSuccess,
|
CLIResultMessageSuccess,
|
||||||
@@ -124,6 +125,9 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
// Last assistant message for result generation
|
// Last assistant message for result generation
|
||||||
protected lastAssistantMessage: CLIAssistantMessage | null = null;
|
protected lastAssistantMessage: CLIAssistantMessage | null = null;
|
||||||
|
|
||||||
|
// Track permission denials (execution denied tool calls)
|
||||||
|
protected permissionDenials: CLIPermissionDenial[] = [];
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor(config: Config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.mainAgentMessageState = this.createMessageState();
|
this.mainAgentMessageState = this.createMessageState();
|
||||||
@@ -936,6 +940,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits a tool result message.
|
* Emits a tool result message.
|
||||||
|
* Collects execution denied tool calls for inclusion in result messages.
|
||||||
* @param request - Tool call request info
|
* @param request - Tool call request info
|
||||||
* @param response - Tool call response info
|
* @param response - Tool call response info
|
||||||
* @param parentToolUseId - Parent tool use ID (null for main agent)
|
* @param parentToolUseId - Parent tool use ID (null for main agent)
|
||||||
@@ -945,6 +950,19 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
response: ToolCallResponseInfo,
|
response: ToolCallResponseInfo,
|
||||||
parentToolUseId: string | null = null,
|
parentToolUseId: string | null = null,
|
||||||
): void {
|
): 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 = {
|
const block: ToolResultBlock = {
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: request.callId,
|
tool_use_id: request.callId,
|
||||||
@@ -988,6 +1006,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
/**
|
/**
|
||||||
* Builds a result message from options.
|
* Builds a result message from options.
|
||||||
* Helper method used by both emitResult implementations.
|
* Helper method used by both emitResult implementations.
|
||||||
|
* Includes permission denials collected from execution denied tool calls.
|
||||||
* @param options - Result options
|
* @param options - Result options
|
||||||
* @param lastAssistantMessage - Last assistant message for text extraction
|
* @param lastAssistantMessage - Last assistant message for text extraction
|
||||||
* @returns CLIResultMessage
|
* @returns CLIResultMessage
|
||||||
@@ -1020,7 +1039,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
duration_api_ms: options.apiDurationMs,
|
duration_api_ms: options.apiDurationMs,
|
||||||
num_turns: options.numTurns,
|
num_turns: options.numTurns,
|
||||||
usage,
|
usage,
|
||||||
permission_denials: [],
|
permission_denials: [...this.permissionDenials],
|
||||||
error: { message: errorMessage },
|
error: { message: errorMessage },
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -1036,7 +1055,7 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
num_turns: options.numTurns,
|
num_turns: options.numTurns,
|
||||||
result: resultText,
|
result: resultText,
|
||||||
usage,
|
usage,
|
||||||
permission_denials: [],
|
permission_denials: [...this.permissionDenials],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.stats) {
|
if (options.stats) {
|
||||||
@@ -1050,6 +1069,8 @@ export abstract class BaseJsonOutputAdapter {
|
|||||||
/**
|
/**
|
||||||
* Builds a subagent error result message.
|
* Builds a subagent error result message.
|
||||||
* Helper method used by both emitSubagentErrorResult implementations.
|
* 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 errorMessage - Error message
|
||||||
* @param numTurns - Number of turns
|
* @param numTurns - Number of turns
|
||||||
* @returns CLIResultMessageError
|
* @returns CLIResultMessageError
|
||||||
@@ -1108,6 +1129,9 @@ export function partsToString(parts: Part[]): string {
|
|||||||
export function toolResultContent(
|
export function toolResultContent(
|
||||||
response: ToolCallResponseInfo,
|
response: ToolCallResponseInfo,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
|
if (response.error) {
|
||||||
|
return response.error.message;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
typeof response.resultDisplay === 'string' &&
|
typeof response.resultDisplay === 'string' &&
|
||||||
response.resultDisplay.trim().length > 0
|
response.resultDisplay.trim().length > 0
|
||||||
@@ -1119,9 +1143,6 @@ export function toolResultContent(
|
|||||||
// functionResponse parts that contain output content
|
// functionResponse parts that contain output content
|
||||||
return functionResponsePartsToString(response.responseParts);
|
return functionResponsePartsToString(response.responseParts);
|
||||||
}
|
}
|
||||||
if (response.error) {
|
|
||||||
return response.error.message;
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -696,7 +696,7 @@ export class CoreToolScheduler {
|
|||||||
response: createErrorResponse(
|
response: createErrorResponse(
|
||||||
reqInfo,
|
reqInfo,
|
||||||
new Error(permissionErrorMessage),
|
new Error(permissionErrorMessage),
|
||||||
ToolErrorType.TOOL_NOT_REGISTERED,
|
ToolErrorType.EXECUTION_DENIED,
|
||||||
),
|
),
|
||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
};
|
};
|
||||||
@@ -811,6 +811,32 @@ export class CoreToolScheduler {
|
|||||||
);
|
);
|
||||||
this.setStatusInternal(reqInfo.callId, 'scheduled');
|
this.setStatusInternal(reqInfo.callId, 'scheduled');
|
||||||
} else {
|
} 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
|
// Allow IDE to resolve confirmation
|
||||||
if (
|
if (
|
||||||
confirmationDetails.type === 'edit' &&
|
confirmationDetails.type === 'edit' &&
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export enum ToolErrorType {
|
|||||||
UNHANDLED_EXCEPTION = 'unhandled_exception',
|
UNHANDLED_EXCEPTION = 'unhandled_exception',
|
||||||
TOOL_NOT_REGISTERED = 'tool_not_registered',
|
TOOL_NOT_REGISTERED = 'tool_not_registered',
|
||||||
EXECUTION_FAILED = 'execution_failed',
|
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 System Errors
|
||||||
FILE_NOT_FOUND = 'file_not_found',
|
FILE_NOT_FOUND = 'file_not_found',
|
||||||
|
|||||||
Reference in New Issue
Block a user