src/vs/platform/agentHost/node/sessionPermissions.ts

591 LOC · 513 covered · 78 uncovered · 132 ranges · 1109 concepts · 55 introducers · 531 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- sessionPermissions.ts ×26
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 { realpath as fsRealpath } from 'fs';
7 > import { homedir } from 'os';
8 > import { promisify } from 'util';
9 > import { match as globMatch } from '../../../base/common/glob.js';
10 > import { untildify } from '../../../base/common/labels.js';
11 > import { Disposable } from '../../../base/common/lifecycle.js';
12 > import { Schemas } from '../../../base/common/network.js';
13 > import * as path from '../../../base/common/path.js';
14 > import { isMacintosh, isWindows } from '../../../base/common/platform.js';
15 > import { extUriBiasedIgnorePathCase, normalizePath } from '../../../base/common/resources.js';
16 > import { isDefined } from '../../../base/common/types.js';
17 > import { URI } from '../../../base/common/uri.js';
18 > import { localize } from '../../../nls.js';
19 > import { ILogService } from '../../log/common/log.js';
20 > import { AgentHostGlobalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, platformRootSchema, platformSessionSchema } from '../common/agentHostSchema.js';
21 > import type { IAgentToolPendingConfirmationSignal } from '../common/agentService.js';
22 > import { SessionConfigKey } from '../common/sessionConfigKeys.js';
23 > import { ConfirmationOptionKind, type ConfirmationOption } from '../common/state/protocol/state.js';
24 > import { ActionType, type IToolCallReadyAction } from '../common/state/sessionActions.js';
25 > import {
26 > isAhpChatChannel,
27 > parseRequiredSessionUriFromChatUri,
28 > ResponsePartKind,
29 > ToolCallConfirmationReason,
30 > type URI as ProtocolURI,
31 > } from '../common/state/sessionState.js';
32 > import { IAgentConfigurationService } from './agentConfigurationService.js';
33 > import { AgentHostStateManager } from './agentHostStateManager.js';
34 > import { CommandAutoApprover } from './commandAutoApprover.js';
35 >
36 > /**
37 > * Event fields needed for auto-approval decisions.
38 > * Matches the subset of {@link IAgentToolPendingConfirmationSignal} used by the
39 > * approval pipeline.
40 > */
41 > export interface IToolApprovalEvent {
42 > readonly toolCallId: string;
43 > readonly session: URI;
44 > readonly permissionKind?: IAgentToolPendingConfirmationSignal['permissionKind'];
45 > readonly permissionPath?: string;
46 > readonly toolInput?: string;
47 > readonly requestSandboxBypass?: boolean;
48 > }
49 >
50 > /** Standard per-tool confirmation options presented to the user. */
51 > const ALLOW_SESSION_OPTION_ID = 'allow-session';
52 > const CONFIRMATION_OPTIONS: readonly ConfirmationOption[] = [
53 > { id: ALLOW_SESSION_OPTION_ID, label: localize('sessionPermissions.allowSession', "Allow in this Session"), kind: ConfirmationOptionKind.Approve, group: 1 },
54 > { id: 'allow-once', label: localize('sessionPermissions.allowOnce', "Allow Once"), kind: ConfirmationOptionKind.Approve },
55 > { id: 'skip', label: localize('sessionPermissions.skip', "Skip"), kind: ConfirmationOptionKind.Deny, group: 2 },
56 > ];
57 >
58 > /** Default write-path glob rules applied to auto-approved edits. */
59 > const DEFAULT_EDIT_AUTO_APPROVE_PATTERNS: Readonly<Record<string, boolean>> = {
60 > '**/*': true,
61 > '**/.vscode/*.json': false,
62 > '**/.git/**': false,
63 > '**/{package.json,server.xml,build.rs,web.config,.gitattributes,.env}': false,
64 > '**/*.{code-workspace,csproj,fsproj,vbproj,vcxproj,proj,targets,props}': false,
65 > '**/*.lock': false,
66 > '**/*-lock.{yaml,json}': false,
67 > // Files that can register lifecycle hooks running arbitrary shell commands.
68 > // Writing them must never be auto-approved. Keep in sync with the hook and
69 > // agent source locations in `promptFileLocations.ts`.
70 > '**/.github/agents/**': false,
71 > '**/.github/hooks/**': false,
72 > '**/.claude/agents/**': false,
73 > '**/.claude/settings.json': false,
74 > '**/.claude/settings.local.json': false,
75 > };
76 >
77 > const HOME_DIR = URI.file(homedir());
78 >
79 > /**
80 > * Absolute directory prefixes whose contents are platform configuration data
81 > * (e.g. `~/Library`, `%APPDATA%`). Writes under these require confirmation
82 > * unless the working directory itself lives inside the restricted directory.
83 > */
84 > const PLATFORM_RESTRICTED_DIRS: readonly string[] = (
85 > isWindows
86 ? [process.env.APPDATA, process.env.LOCALAPPDATA]
87 > : isMacintosh sessionPermissions.ts ×26
88 ? [homedir() + '/Library']
90 > ).filter(isDefined);
91 >
92 > const realpath = promisify(fsRealpath);
93 >
94 > /**
95 > * Validates that a path doesn't contain suspicious characters that could be
96 > * used to bypass security checks on Windows (e.g. NTFS Alternate Data Streams,
97 > * invalid characters, reserved device names). Throws if the path is suspicious.
98 > */
99 > function assertPathIsSafe(fsPath: string, _isWindows = isWindows): void { sessionPermissions.ts ×7
100 > if (fsPath.includes('\0')) {
101 > throw new Error(`Path contains null bytes: ${fsPath}`); sessionPermissions.ts ×2
102 > }
104 > if (!_isWindows) {
105 > return;
106 > }
107
108 // Check for NTFS Alternate Data Streams (ADS)
109 const colonIndex = fsPath.indexOf(':', 2);
110 if (colonIndex !== -1) {
111 throw new Error(`Path contains invalid characters (alternate data stream): ${fsPath}`);
112 }
113
114 // Check for invalid Windows filename characters
115 const invalidChars = /[<>"|?*]/;
116 > const pathAfterDrive = fsPath.length > 2 ? fsPath.substring(2) : fsPath; sessionPermissions.ts ×7
117 > if (invalidChars.test(pathAfterDrive)) {
118 throw new Error(`Path contains invalid characters: ${fsPath}`);
119 }
120
121 // Check for named pipes or device paths
122 > if (fsPath.startsWith('\\\\.') || fsPath.startsWith('\\\\?')) { sessionPermissions.ts ×7
123 throw new Error(`Path is a reserved device path: ${fsPath}`);
124 }
125
126 const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i;
127
128 // Check for trailing dots and spaces on path components (Windows quirk)
129 const parts = fsPath.split('\\');
130 for (const part of parts) {
131 if (part.length === 0) {
132 continue;
133 }
134
135 if (reserved.test(part)) {
136 throw new Error(`Reserved device name in path: ${fsPath}`);
137 }
138
139 if (part.endsWith('.') || part.endsWith(' ')) {
140 throw new Error(`Path contains invalid trailing characters: ${fsPath}`);
141 }
142
143 const tildeIndex = part.indexOf('~');
144 if (tildeIndex !== -1) {
145 const afterTilde = part.substring(tildeIndex + 1);
146 if (afterTilde.length > 0 && /^\d/.test(afterTilde)) {
147 throw new Error(`Path appears to use short filename format (8.3 names): ${fsPath}. Please use the full path.`);
148 }
149 }
150 }
151 }
153 > /**
154 > * Resolves the real path of `resource`, walking up the parent chain when the path
155 > * (or its ancestors) does not yet exist on disk. This ensures a symlink at any
156 > * ancestor is followed even for files that are about to be created.
157 > */
158 > async function resolveRealPathForNonexistent(resource: URI, realpath: (fsPath: string) => Promise<string>): Promise<URI> { sessionPermissions.ts ×7
159 > const fsPath = resource.fsPath;
160 > try {
161 > return URI.file(await realpath(fsPath));
162 > } catch (e) {
163 > if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
164 > throw e; sessionPermissions.ts ×3
165 > }
168 > const tail: string[] = [path.basename(fsPath)];
169 > let current = path.dirname(fsPath);
170 > while (true) {
171 > const parent = path.dirname(current);
172 > if (parent === current) {
173 > // Reached the filesystem root without finding an existing ancestor. sessionPermissions.ts ×1
174 > return resource;
175 > }
177 > const resolved = await realpath(current);
178 > return URI.file(path.join(resolved, ...tail)); sessionPermissions.ts ×1
179 > } catch (e) { sessionPermissions.ts ×2
180 > const code = (e as NodeJS.ErrnoException).code; sessionPermissions.ts ×2
181 > if (code !== 'ENOENT' && code !== 'ENOTDIR') {
182 throw e;
183 }
185 > tail.unshift(path.basename(current));
186 > current = parent;
187 > }
190 > /**
191 > * Single entry point for all tool-call approval logic in the agent host.
192 > *
193 > * Modeled after {@link ILanguageModelToolsConfirmationService} in the
194 > * workbench layer, this manager owns:
195 > *
196 > * - **Auto-approval** (`getAutoApproval`) — checks session-level config,
197 > * per-tool session permissions, read/write path rules, and shell
198 > * command rules. Returns a {@link ToolCallConfirmationReason} when
199 > * the tool should be auto-approved, or `undefined` when user
200 > * confirmation is needed.
201 > *
202 > * - **Confirmation options** (`createToolReadyAction`) — constructs the
203 > * protocol action with the standard "Allow Once / Allow in this
204 > * Session / Skip" options baked in.
205 > *
206 > * - **Post-confirmation side effects** (`handleToolCallConfirmed`) —
207 > * persists the user's choice (e.g. adding a tool to the session
208 > * permissions list).
209 > */
210 > export class SessionPermissionManager extends Disposable {
211 >
212 > // ---- Edit auto-approve patterns -----------------------------------------
213 >
214 > private readonly _commandAutoApprover: CommandAutoApprover;
215 > private readonly _realpath: (fsPath: string) => Promise<string>;
216 >
217 > constructor(
218 > private readonly _stateManager: AgentHostStateManager, sessionPermissions.ts ×1
219 > options: { realpath?: (fsPath: string) => Promise<string> },
220 > @IAgentConfigurationService private readonly _configService: IAgentConfigurationService,
221 > @ILogService private readonly _logService: ILogService,
222 > ) {
223 > super();
224 > this._realpath = options?.realpath ?? realpath;
225 > this._commandAutoApprover = this._register(new CommandAutoApprover(this._logService));
226 > }
228 > /**
229 > * Initializes async resources (tree-sitter WASM) used for shell command
230 > * auto-approval. Await this before any session events can arrive so that
231 > * shell command parsing within {@link getAutoApproval} is synchronous.
232 > */
233 > initialize(): Promise<void> {
234 > return this._commandAutoApprover.initialize(); sessionPermissions.ts ×1
235 > }
237 > // ---- Auto-approval (analogous to getPreConfirmAction) -------------------
238 >
239 > /**
240 > * Checks whether a `tool_ready` event should be auto-approved. Returns a
241 > * {@link ToolCallConfirmationReason} when the tool call should proceed
242 > * without user interaction, or `undefined` when user confirmation is
243 > * required.
244 > *
245 > * Checks are evaluated in order:
246 > * 1. Global auto-approve setting (`chat.tools.global.autoApprove`)
247 > * 2. Session-level bypass (`autoApprove` config)
248 > * 3. Per-tool session permissions (`permissions.allow`)
249 > * 4. Read path rules (within working directory)
250 > * 5. Write path rules (within working directory + glob patterns)
251 > * 6. Shell command rules (tree-sitter parsed, default allow/deny)
252 > */
253 > async getAutoApproval(e: IToolApprovalEvent, sessionKey: ProtocolURI): Promise<ToolCallConfirmationReason | undefined> {
254 > const workDir = this._configService.getEffectiveWorkingDirectory(sessionKey); sessionPermissions.ts ×5
255 > const workingDirectory = workDir ? URI.parse(workDir) : undefined;
256 >
257 > // 0. Sandbox bypass: a shell command that opted out of the
258 > // sandbox (`requestSandboxBypass`) escapes the sandbox's
259 > // containment.
260 > if (e.requestSandboxBypass) {
261 > return undefined; sessionPermissions.ts ×1
262 > }
264 > // 1. Global auto-approve setting
265 > if (this.isGlobalAutoApproveEnabled()) {
266 > return ToolCallConfirmationReason.Setting; sessionPermissions.ts ×1
267 > }
269 > // 2. Session-level auto-approve
270 > if (this.isSessionAutoApproveEnabled(sessionKey)) {
271 > return ToolCallConfirmationReason.Setting; sessionPermissions.ts ×1
272 > }
274 > // 3. Per-tool session permissions
275 > if (this._isToolAllowedByPermissions(sessionKey, e.toolCallId)) {
276 > return ToolCallConfirmationReason.Setting; sessionPermissions.ts ×2
277 > }
279 > // 4. Read auto-approval
280 > if (e.permissionKind === 'read' && e.permissionPath) { sessionPermissions.ts ×5
281 > if (await this._isReadAutoApproved(URI.file(e.permissionPath), workingDirectory)) { sessionPermissions.ts ×4
282 > this._logService.trace(`[SessionPermissionManager] Auto-approving read of ${e.permissionPath}`); sessionPermissions.ts ×1
283 > return ToolCallConfirmationReason.NotNeeded;
284 > }
285 > return undefined; sessionPermissions.ts ×1
286 > }
288 > // 5. Write auto-approval
289 > if (e.permissionKind === 'write' && e.permissionPath) { sessionPermissions.ts ×5
290 > if (await this._isEditAutoApproved(URI.file(e.permissionPath), workingDirectory)) { sessionPermissions.ts ×7
291 > this._logService.trace(`[SessionPermissionManager] Auto-approving write to ${e.permissionPath}`); sessionPermissions.ts ×1
292 > return ToolCallConfirmationReason.NotNeeded;
293 > }
294 > return undefined; sessionPermissions.ts ×1
295 > }
297 > // 6. Shell auto-approval
298 > if (e.permissionKind === 'shell' && e.toolInput) { sessionPermissions.ts ×5
299 > if (this._configService.getRootValue(platformRootSchema, AgentHostTerminalAutoApproveEnabledConfigKey) === false) { sessionPermissions.ts ×1
300 > return undefined; sessionPermissions.ts ×1
301 > }
302 > const result = this._commandAutoApprover.shouldAutoApprove(e.toolInput, { sessionPermissions.ts ×1
303 > autoApproveRules: this._configService.getRootValue(platformRootSchema, AgentHostTerminalAutoApproveRulesConfigKey),
304 > isWriteDestApproved: dest => this._isShellWriteDestApproved(dest, workingDirectory),
305 > });
306 > if (result === 'approved') {
307 > this._logService.trace('[SessionPermissionManager] Auto-approving shell command'); sessionPermissions.ts ×1
308 > return ToolCallConfirmationReason.NotNeeded;
309 > }
310 > if (result === 'denied') { sessionPermissions.ts ×2
311 > this._logService.trace('[SessionPermissionManager] Shell command denied by rule'); sessionPermissions.ts ×1
312 > }
313 > return undefined; sessionPermissions.ts ×2
314 > }
316 > return undefined;
319 > /**
320 > * Returns whether VS Code's global auto-approve setting (`chat.tools.global.autoApprove`) is enabled.
321 > * When enabled, every tool call is auto-approved without changing the session's approval level in the permissions picker.
322 > */
323 > isGlobalAutoApproveEnabled(): boolean {
324 > return this._configService.getRootValue(platformRootSchema, AgentHostGlobalAutoApproveEnabledConfigKey) === true; sessionPermissions.ts ×1
325 > }
327 > getEffectiveApprovalLevel(sessionKey: ProtocolURI): string {
328 > return this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.AutoApprove) ?? 'default'; sessionPermissions.ts ×2
329 > }
331 > isSessionAutoApproveEnabled(sessionKey: ProtocolURI): boolean {
332 > // `autoApprove` (Allow All) auto-approves every tool call. sessionPermissions.ts ×2
333 > return this.getEffectiveApprovalLevel(sessionKey) === 'autoApprove';
334 > }
336 > // ---- Action construction (analogous to getPreConfirmActions) -------------
337 >
338 > /**
339 > * Constructs a `ChatToolCallReady` action from an agent
340 > * `pending_confirmation` signal. When the tool needs user confirmation
341 > * (the protocol state carries `confirmationTitle`), the standard
342 > * confirmation options are baked in so clients can render them directly.
343 > */
344 > createToolReadyAction(e: IAgentToolPendingConfirmationSignal, _sessionKey: ProtocolURI, turnId: string): IToolCallReadyAction {
345 > const state = e.state; agentSideEffects.ts ×3
346 > if (state.confirmationTitle) {
347 > return { sessionPermissions.ts ×1
348 > type: ActionType.ChatToolCallReady,
349 > turnId,
350 > toolCallId: state.toolCallId,
351 > invocationMessage: state.invocationMessage,
352 > toolInput: state.toolInput,
353 > confirmationTitle: state.confirmationTitle,
354 > riskAssessment: state.riskAssessment,
355 > edits: state.edits,
356 > editable: state.editable,
357 > ...(state._meta ? { _meta: state._meta } : {}),
358 > // Agents can supply tool-specific buttons (e.g. ExitPlanMode's
359 > // `Approve`/`Deny`) by populating `state.options`. The standard
360 > // `Allow Once / Allow in this Session / Skip` set is the default.
361 > options: state.options ? state.options.slice() : CONFIRMATION_OPTIONS.slice(),
362 > };
363 > }
364 > return { sessionPermissions.ts ×1
365 > type: ActionType.ChatToolCallReady,
366 > turnId,
367 > toolCallId: state.toolCallId,
368 > invocationMessage: state.invocationMessage,
369 > toolInput: state.toolInput,
370 > confirmed: ToolCallConfirmationReason.NotNeeded,
371 > ...(state._meta ? { _meta: state._meta } : {}), agentSideEffects.ts ×3
372 > };
373 > }
375 > // ---- Post-confirmation side effects -------------------------------------
376 >
377 > /**
378 > * Handles the side effect of a `ChatToolCallConfirmed` action when the
379 > * user selected "Allow in this Session". Adds the tool to the session's
380 > * permission allow list so future calls are auto-approved.
381 > */
382 > handleToolCallConfirmed(chatChannel: ProtocolURI, toolCallId: string, selectedOptionId: string | undefined): void {
383 > if (!isAhpChatChannel(chatChannel)) { sessionPermissions.ts ×3
384 throw new Error(`Tool call confirmations must be handled on an AHP chat channel: ${chatChannel}`);
385 }
386 > const sessionKey = parseRequiredSessionUriFromChatUri(chatChannel); sessionPermissions.ts ×3
387 > if (selectedOptionId === ALLOW_SESSION_OPTION_ID) {
388 > const toolName = this._getToolNameForToolCall(chatChannel, toolCallId); sessionPermissions.ts ×4
389 > if (toolName) {
390 > this._addToolToSessionPermissions(sessionKey, toolName);
391 > }
392 > }
395 > // ---- Internal helpers ---------------------------------------------------
396 >
397 > private async _isReadAutoApproved(resource: URI, workingDirectory: URI | undefined): Promise<boolean> {
398 > if (!workingDirectory) { sessionPermissions.ts ×4
399 return false;
400 }
402 > const [resourcesToCheck, workingDirectories] = await Promise.all([
403 > this._resolveResourcesForApproval(resource),
404 > this._resolveResourcesForApproval(workingDirectory),
405 > ]);
406 > return resourcesToCheck !== undefined
407 > && workingDirectories !== undefined sessionPermissions.ts ×1
408 > && resourcesToCheck.every(candidate => workingDirectories.some(directory => this._isResourceInDirectory(candidate, directory)));
411 > private _isResourceInWorkingDirectory(resource: URI, workingDirectory: URI | undefined): boolean {
412 > return workingDirectory !== undefined && this._isResourceInDirectory(resource, workingDirectory); sessionPermissions.ts ×3
413 > }
415 > private _isResourceInDirectory(resource: URI, directory: URI): boolean {
416 > return extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(resource), normalizePath(directory)); sessionPermissions.ts ×3
417 > }
419 > /**
420 > * Checks whether a shell write-redirection destination (e.g. the `out.txt`
421 > * in `echo hi > out.txt`) should be auto-approved by reusing the same
422 > * rules that govern write tool calls: the destination must resolve to a
423 > * path inside the working directory and must not match a denied glob.
424 > */
425 > private _isShellWriteDestApproved(dest: string, workingDirectory: URI | undefined): boolean {
426 const resource = this._resolveShellRedirectResource(dest, workingDirectory);
427 if (!resource) {
428 return false;
429 }
430 return this._checkWriteResource(resource, workingDirectory);
431 }
433 > /**
434 > * Resolves the raw text of a shell redirect destination to an absolute
435 > * filesystem path. `~` is expanded to the user's home directory; the
436 > * downstream working-directory check rejects paths that end up outside
437 > * the workspace. Returns `undefined` when resolution would require a
438 > * working directory that isn't configured.
439 > */
440 > private _resolveShellRedirectResource(dest: string, workingDirectory: URI | undefined): URI | undefined {
441 const trimmed = untildify(dest.trim(), homedir());
442 if (!trimmed) {
443 return undefined;
444 }
445 if (path.isAbsolute(trimmed)) {
446 return URI.file(trimmed);
447 }
448 if (!workingDirectory) {
449 return undefined;
450 }
451 return URI.file(path.resolve(workingDirectory.fsPath, trimmed));
452 }
454 > /**
455 > * Determines whether a write to `resource` can be auto-approved. Mirrors the
456 > * checks performed by the workbench edit-confirmation pipeline:
457 > *
458 > * 1. The path is resolved through any symlinks (following ancestors that do
459 > * not yet exist) so a link can't redirect an edit outside the working
460 > * directory. Both the literal and resolved paths must pass every check.
461 > * 2. The path must be free of suspicious characters (see {@link assertPathIsSafe}).
462 > * 3. The path must live inside the working directory.
463 > * 4. The path must not target a platform-restricted location (home dotfiles,
464 > * `~/Library`, `%APPDATA%`, ...).
465 > * 5. The path must match the edit auto-approve glob rules.
466 > */
467 > private async _isEditAutoApproved(resource: URI, workingDirectory: URI | undefined): Promise<boolean> {
468 > const resourcesToCheck = await this._resolveResourcesForApproval(resource); sessionPermissions.ts ×7
469 > return resourcesToCheck !== undefined && resourcesToCheck.every(candidate => this._checkWriteResource(candidate, workingDirectory));
470 > }
472 > /**
473 > * Returns the literal path plus, for absolute paths, the symlink-resolved
474 > * real path. Returns `undefined` when the path cannot be resolved due to
475 > * missing permissions, signalling that confirmation is required.
476 > */
477 > private async _resolveResourcesForApproval(resource: URI): Promise<URI[] | undefined> {
478 > const resourcesToCheck = [resource]; sessionPermissions.ts ×7
479 > if (resource.scheme !== Schemas.file) {
480 > return resourcesToCheck; sessionPermissions.ts ×1
481 > }
483 > const resolved = await resolveRealPathForNonexistent(resource, this._realpath);
484 > if (!extUriBiasedIgnorePathCase.isEqual(resolved, resource)) { sessionPermissions.ts ×3
485 > resourcesToCheck.push(resolved); sessionPermissions.ts ×1
486 > }
487 > } catch (e) { sessionPermissions.ts ×7
488 > const code = (e as NodeJS.ErrnoException).code; sessionPermissions.ts ×3
489 > if (code === 'EPERM' || code === 'EACCES') {
490 > // No permission to resolve the path — require confirmation. sessionPermissions.ts ×1
491 > return undefined;
492 > }
493 > // Otherwise fall back to checking the literal resource only. sessionPermissions.ts ×3
494 > }
495 > return resourcesToCheck; sessionPermissions.ts ×1
498 > /** Runs the write checks for a single (already symlink-resolved) resource. */
499 > private _checkWriteResource(resource: URI, workingDirectory: URI | undefined): boolean {
501 > assertPathIsSafe(resource.fsPath);
502 > } catch {
503 > return false; sessionPermissions.ts ×2
504 > }
505 > if (!this._isResourceInWorkingDirectory(resource, workingDirectory)) { sessionPermissions.ts ×3
506 > return false; sessionPermissions.ts ×1
507 > }
508 > if (this._isPlatformRestrictedResource(resource, workingDirectory)) { sessionPermissions.ts ×3
509 > return false; sessionPermissions.ts ×2
510 > }
511 > return this._matchesEditAutoApprovePatterns(resource.fsPath); sessionPermissions.ts ×5
514 > /**
515 > * Returns whether `resource` targets a platform-restricted location that
516 > * should always require confirmation. Edits within home-directory dotfiles
517 > * are never auto-approved. Edits within platform config directories are
518 > * allowed only when the working directory itself lives inside them.
519 > */
520 > private _isPlatformRestrictedResource(resource: URI, workingDirectory: URI | undefined): boolean {
521 > const relativeToHome = extUriBiasedIgnorePathCase.relativePath(HOME_DIR, resource); sessionPermissions.ts ×3
522 > const topLevelName = relativeToHome?.split('/')[0];
523 > if (extUriBiasedIgnorePathCase.isEqualOrParent(resource, HOME_DIR) && topLevelName?.startsWith('.')) {
524 > return true; sessionPermissions.ts ×2
525 > }
527 > for (const restricted of PLATFORM_RESTRICTED_DIRS) {
528 const parentURI = URI.file(restricted);
529 if (extUriBiasedIgnorePathCase.isEqualOrParent(resource, parentURI)) {
530 // Allow edits when the working directory is opened inside the restricted area.
531 return !(workingDirectory && extUriBiasedIgnorePathCase.isEqualOrParent(workingDirectory, parentURI));
532 }
533 }
534 > return false; sessionPermissions.ts ×5
537 > private _matchesEditAutoApprovePatterns(filePath: string): boolean {
538 > let approved = true; sessionPermissions.ts ×5
539 > for (const [pattern, isApproved] of Object.entries(DEFAULT_EDIT_AUTO_APPROVE_PATTERNS)) {
540 > if (isApproved !== approved && globMatch(pattern, filePath)) {
541 > approved = isApproved; sessionPermissions.ts ×1
542 > }
544 > return approved;
545 > }
547 > private _isToolAllowedByPermissions(sessionKey: ProtocolURI, toolCallId: string): boolean {
548 > const toolName = this._getToolNameForToolCall(sessionKey, toolCallId); sessionPermissions.ts ×6
549 > if (!toolName) {
550 > return false; sessionPermissions.ts ×2
551 > }
552 > // `getEffectiveValue` walks session → parent → host, so sessions sessionPermissions.ts ×3
553 > // that haven't materialized their own `permissions` yet transparently
554 > // inherit from the host-level allow/deny lists.
555 > const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions);
556 > const allowed = permissions?.allow.includes(toolName) ?? false; sessionPermissions.ts ×6
557 > if (allowed) {
558 > this._logService.trace(`[SessionPermissionManager] Auto-approving "${toolName}" via permissions`); sessionPermissions.ts ×2
559 > }
560 > return allowed; sessionPermissions.ts ×3
563 > private _getToolNameForToolCall(sessionKey: ProtocolURI, toolCallId: string): string | undefined {
564 > const sessionState = this._stateManager.getSessionState(sessionKey); sessionPermissions.ts ×6
565 > const parts = sessionState?.activeTurn?.responseParts;
566 > if (!parts) {
567 > return undefined; sessionPermissions.ts ×2
568 > }
569 > for (const rp of parts) { sessionPermissions.ts ×3
570 > if (rp.kind === ResponsePartKind.ToolCall && rp.toolCall.toolCallId === toolCallId) {
571 > return rp.toolCall.toolName;
572 > }
573 > }
574 return undefined;
577 > private _addToolToSessionPermissions(sessionKey: ProtocolURI, toolName: string): void {
578 > const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions) sessionPermissions.ts ×4
579 ?? { allow: [], deny: [] };
580 > if (permissions.allow.includes(toolName)) { sessionPermissions.ts ×4
581 return;
582 }
583 > this._configService.updateSessionConfig(sessionKey, { sessionPermissions.ts ×4
584 > [SessionConfigKey.Permissions]: {
585 > allow: [...permissions.allow, toolName],
586 > deny: [...permissions.deny],
587 > },
588 > });
589 > this._logService.info(`[SessionPermissionManager] Added "${toolName}" to session permissions for ${sessionKey}`);
590 > }