@@ -26,6 +26,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
2626
2727let resolvePluginTools : typeof import ( "./tools.js" ) . resolvePluginTools ;
2828let buildPluginToolMetadataKey : typeof import ( "./tools.js" ) . buildPluginToolMetadataKey ;
29+ let resetPluginToolFactoryCache : typeof import ( "./tools.js" ) . resetPluginToolFactoryCache ;
2930let pinActivePluginChannelRegistry : typeof import ( "./runtime.js" ) . pinActivePluginChannelRegistry ;
3031let resetPluginRuntimeStateForTest : typeof import ( "./runtime.js" ) . resetPluginRuntimeStateForTest ;
3132let setActivePluginRegistry : typeof import ( "./runtime.js" ) . setActivePluginRegistry ;
@@ -58,14 +59,15 @@ function createContext() {
5859}
5960
6061function createResolveToolsParams ( params ?: {
62+ context ?: ReturnType < typeof createContext > & Record < string , unknown > ;
6163 toolAllowlist ?: readonly string [ ] ;
6264 existingToolNames ?: Set < string > ;
6365 env ?: NodeJS . ProcessEnv ;
6466 suppressNameConflicts ?: boolean ;
6567 allowGatewaySubagentBinding ?: boolean ;
6668} ) {
6769 return {
68- context : createContext ( ) as never ,
70+ context : ( params ?. context ?? createContext ( ) ) as never ,
6971 ...( params ?. toolAllowlist ? { toolAllowlist : [ ...params . toolAllowlist ] } : { } ) ,
7072 ...( params ?. existingToolNames ? { existingToolNames : params . existingToolNames } : { } ) ,
7173 ...( params ?. env ? { env : params . env } : { } ) ,
@@ -360,7 +362,8 @@ function expectConflictingCoreNameResolution(params: {
360362
361363describe ( "resolvePluginTools optional tools" , ( ) => {
362364 beforeAll ( async ( ) => {
363- ( { buildPluginToolMetadataKey, resolvePluginTools } = await import ( "./tools.js" ) ) ;
365+ ( { buildPluginToolMetadataKey, resetPluginToolFactoryCache, resolvePluginTools } =
366+ await import ( "./tools.js" ) ) ;
364367 ( { pinActivePluginChannelRegistry, resetPluginRuntimeStateForTest, setActivePluginRegistry } =
365368 await import ( "./runtime.js" ) ) ;
366369 ( { clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
@@ -380,11 +383,13 @@ describe("resolvePluginTools optional tools", () => {
380383 } ) ) ;
381384 resetPluginRuntimeStateForTest ?.( ) ;
382385 clearCurrentPluginMetadataSnapshot ?.( ) ;
386+ resetPluginToolFactoryCache ?.( ) ;
383387 } ) ;
384388
385389 afterEach ( ( ) => {
386390 resetPluginRuntimeStateForTest ?.( ) ;
387391 clearCurrentPluginMetadataSnapshot ?.( ) ;
392+ resetPluginToolFactoryCache ?.( ) ;
388393 setLoggerOverride ( null ) ;
389394 loggingState . rawConsole = null ;
390395 resetLogger ( ) ;
@@ -812,6 +817,163 @@ describe("resolvePluginTools optional tools", () => {
812817 expect ( warnSpy ) . not . toHaveBeenCalled ( ) ;
813818 } ) ;
814819
820+ it ( "caches plugin tool factory results for equivalent request context" , ( ) => {
821+ const factory = vi . fn ( ( ) => makeTool ( "cached_tool" ) ) ;
822+ setRegistry ( [
823+ {
824+ pluginId : "cache-test" ,
825+ optional : false ,
826+ source : "/tmp/cache-test.js" ,
827+ names : [ "cached_tool" ] ,
828+ factory,
829+ } ,
830+ ] ) ;
831+
832+ const first = resolvePluginTools ( createResolveToolsParams ( { context : createContext ( ) } ) ) ;
833+ const second = resolvePluginTools ( createResolveToolsParams ( { context : createContext ( ) } ) ) ;
834+
835+ expectResolvedToolNames ( first , [ "cached_tool" ] ) ;
836+ expectResolvedToolNames ( second , [ "cached_tool" ] ) ;
837+ expect ( factory ) . toHaveBeenCalledTimes ( 1 ) ;
838+ expect ( second [ 0 ] ) . toBe ( first [ 0 ] ) ;
839+ } ) ;
840+
841+ it ( "does not reuse plugin tool factory results across sandbox context changes" , ( ) => {
842+ const factory = vi . fn ( ( rawCtx : unknown ) => {
843+ const ctx = rawCtx as { sandboxed ?: boolean } ;
844+ return ctx . sandboxed ? null : makeTool ( "sandbox_sensitive_tool" ) ;
845+ } ) ;
846+ setRegistry ( [
847+ {
848+ pluginId : "sandbox-sensitive" ,
849+ optional : false ,
850+ source : "/tmp/sandbox-sensitive.js" ,
851+ names : [ "sandbox_sensitive_tool" ] ,
852+ factory,
853+ } ,
854+ ] ) ;
855+
856+ const hostTools = resolvePluginTools (
857+ createResolveToolsParams ( {
858+ context : { ...createContext ( ) , sandboxed : false } ,
859+ } ) ,
860+ ) ;
861+ const sandboxedTools = resolvePluginTools (
862+ createResolveToolsParams ( {
863+ context : { ...createContext ( ) , sandboxed : true } ,
864+ } ) ,
865+ ) ;
866+
867+ expectResolvedToolNames ( hostTools , [ "sandbox_sensitive_tool" ] ) ;
868+ expect ( sandboxedTools ) . toEqual ( [ ] ) ;
869+ expect ( factory ) . toHaveBeenCalledTimes ( 2 ) ;
870+ } ) ;
871+
872+ it ( "does not reuse plugin tool factory results across runtime config changes" , ( ) => {
873+ const firstRuntimeConfig = {
874+ ...createContext ( ) . config ,
875+ plugins : { ...createContext ( ) . config . plugins , allow : [ "runtime_sensitive_tool" ] } ,
876+ } ;
877+ const secondRuntimeConfig = {
878+ ...createContext ( ) . config ,
879+ plugins : { ...createContext ( ) . config . plugins , allow : [ "runtime_sensitive_next_tool" ] } ,
880+ } ;
881+ const factory = vi . fn ( ( rawCtx : unknown ) => {
882+ const ctx = rawCtx as { runtimeConfig ?: { plugins ?: { allow ?: string [ ] } } } ;
883+ return makeTool ( ctx . runtimeConfig ?. plugins ?. allow ?. [ 0 ] ?? "runtime_missing_tool" ) ;
884+ } ) ;
885+ setRegistry ( [
886+ {
887+ pluginId : "runtime-sensitive" ,
888+ optional : false ,
889+ source : "/tmp/runtime-sensitive.js" ,
890+ names : [ "runtime_sensitive_tool" , "runtime_sensitive_next_tool" ] ,
891+ factory,
892+ } ,
893+ ] ) ;
894+
895+ const first = resolvePluginTools (
896+ createResolveToolsParams ( {
897+ context : { ...createContext ( ) , runtimeConfig : firstRuntimeConfig as never } ,
898+ } ) ,
899+ ) ;
900+ const second = resolvePluginTools (
901+ createResolveToolsParams ( {
902+ context : { ...createContext ( ) , runtimeConfig : secondRuntimeConfig as never } ,
903+ } ) ,
904+ ) ;
905+
906+ expectResolvedToolNames ( first , [ "runtime_sensitive_tool" ] ) ;
907+ expectResolvedToolNames ( second , [ "runtime_sensitive_next_tool" ] ) ;
908+ expect ( factory ) . toHaveBeenCalledTimes ( 2 ) ;
909+ } ) ;
910+
911+ it ( "reuses plugin tool factory results when only runtime config getter identity changes" , ( ) => {
912+ const runtimeConfig = {
913+ ...createContext ( ) . config ,
914+ plugins : { ...createContext ( ) . config . plugins , allow : [ "getter_sensitive_tool" ] } ,
915+ } ;
916+ const factory = vi . fn ( ( rawCtx : unknown ) => {
917+ const ctx = rawCtx as { getRuntimeConfig ?: ( ) => { plugins ?: { allow ?: string [ ] } } } ;
918+ return makeTool ( ctx . getRuntimeConfig ?.( ) ?. plugins ?. allow ?. [ 0 ] ?? "getter_missing_tool" ) ;
919+ } ) ;
920+ setRegistry ( [
921+ {
922+ pluginId : "getter-sensitive" ,
923+ optional : false ,
924+ source : "/tmp/getter-sensitive.js" ,
925+ names : [ "getter_sensitive_tool" ] ,
926+ factory,
927+ } ,
928+ ] ) ;
929+
930+ const context = createContext ( ) ;
931+ const first = resolvePluginTools (
932+ createResolveToolsParams ( {
933+ context : { ...context , getRuntimeConfig : ( ) => runtimeConfig as never } ,
934+ } ) ,
935+ ) ;
936+ const second = resolvePluginTools (
937+ createResolveToolsParams ( {
938+ context : { ...context , getRuntimeConfig : ( ) => runtimeConfig as never } ,
939+ } ) ,
940+ ) ;
941+
942+ expectResolvedToolNames ( first , [ "getter_sensitive_tool" ] ) ;
943+ expectResolvedToolNames ( second , [ "getter_sensitive_tool" ] ) ;
944+ expect ( factory ) . toHaveBeenCalledTimes ( 1 ) ;
945+ } ) ;
946+
947+ it ( "reads live runtime config once per plugin tool resolution for cache keys" , ( ) => {
948+ const runtimeConfig = createContext ( ) . config ;
949+ const getRuntimeConfig = vi . fn ( ( ) => runtimeConfig ) ;
950+ setRegistry ( [
951+ {
952+ pluginId : "getter-a" ,
953+ optional : false ,
954+ source : "/tmp/getter-a.js" ,
955+ names : [ "getter_a_tool" ] ,
956+ factory : ( ) => makeTool ( "getter_a_tool" ) ,
957+ } ,
958+ {
959+ pluginId : "getter-b" ,
960+ optional : false ,
961+ source : "/tmp/getter-b.js" ,
962+ names : [ "getter_b_tool" ] ,
963+ factory : ( ) => makeTool ( "getter_b_tool" ) ,
964+ } ,
965+ ] ) ;
966+
967+ const tools = resolvePluginTools (
968+ createResolveToolsParams ( {
969+ context : { ...createContext ( ) , getRuntimeConfig : getRuntimeConfig as never } ,
970+ } ) ,
971+ ) ;
972+
973+ expectResolvedToolNames ( tools , [ "getter_a_tool" , "getter_b_tool" ] ) ;
974+ expect ( getRuntimeConfig ) . toHaveBeenCalledTimes ( 1 ) ;
975+ } ) ;
976+
815977 it ( "skips factory-returned tools outside the manifest tool contract" , ( ) => {
816978 const registry = setRegistry ( [
817979 {
0 commit comments