@@ -3878,6 +3878,106 @@ describe("codex command", () => {
38783878 } ) ;
38793879 } ) ;
38803880
3881+ it ( "requires an owner or operator.admin for Codex binding and permission changes" , async ( ) => {
3882+ const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
3883+ const startCodexConversationThread = vi . fn ( ) ;
3884+ const codexControlRequest = vi . fn ( ) ;
3885+ const resolveCodexCliSessionForBindingOnNode = vi . fn ( ) ;
3886+ const stopCodexConversationTurn = vi . fn ( ) ;
3887+ const steerCodexConversationTurn = vi . fn ( ) ;
3888+ const setCodexConversationModel = vi . fn ( ) ;
3889+ const setCodexConversationFastMode = vi . fn ( ) ;
3890+ const setCodexConversationPermissions = vi . fn (
3891+ async ( ) => "Codex permissions set to full access." ,
3892+ ) ;
3893+ const cases = [
3894+ [ "bind" , createDeps ( { startCodexConversationThread } ) , startCodexConversationThread ] ,
3895+ [ "resume thread-123" , createDeps ( { codexControlRequest } ) , codexControlRequest ] ,
3896+ [
3897+ "resume cli-session --host worker-1 --bind here" ,
3898+ createDeps ( { resolveCodexCliSessionForBindingOnNode } ) ,
3899+ resolveCodexCliSessionForBindingOnNode ,
3900+ ] ,
3901+ [ "stop" , createDeps ( { stopCodexConversationTurn } ) , stopCodexConversationTurn ] ,
3902+ [
3903+ "permissions yolo" ,
3904+ createDeps ( { setCodexConversationPermissions } ) ,
3905+ setCodexConversationPermissions ,
3906+ ] ,
3907+ [ "steer continue" , createDeps ( { steerCodexConversationTurn } ) , steerCodexConversationTurn ] ,
3908+ [ "model gpt-5.5" , createDeps ( { setCodexConversationModel } ) , setCodexConversationModel ] ,
3909+ [ "fast on" , createDeps ( { setCodexConversationFastMode } ) , setCodexConversationFastMode ] ,
3910+ [ "compact" , createDeps ( { codexControlRequest } ) , codexControlRequest ] ,
3911+ [ "review" , createDeps ( { codexControlRequest } ) , codexControlRequest ] ,
3912+ ] as const ;
3913+
3914+ for ( const [ args , deps , sideEffect ] of cases ) {
3915+ await expect (
3916+ handleCodexCommand (
3917+ createContext ( args , sessionFile , {
3918+ senderIsOwner : false ,
3919+ gatewayClientScopes : [ "operator.write" ] ,
3920+ } ) ,
3921+ { deps } ,
3922+ ) ,
3923+ ) . resolves . toEqual ( {
3924+ text : "Only an owner or operator.admin can control Codex native execution." ,
3925+ } ) ;
3926+ expect ( sideEffect ) . not . toHaveBeenCalled ( ) ;
3927+ }
3928+
3929+ const detachConversationBinding = vi . fn ( ) ;
3930+ for ( const args of [ "detach" , "unbind" ] ) {
3931+ await expect (
3932+ handleCodexCommand (
3933+ createContext ( args , sessionFile , {
3934+ senderIsOwner : false ,
3935+ gatewayClientScopes : [ "operator.write" ] ,
3936+ detachConversationBinding,
3937+ } ) ,
3938+ { deps : createDeps ( ) } ,
3939+ ) ,
3940+ ) . resolves . toEqual ( {
3941+ text : "Only an owner or operator.admin can control Codex native execution." ,
3942+ } ) ;
3943+ }
3944+ expect ( detachConversationBinding ) . not . toHaveBeenCalled ( ) ;
3945+
3946+ const readCodexPermissions = vi . fn ( async ( ) => "Codex permissions: full access." ) ;
3947+ await expect (
3948+ handleCodexCommand (
3949+ createContext ( "permissions status" , sessionFile , {
3950+ senderIsOwner : false ,
3951+ gatewayClientScopes : [ "operator.write" ] ,
3952+ } ) ,
3953+ { deps : createDeps ( { setCodexConversationPermissions : readCodexPermissions } ) } ,
3954+ ) ,
3955+ ) . resolves . toEqual ( { text : "Codex permissions: full access." } ) ;
3956+ expect ( readCodexPermissions ) . toHaveBeenCalledTimes ( 1 ) ;
3957+
3958+ await expect (
3959+ handleCodexCommand (
3960+ createContext ( "permissions yolo" , sessionFile , {
3961+ senderIsOwner : true ,
3962+ gatewayClientScopes : [ "operator.write" ] ,
3963+ } ) ,
3964+ { deps : createDeps ( { setCodexConversationPermissions } ) } ,
3965+ ) ,
3966+ ) . resolves . toEqual ( { text : "Codex permissions set to full access." } ) ;
3967+ expect ( setCodexConversationPermissions ) . toHaveBeenCalledTimes ( 1 ) ;
3968+
3969+ await expect (
3970+ handleCodexCommand (
3971+ createContext ( "permissions yolo" , sessionFile , {
3972+ senderIsOwner : false ,
3973+ gatewayClientScopes : [ "operator.admin" ] ,
3974+ } ) ,
3975+ { deps : createDeps ( { setCodexConversationPermissions } ) } ,
3976+ ) ,
3977+ ) . resolves . toEqual ( { text : "Codex permissions set to full access." } ) ;
3978+ expect ( setCodexConversationPermissions ) . toHaveBeenCalledTimes ( 2 ) ;
3979+ } ) ;
3980+
38813981 it ( "escapes current bound model status before chat display" , async ( ) => {
38823982 const sessionFile = path . join ( tempDir , "session.jsonl" ) ;
38833983 await fs . writeFile (
0 commit comments