sessionPermissions.ts ×26

Frontier kind: Code frontier

unlabeled · c_efa07079df33

531 tests · 22782 LOC · 96 files · introduces 0 tests · 252 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
26 ranges252 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1894 ranges22782 lines · 96 files · Browse complete extent
All tests (intent)
531 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

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 native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 252 introduced LOC across 26 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/sessionPermissions.ts 252 introduced LOC · 26 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- sessionPermissions.ts
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
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 {
100 if (fsPath.includes('\0')) {
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> {
159 const fsPath = resource.fsPath;
187 }
188 }
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,
219 options: { realpath?: (fsPath: string) => Promise<string> },
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();
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);
255 const workingDirectory = workDir ? URI.parse(workDir) : undefined;
316 return undefined;
317 }
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;
325 }
327 > getEffectiveApprovalLevel(sessionKey: ProtocolURI): string {
328 return this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.AutoApprove) ?? 'default';
329 }
331 > isSessionAutoApproveEnabled(sessionKey: ProtocolURI): boolean {
332 // `autoApprove` (Allow All) auto-approves every tool call.
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;
346 if (state.confirmationTitle) {
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)) {
384 throw new Error(`Tool call confirmations must be handled on an AHP chat channel: ${chatChannel}`);
392 }
393 }
395 > // ---- Internal helpers ---------------------------------------------------
396 >
397 > private async _isReadAutoApproved(resource: URI, workingDirectory: URI | undefined): Promise<boolean> {
398 if (!workingDirectory) {
399 return false;
408 && resourcesToCheck.every(candidate => workingDirectories.some(directory => this._isResourceInDirectory(candidate, directory)));
409 }
411 > private _isResourceInWorkingDirectory(resource: URI, workingDirectory: URI | undefined): boolean {
412 return workingDirectory !== undefined && this._isResourceInDirectory(resource, workingDirectory);
413 }
415 > private _isResourceInDirectory(resource: URI, directory: URI): boolean {
416 return extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(resource), normalizePath(directory));
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) {
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) {
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);
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];
479 if (resource.scheme !== Schemas.file) {
495 return resourcesToCheck;
496 }
498 > /** Runs the write checks for a single (already symlink-resolved) resource. */
499 > private _checkWriteResource(resource: URI, workingDirectory: URI | undefined): boolean {
500 try {
501 assertPathIsSafe(resource.fsPath);
511 return this._matchesEditAutoApprovePatterns(resource.fsPath);
512 }
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);
522 const topLevelName = relativeToHome?.split('/')[0];
534 return false;
535 }
537 > private _matchesEditAutoApprovePatterns(filePath: string): boolean {
538 let approved = true;
539 for (const [pattern, isApproved] of Object.entries(DEFAULT_EDIT_AUTO_APPROVE_PATTERNS)) {
544 return approved;
545 }
547 > private _isToolAllowedByPermissions(sessionKey: ProtocolURI, toolCallId: string): boolean {
548 const toolName = this._getToolNameForToolCall(sessionKey, toolCallId);
549 if (!toolName) {
560 return allowed;
561 }
563 > private _getToolNameForToolCall(sessionKey: ProtocolURI, toolCallId: string): string | undefined {
564 const sessionState = this._stateManager.getSessionState(sessionKey);
565 const parts = sessionState?.activeTurn?.responseParts;
574 return undefined;
575 }
577 > private _addToolToSessionPermissions(sessionKey: ProtocolURI, toolName: string): void {
578 const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions)
579 ?? { allow: [], deny: [] };
589 this._logService.info(`[SessionPermissionManager] Added "${toolName}" to session permissions for ${sessionKey}`);
590 }