@@ -36,6 +36,25 @@ function createTool(overrides: Partial<AnyAgentTool>): AnyAgentTool {
3636 } as unknown as AnyAgentTool ;
3737}
3838
39+ function createToolWithUnreadableField (
40+ field : "description" | "parameters" ,
41+ params : { name : string ; execute : ReturnType < typeof vi . fn > } ,
42+ ) : AnyAgentTool {
43+ const base = {
44+ name : params . name ,
45+ description : "Unreadable descriptor tool." ,
46+ parameters : { type : "object" , properties : { } } ,
47+ execute : params . execute ,
48+ } ;
49+ Object . defineProperty ( base , field , {
50+ enumerable : true ,
51+ get ( ) {
52+ throw new Error ( `${ field } getter exploded` ) ;
53+ } ,
54+ } ) ;
55+ return base as unknown as AnyAgentTool ;
56+ }
57+
3958function mediaResult ( mediaUrl : string , audioAsVoice ?: boolean ) : AgentToolResult < unknown > {
4059 return {
4160 content : [ { type : "text" , text : "Generated media reply." } ] ,
@@ -371,6 +390,141 @@ describe("createCodexDynamicToolBridge", () => {
371390 expect ( badExecute ) . not . toHaveBeenCalled ( ) ;
372391 } ) ;
373392
393+ it ( "quarantines unreadable dynamic tool descriptors before wrapping" , async ( ) => {
394+ const warn = vi . spyOn ( embeddedAgentLog , "warn" ) . mockImplementation ( ( ) => undefined ) ;
395+ const diagnosticEvents : DiagnosticEventPayload [ ] = [ ] ;
396+ const unsubscribeDiagnostics = onInternalDiagnosticEvent ( ( event ) =>
397+ diagnosticEvents . push ( event ) ,
398+ ) ;
399+ const unreadableParametersExecute = vi . fn ( ) ;
400+ const unreadableDescriptionExecute = vi . fn ( ) ;
401+ let bridge ! : ReturnType < typeof createCodexDynamicToolBridge > ;
402+ try {
403+ bridge = createCodexDynamicToolBridge ( {
404+ tools : [
405+ createTool ( {
406+ name : "message" ,
407+ execute : vi . fn ( async ( ) => textToolResult ( "healthy reply" ) ) ,
408+ } ) ,
409+ createToolWithUnreadableField ( "parameters" , {
410+ name : "fuzzplugin_unreadable_parameters" ,
411+ execute : unreadableParametersExecute ,
412+ } ) ,
413+ createToolWithUnreadableField ( "description" , {
414+ name : "fuzzplugin_unreadable_description" ,
415+ execute : unreadableDescriptionExecute ,
416+ } ) ,
417+ ] ,
418+ signal : new AbortController ( ) . signal ,
419+ hookContext : {
420+ runId : "run-unreadable" ,
421+ sessionId : "session-unreadable" ,
422+ sessionKey : "agent:main:session-unreadable" ,
423+ } ,
424+ } ) ;
425+ await waitForDiagnosticEventsDrained ( ) ;
426+ } finally {
427+ unsubscribeDiagnostics ( ) ;
428+ }
429+
430+ expect ( bridge . availableSpecs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
431+ expect ( bridge . specs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" ] ) ;
432+ expect ( bridge . telemetry . quarantinedTools ) . toEqual ( [
433+ {
434+ tool : "fuzzplugin_unreadable_parameters" ,
435+ violations : [ "fuzzplugin_unreadable_parameters.parameters is unreadable" ] ,
436+ } ,
437+ {
438+ tool : "fuzzplugin_unreadable_description" ,
439+ violations : [ "fuzzplugin_unreadable_description.description is unreadable" ] ,
440+ } ,
441+ ] ) ;
442+ expect ( warn ) . toHaveBeenCalledWith (
443+ expect . stringContaining ( "fuzzplugin_unreadable_parameters" ) ,
444+ expect . objectContaining ( {
445+ tools : expect . arrayContaining ( [
446+ {
447+ tool : "fuzzplugin_unreadable_parameters" ,
448+ violations : [ "fuzzplugin_unreadable_parameters.parameters is unreadable" ] ,
449+ } ,
450+ {
451+ tool : "fuzzplugin_unreadable_description" ,
452+ violations : [ "fuzzplugin_unreadable_description.description is unreadable" ] ,
453+ } ,
454+ ] ) ,
455+ } ) ,
456+ ) ;
457+ const blockedToolNames = diagnosticEvents
458+ . filter (
459+ ( event ) : event is Extract < DiagnosticEventPayload , { type : "tool.execution.blocked" } > =>
460+ event . type === "tool.execution.blocked" ,
461+ )
462+ . map ( ( event ) => event . toolName )
463+ . toSorted ( ) ;
464+ expect ( blockedToolNames ) . toEqual ( [
465+ "fuzzplugin_unreadable_description" ,
466+ "fuzzplugin_unreadable_parameters" ,
467+ ] ) ;
468+
469+ const result = await bridge . handleToolCall ( {
470+ threadId : "thread-1" ,
471+ turnId : "turn-1" ,
472+ callId : "call-1" ,
473+ namespace : null ,
474+ tool : "message" ,
475+ arguments : { } ,
476+ } ) ;
477+
478+ expect ( result ) . toEqual ( expectInputText ( "healthy reply" ) ) ;
479+ expect ( unreadableParametersExecute ) . not . toHaveBeenCalled ( ) ;
480+ expect ( unreadableDescriptionExecute ) . not . toHaveBeenCalled ( ) ;
481+ } ) ;
482+
483+ it ( "quarantines unreadable dynamic tool entries while keeping healthy siblings" , async ( ) => {
484+ const warn = vi . spyOn ( embeddedAgentLog , "warn" ) . mockImplementation ( ( ) => undefined ) ;
485+ const hiddenExecute = vi . fn ( ) ;
486+ const tools = [
487+ createTool ( { name : "message" } ) ,
488+ createTool ( {
489+ name : "hidden_bad_tool" ,
490+ execute : hiddenExecute ,
491+ } ) ,
492+ createTool ( { name : "web_search" } ) ,
493+ ] ;
494+ Object . defineProperty ( tools , "1" , {
495+ configurable : true ,
496+ get ( ) {
497+ throw new Error ( "tool entry getter exploded" ) ;
498+ } ,
499+ } ) ;
500+
501+ const bridge = createCodexDynamicToolBridge ( {
502+ tools,
503+ signal : new AbortController ( ) . signal ,
504+ } ) ;
505+
506+ expect ( bridge . availableSpecs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" , "web_search" ] ) ;
507+ expect ( bridge . specs . map ( ( tool ) => tool . name ) ) . toEqual ( [ "message" , "web_search" ] ) ;
508+ expect ( bridge . telemetry . quarantinedTools ) . toEqual ( [
509+ {
510+ tool : "tool[1]" ,
511+ violations : [ "tool[1] is unreadable" ] ,
512+ } ,
513+ ] ) ;
514+ expect ( warn ) . toHaveBeenCalledWith (
515+ expect . stringContaining ( "tool[1]" ) ,
516+ expect . objectContaining ( {
517+ tools : [
518+ {
519+ tool : "tool[1]" ,
520+ violations : [ "tool[1] is unreadable" ] ,
521+ } ,
522+ ] ,
523+ } ) ,
524+ ) ;
525+ expect ( hiddenExecute ) . not . toHaveBeenCalled ( ) ;
526+ } ) ;
527+
374528 it ( "can expose all dynamic tools directly for compatibility" , ( ) => {
375529 const bridge = createCodexDynamicToolBridge ( {
376530 tools : [ createTool ( { name : "web_search" } ) , createTool ( { name : "message" } ) ] ,
0 commit comments