@@ -30,6 +30,7 @@ import type {
3030 Model ,
3131 SkillsListParams ,
3232 SkillsListResponse ,
33+ SandboxPolicy ,
3334 Thread ,
3435 ThreadSourceKind ,
3536 TurnCompletedNotification ,
@@ -50,6 +51,7 @@ export class CodexAcpClient {
5051 private pendingLoginCompleted : Promise < AccountLoginCompletedNotification > | null = null ;
5152 private pendingAccountUpdated : Promise < AccountUpdatedNotification > | null = null ;
5253 private readonly sessionNotificationQueues = new Map < string , Promise < void > > ( ) ;
54+ private skillExtraRoots : string [ ] = [ ] ;
5355
5456
5557 constructor ( codexClient : CodexAppServerClient , codexConfig ?: JsonObject , modelProvider ?: string ) {
@@ -202,10 +204,11 @@ export class CodexAcpClient {
202204 }
203205
204206 async resumeSession ( request : acp . ResumeSessionRequest , onSubscribed ?: ( ) => void ) : Promise < SessionMetadata > {
205- await this . refreshSkills ( request . cwd , request . _meta ) ;
207+ const additionalDirectories = readAdditionalDirectories ( request . cwd , request . additionalDirectories ) ;
208+ await this . refreshSkills ( request . cwd , additionalDirectories , request . _meta ) ;
206209
207210 const response = await this . codexClient . threadResume ( {
208- config : await this . createSessionConfig ( request . cwd , request . mcpServers ?? [ ] ) ,
211+ config : await this . createSessionConfig ( request . cwd , additionalDirectories , request . mcpServers ?? [ ] ) ,
209212 cwd : request . cwd ,
210213 modelProvider : this . getResumeModelProvider ( ) ,
211214 threadId : request . sessionId ,
@@ -218,12 +221,16 @@ export class CodexAcpClient {
218221 currentModelId : currentModelId ,
219222 models : codexModels ,
220223 currentServiceTier : response . serviceTier as ServiceTier ?? null ,
224+ additionalDirectories,
221225 }
222226 }
223227
224228 async loadSession ( request : acp . LoadSessionRequest , onSubscribed ?: ( ) => void ) : Promise < SessionMetadataWithThread > {
229+ const additionalDirectories = readAdditionalDirectories ( request . cwd , request . additionalDirectories ) ;
230+ await this . refreshSkills ( request . cwd , additionalDirectories , request . _meta ) ;
231+
225232 const response = await this . codexClient . threadResume ( {
226- config : await this . createSessionConfig ( request . cwd , request . mcpServers ?? [ ] ) ,
233+ config : await this . createSessionConfig ( request . cwd , additionalDirectories , request . mcpServers ?? [ ] ) ,
227234 cwd : request . cwd ,
228235 modelProvider : this . getResumeModelProvider ( ) ,
229236 threadId : request . sessionId ,
@@ -237,14 +244,16 @@ export class CodexAcpClient {
237244 models : codexModels ,
238245 currentServiceTier : response . serviceTier as ServiceTier ?? null ,
239246 thread : response . thread ,
247+ additionalDirectories,
240248 } ;
241249 }
242250
243251 async newSession ( request : acp . NewSessionRequest ) : Promise < SessionMetadata > {
244- await this . refreshSkills ( request . cwd , request . _meta ) ;
252+ const additionalDirectories = readAdditionalDirectories ( request . cwd , request . additionalDirectories ) ;
253+ await this . refreshSkills ( request . cwd , additionalDirectories , request . _meta ) ;
245254
246255 const response = await this . codexClient . threadStart ( {
247- config : await this . createSessionConfig ( request . cwd , request . mcpServers ) ,
256+ config : await this . createSessionConfig ( request . cwd , additionalDirectories , request . mcpServers ) ,
248257 modelProvider : this . getModelProvider ( ) ,
249258 cwd : request . cwd ,
250259 } ) ;
@@ -259,6 +268,7 @@ export class CodexAcpClient {
259268 currentModelId : currentModelId ,
260269 models : codexModels ,
261270 currentServiceTier : response . serviceTier as ServiceTier ?? null ,
271+ additionalDirectories,
262272 } ;
263273 }
264274
@@ -278,17 +288,21 @@ export class CodexAcpClient {
278288 return this . codexClient . getMcpServerStartupVersion ( ) ;
279289 }
280290
281- private async createSessionConfig ( projectPath : string , mcpServers : Array < McpServer > ) : Promise < JsonObject > {
291+ private async createSessionConfig (
292+ projectPath : string ,
293+ additionalDirectories : string [ ] ,
294+ mcpServers : Array < McpServer >
295+ ) : Promise < JsonObject > {
296+ const sessionRoots = [ projectPath , ...additionalDirectories ] ;
282297 const mergedConfig = {
283298 ...mergeGatewayConfig ( this . config , this . gatewayConfig ) ,
284- projects : {
285- [ projectPath ] : {
286- trust_level : "trusted" ,
287- }
288- } ,
299+ projects : Object . fromEntries ( sessionRoots . map ( root => [ root , {
300+ trust_level : "trusted" ,
301+ } ] ) ) ,
289302 } ;
303+ const configWithWorkspaceRoots = mergeSandboxWorkspaceWriteRoots ( mergedConfig , additionalDirectories ) ;
290304 if ( mcpServers . length === 0 ) {
291- return mergedConfig ;
305+ return configWithWorkspaceRoots ;
292306 }
293307
294308 // Deduplicates new servers against existing config to prevent Codex from deep-merging
@@ -300,11 +314,11 @@ export class CodexAcpClient {
300314 } ) ) ;
301315 const uniqueServers = requestedServers . filter ( mcp => ! existingNames . has ( mcp . name ) ) ;
302316 if ( uniqueServers . length === 0 ) {
303- return mergedConfig ;
317+ return configWithWorkspaceRoots ;
304318 }
305319
306320 return {
307- ...mergedConfig ,
321+ ...configWithWorkspaceRoots ,
308322 "mcp_servers" : Object . fromEntries ( uniqueServers . map ( mcp => [ mcp . name , this . createMcpSeverConfig ( mcp . server ) ] ) ) ,
309323 } ;
310324 }
@@ -328,17 +342,25 @@ export class CodexAcpClient {
328342 return this . getModelProvider ( ) ?? "openai" ;
329343 }
330344
331- private async refreshSkills ( cwd : string , meta ?: Record < string , unknown > | null ) : Promise < void > {
345+ private async refreshSkills (
346+ cwd : string ,
347+ additionalDirectories : string [ ] ,
348+ meta ?: Record < string , unknown > | null
349+ ) : Promise < void > {
332350 if ( ! cwd ) {
333351 return ;
334352 }
335353
336- const additionalRoots = readAdditionalRoots ( meta ) . map ( root => path . join ( root , ".agents" , "skills" ) ) ;
337- if ( additionalRoots . length > 0 ) {
354+ const additionalRoots = uniqueStrings ( [
355+ ...readAdditionalRoots ( meta ) ,
356+ ...additionalDirectories ,
357+ ] ) . map ( root => path . join ( root , ".agents" , "skills" ) ) ;
358+ if ( ! arraysEqual ( this . skillExtraRoots , additionalRoots ) ) {
338359 await this . codexClient . skillsExtraRootsSet ( { extraRoots : additionalRoots } ) ;
360+ this . skillExtraRoots = additionalRoots ;
339361 }
340362 await this . codexClient . listSkills ( {
341- cwds : [ cwd ] ,
363+ cwds : [ cwd , ... additionalDirectories ] ,
342364 forceReload : true ,
343365 } ) ;
344366 }
@@ -444,21 +466,22 @@ export class CodexAcpClient {
444466 serviceTier : ServiceTier | null ,
445467 disableSummary : boolean ,
446468 cwd : string ,
469+ additionalDirectories : string [ ] ,
447470 onTurnStarted ?: ( turnId : string ) => void ,
448471 shouldCancel ?: ( ) => boolean ,
449472 ) : Promise < TurnCompletedNotification | null > {
450473 const input = buildPromptItems ( request . prompt ) ;
451474 const effort = modelId . effort as ReasoningEffort | null ; //TODO remove unsafe conversion
452475
453- await this . refreshSkills ( cwd , request . _meta ) ;
476+ await this . refreshSkills ( cwd , additionalDirectories , request . _meta ) ;
454477 if ( shouldCancel ?.( ) ) {
455478 return null ;
456479 }
457480 return await this . codexClient . runTurn ( {
458481 threadId : request . sessionId ,
459482 input : input ,
460483 approvalPolicy : agentMode . approvalPolicy ,
461- sandboxPolicy : agentMode . sandboxPolicy ,
484+ sandboxPolicy : addAdditionalDirectoriesToSandboxPolicy ( agentMode . sandboxPolicy , additionalDirectories ) ,
462485 summary : disableSummary ? "none" : null ,
463486 effort : effort ,
464487 model : modelId . model ,
@@ -641,6 +664,7 @@ export type SessionMetadata = {
641664 currentModelId : string ,
642665 models : Model [ ] ,
643666 currentServiceTier ?: ServiceTier | null ,
667+ additionalDirectories : string [ ] ,
644668}
645669
646670export type SessionMetadataWithThread = SessionMetadata & {
@@ -701,10 +725,92 @@ function readAdditionalRoots(meta: Record<string, unknown> | null | undefined):
701725 return [ ] ;
702726 }
703727
704- return Array . from ( new Set ( rawRoots
728+ return uniqueStrings ( rawRoots
705729 . filter ( ( value ) : value is string => typeof value === "string" )
706730 . map ( value => value . trim ( ) )
707- . filter ( value => value . length > 0 ) ) ) ;
731+ . filter ( value => value . length > 0 ) ) ;
732+ }
733+
734+ function readAdditionalDirectories ( cwd : string , rawDirectories : unknown ) : string [ ] {
735+ if ( rawDirectories === undefined ) {
736+ return [ ] ;
737+ }
738+ if ( rawDirectories === null ) {
739+ throw RequestError . invalidParams ( undefined , "additionalDirectories must be an array" ) ;
740+ }
741+ if ( ! Array . isArray ( rawDirectories ) ) {
742+ throw RequestError . invalidParams ( undefined , "additionalDirectories must be an array" ) ;
743+ }
744+
745+ const directories : string [ ] = [ ] ;
746+ const seen = new Set < string > ( [ cwd ] ) ;
747+ for ( const directory of rawDirectories ) {
748+ if ( typeof directory !== "string" ) {
749+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must be strings" ) ;
750+ }
751+ if ( directory . length === 0 ) {
752+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must not be empty" ) ;
753+ }
754+ if ( ! path . isAbsolute ( directory ) ) {
755+ throw RequestError . invalidParams ( undefined , "additionalDirectories entries must be absolute paths" ) ;
756+ }
757+ if ( ! seen . has ( directory ) ) {
758+ seen . add ( directory ) ;
759+ directories . push ( directory ) ;
760+ }
761+ }
762+
763+ return directories ;
764+ }
765+
766+ function mergeSandboxWorkspaceWriteRoots ( config : JsonObject , roots : string [ ] ) : JsonObject {
767+ if ( roots . length === 0 ) {
768+ return config ;
769+ }
770+
771+ const existingSandboxConfig = isJsonObject ( config [ "sandbox_workspace_write" ] )
772+ ? config [ "sandbox_workspace_write" ]
773+ : { } ;
774+ const existingWritableRoots = Array . isArray ( existingSandboxConfig [ "writable_roots" ] )
775+ ? existingSandboxConfig [ "writable_roots" ] . filter ( ( value ) : value is string => typeof value === "string" )
776+ : [ ] ;
777+
778+ return {
779+ ...config ,
780+ sandbox_workspace_write : {
781+ ...existingSandboxConfig ,
782+ writable_roots : uniqueStrings ( [ ...existingWritableRoots , ...roots ] ) ,
783+ } ,
784+ } ;
785+ }
786+
787+ function addAdditionalDirectoriesToSandboxPolicy (
788+ sandboxPolicy : SandboxPolicy ,
789+ additionalDirectories : string [ ]
790+ ) : SandboxPolicy {
791+ if ( additionalDirectories . length === 0 || sandboxPolicy . type !== "workspaceWrite" ) {
792+ return sandboxPolicy ;
793+ }
794+
795+ return {
796+ ...sandboxPolicy ,
797+ writableRoots : uniqueStrings ( [ ...sandboxPolicy . writableRoots , ...additionalDirectories ] ) ,
798+ } ;
799+ }
800+
801+ function uniqueStrings ( values : string [ ] ) : string [ ] {
802+ return Array . from ( new Set ( values ) ) ;
803+ }
804+
805+ function arraysEqual ( left : string [ ] , right : string [ ] ) : boolean {
806+ if ( left . length !== right . length ) {
807+ return false ;
808+ }
809+ return left . every ( ( value , index ) => value === right [ index ] ) ;
810+ }
811+
812+ function isJsonObject ( value : JsonValue | undefined ) : value is JsonObject {
813+ return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
708814}
709815
710816function mergeGatewayConfig ( config : JsonObject , gatewayConfig : GatewayConfig | null ) : JsonObject {
0 commit comments