@@ -254,7 +254,7 @@ describe("Codex plugin thread config", () => {
254254 const request = vi . fn ( async ( method : string , params ?: unknown ) => {
255255 if ( method === "app/list" ) {
256256 appListParams . push ( params as v2 . AppsListParams ) ;
257- return { data : [ appInfo ( "google-calendar-app" , true ) ] , nextCursor : null } ;
257+ return { data : [ appInfo ( "google-calendar-app" , true , false ) ] , nextCursor : null } ;
258258 }
259259 if ( method === "plugin/list" ) {
260260 return pluginList ( [ pluginSummary ( "google-calendar" , { installed : true , enabled : true } ) ] ) ;
@@ -317,6 +317,117 @@ describe("Codex plugin thread config", () => {
317317 ] ) ;
318318 } ) ;
319319
320+ it ( "re-enables an OpenClaw-allowed app even when app/list reports it disabled" , async ( ) => {
321+ const appCache = new CodexAppInventoryCache ( ) ;
322+ await appCache . refreshNow ( {
323+ key : "runtime" ,
324+ nowMs : 0 ,
325+ request : async ( ) => ( {
326+ data : [ appInfo ( "google-calendar-app" , true , false ) ] ,
327+ nextCursor : null ,
328+ } ) ,
329+ } ) ;
330+
331+ const config = await buildCodexPluginThreadConfig ( {
332+ pluginConfig : {
333+ codexPlugins : {
334+ enabled : true ,
335+ plugins : {
336+ "google-calendar" : {
337+ marketplaceName : CODEX_PLUGINS_MARKETPLACE_NAME ,
338+ pluginName : "google-calendar" ,
339+ } ,
340+ } ,
341+ } ,
342+ } ,
343+ appCache,
344+ appCacheKey : "runtime" ,
345+ nowMs : 1 ,
346+ request : async ( method ) => {
347+ if ( method === "plugin/list" ) {
348+ return pluginList ( [ pluginSummary ( "google-calendar" , { installed : true , enabled : true } ) ] ) ;
349+ }
350+ if ( method === "plugin/read" ) {
351+ return pluginDetail ( "google-calendar" , [ appSummary ( "google-calendar-app" ) ] ) ;
352+ }
353+ throw new Error ( `unexpected request ${ method } ` ) ;
354+ } ,
355+ } ) ;
356+
357+ expect ( config . inventory ?. records [ 0 ] ?. apps ) . toStrictEqual ( [
358+ {
359+ id : "google-calendar-app" ,
360+ name : "google-calendar-app" ,
361+ accessible : true ,
362+ enabled : false ,
363+ needsAuth : false ,
364+ } ,
365+ ] ) ;
366+ expect ( config . configPatch ?. apps ) . toMatchObject ( {
367+ "google-calendar-app" : {
368+ enabled : true ,
369+ } ,
370+ } ) ;
371+ expect ( config . diagnostics ) . toStrictEqual ( [ ] ) ;
372+ } ) ;
373+
374+ it ( "refreshes missing app inventory when plugin activation becomes unnecessary" , async ( ) => {
375+ const appCache = new CodexAppInventoryCache ( ) ;
376+ const appListParams : v2 . AppsListParams [ ] = [ ] ;
377+ let pluginListCalls = 0 ;
378+ const request = vi . fn ( async ( method : string , params ?: unknown ) => {
379+ if ( method === "plugin/list" ) {
380+ pluginListCalls += 1 ;
381+ const active = pluginListCalls > 1 ;
382+ return pluginList ( [
383+ pluginSummary ( "google-calendar" , { installed : active , enabled : active } ) ,
384+ ] ) ;
385+ }
386+ if ( method === "plugin/read" ) {
387+ return pluginDetail ( "google-calendar" , [ appSummary ( "google-calendar-app" ) ] ) ;
388+ }
389+ if ( method === "app/list" ) {
390+ appListParams . push ( params as v2 . AppsListParams ) ;
391+ return {
392+ data : [ appInfo ( "google-calendar-app" , true ) ] ,
393+ nextCursor : null ,
394+ } satisfies v2 . AppsListResponse ;
395+ }
396+ throw new Error ( `unexpected request ${ method } ` ) ;
397+ } ) ;
398+
399+ const config = await buildCodexPluginThreadConfig ( {
400+ pluginConfig : {
401+ codexPlugins : {
402+ enabled : true ,
403+ plugins : {
404+ "google-calendar" : {
405+ marketplaceName : CODEX_PLUGINS_MARKETPLACE_NAME ,
406+ pluginName : "google-calendar" ,
407+ } ,
408+ } ,
409+ } ,
410+ } ,
411+ appCache,
412+ appCacheKey : "runtime" ,
413+ request,
414+ } ) ;
415+
416+ expect ( config . configPatch ?. apps ) . toMatchObject ( {
417+ "google-calendar-app" : {
418+ enabled : true ,
419+ } ,
420+ } ) ;
421+ expect ( request . mock . calls . map ( ( [ method ] ) => method ) ) . not . toContain ( "plugin/install" ) ;
422+ expect ( appListParams ) . toEqual ( [
423+ {
424+ cursor : undefined ,
425+ limit : 100 ,
426+ forceRefetch : true ,
427+ } ,
428+ ] ) ;
429+ } ) ;
430+
320431 it ( "does not expose plugin apps missing from the app inventory snapshot" , async ( ) => {
321432 const appCache = new CodexAppInventoryCache ( ) ;
322433 await appCache . refreshNow ( {
@@ -375,11 +486,59 @@ describe("Codex plugin thread config", () => {
375486 allowDestructiveActions : true ,
376487 destructiveApprovalMode : "allow" ,
377488 } ,
378- message : "google-calendar-app is not accessible or enabled for google-calendar." ,
489+ message : "google-calendar-app is not accessible for google-calendar." ,
379490 } ,
380491 ] ) ;
381492 } ) ;
382493
494+ it ( "does not expose apps for plugins that OpenClaw policy leaves disabled" , async ( ) => {
495+ const appCache = new CodexAppInventoryCache ( ) ;
496+ await appCache . refreshNow ( {
497+ key : "runtime" ,
498+ nowMs : 0 ,
499+ request : async ( ) => ( {
500+ data : [ appInfo ( "google-calendar-app" , true ) ] ,
501+ nextCursor : null ,
502+ } ) ,
503+ } ) ;
504+
505+ const config = await buildCodexPluginThreadConfig ( {
506+ pluginConfig : {
507+ codexPlugins : {
508+ enabled : true ,
509+ plugins : {
510+ "google-calendar" : {
511+ enabled : false ,
512+ marketplaceName : CODEX_PLUGINS_MARKETPLACE_NAME ,
513+ pluginName : "google-calendar" ,
514+ } ,
515+ } ,
516+ } ,
517+ } ,
518+ appCache,
519+ appCacheKey : "runtime" ,
520+ nowMs : 1 ,
521+ request : async ( method ) => {
522+ if ( method === "plugin/list" ) {
523+ return pluginList ( [ pluginSummary ( "google-calendar" , { installed : true , enabled : true } ) ] ) ;
524+ }
525+ throw new Error ( `unexpected request ${ method } ` ) ;
526+ } ,
527+ } ) ;
528+
529+ expect ( config . configPatch ) . toEqual ( {
530+ apps : {
531+ _default : {
532+ enabled : false ,
533+ destructive_enabled : false ,
534+ open_world_enabled : false ,
535+ } ,
536+ } ,
537+ } ) ;
538+ expect ( config . policyContext . apps ) . toStrictEqual ( { } ) ;
539+ expect ( config . diagnostics ) . toStrictEqual ( [ ] ) ;
540+ } ) ;
541+
383542 it ( "force-refreshes app inventory when proven plugin apps are not ready" , async ( ) => {
384543 const appCache = new CodexAppInventoryCache ( ) ;
385544 await appCache . refreshNow ( {
@@ -572,9 +731,7 @@ describe("Codex plugin thread config", () => {
572731 let installed = false ;
573732 const request = vi . fn ( async ( method : string , params ?: unknown ) => {
574733 if ( method === "plugin/list" ) {
575- return pluginList ( [
576- pluginSummary ( "google-calendar" , { installed, enabled : installed } ) ,
577- ] ) ;
734+ return pluginList ( [ pluginSummary ( "google-calendar" , { installed, enabled : installed } ) ] ) ;
578735 }
579736 if ( method === "plugin/read" ) {
580737 return pluginDetail ( "google-calendar" , [ appSummary ( "google-calendar-app" ) ] ) ;
@@ -738,6 +895,70 @@ describe("Codex plugin thread config", () => {
738895 ] ) ;
739896 } ) ;
740897
898+ it ( "fails closed when app inventory entries are malformed" , async ( ) => {
899+ const appCache = new CodexAppInventoryCache ( ) ;
900+ await appCache . refreshNow ( {
901+ key : "runtime" ,
902+ nowMs : 0 ,
903+ request : async ( ) =>
904+ ( {
905+ data : [ { ...appInfo ( "google-calendar-app" , true ) , id : "" } ] as unknown as v2 . AppInfo [ ] ,
906+ nextCursor : null ,
907+ } ) satisfies v2 . AppsListResponse ,
908+ } ) ;
909+
910+ const config = await buildCodexPluginThreadConfig ( {
911+ pluginConfig : {
912+ codexPlugins : {
913+ enabled : true ,
914+ plugins : {
915+ "google-calendar" : {
916+ marketplaceName : CODEX_PLUGINS_MARKETPLACE_NAME ,
917+ pluginName : "google-calendar" ,
918+ } ,
919+ } ,
920+ } ,
921+ } ,
922+ appCache,
923+ appCacheKey : "runtime" ,
924+ nowMs : 1 ,
925+ request : async ( method ) => {
926+ if ( method === "plugin/list" ) {
927+ return pluginList ( [ pluginSummary ( "google-calendar" , { installed : true , enabled : true } ) ] ) ;
928+ }
929+ if ( method === "plugin/read" ) {
930+ return pluginDetail ( "google-calendar" , [ appSummary ( "google-calendar-app" ) ] ) ;
931+ }
932+ throw new Error ( `unexpected request ${ method } ` ) ;
933+ } ,
934+ } ) ;
935+
936+ expect ( config . configPatch ) . toEqual ( {
937+ apps : {
938+ _default : {
939+ enabled : false ,
940+ destructive_enabled : false ,
941+ open_world_enabled : false ,
942+ } ,
943+ } ,
944+ } ) ;
945+ expect ( config . policyContext . apps ) . toStrictEqual ( { } ) ;
946+ expect ( config . diagnostics ) . toStrictEqual ( [
947+ {
948+ code : "app_not_ready" ,
949+ plugin : {
950+ configKey : "google-calendar" ,
951+ marketplaceName : CODEX_PLUGINS_MARKETPLACE_NAME ,
952+ pluginName : "google-calendar" ,
953+ enabled : true ,
954+ allowDestructiveActions : true ,
955+ destructiveApprovalMode : "allow" ,
956+ } ,
957+ message : "google-calendar-app is not accessible for google-calendar." ,
958+ } ,
959+ ] ) ;
960+ } ) ;
961+
741962 it ( "uses durable policy and app cache key in the cheap input fingerprint" , async ( ) => {
742963 const appCache = new CodexAppInventoryCache ( ) ;
743964 const first = buildCodexPluginThreadConfigInputFingerprint ( {
0 commit comments