@@ -20,7 +20,6 @@ import { CancellationToken } from '../../../../util/vs/base/common/cancellation'
2020import { Codicon } from '../../../../util/vs/base/common/codicons' ;
2121import { Emitter } from '../../../../util/vs/base/common/event' ;
2222import { DisposableStore , IDisposable , toDisposable } from '../../../../util/vs/base/common/lifecycle' ;
23- import { extUriBiasedIgnorePathCase , isEqual } from '../../../../util/vs/base/common/resources' ;
2423import { truncate } from '../../../../util/vs/base/common/strings' ;
2524import { ThemeIcon } from '../../../../util/vs/base/common/themables' ;
2625import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation' ;
@@ -29,12 +28,11 @@ import { IToolsService } from '../../../tools/common/toolsService';
2928import { IChatSessionMetadataStore } from '../../common/chatSessionMetadataStore' ;
3029import { ExternalEditTracker } from '../../common/externalEditTracker' ;
3130import { getWorkingDirectory , isIsolationEnabled , IWorkspaceInfo } from '../../common/workspaceInfo' ;
32- import { enrichToolInvocationWithSubagentMetadata , getAffectedUrisForEditTool , isCopilotCliEditToolCall , isCopilotCLIToolThatCouldRequirePermissions , processToolExecutionComplete , processToolExecutionStart , ToolCall , updateTodoList } from '../common/copilotCLITools' ;
33- import { getCopilotCLISessionStateDir } from './cliHelpers' ;
31+ import { enrichToolInvocationWithSubagentMetadata , isCopilotCliEditToolCall , isCopilotCLIToolThatCouldRequirePermissions , processToolExecutionComplete , processToolExecutionStart , ToolCall , updateTodoList } from '../common/copilotCLITools' ;
3432import type { CopilotCliBridgeSpanProcessor } from './copilotCliBridgeSpanProcessor' ;
3533import { ICopilotCLIImageSupport } from './copilotCLIImageSupport' ;
3634import { handleExitPlanMode } from './exitPlanModeHandler' ;
37- import { PermissionRequest , requestPermission , requiresFileEditconfirmation } from './permissionHelpers' ;
35+ import { handleMcpPermission , handleReadPermission , handleShellPermission , handleWritePermission , type PermissionRequest , type PermissionRequestResult , showInteractivePermissionPrompt } from './permissionHelpers' ;
3836import { IQuestion , IUserQuestionHandler } from './userInputHelpers' ;
3937
4038/**
@@ -429,32 +427,80 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
429427 disposables . add ( toDisposable ( this . _sdkSession . on ( 'permission.requested' , async ( event ) => {
430428 const permissionRequest = event . data . permissionRequest ;
431429 const requestId = event . data . requestId ;
432- const response = await this . requestPermission ( permissionRequest , editTracker ,
433- ( toolCallId : string ) => {
434- const toolData = toolCalls . get ( toolCallId ) ;
435- if ( ! toolData ) {
436- return undefined ;
437- }
438- const data = pendingToolInvocations . get ( toolCallId ) ;
439- if ( data ) {
440- return [ toolData , data [ 2 ] ] as const ;
430+
431+ // Auto-approve all requests when the permission level allows it.
432+ if ( this . _permissionLevel === 'autoApprove' || this . _permissionLevel === 'autopilot' ) {
433+ this . logService . trace ( `[CopilotCLISession] Auto Approving ${ permissionRequest . kind } request (permission level: ${ this . _permissionLevel } )` ) ;
434+ this . _sdkSession . respondToPermission ( requestId , { kind : 'approved' } ) ;
435+ return ;
436+ }
437+
438+ // Resolve tool call data for the permission request.
439+ const toolData = permissionRequest . toolCallId ? toolCalls . get ( permissionRequest . toolCallId ) : undefined ;
440+ const pendingData = permissionRequest . toolCallId ? pendingToolInvocations . get ( permissionRequest . toolCallId ) : undefined ;
441+ const toolParentCallId = pendingData ? pendingData [ 2 ] : undefined ;
442+ const toolInvocationToken = this . _toolInvocationToken as unknown as never ;
443+
444+ try {
445+ let response : PermissionRequestResult ;
446+ if ( this . _permissionLevel === 'autoApprove' || this . _permissionLevel === 'autopilot' ) {
447+ this . logService . trace ( `[CopilotCLISession] Auto Approving ${ permissionRequest . kind } request (permission level: ${ this . _permissionLevel } )` ) ;
448+ response = { kind : 'approved' } ;
449+ } else {
450+ switch ( permissionRequest . kind ) {
451+ case 'read' :
452+ response = await handleReadPermission (
453+ this . sessionId , permissionRequest , toolParentCallId ,
454+ this . attachments , this . _imageSupport , this . workspace , this . workspaceService ,
455+ this . _toolsService , toolInvocationToken , this . logService , token ,
456+ ) ;
457+ break ;
458+ case 'write' :
459+ response = await handleWritePermission (
460+ this . sessionId , permissionRequest , toolData , toolParentCallId ,
461+ this . _stream , editTracker , this . workspace , this . workspaceService ,
462+ this . instantiationService , this . _toolsService , toolInvocationToken , this . logService , token ,
463+ ) ;
464+ break ;
465+ case 'shell' :
466+ response = await handleShellPermission (
467+ permissionRequest , toolParentCallId ,
468+ this . workspace , this . _toolsService , toolInvocationToken , this . logService , token ,
469+ ) ;
470+ break ;
471+ case 'mcp' :
472+ response = await handleMcpPermission (
473+ permissionRequest , toolParentCallId ,
474+ this . _toolsService , toolInvocationToken , this . logService , token ,
475+ ) ;
476+ break ;
477+ default :
478+ response = await showInteractivePermissionPrompt (
479+ permissionRequest , toolParentCallId ,
480+ this . _toolsService , toolInvocationToken , this . logService , token ,
481+ ) ;
482+ break ;
441483 }
442- return [ toolData , undefined ] as const ;
443- } ,
444- token
445- ) ;
446- flushPendingInvocationMessageForToolCallId ( permissionRequest . toolCallId ) ;
484+ }
447485
448- this . _requestLogger . addEntry ( {
449- type : LoggedRequestKind . MarkdownContentRequest ,
450- debugName : `Permission Request` ,
451- startTimeMs : Date . now ( ) ,
452- icon : Codicon . question ,
453- markdownContent : this . _renderPermissionToMarkdown ( permissionRequest , response . kind ) ,
454- isConversationRequest : true
455- } ) ;
486+ flushPendingInvocationMessageForToolCallId ( permissionRequest . toolCallId ) ;
456487
457- this . _sdkSession . respondToPermission ( requestId , response ) ;
488+ this . _requestLogger . addEntry ( {
489+ type : LoggedRequestKind . MarkdownContentRequest ,
490+ debugName : `Permission Request` ,
491+ startTimeMs : Date . now ( ) ,
492+ icon : Codicon . question ,
493+ markdownContent : this . _renderPermissionToMarkdown ( permissionRequest , response . kind ) ,
494+ isConversationRequest : true
495+ } ) ;
496+
497+ this . _sdkSession . respondToPermission ( requestId , response ) ;
498+ }
499+ catch ( error ) {
500+ this . logService . error ( error , `[CopilotCLISession] Error handling permission request of kind ${ permissionRequest . kind } ` ) ;
501+ flushPendingInvocationMessageForToolCallId ( permissionRequest . toolCallId ) ;
502+ this . _sdkSession . respondToPermission ( requestId , { kind : 'denied-interactively-by-user' } ) ;
503+ }
458504 } ) ) ) ;
459505 if ( shouldHandleExitPlanModeRequests ) {
460506 disposables . add ( toDisposable ( this . _sdkSession . on ( 'exit_plan_mode.requested' , async ( event ) => {
@@ -868,132 +914,6 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
868914 return this . _sdkSession . getSelectedModel ( ) ;
869915 }
870916
871- private isFileFromSessionWorkspace ( file : Uri ) : boolean {
872- const workingDirectory = getWorkingDirectory ( this . workspace ) ;
873- if ( workingDirectory && extUriBiasedIgnorePathCase . isEqualOrParent ( file , workingDirectory ) ) {
874- return true ;
875- }
876- if ( this . workspace . folder && extUriBiasedIgnorePathCase . isEqualOrParent ( file , this . workspace . folder ) ) {
877- return true ;
878- }
879- // Only if we have a worktree should we check the repository.
880- // As this means the user created a worktree and we have a repository.
881- // & if the worktree is automatically trusted, then so is the repository as we created the worktree from that.
882- if ( this . workspace . worktree && this . workspace . repository && extUriBiasedIgnorePathCase . isEqualOrParent ( file , this . workspace . repository ) ) {
883- return true ;
884- }
885-
886- return false ;
887- }
888- private async requestPermission (
889- permissionRequest : PermissionRequest ,
890- editTracker : ExternalEditTracker ,
891- getToolCall : ( toolCallId : string ) => undefined | [ ToolCall , parentToolCallId : string | undefined ] ,
892- token : vscode . CancellationToken
893- ) : Promise < { kind : 'approved' } | { kind : 'denied-interactively-by-user' } > {
894- if ( this . _permissionLevel === 'autoApprove' || this . _permissionLevel === 'autopilot' ) {
895- this . logService . trace ( `[CopilotCLISession] Auto Approving ${ permissionRequest . kind } request (permission level: ${ this . _permissionLevel } )` ) ;
896- return { kind : 'approved' } ;
897- }
898-
899- const workingDirectory = getWorkingDirectory ( this . workspace ) ;
900-
901- if ( permissionRequest . kind === 'read' ) {
902- // If user is reading a file in the working directory or workspace, auto-approve
903- // read requests. Outside workspace reads (e.g., /etc/passwd) will still require
904- // approval.
905- const data = Uri . file ( permissionRequest . path ) ;
906-
907- if ( this . _imageSupport . isTrustedImage ( data ) ) {
908- return { kind : 'approved' } ;
909- }
910-
911- if ( this . isFileFromSessionWorkspace ( data ) ) {
912- this . logService . trace ( `[CopilotCLISession] Auto Approving request to read file in session workspace ${ permissionRequest . path } ` ) ;
913- return { kind : 'approved' } ;
914- }
915-
916- if ( this . workspaceService . getWorkspaceFolder ( data ) ) {
917- this . logService . trace ( `[CopilotCLISession] Auto Approving request to read workspace file ${ permissionRequest . path } ` ) ;
918- return { kind : 'approved' } ;
919- }
920-
921- // If reading a file from session directory, e.g. plan.md, then auto approve it, this is internal file to Cli.
922- const sessionDir = Uri . joinPath ( Uri . file ( getCopilotCLISessionStateDir ( ) ) , this . sessionId ) ;
923- if ( extUriBiasedIgnorePathCase . isEqualOrParent ( data , sessionDir ) ) {
924- this . logService . trace ( `[CopilotCLISession] Auto Approving request to read Copilot CLI session resource ${ permissionRequest . path } ` ) ;
925- return { kind : 'approved' } ;
926- }
927-
928- // If model is trying to read the contents of a file thats attached, then auto-approve it, as this is an explicit action by the user to share the file with the model.
929- if ( this . attachments . some ( attachment => attachment . type === 'file' && isEqual ( Uri . file ( attachment . path ) , data ) ) ) {
930- this . logService . trace ( `[CopilotCLISession] Auto Approving request to read attached file ${ permissionRequest . path } ` ) ;
931- return { kind : 'approved' } ;
932- }
933- }
934-
935- // Get hold of file thats being edited if this is a edit tool call (requiring write permissions).
936- const toolData = permissionRequest . toolCallId ? getToolCall ( permissionRequest . toolCallId ) : undefined ;
937- const toolCall = toolData ? toolData [ 0 ] : undefined ;
938- const toolParentCallId = toolData ? toolData [ 1 ] : undefined ;
939- const editFiles = toolCall ? getAffectedUrisForEditTool ( toolCall ) : undefined ;
940- // Sometimes we don't get a tool call id for the edit permission request
941- const editFile = permissionRequest . kind === 'write' ? ( editFiles && editFiles . length ? editFiles [ 0 ] : ( permissionRequest . fileName ? Uri . file ( permissionRequest . fileName ) : undefined ) ) : undefined ;
942- if ( workingDirectory && permissionRequest . kind === 'write' && editFile ) {
943- const isWorkspaceFile = this . workspaceService . getWorkspaceFolder ( editFile ) ;
944- const isWorkingDirectoryFile = ! this . workspaceService . getWorkspaceFolder ( workingDirectory ) && extUriBiasedIgnorePathCase . isEqualOrParent ( editFile , workingDirectory ) ;
945-
946- let autoApprove = false ;
947- // If isolation is enabled, we only auto-approve writes within the working directory.
948- if ( isIsolationEnabled ( this . workspace ) && isWorkingDirectoryFile ) {
949- autoApprove = true ;
950- }
951- // If its a workspace file, and not editing protected files, we auto-approve.
952- if ( ! autoApprove && isWorkspaceFile && ! ( await requiresFileEditconfirmation ( this . instantiationService , permissionRequest , toolCall ) ) ) {
953- autoApprove = true ;
954- }
955- // If we're working in the working directory (non-isolation), and not editing protected files, we auto-approve.
956- if ( ! autoApprove && isWorkingDirectoryFile && ! ( await requiresFileEditconfirmation ( this . instantiationService , permissionRequest , toolCall , workingDirectory ) ) ) {
957- autoApprove = true ;
958- }
959-
960- if ( autoApprove ) {
961- this . logService . trace ( `[CopilotCLISession] Auto Approving request ${ editFile . fsPath } ` ) ;
962-
963- // If we're editing a file, start tracking the edit & wait for core to acknowledge it.
964- if ( toolCall && this . _stream ) {
965- this . logService . trace ( `[CopilotCLISession] Starting to track edit for toolCallId ${ toolCall . toolCallId } & file ${ editFile . fsPath } ` ) ;
966- await editTracker . trackEdit ( toolCall . toolCallId , [ editFile ] , this . _stream ) ;
967- }
968-
969- return { kind : 'approved' } ;
970- }
971- }
972- // If reading a file from session directory, e.g. plan.md, then auto approve it, this is internal file to Cli.
973- const sessionDir = Uri . joinPath ( Uri . file ( getCopilotCLISessionStateDir ( ) ) , this . sessionId ) ;
974- if ( permissionRequest . kind === 'write' && editFile && extUriBiasedIgnorePathCase . isEqualOrParent ( editFile , sessionDir ) ) {
975- this . logService . trace ( `[CopilotCLISession] Auto Approving request to write to Copilot CLI session resource ${ editFile . fsPath } ` ) ;
976- return { kind : 'approved' } ;
977- }
978-
979- try {
980- if ( await requestPermission ( this . instantiationService , permissionRequest , toolCall , getWorkingDirectory ( this . workspace ) , this . _toolsService , this . _toolInvocationToken as unknown as never , toolParentCallId , token ) ) {
981- // If we're editing a file, start tracking the edit & wait for core to acknowledge it.
982- if ( editFile && toolCall && this . _stream ) {
983- this . logService . trace ( `[CopilotCLISession] Starting to track edit for toolCallId ${ toolCall . toolCallId } & file ${ editFile . fsPath } ` ) ;
984- await editTracker . trackEdit ( toolCall . toolCallId , [ editFile ] , this . _stream ) ;
985- }
986- return { kind : 'approved' } ;
987- }
988- } catch ( error ) {
989- this . logService . error ( `[CopilotCLISession] Permission request error: ${ error } ` ) ;
990- } finally {
991- this . _permissionRequested = undefined ;
992- }
993-
994- return { kind : 'denied-interactively-by-user' } ;
995- }
996-
997917 private _logRequest ( userPrompt : string , modelId : string , attachments : Attachment [ ] , startTimeMs : number ) : void {
998918 const markdownContent = this . _renderRequestToMarkdown ( userPrompt , modelId , attachments , startTimeMs ) ;
999919 this . _requestLogger . addEntry ( {
0 commit comments