Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/sessionPermissions.ts
13394 views
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 { match as globMatch } from '../../../base/common/glob.js';
7
import { Disposable } from '../../../base/common/lifecycle.js';
8
import { extUriBiasedIgnorePathCase, normalizePath } from '../../../base/common/resources.js';
9
import { URI } from '../../../base/common/uri.js';
10
import { localize } from '../../../nls.js';
11
import { ILogService } from '../../log/common/log.js';
12
import type { IAgentToolPendingConfirmationSignal } from '../common/agentService.js';
13
import { platformSessionSchema } from '../common/agentHostSchema.js';
14
import { SessionConfigKey } from '../common/sessionConfigKeys.js';
15
import { ConfirmationOptionKind, type ConfirmationOption } from '../common/state/protocol/state.js';
16
import { ActionType, type IToolCallReadyAction } from '../common/state/sessionActions.js';
17
import {
18
ResponsePartKind,
19
ToolCallConfirmationReason,
20
type URI as ProtocolURI,
21
} from '../common/state/sessionState.js';
22
import { IAgentConfigurationService } from './agentConfigurationService.js';
23
import { AgentHostStateManager } from './agentHostStateManager.js';
24
import { CommandAutoApprover } from './commandAutoApprover.js';
25
26
/**
27
* Event fields needed for auto-approval decisions.
28
* Matches the subset of {@link IAgentToolPendingConfirmationSignal} used by the
29
* approval pipeline.
30
*/
31
export interface IToolApprovalEvent {
32
readonly toolCallId: string;
33
readonly session: URI;
34
readonly permissionKind?: IAgentToolPendingConfirmationSignal['permissionKind'];
35
readonly permissionPath?: string;
36
readonly toolInput?: string;
37
}
38
39
/** Standard per-tool confirmation options presented to the user. */
40
const ALLOW_SESSION_OPTION_ID = 'allow-session';
41
const CONFIRMATION_OPTIONS: readonly ConfirmationOption[] = [
42
{ id: ALLOW_SESSION_OPTION_ID, label: localize('sessionPermissions.allowSession', "Allow in this Session"), kind: ConfirmationOptionKind.Approve, group: 1 },
43
{ id: 'allow-once', label: localize('sessionPermissions.allowOnce', "Allow Once"), kind: ConfirmationOptionKind.Approve },
44
{ id: 'skip', label: localize('sessionPermissions.skip', "Skip"), kind: ConfirmationOptionKind.Deny, group: 2 },
45
];
46
47
/** Default write-path glob rules applied to auto-approved edits. */
48
const DEFAULT_EDIT_AUTO_APPROVE_PATTERNS: Readonly<Record<string, boolean>> = {
49
'**/*': true,
50
'**/.vscode/*.json': false,
51
'**/.git/**': false,
52
'**/{package.json,server.xml,build.rs,web.config,.gitattributes,.env}': false,
53
'**/*.{code-workspace,csproj,fsproj,vbproj,vcxproj,proj,targets,props}': false,
54
'**/*.lock': false,
55
'**/*-lock.{yaml,json}': false,
56
};
57
58
/**
59
* Single entry point for all tool-call approval logic in the agent host.
60
*
61
* Modeled after {@link ILanguageModelToolsConfirmationService} in the
62
* workbench layer, this manager owns:
63
*
64
* - **Auto-approval** (`getAutoApproval`) — checks session-level config,
65
* per-tool session permissions, read/write path rules, and shell
66
* command rules. Returns a {@link ToolCallConfirmationReason} when
67
* the tool should be auto-approved, or `undefined` when user
68
* confirmation is needed.
69
*
70
* - **Confirmation options** (`createToolReadyAction`) — constructs the
71
* protocol action with the standard "Allow Once / Allow in this
72
* Session / Skip" options baked in.
73
*
74
* - **Post-confirmation side effects** (`handleToolCallConfirmed`) —
75
* persists the user's choice (e.g. adding a tool to the session
76
* permissions list).
77
*/
78
export class SessionPermissionManager extends Disposable {
79
80
81
// ---- Edit auto-approve patterns -----------------------------------------
82
83
private readonly _commandAutoApprover: CommandAutoApprover;
84
85
constructor(
86
private readonly _stateManager: AgentHostStateManager,
87
@IAgentConfigurationService private readonly _configService: IAgentConfigurationService,
88
@ILogService private readonly _logService: ILogService,
89
) {
90
super();
91
this._commandAutoApprover = this._register(new CommandAutoApprover(this._logService));
92
}
93
94
/**
95
* Initializes async resources (tree-sitter WASM) used for shell command
96
* auto-approval. Await this before any session events can arrive to
97
* guarantee that {@link getAutoApproval} is fully synchronous.
98
*/
99
initialize(): Promise<void> {
100
return this._commandAutoApprover.initialize();
101
}
102
103
// ---- Auto-approval (analogous to getPreConfirmAction) -------------------
104
105
/**
106
* Synchronously checks whether a `tool_ready` event should be
107
* auto-approved. Returns a {@link ToolCallConfirmationReason} when the
108
* tool call should proceed without user interaction, or `undefined`
109
* when user confirmation is required.
110
*
111
* Checks are evaluated in order:
112
* 1. Session-level bypass (`autoApprove` / `autopilot` config)
113
* 2. Per-tool session permissions (`permissions.allow`)
114
* 3. Read path rules (within working directory)
115
* 4. Write path rules (within working directory + glob patterns)
116
* 5. Shell command rules (tree-sitter parsed, default allow/deny)
117
*/
118
getAutoApproval(e: IToolApprovalEvent, sessionKey: ProtocolURI): ToolCallConfirmationReason | undefined {
119
const autoApproveLevel = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.AutoApprove);
120
const workDir = this._configService.getEffectiveWorkingDirectory(sessionKey);
121
122
// 1. Session-level auto-approve
123
if (autoApproveLevel === 'autoApprove' || autoApproveLevel === 'autopilot') {
124
this._logService.trace(`[SessionPermissionManager] Auto-approving tool call (session autoApprove=${autoApproveLevel})`);
125
return ToolCallConfirmationReason.Setting;
126
}
127
128
// 2. Per-tool session permissions
129
if (this._isToolAllowedByPermissions(sessionKey, e.toolCallId)) {
130
return ToolCallConfirmationReason.Setting;
131
}
132
133
// 3. Read auto-approval
134
if (e.permissionKind === 'read' && e.permissionPath) {
135
if (this._isPathInWorkingDirectory(e.permissionPath, workDir)) {
136
this._logService.trace(`[SessionPermissionManager] Auto-approving read of ${e.permissionPath}`);
137
return ToolCallConfirmationReason.NotNeeded;
138
}
139
return undefined;
140
}
141
142
// 4. Write auto-approval
143
if (e.permissionKind === 'write' && e.permissionPath) {
144
if (this._isPathInWorkingDirectory(e.permissionPath, workDir) && this._isEditAutoApproved(e.permissionPath)) {
145
this._logService.trace(`[SessionPermissionManager] Auto-approving write to ${e.permissionPath}`);
146
return ToolCallConfirmationReason.NotNeeded;
147
}
148
return undefined;
149
}
150
151
// 5. Shell auto-approval
152
if (e.permissionKind === 'shell' && e.toolInput) {
153
const result = this._commandAutoApprover.shouldAutoApprove(e.toolInput);
154
if (result === 'approved') {
155
this._logService.trace('[SessionPermissionManager] Auto-approving shell command');
156
return ToolCallConfirmationReason.NotNeeded;
157
}
158
if (result === 'denied') {
159
this._logService.trace('[SessionPermissionManager] Shell command denied by rule');
160
}
161
return undefined;
162
}
163
164
return undefined;
165
}
166
167
// ---- Action construction (analogous to getPreConfirmActions) -------------
168
169
/**
170
* Constructs a `SessionToolCallReady` action from an agent
171
* `pending_confirmation` signal. When the tool needs user confirmation
172
* (the protocol state carries `confirmationTitle`), the standard
173
* confirmation options are baked in so clients can render them directly.
174
*/
175
createToolReadyAction(e: IAgentToolPendingConfirmationSignal, sessionKey: ProtocolURI, turnId: string): IToolCallReadyAction {
176
const state = e.state;
177
if (state.confirmationTitle) {
178
return {
179
type: ActionType.SessionToolCallReady,
180
session: sessionKey,
181
turnId,
182
toolCallId: state.toolCallId,
183
invocationMessage: state.invocationMessage,
184
toolInput: state.toolInput,
185
confirmationTitle: state.confirmationTitle,
186
edits: state.edits,
187
editable: state.editable,
188
options: CONFIRMATION_OPTIONS.slice(),
189
};
190
}
191
return {
192
type: ActionType.SessionToolCallReady,
193
session: sessionKey,
194
turnId,
195
toolCallId: state.toolCallId,
196
invocationMessage: state.invocationMessage,
197
toolInput: state.toolInput,
198
confirmed: ToolCallConfirmationReason.NotNeeded,
199
};
200
}
201
202
// ---- Post-confirmation side effects -------------------------------------
203
204
/**
205
* Handles the side effect of a `SessionToolCallConfirmed` action when the
206
* user selected "Allow in this Session". Adds the tool to the session's
207
* permission allow list so future calls are auto-approved.
208
*/
209
handleToolCallConfirmed(sessionKey: ProtocolURI, toolCallId: string, selectedOptionId: string | undefined): void {
210
if (selectedOptionId === ALLOW_SESSION_OPTION_ID) {
211
const toolName = this._getToolNameForToolCall(sessionKey, toolCallId);
212
if (toolName) {
213
this._addToolToSessionPermissions(sessionKey, toolName);
214
}
215
}
216
}
217
218
// ---- Internal helpers ---------------------------------------------------
219
220
private _isPathInWorkingDirectory(filePath: string, workDir: string | undefined): boolean {
221
if (!workDir) {
222
return false;
223
}
224
const workingDirectory = URI.parse(workDir);
225
return extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(URI.file(filePath)), workingDirectory);
226
}
227
228
private _isEditAutoApproved(filePath: string): boolean {
229
let approved = true;
230
for (const [pattern, isApproved] of Object.entries(DEFAULT_EDIT_AUTO_APPROVE_PATTERNS)) {
231
if (isApproved !== approved && globMatch(pattern, filePath)) {
232
approved = isApproved;
233
}
234
}
235
return approved;
236
}
237
238
private _isToolAllowedByPermissions(sessionKey: ProtocolURI, toolCallId: string): boolean {
239
const toolName = this._getToolNameForToolCall(sessionKey, toolCallId);
240
if (!toolName) {
241
return false;
242
}
243
// `getEffectiveValue` walks session → parent → host, so sessions
244
// that haven't materialized their own `permissions` yet transparently
245
// inherit from the host-level allow/deny lists.
246
const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions);
247
const allowed = permissions?.allow.includes(toolName) ?? false;
248
if (allowed) {
249
this._logService.trace(`[SessionPermissionManager] Auto-approving "${toolName}" via permissions`);
250
}
251
return allowed;
252
}
253
254
private _getToolNameForToolCall(sessionKey: ProtocolURI, toolCallId: string): string | undefined {
255
const sessionState = this._stateManager.getSessionState(sessionKey);
256
const parts = sessionState?.activeTurn?.responseParts;
257
if (!parts) {
258
return undefined;
259
}
260
for (const rp of parts) {
261
if (rp.kind === ResponsePartKind.ToolCall && rp.toolCall.toolCallId === toolCallId) {
262
return rp.toolCall.toolName;
263
}
264
}
265
return undefined;
266
}
267
268
private _addToolToSessionPermissions(sessionKey: ProtocolURI, toolName: string): void {
269
const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions)
270
?? { allow: [], deny: [] };
271
if (permissions.allow.includes(toolName)) {
272
return;
273
}
274
this._configService.updateSessionConfig(sessionKey, {
275
[SessionConfigKey.Permissions]: {
276
allow: [...permissions.allow, toolName],
277
deny: [...permissions.deny],
278
},
279
});
280
this._logService.info(`[SessionPermissionManager] Added "${toolName}" to session permissions for ${sessionKey}`);
281
}
282
}
283
284