@@ -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 , request . _meta ) ;
209+ await this . refreshSkills ( request . cwd , additionalDirectories ) ;
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 , request . _meta ) ;
231+ await this . refreshSkills ( request . cwd , additionalDirectories ) ;
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 , request . _meta ) ;
254+ await this . refreshSkills ( request . cwd , additionalDirectories ) ;
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,21 @@ 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+ additionalRoots : string [ ]
369+ ) : Promise < void > {
353370 if ( ! cwd ) {
354371 return ;
355372 }
356373
357- const additionalRoots = readAdditionalRoots ( meta ) . map ( root => path . join ( root , ".agents" , "skills" ) ) ;
358- if ( additionalRoots . length > 0 ) {
359- await this . codexClient . skillsExtraRootsSet ( { extraRoots : additionalRoots } ) ;
374+ const skillExtraRoots = additionalRoots . map ( root => path . join ( root , ".agents" , "skills" ) ) ;
375+ if ( ! arraysEqual ( this . skillExtraRoots , skillExtraRoots ) ) {
376+ await this . codexClient . skillsExtraRootsSet ( { extraRoots : skillExtraRoots } ) ;
377+ this . skillExtraRoots = skillExtraRoots ;
360378 }
361379 await this . codexClient . listSkills ( {
362- cwds : [ cwd ] ,
380+ cwds : [ cwd , ... additionalRoots ] ,
363381 forceReload : true ,
364382 } ) ;
365383 }
@@ -465,21 +483,21 @@ export class CodexAcpClient {
465483 serviceTier : ServiceTier | null ,
466484 disableSummary : boolean ,
467485 cwd : string ,
486+ additionalDirectories : string [ ] ,
468487 onTurnStarted ?: ( turnId : string ) => void ,
469488 shouldCancel ?: ( ) => boolean ,
470489 ) : Promise < TurnCompletedNotification | null > {
471490 const input = buildPromptItems ( request . prompt ) ;
472491 const effort = modelId . effort as ReasoningEffort | null ; //TODO remove unsafe conversion
473-
474- await this . refreshSkills ( cwd , request . _meta ) ;
492+ await this . refreshSkills ( cwd , additionalDirectories ) ;
475493 if ( shouldCancel ?.( ) ) {
476494 return null ;
477495 }
478496 return await this . codexClient . runTurn ( {
479497 threadId : request . sessionId ,
480498 input : input ,
481499 approvalPolicy : agentMode . approvalPolicy ,
482- sandboxPolicy : agentMode . sandboxPolicy ,
500+ sandboxPolicy : addAdditionalDirectoriesToSandboxPolicy ( agentMode . sandboxPolicy , additionalDirectories ) ,
483501 summary : disableSummary ? "none" : "auto" ,
484502 effort : effort ,
485503 model : modelId . model ,
@@ -662,6 +680,7 @@ export type SessionMetadata = {
662680 currentModelId : string ,
663681 models : Model [ ] ,
664682 currentServiceTier ?: ServiceTier | null ,
683+ additionalDirectories : string [ ] ,
665684}
666685
667686export type SessionMetadataWithThread = SessionMetadata & {
@@ -726,16 +745,93 @@ interface GatewayConfig {
726745 }
727746}
728747
729- function readAdditionalRoots ( meta : Record < string , unknown > | null | undefined ) : string [ ] {
748+ function readMetaAdditionalRoots ( meta ? : Record < string , unknown > | null ) : string [ ] | undefined {
730749 const rawRoots = meta ?. [ "additionalRoots" ] ;
731750 if ( ! Array . isArray ( rawRoots ) ) {
732- return [ ] ;
751+ return undefined ;
733752 }
734753
735- return Array . from ( new Set ( rawRoots
754+ return uniqueStrings ( rawRoots
736755 . filter ( ( value ) : value is string => typeof value === "string" )
737756 . map ( value => value . trim ( ) )
738- . filter ( value => value . length > 0 ) ) ) ;
757+ . filter ( value => value . length > 0 ) ) ;
758+ }
759+
760+ function readAdditionalDirectories ( cwd : string , additionalDirectories ?: string [ ] , meta ?: Record < string , unknown > | null ) : string [ ] {
761+ const rawDirectories = additionalDirectories ?? readMetaAdditionalRoots ( meta ) ;
762+ if ( ! rawDirectories ) {
763+ return [ ] ;
764+ }
765+
766+ const directories : string [ ] = [ ] ;
767+ const seen = new Set < string > ( [ cwd ] ) ;
768+ for ( const directory of rawDirectories ) {
769+ if ( typeof directory !== "string" ) {
770+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must be strings" ) ;
771+ }
772+ if ( directory . length === 0 ) {
773+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must not be empty" ) ;
774+ }
775+ if ( ! path . isAbsolute ( directory ) ) {
776+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must be absolute paths" ) ;
777+ }
778+ if ( ! seen . has ( directory ) ) {
779+ seen . add ( directory ) ;
780+ directories . push ( directory ) ;
781+ }
782+ }
783+
784+ return directories ;
785+ }
786+
787+ function mergeSandboxWorkspaceWriteRoots ( config : JsonObject , roots : string [ ] ) : JsonObject {
788+ if ( roots . length === 0 ) {
789+ return config ;
790+ }
791+
792+ const existingSandboxConfig = isJsonObject ( config [ "sandbox_workspace_write" ] )
793+ ? config [ "sandbox_workspace_write" ]
794+ : { } ;
795+ const existingWritableRoots = Array . isArray ( existingSandboxConfig [ "writable_roots" ] )
796+ ? existingSandboxConfig [ "writable_roots" ] . filter ( ( value ) : value is string => typeof value === "string" )
797+ : [ ] ;
798+
799+ return {
800+ ...config ,
801+ sandbox_workspace_write : {
802+ ...existingSandboxConfig ,
803+ writable_roots : uniqueStrings ( [ ...existingWritableRoots , ...roots ] ) ,
804+ } ,
805+ } ;
806+ }
807+
808+ function addAdditionalDirectoriesToSandboxPolicy (
809+ sandboxPolicy : SandboxPolicy ,
810+ additionalDirectories : string [ ]
811+ ) : SandboxPolicy {
812+ if ( additionalDirectories . length === 0 || sandboxPolicy . type !== "workspaceWrite" ) {
813+ return sandboxPolicy ;
814+ }
815+
816+ return {
817+ ...sandboxPolicy ,
818+ writableRoots : uniqueStrings ( [ ...sandboxPolicy . writableRoots , ...additionalDirectories ] ) ,
819+ } ;
820+ }
821+
822+ function uniqueStrings ( values : string [ ] ) : string [ ] {
823+ return Array . from ( new Set ( values ) ) ;
824+ }
825+
826+ function arraysEqual ( left : string [ ] , right : string [ ] ) : boolean {
827+ if ( left . length !== right . length ) {
828+ return false ;
829+ }
830+ return left . every ( ( value , index ) => value === right [ index ] ) ;
831+ }
832+
833+ function isJsonObject ( value : JsonValue | undefined ) : value is JsonObject {
834+ return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
739835}
740836
741837function mergeGatewayConfig ( config : JsonObject , gatewayConfig : GatewayConfig | null ) : JsonObject {
0 commit comments