@@ -31,6 +31,7 @@ import type {
3131 ReviewTarget ,
3232 SkillsListParams ,
3333 SkillsListResponse ,
34+ SandboxPolicy ,
3435 Thread ,
3536 ThreadSourceKind ,
3637 TurnCompletedNotification ,
@@ -51,6 +52,7 @@ export class CodexAcpClient {
5152 private pendingLoginCompleted : Promise < AccountLoginCompletedNotification > | null = null ;
5253 private pendingAccountUpdated : Promise < AccountUpdatedNotification > | null = null ;
5354 private readonly sessionNotificationQueues = new Map < string , Promise < void > > ( ) ;
55+ private skillExtraRoots : string [ ] = [ ] ;
5456
5557
5658 constructor ( codexClient : CodexAppServerClient , codexConfig ?: JsonObject , modelProvider ?: string ) {
@@ -203,10 +205,11 @@ export class CodexAcpClient {
203205 }
204206
205207 async resumeSession ( request : acp . ResumeSessionRequest , onSubscribed ?: ( ) => void ) : Promise < SessionMetadata > {
206- await this . refreshSkills ( request . cwd , request . _meta ) ;
208+ const additionalDirectories = readAdditionalDirectories ( request . cwd , request . additionalDirectories ) ;
209+ await this . refreshSkills ( request . cwd , additionalDirectories , request . _meta ) ;
207210
208211 const response = await this . codexClient . threadResume ( {
209- config : await this . createSessionConfig ( request . cwd , request . mcpServers ?? [ ] ) ,
212+ config : await this . createSessionConfig ( request . cwd , additionalDirectories , request . mcpServers ?? [ ] ) ,
210213 cwd : request . cwd ,
211214 modelProvider : this . getResumeModelProvider ( ) ,
212215 threadId : request . sessionId ,
@@ -219,12 +222,16 @@ export class CodexAcpClient {
219222 currentModelId : currentModelId ,
220223 models : codexModels ,
221224 currentServiceTier : response . serviceTier as ServiceTier ?? null ,
225+ additionalDirectories,
222226 }
223227 }
224228
225229 async loadSession ( request : acp . LoadSessionRequest , onSubscribed ?: ( ) => void ) : Promise < SessionMetadataWithThread > {
230+ const additionalDirectories = readAdditionalDirectories ( request . cwd , request . additionalDirectories ) ;
231+ await this . refreshSkills ( request . cwd , additionalDirectories , request . _meta ) ;
232+
226233 const response = await this . codexClient . threadResume ( {
227- config : await this . createSessionConfig ( request . cwd , request . mcpServers ?? [ ] ) ,
234+ config : await this . createSessionConfig ( request . cwd , additionalDirectories , request . mcpServers ?? [ ] ) ,
228235 cwd : request . cwd ,
229236 modelProvider : this . getResumeModelProvider ( ) ,
230237 threadId : request . sessionId ,
@@ -238,14 +245,16 @@ export class CodexAcpClient {
238245 models : codexModels ,
239246 currentServiceTier : response . serviceTier as ServiceTier ?? null ,
240247 thread : response . thread ,
248+ additionalDirectories,
241249 } ;
242250 }
243251
244252 async newSession ( request : acp . NewSessionRequest ) : Promise < SessionMetadata > {
245- await this . refreshSkills ( request . cwd , request . _meta ) ;
253+ const additionalDirectories = readAdditionalDirectories ( request . cwd , request . additionalDirectories ) ;
254+ await this . refreshSkills ( request . cwd , additionalDirectories , request . _meta ) ;
246255
247256 const response = await this . codexClient . threadStart ( {
248- config : await this . createSessionConfig ( request . cwd , request . mcpServers ) ,
257+ config : await this . createSessionConfig ( request . cwd , additionalDirectories , request . mcpServers ) ,
249258 modelProvider : this . getModelProvider ( ) ,
250259 cwd : request . cwd ,
251260 } ) ;
@@ -260,6 +269,7 @@ export class CodexAcpClient {
260269 currentModelId : currentModelId ,
261270 models : codexModels ,
262271 currentServiceTier : response . serviceTier as ServiceTier ?? null ,
272+ additionalDirectories,
263273 } ;
264274 }
265275
@@ -299,17 +309,21 @@ export class CodexAcpClient {
299309 return this . codexClient . getMcpServerStartupVersion ( ) ;
300310 }
301311
302- private async createSessionConfig ( projectPath : string , mcpServers : Array < McpServer > ) : Promise < JsonObject > {
312+ private async createSessionConfig (
313+ projectPath : string ,
314+ additionalDirectories : string [ ] ,
315+ mcpServers : Array < McpServer >
316+ ) : Promise < JsonObject > {
317+ const sessionRoots = [ projectPath , ...additionalDirectories ] ;
303318 const mergedConfig = {
304319 ...mergeGatewayConfig ( this . config , this . gatewayConfig ) ,
305- projects : {
306- [ projectPath ] : {
307- trust_level : "trusted" ,
308- }
309- } ,
320+ projects : Object . fromEntries ( sessionRoots . map ( root => [ root , {
321+ trust_level : "trusted" ,
322+ } ] ) ) ,
310323 } ;
324+ const configWithWorkspaceRoots = mergeSandboxWorkspaceWriteRoots ( mergedConfig , additionalDirectories ) ;
311325 if ( mcpServers . length === 0 ) {
312- return mergedConfig ;
326+ return configWithWorkspaceRoots ;
313327 }
314328
315329 // Deduplicates new servers against existing config to prevent Codex from deep-merging
@@ -321,11 +335,11 @@ export class CodexAcpClient {
321335 } ) ) ;
322336 const uniqueServers = requestedServers . filter ( mcp => ! existingNames . has ( mcp . name ) ) ;
323337 if ( uniqueServers . length === 0 ) {
324- return mergedConfig ;
338+ return configWithWorkspaceRoots ;
325339 }
326340
327341 return {
328- ...mergedConfig ,
342+ ...configWithWorkspaceRoots ,
329343 "mcp_servers" : Object . fromEntries ( uniqueServers . map ( mcp => [ mcp . name , this . createMcpSeverConfig ( mcp . server ) ] ) ) ,
330344 } ;
331345 }
@@ -349,17 +363,25 @@ export class CodexAcpClient {
349363 return this . getModelProvider ( ) ?? "openai" ;
350364 }
351365
352- private async refreshSkills ( cwd : string , meta ?: Record < string , unknown > | null ) : Promise < void > {
366+ private async refreshSkills (
367+ cwd : string ,
368+ additionalDirectories : string [ ] ,
369+ meta ?: Record < string , unknown > | null
370+ ) : Promise < void > {
353371 if ( ! cwd ) {
354372 return ;
355373 }
356374
357- const additionalRoots = readAdditionalRoots ( meta ) . map ( root => path . join ( root , ".agents" , "skills" ) ) ;
358- if ( additionalRoots . length > 0 ) {
375+ const additionalRoots = uniqueStrings ( [
376+ ...readAdditionalRoots ( meta ) ,
377+ ...additionalDirectories ,
378+ ] ) . map ( root => path . join ( root , ".agents" , "skills" ) ) ;
379+ if ( ! arraysEqual ( this . skillExtraRoots , additionalRoots ) ) {
359380 await this . codexClient . skillsExtraRootsSet ( { extraRoots : additionalRoots } ) ;
381+ this . skillExtraRoots = additionalRoots ;
360382 }
361383 await this . codexClient . listSkills ( {
362- cwds : [ cwd ] ,
384+ cwds : [ cwd , ... additionalDirectories ] ,
363385 forceReload : true ,
364386 } ) ;
365387 }
@@ -465,21 +487,22 @@ export class CodexAcpClient {
465487 serviceTier : ServiceTier | null ,
466488 disableSummary : boolean ,
467489 cwd : string ,
490+ additionalDirectories : string [ ] ,
468491 onTurnStarted ?: ( turnId : string ) => void ,
469492 shouldCancel ?: ( ) => boolean ,
470493 ) : Promise < TurnCompletedNotification | null > {
471494 const input = buildPromptItems ( request . prompt ) ;
472495 const effort = modelId . effort as ReasoningEffort | null ; //TODO remove unsafe conversion
473496
474- await this . refreshSkills ( cwd , request . _meta ) ;
497+ await this . refreshSkills ( cwd , additionalDirectories , request . _meta ) ;
475498 if ( shouldCancel ?.( ) ) {
476499 return null ;
477500 }
478501 return await this . codexClient . runTurn ( {
479502 threadId : request . sessionId ,
480503 input : input ,
481504 approvalPolicy : agentMode . approvalPolicy ,
482- sandboxPolicy : agentMode . sandboxPolicy ,
505+ sandboxPolicy : addAdditionalDirectoriesToSandboxPolicy ( agentMode . sandboxPolicy , additionalDirectories ) ,
483506 summary : disableSummary ? "none" : "auto" ,
484507 effort : effort ,
485508 model : modelId . model ,
@@ -662,6 +685,7 @@ export type SessionMetadata = {
662685 currentModelId : string ,
663686 models : Model [ ] ,
664687 currentServiceTier ?: ServiceTier | null ,
688+ additionalDirectories : string [ ] ,
665689}
666690
667691export type SessionMetadataWithThread = SessionMetadata & {
@@ -732,10 +756,92 @@ function readAdditionalRoots(meta: Record<string, unknown> | null | undefined):
732756 return [ ] ;
733757 }
734758
735- return Array . from ( new Set ( rawRoots
759+ return uniqueStrings ( rawRoots
736760 . filter ( ( value ) : value is string => typeof value === "string" )
737761 . map ( value => value . trim ( ) )
738- . filter ( value => value . length > 0 ) ) ) ;
762+ . filter ( value => value . length > 0 ) ) ;
763+ }
764+
765+ function readAdditionalDirectories ( cwd : string , rawDirectories : unknown ) : string [ ] {
766+ if ( rawDirectories === undefined ) {
767+ return [ ] ;
768+ }
769+ if ( rawDirectories === null ) {
770+ throw RequestError . invalidParams ( undefined , "additionalDirectories must be an array" ) ;
771+ }
772+ if ( ! Array . isArray ( rawDirectories ) ) {
773+ throw RequestError . invalidParams ( undefined , "additionalDirectories must be an array" ) ;
774+ }
775+
776+ const directories : string [ ] = [ ] ;
777+ const seen = new Set < string > ( [ cwd ] ) ;
778+ for ( const directory of rawDirectories ) {
779+ if ( typeof directory !== "string" ) {
780+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must be strings" ) ;
781+ }
782+ if ( directory . length === 0 ) {
783+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must not be empty" ) ;
784+ }
785+ if ( ! path . isAbsolute ( directory ) ) {
786+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must be absolute paths" ) ;
787+ }
788+ if ( ! seen . has ( directory ) ) {
789+ seen . add ( directory ) ;
790+ directories . push ( directory ) ;
791+ }
792+ }
793+
794+ return directories ;
795+ }
796+
797+ function mergeSandboxWorkspaceWriteRoots ( config : JsonObject , roots : string [ ] ) : JsonObject {
798+ if ( roots . length === 0 ) {
799+ return config ;
800+ }
801+
802+ const existingSandboxConfig = isJsonObject ( config [ "sandbox_workspace_write" ] )
803+ ? config [ "sandbox_workspace_write" ]
804+ : { } ;
805+ const existingWritableRoots = Array . isArray ( existingSandboxConfig [ "writable_roots" ] )
806+ ? existingSandboxConfig [ "writable_roots" ] . filter ( ( value ) : value is string => typeof value === "string" )
807+ : [ ] ;
808+
809+ return {
810+ ...config ,
811+ sandbox_workspace_write : {
812+ ...existingSandboxConfig ,
813+ writable_roots : uniqueStrings ( [ ...existingWritableRoots , ...roots ] ) ,
814+ } ,
815+ } ;
816+ }
817+
818+ function addAdditionalDirectoriesToSandboxPolicy (
819+ sandboxPolicy : SandboxPolicy ,
820+ additionalDirectories : string [ ]
821+ ) : SandboxPolicy {
822+ if ( additionalDirectories . length === 0 || sandboxPolicy . type !== "workspaceWrite" ) {
823+ return sandboxPolicy ;
824+ }
825+
826+ return {
827+ ...sandboxPolicy ,
828+ writableRoots : uniqueStrings ( [ ...sandboxPolicy . writableRoots , ...additionalDirectories ] ) ,
829+ } ;
830+ }
831+
832+ function uniqueStrings ( values : string [ ] ) : string [ ] {
833+ return Array . from ( new Set ( values ) ) ;
834+ }
835+
836+ function arraysEqual ( left : string [ ] , right : string [ ] ) : boolean {
837+ if ( left . length !== right . length ) {
838+ return false ;
839+ }
840+ return left . every ( ( value , index ) => value === right [ index ] ) ;
841+ }
842+
843+ function isJsonObject ( value : JsonValue | undefined ) : value is JsonObject {
844+ return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
739845}
740846
741847function mergeGatewayConfig ( config : JsonObject , gatewayConfig : GatewayConfig | null ) : JsonObject {
0 commit comments