@@ -51,6 +51,8 @@ type CodexToolResultHookContext = Omit<CodexDynamicToolHookContext, "config">;
5151
5252type ProjectedCodexDynamicTool = {
5353 tool : AnyAgentTool ;
54+ name : string ;
55+ description : string ;
5456 inputSchema : JsonValue ;
5557} ;
5658
@@ -86,6 +88,7 @@ export const CODEX_OPENCLAW_DYNAMIC_TOOL_NAMESPACE = "openclaw";
8688// spawn_agent remains the primary Codex subagent surface.
8789const ALWAYS_DIRECT_DYNAMIC_TOOL_NAMES = new Set ( [ "sessions_yield" ] ) ;
8890const DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS = 16_000 ;
91+ const UNREADABLE_DYNAMIC_TOOL_NAME = "<unreadable dynamic tool name>" ;
8992
9093export function createCodexDynamicToolBridge ( params : {
9194 tools : AnyAgentTool [ ] ;
@@ -101,19 +104,19 @@ export function createCodexDynamicToolBridge(params: {
101104 const registeredProjection = params . registeredTools
102105 ? projectCodexDynamicTools ( params . registeredTools )
103106 : availableProjection ;
104- const availableTools = availableProjection . tools . map ( ( { tool, inputSchema } ) => {
107+ const availableTools = availableProjection . tools . map ( ( projectedTool ) => {
108+ const { tool } = projectedTool ;
105109 if ( isToolWrappedWithBeforeToolCallHook ( tool ) ) {
106110 setBeforeToolCallDiagnosticsEnabled ( tool , false ) ;
107- return { tool , inputSchema } ;
111+ return projectedTool ;
108112 }
109113 return {
114+ ...projectedTool ,
110115 tool : wrapToolWithBeforeToolCallHook ( tool , params . hookContext , { emitDiagnostics : false } ) ,
111- inputSchema,
112116 } ;
113117 } ) ;
114- const toolMap = new Map ( availableTools . map ( ( { tool } ) => [ tool . name , tool ] ) ) ;
115- const registeredTools = registeredProjection . tools . map ( ( { tool } ) => tool ) ;
116- const registeredToolNames = new Set ( registeredTools . map ( ( tool ) => tool . name ) ) ;
118+ const toolMap = new Map ( availableTools . map ( ( tool ) => [ tool . name , tool ] ) ) ;
119+ const registeredToolNames = new Set ( registeredProjection . tools . map ( ( tool ) => tool . name ) ) ;
117120 const quarantinedTools = dedupeQuarantinedDynamicTools ( [
118121 ...availableProjection . quarantinedTools ,
119122 ...registeredProjection . quarantinedTools ,
@@ -142,26 +145,28 @@ export function createCodexDynamicToolBridge(params: {
142145 ] ) ;
143146
144147 return {
145- availableSpecs : availableTools . map ( ( { tool, inputSchema } ) =>
148+ availableSpecs : availableTools . map ( ( tool ) =>
146149 createCodexDynamicToolSpec ( {
147- tool,
148- inputSchema,
150+ name : tool . name ,
151+ description : tool . description ,
152+ inputSchema : tool . inputSchema ,
149153 loading : params . loading ?? "searchable" ,
150154 directToolNames,
151155 } ) ,
152156 ) ,
153- specs : registeredProjection . tools . map ( ( { tool, inputSchema } ) =>
157+ specs : registeredProjection . tools . map ( ( tool ) =>
154158 createCodexDynamicToolSpec ( {
155- tool,
156- inputSchema,
159+ name : tool . name ,
160+ description : tool . description ,
161+ inputSchema : tool . inputSchema ,
157162 loading : params . loading ?? "searchable" ,
158163 directToolNames,
159164 } ) ,
160165 ) ,
161166 telemetry,
162167 handleToolCall : async ( call , options ) => {
163- const tool = toolMap . get ( call . tool ) ;
164- if ( ! tool ) {
168+ const toolEntry = toolMap . get ( call . tool ) ;
169+ if ( ! toolEntry ) {
165170 if ( registeredToolNames . has ( call . tool ) ) {
166171 return {
167172 contentItems : [
@@ -178,6 +183,7 @@ export function createCodexDynamicToolBridge(params: {
178183 success : false ,
179184 } ;
180185 }
186+ const { tool, name : toolName } = toolEntry ;
181187 const args = jsonObjectToRecord ( call . arguments ) ;
182188 const startedAt = Date . now ( ) ;
183189 const signal = composeAbortSignals ( params . signal , options ?. signal ) ;
@@ -191,7 +197,7 @@ export function createCodexDynamicToolBridge(params: {
191197 threadId : call . threadId ,
192198 turnId : call . turnId ,
193199 toolCallId : call . callId ,
194- toolName : tool . name ,
200+ toolName,
195201 args,
196202 isError : rawIsError ,
197203 result : rawResult ,
@@ -200,21 +206,21 @@ export function createCodexDynamicToolBridge(params: {
200206 threadId : call . threadId ,
201207 turnId : call . turnId ,
202208 toolCallId : call . callId ,
203- toolName : tool . name ,
209+ toolName,
204210 args,
205211 result : middlewareResult ,
206212 } ) ;
207213 const resultIsError = rawIsError || isToolResultError ( result ) ;
208214 collectToolTelemetry ( {
209- toolName : tool . name ,
215+ toolName,
210216 args,
211217 result,
212218 mediaTrustResult : rawResult ,
213219 telemetry,
214220 isError : resultIsError ,
215221 } ) ;
216222 void runAgentHarnessAfterToolCallHook ( {
217- toolName : tool . name ,
223+ toolName,
218224 toolCallId : call . callId ,
219225 runId : toolResultHookContext . runId ,
220226 agentId : toolResultHookContext . agentId ,
@@ -247,14 +253,14 @@ export function createCodexDynamicToolBridge(params: {
247253 return withSideEffectEvidence ( response , terminalType !== "blocked" ) ;
248254 } catch ( error ) {
249255 collectToolTelemetry ( {
250- toolName : tool . name ,
256+ toolName,
251257 args,
252258 result : undefined ,
253259 telemetry,
254260 isError : true ,
255261 } ) ;
256262 void runAgentHarnessAfterToolCallHook ( {
257- toolName : tool . name ,
263+ toolName,
258264 toolCallId : call . callId ,
259265 runId : toolResultHookContext . runId ,
260266 agentId : toolResultHookContext . agentId ,
@@ -286,17 +292,18 @@ export function createCodexDynamicToolBridge(params: {
286292}
287293
288294function createCodexDynamicToolSpec ( params : {
289- tool : AnyAgentTool ;
295+ name : string ;
296+ description : string ;
290297 inputSchema : JsonValue ;
291298 loading : CodexDynamicToolsLoading ;
292299 directToolNames : ReadonlySet < string > ;
293300} ) : CodexDynamicToolSpec {
294301 const base = {
295- name : params . tool . name ,
296- description : params . tool . description ,
302+ name : params . name ,
303+ description : params . description ,
297304 inputSchema : params . inputSchema ,
298305 } ;
299- if ( params . loading === "direct" || params . directToolNames . has ( params . tool . name ) ) {
306+ if ( params . loading === "direct" || params . directToolNames . has ( params . name ) ) {
300307 return base ;
301308 }
302309 return {
@@ -313,16 +320,92 @@ function projectCodexDynamicTools(tools: readonly AnyAgentTool[]): {
313320 const projectedTools : ProjectedCodexDynamicTool [ ] = [ ] ;
314321 const quarantinedTools : CodexDynamicToolSchemaQuarantine [ ] = [ ] ;
315322 for ( const tool of tools ) {
316- const projection = projectRuntimeToolInputSchema ( tool . parameters , `${ tool . name } .inputSchema` ) ;
323+ const fields = readCodexDynamicToolFields ( tool ) ;
324+ if ( fields . kind === "quarantined" ) {
325+ quarantinedTools . push ( {
326+ tool : fields . name ,
327+ violations : fields . violations ,
328+ } ) ;
329+ continue ;
330+ }
331+ const projection = fields . projection ;
317332 if ( projection . violations . length > 0 ) {
318- quarantinedTools . push ( { tool : tool . name , violations : projection . violations } ) ;
333+ quarantinedTools . push ( { tool : fields . name , violations : projection . violations } ) ;
319334 continue ;
320335 }
321- projectedTools . push ( { tool, inputSchema : projection . schema as JsonValue } ) ;
336+ projectedTools . push ( {
337+ tool,
338+ name : fields . name ,
339+ description : fields . description ,
340+ inputSchema : projection . schema as JsonValue ,
341+ } ) ;
322342 }
323343 return { tools : projectedTools , quarantinedTools } ;
324344}
325345
346+ type ReadCodexDynamicToolFieldsResult =
347+ | {
348+ kind : "projected" ;
349+ name : string ;
350+ description : string ;
351+ projection : ReturnType < typeof projectRuntimeToolInputSchema > ;
352+ }
353+ | {
354+ kind : "quarantined" ;
355+ name : string ;
356+ violations : readonly string [ ] ;
357+ } ;
358+
359+ function readCodexDynamicToolFields ( tool : AnyAgentTool ) : ReadCodexDynamicToolFieldsResult {
360+ let name = UNREADABLE_DYNAMIC_TOOL_NAME ;
361+ try {
362+ name = tool . name ;
363+ } catch ( error ) {
364+ return {
365+ kind : "quarantined" ,
366+ name,
367+ violations : [ `${ name } .name could not be read: ${ formatUnknownError ( error ) } ` ] ,
368+ } ;
369+ }
370+
371+ let description : string ;
372+ try {
373+ description = tool . description ;
374+ } catch {
375+ description = "" ;
376+ }
377+
378+ let parameters : AnyAgentTool [ "parameters" ] ;
379+ try {
380+ parameters = tool . parameters ;
381+ } catch ( error ) {
382+ return {
383+ kind : "quarantined" ,
384+ name,
385+ violations : [ `${ name } .inputSchema could not be read: ${ formatUnknownError ( error ) } ` ] ,
386+ } ;
387+ }
388+
389+ try {
390+ return {
391+ kind : "projected" ,
392+ name,
393+ description,
394+ projection : projectRuntimeToolInputSchema ( parameters , `${ name } .inputSchema` ) ,
395+ } ;
396+ } catch ( error ) {
397+ return {
398+ kind : "quarantined" ,
399+ name,
400+ violations : [ `${ name } .inputSchema could not be projected: ${ formatUnknownError ( error ) } ` ] ,
401+ } ;
402+ }
403+ }
404+
405+ function formatUnknownError ( error : unknown ) : string {
406+ return error instanceof Error ? error . message : String ( error ) ;
407+ }
408+
326409function warnQuarantinedDynamicTools ( tools : readonly CodexDynamicToolSchemaQuarantine [ ] ) : void {
327410 if ( tools . length === 0 ) {
328411 return ;
0 commit comments