Path: blob/main/src/vs/platform/agentHost/node/sessionPermissions.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { match as globMatch } from '../../../base/common/glob.js';6import { Disposable } from '../../../base/common/lifecycle.js';7import { extUriBiasedIgnorePathCase, normalizePath } from '../../../base/common/resources.js';8import { URI } from '../../../base/common/uri.js';9import { localize } from '../../../nls.js';10import { ILogService } from '../../log/common/log.js';11import type { IAgentToolPendingConfirmationSignal } from '../common/agentService.js';12import { platformSessionSchema } from '../common/agentHostSchema.js';13import { SessionConfigKey } from '../common/sessionConfigKeys.js';14import { ConfirmationOptionKind, type ConfirmationOption } from '../common/state/protocol/state.js';15import { ActionType, type IToolCallReadyAction } from '../common/state/sessionActions.js';16import {17ResponsePartKind,18ToolCallConfirmationReason,19type URI as ProtocolURI,20} from '../common/state/sessionState.js';21import { IAgentConfigurationService } from './agentConfigurationService.js';22import { AgentHostStateManager } from './agentHostStateManager.js';23import { CommandAutoApprover } from './commandAutoApprover.js';2425/**26* Event fields needed for auto-approval decisions.27* Matches the subset of {@link IAgentToolPendingConfirmationSignal} used by the28* approval pipeline.29*/30export interface IToolApprovalEvent {31readonly toolCallId: string;32readonly session: URI;33readonly permissionKind?: IAgentToolPendingConfirmationSignal['permissionKind'];34readonly permissionPath?: string;35readonly toolInput?: string;36}3738/** Standard per-tool confirmation options presented to the user. */39const ALLOW_SESSION_OPTION_ID = 'allow-session';40const CONFIRMATION_OPTIONS: readonly ConfirmationOption[] = [41{ id: ALLOW_SESSION_OPTION_ID, label: localize('sessionPermissions.allowSession', "Allow in this Session"), kind: ConfirmationOptionKind.Approve, group: 1 },42{ id: 'allow-once', label: localize('sessionPermissions.allowOnce', "Allow Once"), kind: ConfirmationOptionKind.Approve },43{ id: 'skip', label: localize('sessionPermissions.skip', "Skip"), kind: ConfirmationOptionKind.Deny, group: 2 },44];4546/** Default write-path glob rules applied to auto-approved edits. */47const DEFAULT_EDIT_AUTO_APPROVE_PATTERNS: Readonly<Record<string, boolean>> = {48'**/*': true,49'**/.vscode/*.json': false,50'**/.git/**': false,51'**/{package.json,server.xml,build.rs,web.config,.gitattributes,.env}': false,52'**/*.{code-workspace,csproj,fsproj,vbproj,vcxproj,proj,targets,props}': false,53'**/*.lock': false,54'**/*-lock.{yaml,json}': false,55};5657/**58* Single entry point for all tool-call approval logic in the agent host.59*60* Modeled after {@link ILanguageModelToolsConfirmationService} in the61* workbench layer, this manager owns:62*63* - **Auto-approval** (`getAutoApproval`) — checks session-level config,64* per-tool session permissions, read/write path rules, and shell65* command rules. Returns a {@link ToolCallConfirmationReason} when66* the tool should be auto-approved, or `undefined` when user67* confirmation is needed.68*69* - **Confirmation options** (`createToolReadyAction`) — constructs the70* protocol action with the standard "Allow Once / Allow in this71* Session / Skip" options baked in.72*73* - **Post-confirmation side effects** (`handleToolCallConfirmed`) —74* persists the user's choice (e.g. adding a tool to the session75* permissions list).76*/77export class SessionPermissionManager extends Disposable {787980// ---- Edit auto-approve patterns -----------------------------------------8182private readonly _commandAutoApprover: CommandAutoApprover;8384constructor(85private readonly _stateManager: AgentHostStateManager,86@IAgentConfigurationService private readonly _configService: IAgentConfigurationService,87@ILogService private readonly _logService: ILogService,88) {89super();90this._commandAutoApprover = this._register(new CommandAutoApprover(this._logService));91}9293/**94* Initializes async resources (tree-sitter WASM) used for shell command95* auto-approval. Await this before any session events can arrive to96* guarantee that {@link getAutoApproval} is fully synchronous.97*/98initialize(): Promise<void> {99return this._commandAutoApprover.initialize();100}101102// ---- Auto-approval (analogous to getPreConfirmAction) -------------------103104/**105* Synchronously checks whether a `tool_ready` event should be106* auto-approved. Returns a {@link ToolCallConfirmationReason} when the107* tool call should proceed without user interaction, or `undefined`108* when user confirmation is required.109*110* Checks are evaluated in order:111* 1. Session-level bypass (`autoApprove` / `autopilot` config)112* 2. Per-tool session permissions (`permissions.allow`)113* 3. Read path rules (within working directory)114* 4. Write path rules (within working directory + glob patterns)115* 5. Shell command rules (tree-sitter parsed, default allow/deny)116*/117getAutoApproval(e: IToolApprovalEvent, sessionKey: ProtocolURI): ToolCallConfirmationReason | undefined {118const autoApproveLevel = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.AutoApprove);119const workDir = this._configService.getEffectiveWorkingDirectory(sessionKey);120121// 1. Session-level auto-approve122if (autoApproveLevel === 'autoApprove' || autoApproveLevel === 'autopilot') {123this._logService.trace(`[SessionPermissionManager] Auto-approving tool call (session autoApprove=${autoApproveLevel})`);124return ToolCallConfirmationReason.Setting;125}126127// 2. Per-tool session permissions128if (this._isToolAllowedByPermissions(sessionKey, e.toolCallId)) {129return ToolCallConfirmationReason.Setting;130}131132// 3. Read auto-approval133if (e.permissionKind === 'read' && e.permissionPath) {134if (this._isPathInWorkingDirectory(e.permissionPath, workDir)) {135this._logService.trace(`[SessionPermissionManager] Auto-approving read of ${e.permissionPath}`);136return ToolCallConfirmationReason.NotNeeded;137}138return undefined;139}140141// 4. Write auto-approval142if (e.permissionKind === 'write' && e.permissionPath) {143if (this._isPathInWorkingDirectory(e.permissionPath, workDir) && this._isEditAutoApproved(e.permissionPath)) {144this._logService.trace(`[SessionPermissionManager] Auto-approving write to ${e.permissionPath}`);145return ToolCallConfirmationReason.NotNeeded;146}147return undefined;148}149150// 5. Shell auto-approval151if (e.permissionKind === 'shell' && e.toolInput) {152const result = this._commandAutoApprover.shouldAutoApprove(e.toolInput);153if (result === 'approved') {154this._logService.trace('[SessionPermissionManager] Auto-approving shell command');155return ToolCallConfirmationReason.NotNeeded;156}157if (result === 'denied') {158this._logService.trace('[SessionPermissionManager] Shell command denied by rule');159}160return undefined;161}162163return undefined;164}165166// ---- Action construction (analogous to getPreConfirmActions) -------------167168/**169* Constructs a `SessionToolCallReady` action from an agent170* `pending_confirmation` signal. When the tool needs user confirmation171* (the protocol state carries `confirmationTitle`), the standard172* confirmation options are baked in so clients can render them directly.173*/174createToolReadyAction(e: IAgentToolPendingConfirmationSignal, sessionKey: ProtocolURI, turnId: string): IToolCallReadyAction {175const state = e.state;176if (state.confirmationTitle) {177return {178type: ActionType.SessionToolCallReady,179session: sessionKey,180turnId,181toolCallId: state.toolCallId,182invocationMessage: state.invocationMessage,183toolInput: state.toolInput,184confirmationTitle: state.confirmationTitle,185edits: state.edits,186editable: state.editable,187options: CONFIRMATION_OPTIONS.slice(),188};189}190return {191type: ActionType.SessionToolCallReady,192session: sessionKey,193turnId,194toolCallId: state.toolCallId,195invocationMessage: state.invocationMessage,196toolInput: state.toolInput,197confirmed: ToolCallConfirmationReason.NotNeeded,198};199}200201// ---- Post-confirmation side effects -------------------------------------202203/**204* Handles the side effect of a `SessionToolCallConfirmed` action when the205* user selected "Allow in this Session". Adds the tool to the session's206* permission allow list so future calls are auto-approved.207*/208handleToolCallConfirmed(sessionKey: ProtocolURI, toolCallId: string, selectedOptionId: string | undefined): void {209if (selectedOptionId === ALLOW_SESSION_OPTION_ID) {210const toolName = this._getToolNameForToolCall(sessionKey, toolCallId);211if (toolName) {212this._addToolToSessionPermissions(sessionKey, toolName);213}214}215}216217// ---- Internal helpers ---------------------------------------------------218219private _isPathInWorkingDirectory(filePath: string, workDir: string | undefined): boolean {220if (!workDir) {221return false;222}223const workingDirectory = URI.parse(workDir);224return extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(URI.file(filePath)), workingDirectory);225}226227private _isEditAutoApproved(filePath: string): boolean {228let approved = true;229for (const [pattern, isApproved] of Object.entries(DEFAULT_EDIT_AUTO_APPROVE_PATTERNS)) {230if (isApproved !== approved && globMatch(pattern, filePath)) {231approved = isApproved;232}233}234return approved;235}236237private _isToolAllowedByPermissions(sessionKey: ProtocolURI, toolCallId: string): boolean {238const toolName = this._getToolNameForToolCall(sessionKey, toolCallId);239if (!toolName) {240return false;241}242// `getEffectiveValue` walks session → parent → host, so sessions243// that haven't materialized their own `permissions` yet transparently244// inherit from the host-level allow/deny lists.245const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions);246const allowed = permissions?.allow.includes(toolName) ?? false;247if (allowed) {248this._logService.trace(`[SessionPermissionManager] Auto-approving "${toolName}" via permissions`);249}250return allowed;251}252253private _getToolNameForToolCall(sessionKey: ProtocolURI, toolCallId: string): string | undefined {254const sessionState = this._stateManager.getSessionState(sessionKey);255const parts = sessionState?.activeTurn?.responseParts;256if (!parts) {257return undefined;258}259for (const rp of parts) {260if (rp.kind === ResponsePartKind.ToolCall && rp.toolCall.toolCallId === toolCallId) {261return rp.toolCall.toolName;262}263}264return undefined;265}266267private _addToolToSessionPermissions(sessionKey: ProtocolURI, toolName: string): void {268const permissions = this._configService.getEffectiveValue(sessionKey, platformSessionSchema, SessionConfigKey.Permissions)269?? { allow: [], deny: [] };270if (permissions.allow.includes(toolName)) {271return;272}273this._configService.updateSessionConfig(sessionKey, {274[SessionConfigKey.Permissions]: {275allow: [...permissions.allow, toolName],276deny: [...permissions.deny],277},278});279this._logService.info(`[SessionPermissionManager] Added "${toolName}" to session permissions for ${sessionKey}`);280}281}282283284