88// higher-level API matching IAgentService.
99
1010import { DeferredPromise } from '../../../base/common/async.js' ;
11+ import { CancellationError } from '../../../base/common/errors.js' ;
1112import { Emitter } from '../../../base/common/event.js' ;
1213import { Disposable , IReference } from '../../../base/common/lifecycle.js' ;
1314import { Schemas } from '../../../base/common/network.js' ;
@@ -19,36 +20,25 @@ import { FileSystemProviderErrorCode, IFileService, toFileSystemProviderErrorCod
1920import { AgentSession , IAgentConnection , IAgentCreateSessionConfig , IAgentResolveSessionConfigParams , IAgentSessionConfigCompletionsParams , IAgentSessionMetadata , AuthenticateParams , AuthenticateResult } from '../common/agentService.js' ;
2021import { AgentSubscriptionManager , type IAgentSubscription } from '../common/state/agentSubscription.js' ;
2122import { agentHostAuthority , fromAgentHostUri , toAgentHostUri } from '../common/agentHostUri.js' ;
23+ import { AgentHostPermissionMode , IAgentHostPermissionService } from '../common/agentHostPermissionService.js' ;
2224import type { ClientNotificationMap , CommandMap , JsonRpcErrorResponse , JsonRpcRequest } from '../common/state/protocol/messages.js' ;
23- import type { ActionEnvelope , INotification , IRootConfigChangedAction , SessionAction , TerminalAction } from '../common/state/sessionActions.js' ;
24- import { SessionSummary , SessionStatus , ROOT_STATE_URI , StateComponents , type RootState } from '../common/state/sessionState.js' ;
25+ import { ActionType , type ActionEnvelope , type INotification , type IRootConfigChangedAction , type SessionAction , type TerminalAction } from '../common/state/sessionActions.js' ;
26+ import { SessionSummary , SessionStatus , ROOT_STATE_URI , StateComponents , type CustomizationRef , type RootState } from '../common/state/sessionState.js' ;
2527import { PROTOCOL_VERSION } from '../common/state/sessionCapabilities.js' ;
26- import { isJsonRpcNotification , isJsonRpcRequest , isJsonRpcResponse , type ProtocolMessage , type IStateSnapshot } from '../common/state/sessionProtocol.js' ;
28+ import { isJsonRpcNotification , isJsonRpcRequest , isJsonRpcResponse , ProtocolError , type ProtocolMessage , type IStateSnapshot } from '../common/state/sessionProtocol.js' ;
2729import { isClientTransport , type IProtocolTransport } from '../common/state/sessionTransport.js' ;
2830import { AhpErrorCodes } from '../common/state/protocol/errors.js' ;
29- import { ContentEncoding , type CreateTerminalParams , type ResolveSessionConfigResult , type SessionConfigCompletionsResult } from '../common/state/protocol/commands.js' ;
31+ import { ContentEncoding , ResourceRequestParams , type CreateTerminalParams , type ResolveSessionConfigResult , type SessionConfigCompletionsResult } from '../common/state/protocol/commands.js' ;
3032import { decodeBase64 , encodeBase64 , VSBuffer } from '../../../base/common/buffer.js' ;
3133
3234const AHP_CLIENT_CONNECTION_CLOSED = - 32000 ;
3335
34- export class RemoteAgentHostProtocolError extends Error {
35-
36- readonly code : number ;
37- readonly data : unknown | undefined ;
38-
39- constructor ( error : JsonRpcErrorResponse [ 'error' ] ) {
40- super ( error . message ) ;
41- this . code = error . code ;
42- this . data = error . data ;
43- }
44-
45- static connectionClosed ( address : string ) : RemoteAgentHostProtocolError {
46- return new RemoteAgentHostProtocolError ( { code : AHP_CLIENT_CONNECTION_CLOSED , message : `Connection closed: ${ address } ` } ) ;
47- }
36+ function connectionClosedError ( address : string ) : ProtocolError {
37+ return new ProtocolError ( AHP_CLIENT_CONNECTION_CLOSED , `Connection closed: ${ address } ` ) ;
38+ }
4839
49- static disposed ( address : string ) : RemoteAgentHostProtocolError {
50- return new RemoteAgentHostProtocolError ( { code : AHP_CLIENT_CONNECTION_CLOSED , message : `Connection disposed: ${ address } ` } ) ;
51- }
40+ function connectionDisposedError ( address : string ) : ProtocolError {
41+ return new ProtocolError ( AHP_CLIENT_CONNECTION_CLOSED , `Connection disposed: ${ address } ` ) ;
5242}
5343
5444interface IRemoteAgentHostExtensionCommandMap {
@@ -89,7 +79,14 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
8979 private readonly _pendingRequests = new Map < number , DeferredPromise < unknown > > ( ) ;
9080 private _nextRequestId = 1 ;
9181 private _isClosed = false ;
92- private _closeError : RemoteAgentHostProtocolError | undefined ;
82+ private _closeError : ProtocolError | undefined ;
83+
84+ /**
85+ * Comparison keys of customization URIs we have already granted implicit
86+ * read access for on this connection. Dedupes repeat sends so we don't
87+ * pile up grants per dispatch. Cleared with the connection.
88+ */
89+ private readonly _grantedCustomizationUris = new Set < string > ( ) ;
9390
9491 get clientId ( ) : string {
9592 return this . _clientId ;
@@ -108,14 +105,15 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
108105 transport : IProtocolTransport ,
109106 @ILogService private readonly _logService : ILogService ,
110107 @IFileService private readonly _fileService : IFileService ,
108+ @IAgentHostPermissionService private readonly _permissionService : IAgentHostPermissionService ,
111109 ) {
112110 super ( ) ;
113111 this . _address = address ;
114112 this . _connectionAuthority = agentHostAuthority ( address ) ;
115113 this . _transport = transport ;
116114 this . _register ( this . _transport ) ;
117115 this . _register ( this . _transport . onMessage ( msg => this . _handleMessage ( msg ) ) ) ;
118- this . _register ( this . _transport . onClose ( ( ) => this . _handleClose ( RemoteAgentHostProtocolError . connectionClosed ( this . _address ) ) ) ) ;
116+ this . _register ( this . _transport . onClose ( ( ) => this . _handleClose ( connectionClosedError ( this . _address ) ) ) ) ;
119117
120118 this . _subscriptionManager = this . _register ( new AgentSubscriptionManager (
121119 this . _clientId ,
@@ -132,7 +130,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
132130 }
133131
134132 override dispose ( ) : void {
135- this . _handleClose ( RemoteAgentHostProtocolError . disposed ( this . _address ) ) ;
133+ this . _handleClose ( connectionDisposedError ( this . _address ) ) ;
136134 super . dispose ( ) ;
137135 }
138136
@@ -206,6 +204,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
206204 * Dispatch a client action to the server. Returns the clientSeq used.
207205 */
208206 dispatchAction ( action : SessionAction | TerminalAction | IRootConfigChangedAction , _clientId : string , clientSeq : number ) : void {
207+ this . _grantImplicitReadsForOutgoingAction ( action ) ;
209208 this . _sendNotification ( 'dispatchAction' , { clientSeq, action } ) ;
210209 }
211210
@@ -218,6 +217,9 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
218217 throw new Error ( 'Cannot create remote agent host session without a provider.' ) ;
219218 }
220219 const session = config ?. session ?? AgentSession . uri ( provider , generateUuid ( ) ) ;
220+ if ( config ?. activeClient ?. customizations ) {
221+ this . _grantImplicitReadsForCustomizations ( config . activeClient . customizations ) ;
222+ }
221223 await this . _sendRequest ( 'createSession' , {
222224 session : session . toString ( ) ,
223225 provider,
@@ -312,6 +314,43 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
312314 return uri . scheme === Schemas . file ? toAgentHostUri ( uri , this . _connectionAuthority ) : uri ;
313315 }
314316
317+ /**
318+ * Inspect an outgoing client-dispatched action and grant implicit reads
319+ * for any customization URIs it carries. Today this covers
320+ * `SessionActiveClientChanged`, which is the only client-dispatched
321+ * action that ships customization URIs to the host.
322+ */
323+ private _grantImplicitReadsForOutgoingAction ( action : SessionAction | TerminalAction | IRootConfigChangedAction ) : void {
324+ if ( action . type === ActionType . SessionActiveClientChanged && action . activeClient ?. customizations ) {
325+ this . _grantImplicitReadsForCustomizations ( action . activeClient . customizations ) ;
326+ }
327+ }
328+
329+ /**
330+ * Register implicit read grants for each customization URI that we are
331+ * about to send to the host. The host needs to read these to materialize
332+ * the customization, but should not need to write them. Grants are
333+ * deduped per connection and revoked when the connection closes.
334+ */
335+ private _grantImplicitReadsForCustomizations ( refs : readonly CustomizationRef [ ] ) : void {
336+ for ( const ref of refs ) {
337+ let uri : URI ;
338+ try {
339+ uri = URI . parse ( ref . uri ) ;
340+ } catch {
341+ continue ;
342+ }
343+ const key = uri . toString ( ) ;
344+ if ( this . _grantedCustomizationUris . has ( key ) ) {
345+ continue ;
346+ }
347+ this . _grantedCustomizationUris . add ( key ) ;
348+ // Disposable is owned by the permission service; cleared on
349+ // connectionClosed.
350+ this . _permissionService . grantImplicitRead ( this . _address , uri ) ;
351+ }
352+ }
353+
315354 /**
316355 * List the contents of a directory on the remote host's filesystem.
317356 */
@@ -382,14 +421,16 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
382421 }
383422 }
384423
385- private _handleClose ( error : RemoteAgentHostProtocolError ) : void {
424+ private _handleClose ( error : ProtocolError ) : void {
386425 if ( this . _isClosed ) {
387426 return ;
388427 }
389428
390429 this . _isClosed = true ;
391430 this . _closeError = error ;
392431 this . _rejectPendingRequests ( error ) ;
432+ this . _permissionService . connectionClosed ( this . _address ) ;
433+ this . _grantedCustomizationUris . clear ( ) ;
393434 this . _onDidClose . fire ( ) ;
394435 }
395436
@@ -413,6 +454,12 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
413454 /**
414455 * Handles reverse RPC requests from the server (e.g. resourceList,
415456 * resourceRead). Reads from the local file service and sends a response.
457+ *
458+ * Filesystem-mutating reverse requests are gated through
459+ * {@link IAgentHostPermissionService} — denied operations return a typed
460+ * `PermissionDenied` error advertising a `resourceRequest` payload that,
461+ * if granted, would unlock the operation. Hosts SHOULD then issue a
462+ * `resourceRequest` and retry.
416463 */
417464 private _handleReverseRequest ( id : number , method : string , params : unknown ) : void {
418465 const sendResult = ( result : unknown ) => {
@@ -428,28 +475,65 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
428475 }
429476 this . _transport . send ( { jsonrpc : '2.0' , id, error : { code, message : err instanceof Error ? err . message : String ( err ) } } ) ;
430477 } ;
431- const handle = ( fn : ( ) => Promise < unknown > ) => {
432- fn ( ) . then ( sendResult , sendError ) ;
478+ const sendPermissionDenied = ( request : ResourceRequestParams | undefined ) => {
479+ this . _transport . send ( {
480+ jsonrpc : '2.0' ,
481+ id,
482+ error : {
483+ code : AhpErrorCodes . PermissionDenied ,
484+ message : request
485+ ? `Access to ${ request . uri } is not granted.`
486+ : 'Access to the requested resource is not granted.' ,
487+ data : request ? { request } : undefined ,
488+ } ,
489+ } ) ;
490+ } ;
491+
492+ /**
493+ * Runs `fn` if the permission service grants access for `(uri, mode)`.
494+ * Otherwise replies with `PermissionDenied` advertising the request
495+ * that, if granted, would unlock the operation. Errors thrown from
496+ * `fn` are reported via `sendError`.
497+ */
498+ const gateAndHandle = async (
499+ uri : URI ,
500+ mode : AgentHostPermissionMode ,
501+ deniedRequest : ResourceRequestParams ,
502+ fn : ( ) => Promise < unknown > ,
503+ ) : Promise < void > => {
504+ try {
505+ if ( ! await this . _permissionService . check ( this . _address , uri , mode ) ) {
506+ sendPermissionDenied ( deniedRequest ) ;
507+ return ;
508+ }
509+ sendResult ( await fn ( ) ) ;
510+ } catch ( err ) {
511+ sendError ( err ) ;
512+ }
433513 } ;
434514
435515 const p = params as Record < string , unknown > ;
436516 switch ( method ) {
437- case 'resourceList' :
517+ case 'resourceList' : {
438518 if ( ! p . uri ) { sendError ( new Error ( 'Missing uri' ) ) ; return ; }
439- return handle ( async ( ) => {
440- const stat = await this . _fileService . resolve ( URI . parse ( p . uri as string ) ) ;
519+ const uri = URI . parse ( p . uri as string ) ;
520+ return void gateAndHandle ( uri , AgentHostPermissionMode . Read , { uri : uri . toString ( ) , read : true } , async ( ) => {
521+ const stat = await this . _fileService . resolve ( uri ) ;
441522 return { entries : ( stat . children ?? [ ] ) . map ( c => ( { name : c . name , type : c . isDirectory ? 'directory' as const : 'file' as const } ) ) } ;
442523 } ) ;
443- case 'resourceRead' :
524+ }
525+ case 'resourceRead' : {
444526 if ( ! p . uri ) { sendError ( new Error ( 'Missing uri' ) ) ; return ; }
445- return handle ( async ( ) => {
446- const content = await this . _fileService . readFile ( URI . parse ( p . uri as string ) ) ;
527+ const uri = URI . parse ( p . uri as string ) ;
528+ return void gateAndHandle ( uri , AgentHostPermissionMode . Read , { uri : uri . toString ( ) , read : true } , async ( ) => {
529+ const content = await this . _fileService . readFile ( uri ) ;
447530 return { data : encodeBase64 ( content . value ) , encoding : ContentEncoding . Base64 } ;
448531 } ) ;
449- case 'resourceWrite' :
532+ }
533+ case 'resourceWrite' : {
450534 if ( ! p . uri || ! p . data ) { sendError ( new Error ( 'Missing uri or data' ) ) ; return ; }
451- return handle ( async ( ) => {
452- const writeUri = URI . parse ( p . uri as string ) ;
535+ const writeUri = URI . parse ( p . uri as string ) ;
536+ return void gateAndHandle ( writeUri , AgentHostPermissionMode . Write , { uri : writeUri . toString ( ) , write : true } , async ( ) => {
453537 const buf = p . encoding === ContentEncoding . Base64
454538 ? decodeBase64 ( p . data as string )
455539 : VSBuffer . fromString ( p . data as string ) ;
@@ -460,12 +544,51 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
460544 }
461545 return { } ;
462546 } ) ;
463- case 'resourceDelete' :
547+ }
548+ case 'resourceDelete' : {
464549 if ( ! p . uri ) { sendError ( new Error ( 'Missing uri' ) ) ; return ; }
465- return handle ( ( ) => this . _fileService . del ( URI . parse ( p . uri as string ) , { recursive : ! ! p . recursive } ) . then ( ( ) => ( { } ) ) ) ;
466- case 'resourceMove' :
550+ const deleteUri = URI . parse ( p . uri as string ) ;
551+ return void gateAndHandle ( deleteUri , AgentHostPermissionMode . Write , { uri : deleteUri . toString ( ) , write : true } , ( ) =>
552+ this . _fileService . del ( deleteUri , { recursive : ! ! p . recursive } ) . then ( ( ) => ( { } ) ) ) ;
553+ }
554+ case 'resourceMove' : {
467555 if ( ! p . source || ! p . destination ) { sendError ( new Error ( 'Missing source or destination' ) ) ; return ; }
468- return handle ( ( ) => this . _fileService . move ( URI . parse ( p . source as string ) , URI . parse ( p . destination as string ) , ! p . failIfExists ) . then ( ( ) => ( { } ) ) ) ;
556+ const sourceUri = URI . parse ( p . source as string ) ;
557+ const destUri = URI . parse ( p . destination as string ) ;
558+ return void ( async ( ) => {
559+ try {
560+ const [ sourceOk , destOk ] = await Promise . all ( [
561+ this . _permissionService . check ( this . _address , sourceUri , AgentHostPermissionMode . Write ) ,
562+ this . _permissionService . check ( this . _address , destUri , AgentHostPermissionMode . Write ) ,
563+ ] ) ;
564+ if ( ! sourceOk ) {
565+ sendPermissionDenied ( { uri : sourceUri . toString ( ) , write : true } ) ;
566+ return ;
567+ }
568+ if ( ! destOk ) {
569+ sendPermissionDenied ( { uri : destUri . toString ( ) , write : true } ) ;
570+ return ;
571+ }
572+ await this . _fileService . move ( sourceUri , destUri , ! p . failIfExists ) ;
573+ sendResult ( { } ) ;
574+ } catch ( err ) {
575+ sendError ( err ) ;
576+ }
577+ } ) ( ) ;
578+ }
579+ case 'resourceRequest' : {
580+ const requestParams = p as unknown as ResourceRequestParams ;
581+ this . _permissionService . request ( this . _address , requestParams )
582+ . then ( ( ) => sendResult ( { } ) )
583+ . catch ( err => {
584+ if ( err instanceof CancellationError ) {
585+ sendPermissionDenied ( undefined ) ;
586+ } else {
587+ sendError ( err ) ;
588+ }
589+ } ) ;
590+ return ;
591+ }
469592 default :
470593 this . _logService . warn ( `[RemoteAgentHostProtocol] Unhandled reverse request: ${ method } ` ) ;
471594 sendError ( new Error ( `Unknown method: ${ method } ` ) ) ;
@@ -508,11 +631,11 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
508631 return deferred . p as Promise < IRemoteAgentHostExtensionCommandMap [ M ] [ 'result' ] > ;
509632 }
510633
511- private _toProtocolError ( error : JsonRpcErrorResponse [ 'error' ] ) : RemoteAgentHostProtocolError {
512- return new RemoteAgentHostProtocolError ( error ) ;
634+ private _toProtocolError ( error : JsonRpcErrorResponse [ 'error' ] ) : ProtocolError {
635+ return new ProtocolError ( error . code , error . message , error . data ) ;
513636 }
514637
515- private _rejectPendingRequests ( error : RemoteAgentHostProtocolError ) : void {
638+ private _rejectPendingRequests ( error : ProtocolError ) : void {
516639 for ( const pending of this . _pendingRequests . values ( ) ) {
517640 pending . error ( error ) ;
518641 }
0 commit comments