Skip to content

Commit 9b2ef80

Browse files
Move inlined permission handling into a concrete registry and add AskUserQuestion handling (#2901)
This is mostly just fleshing out code so it's easier to extension... but with: * A quick and dirty AskUserQuestion tool handling * localized some strings * Tests
1 parent 3f10fb3 commit 9b2ef80

20 files changed

+1107
-141
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type * as vscode from 'vscode';
7+
import { ClaudeToolInputMap, ClaudeToolNames } from './claudeTools';
8+
9+
/**
10+
* Result type for tool permission checks, matching the Claude SDK canUseTool return type
11+
*/
12+
export type ClaudeToolPermissionResult =
13+
| { behavior: 'allow'; updatedInput: Record<string, unknown> }
14+
| { behavior: 'deny'; message: string };
15+
16+
/**
17+
* Context passed to tool permission handlers
18+
*/
19+
export interface ClaudeToolPermissionContext {
20+
readonly toolInvocationToken: vscode.ChatParticipantToolToken;
21+
}
22+
23+
/**
24+
* Parameters for showing a confirmation dialog to the user
25+
*/
26+
export interface IClaudeToolConfirmationParams {
27+
readonly title: string;
28+
readonly message: string;
29+
readonly confirmationType?: 'basic' | 'terminal';
30+
readonly terminalCommand?: string;
31+
}
32+
33+
/**
34+
* Handler interface for tool permission checks.
35+
* Implement any combination of these methods to customize behavior:
36+
* - canAutoApprove: Return true to skip confirmation dialog
37+
* - getConfirmationParams: Customize the confirmation dialog
38+
* - handle: Full implementation that bypasses default confirmation flow
39+
*
40+
* @template TToolName The tool name(s) this handler supports
41+
*/
42+
export interface IClaudeToolPermissionHandler<TToolName extends ClaudeToolNames = ClaudeToolNames> {
43+
/**
44+
* The tool name(s) this handler is registered for
45+
*/
46+
readonly toolNames: readonly TToolName[];
47+
48+
/**
49+
* Check if the tool can be auto-approved without user confirmation.
50+
* If not implemented or returns false, continues to confirmation dialog.
51+
*/
52+
canAutoApprove?(
53+
toolName: TToolName,
54+
input: ClaudeToolInputMap[TToolName],
55+
context: ClaudeToolPermissionContext
56+
): Promise<boolean>;
57+
58+
/**
59+
* Get custom confirmation dialog parameters.
60+
* If not implemented, uses default confirmation params.
61+
*/
62+
getConfirmationParams?(
63+
toolName: TToolName,
64+
input: ClaudeToolInputMap[TToolName]
65+
): IClaudeToolConfirmationParams;
66+
67+
/**
68+
* Full custom handler implementation.
69+
* If implemented, bypasses canAutoApprove and getConfirmationParams entirely.
70+
* Use this for tools like AskUserQuestion that need complete custom behavior.
71+
*/
72+
handle?(
73+
toolName: TToolName,
74+
input: ClaudeToolInputMap[TToolName],
75+
context: ClaudeToolPermissionContext
76+
): Promise<ClaudeToolPermissionResult>;
77+
}
78+
79+
/**
80+
* Constructor type for tool permission handlers
81+
*/
82+
export type IClaudeToolPermissionHandlerCtor<TToolName extends ClaudeToolNames = ClaudeToolNames> =
83+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Constructor types for DI require 'any' for parameter compatibility
84+
new (...args: any[]) => IClaudeToolPermissionHandler<TToolName>;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IClaudeToolPermissionHandlerCtor } from './claudeToolPermission';
7+
import { ClaudeToolNames } from './claudeTools';
8+
9+
/**
10+
* Registry entry for tool permission handlers
11+
*/
12+
export interface IToolPermissionHandlerRegistration {
13+
readonly toolNames: readonly ClaudeToolNames[];
14+
readonly ctor: IClaudeToolPermissionHandlerCtor;
15+
}
16+
17+
/**
18+
* Registry of tool permission handlers.
19+
* Handlers can register from common/, node/, or vscode-node/ folders.
20+
*/
21+
const handlerRegistry: IToolPermissionHandlerRegistration[] = [];
22+
23+
/**
24+
* Register a tool permission handler for one or more tools.
25+
* Handlers are instantiated lazily via the instantiation service.
26+
*
27+
* @param toolNames The tool name(s) to register the handler for
28+
* @param ctor The handler constructor
29+
*/
30+
export function registerToolPermissionHandler<T extends ClaudeToolNames>(
31+
toolNames: readonly T[],
32+
ctor: IClaudeToolPermissionHandlerCtor<T>
33+
): void {
34+
handlerRegistry.push({ toolNames, ctor });
35+
}
36+
37+
/**
38+
* Get all registered tool permission handlers.
39+
* Used by ClaudeToolPermissionService to look up handlers.
40+
*/
41+
export function getToolPermissionHandlerRegistry(): readonly IToolPermissionHandlerRegistration[] {
42+
return handlerRegistry;
43+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as l10n from '@vscode/l10n';
7+
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
8+
import { IInstantiationService, createDecorator } from '../../../../util/vs/platform/instantiation/common/instantiation';
9+
import { LanguageModelTextPart } from '../../../../vscodeTypes';
10+
import { ToolName } from '../../../tools/common/toolNames';
11+
import { IToolsService } from '../../../tools/common/toolsService';
12+
import {
13+
ClaudeToolPermissionContext,
14+
ClaudeToolPermissionResult,
15+
IClaudeToolConfirmationParams,
16+
IClaudeToolPermissionHandler
17+
} from './claudeToolPermission';
18+
import { getToolPermissionHandlerRegistry } from './claudeToolPermissionRegistry';
19+
import { ClaudeToolInputMap, ClaudeToolNames } from './claudeTools';
20+
21+
export const IClaudeToolPermissionService = createDecorator<IClaudeToolPermissionService>('claudeToolPermissionService');
22+
23+
export interface IClaudeToolPermissionService {
24+
readonly _serviceBrand: undefined;
25+
26+
/**
27+
* Check if a tool can be used, showing confirmation if needed.
28+
* @param toolName The name of the Claude tool
29+
* @param input The tool input parameters
30+
* @param context Context including the tool invocation token
31+
* @returns Permission result (allow with updated input, or deny with message)
32+
*/
33+
canUseTool(
34+
toolName: string,
35+
input: Record<string, unknown>,
36+
context: ClaudeToolPermissionContext
37+
): Promise<ClaudeToolPermissionResult>;
38+
}
39+
40+
/**
41+
* Default deny message when user declines a tool
42+
*/
43+
const DenyToolMessage = 'The user declined to run the tool';
44+
45+
export class ClaudeToolPermissionService implements IClaudeToolPermissionService {
46+
declare readonly _serviceBrand: undefined;
47+
48+
private readonly _handlerCache = new Map<ClaudeToolNames, IClaudeToolPermissionHandler>();
49+
50+
constructor(
51+
@IInstantiationService private readonly instantiationService: IInstantiationService,
52+
@IToolsService private readonly toolsService: IToolsService,
53+
) { }
54+
55+
public async canUseTool(
56+
toolName: string,
57+
input: Record<string, unknown>,
58+
context: ClaudeToolPermissionContext
59+
): Promise<ClaudeToolPermissionResult> {
60+
const handler = this._getHandler(toolName as ClaudeToolNames);
61+
62+
// If handler has full custom implementation, use it
63+
if (handler?.handle) {
64+
return handler.handle(toolName as ClaudeToolNames, input as ClaudeToolInputMap[ClaudeToolNames], context);
65+
}
66+
67+
// Check auto-approve
68+
if (handler?.canAutoApprove) {
69+
const canAutoApprove = await handler.canAutoApprove(toolName as ClaudeToolNames, input as ClaudeToolInputMap[ClaudeToolNames], context);
70+
if (canAutoApprove) {
71+
return { behavior: 'allow', updatedInput: input };
72+
}
73+
}
74+
75+
// Get confirmation params (custom or default)
76+
const confirmationParams = handler?.getConfirmationParams
77+
? handler.getConfirmationParams(toolName as ClaudeToolNames, input as ClaudeToolInputMap[ClaudeToolNames])
78+
: this._getDefaultConfirmationParams(toolName, input);
79+
80+
// Show confirmation dialog
81+
return this._showConfirmation(confirmationParams, input, context);
82+
}
83+
84+
private _getHandler(toolName: ClaudeToolNames): IClaudeToolPermissionHandler | undefined {
85+
// Check cache first
86+
if (this._handlerCache.has(toolName)) {
87+
return this._handlerCache.get(toolName);
88+
}
89+
90+
// Find registration for this tool
91+
const registration = getToolPermissionHandlerRegistry().find(r => r.toolNames.includes(toolName));
92+
if (!registration) {
93+
return undefined;
94+
}
95+
96+
// Create handler instance
97+
const handler = this.instantiationService.createInstance(registration.ctor);
98+
// Cache for all tool names this handler supports
99+
for (const name of registration.toolNames) {
100+
this._handlerCache.set(name, handler);
101+
}
102+
103+
return handler;
104+
}
105+
106+
private _getDefaultConfirmationParams(toolName: string, input: Record<string, unknown>): IClaudeToolConfirmationParams {
107+
return {
108+
title: l10n.t('Use {0}?', toolName),
109+
message: `\`\`\`\n${JSON.stringify(input, null, 2)}\n\`\`\``,
110+
confirmationType: 'basic'
111+
};
112+
}
113+
114+
private async _showConfirmation(
115+
params: IClaudeToolConfirmationParams,
116+
input: Record<string, unknown>,
117+
context: ClaudeToolPermissionContext
118+
): Promise<ClaudeToolPermissionResult> {
119+
try {
120+
const result = await this.toolsService.invokeTool(ToolName.CoreConfirmationTool, {
121+
input: params,
122+
toolInvocationToken: context.toolInvocationToken,
123+
}, CancellationToken.None);
124+
125+
const firstResultPart = result.content.at(0);
126+
if (firstResultPart instanceof LanguageModelTextPart && firstResultPart.value === 'yes') {
127+
return {
128+
behavior: 'allow',
129+
updatedInput: input
130+
};
131+
}
132+
} catch { }
133+
134+
return {
135+
behavior: 'deny',
136+
message: DenyToolMessage
137+
};
138+
}
139+
}

0 commit comments

Comments
 (0)