use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use std::time::Instant; use crate::function_tool::FunctionCallError; use crate::hook_runtime::record_additional_contexts; use crate::hook_runtime::run_post_tool_use_hooks; use crate::hook_runtime::run_pre_tool_use_hooks; use crate::memories::usage::emit_metric_for_tool_read; use crate::sandbox_tags::sandbox_tag; use crate::session::turn_context::TurnContext; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; use codex_hooks::HookEvent; use codex_hooks::HookEventAfterToolUse; use codex_hooks::HookPayload; use codex_hooks::HookResult; use codex_hooks::HookToolInput; use codex_hooks::HookToolInputLocalShell; use codex_hooks::HookToolKind; use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::SandboxPolicy; use codex_tools::ConfiguredToolSpec; use codex_tools::ToolName; use codex_tools::ToolSpec; use codex_utils_readiness::Readiness; use futures::future::BoxFuture; use serde_json::Value; use tracing::warn; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ToolKind { Function, Mcp, } pub trait ToolHandler: Send + Sync { type Output: ToolOutput + 'static; fn kind(&self) -> ToolKind; fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!( (self.kind(), payload), (ToolKind::Function, ToolPayload::Function { .. }) | (ToolKind::Function, ToolPayload::ToolSearch { .. }) | (ToolKind::Mcp, ToolPayload::Mcp { .. }) ) } /// Returns `true` if the [ToolInvocation] *might* mutate the environment of the /// user (through file system, OS operations, ...). /// This function must remains defensive and return `true` if a doubt exist on the /// exact effect of a ToolInvocation. fn is_mutating( &self, _invocation: &ToolInvocation, ) -> impl std::future::Future + Send { async { false } } fn pre_tool_use_payload(&self, _invocation: &ToolInvocation) -> Option { None } fn post_tool_use_payload( &self, _call_id: &str, _payload: &ToolPayload, _result: &dyn ToolOutput, ) -> Option { None } /// Creates an optional consumer for streamed tool argument diffs. fn create_diff_consumer(&self) -> Option> { None } /// Perform the actual [ToolInvocation] and returns a [ToolOutput] containing /// the final output to return to the model. fn handle( &self, invocation: ToolInvocation, ) -> impl std::future::Future> + Send; } /// Consumes streamed argument diffs for a tool call and emits protocol events /// derived from partial tool input. pub(crate) trait ToolArgumentDiffConsumer: Send { /// Consume the next argument diff for a tool call. fn consume_diff(&mut self, turn: &TurnContext, call_id: String, diff: &str) -> Option; } pub(crate) struct AnyToolResult { pub(crate) call_id: String, pub(crate) payload: ToolPayload, pub(crate) result: Box, } impl AnyToolResult { pub(crate) fn into_response(self) -> ResponseInputItem { let Self { call_id, payload, result, .. } = self; result.to_response_item(&call_id, &payload) } pub(crate) fn code_mode_result(self) -> serde_json::Value { let Self { payload, result, .. } = self; result.code_mode_result(&payload) } } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct PreToolUsePayload { pub(crate) command: String, } #[derive(Debug, Clone, PartialEq)] pub(crate) struct PostToolUsePayload { pub(crate) command: String, pub(crate) tool_response: Value, } trait AnyToolHandler: Send + Sync { fn matches_kind(&self, payload: &ToolPayload) -> bool; fn is_mutating<'a>(&'a self, invocation: &'a ToolInvocation) -> BoxFuture<'a, bool>; fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option; fn post_tool_use_payload( &self, call_id: &str, payload: &ToolPayload, result: &dyn ToolOutput, ) -> Option; fn create_diff_consumer(&self) -> Option>; fn handle_any<'a>( &'a self, invocation: ToolInvocation, ) -> BoxFuture<'a, Result>; } impl AnyToolHandler for T where T: ToolHandler, { fn matches_kind(&self, payload: &ToolPayload) -> bool { ToolHandler::matches_kind(self, payload) } fn is_mutating<'a>(&'a self, invocation: &'a ToolInvocation) -> BoxFuture<'a, bool> { Box::pin(ToolHandler::is_mutating(self, invocation)) } fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { ToolHandler::pre_tool_use_payload(self, invocation) } fn post_tool_use_payload( &self, call_id: &str, payload: &ToolPayload, result: &dyn ToolOutput, ) -> Option { ToolHandler::post_tool_use_payload(self, call_id, payload, result) } fn create_diff_consumer(&self) -> Option> { ToolHandler::create_diff_consumer(self) } fn handle_any<'a>( &'a self, invocation: ToolInvocation, ) -> BoxFuture<'a, Result> { Box::pin(async move { let call_id = invocation.call_id.clone(); let payload = invocation.payload.clone(); let output = self.handle(invocation).await?; Ok(AnyToolResult { call_id, payload, result: Box::new(output), }) }) } } pub struct ToolRegistry { handlers: HashMap>, } impl ToolRegistry { fn new(handlers: HashMap>) -> Self { Self { handlers } } fn handler(&self, name: &ToolName) -> Option> { self.handlers.get(name).map(Arc::clone) } #[cfg(test)] pub(crate) fn has_handler(&self, name: &ToolName) -> bool { self.handler(name).is_some() } pub(crate) fn create_diff_consumer( &self, name: &ToolName, ) -> Option> { self.handler(name)?.create_diff_consumer() } // TODO(jif) for dynamic tools. // pub fn register(&mut self, name: impl Into, handler: Arc) { // let name = name.into(); // if self.handlers.insert(name.clone(), handler).is_some() { // warn!("overwriting handler for tool {name}"); // } // } pub(crate) async fn dispatch_any( &self, invocation: ToolInvocation, ) -> Result { let tool_name = invocation.tool_name.clone(); let display_name = tool_name.display(); let call_id_owned = invocation.call_id.clone(); let otel = invocation.turn.session_telemetry.clone(); let payload_for_response = invocation.payload.clone(); let log_payload = payload_for_response.log_payload(); let metric_tags = [ ( "sandbox", sandbox_tag( &invocation.turn.sandbox_policy, invocation.turn.windows_sandbox_level, ), ), ( "sandbox_policy", sandbox_policy_tag(&invocation.turn.sandbox_policy), ), ]; let (mcp_server, mcp_server_origin) = match &invocation.payload { ToolPayload::Mcp { server, .. } => { let manager = invocation .session .services .mcp_connection_manager .read() .await; let origin = manager.server_origin(server).map(str::to_owned); (Some(server.clone()), origin) } _ => (None, None), }; let mcp_server_ref = mcp_server.as_deref(); let mcp_server_origin_ref = mcp_server_origin.as_deref(); { let mut active = invocation.session.active_turn.lock().await; if let Some(active_turn) = active.as_mut() { let mut turn_state = active_turn.turn_state.lock().await; turn_state.tool_calls = turn_state.tool_calls.saturating_add(1); } } let handler = match self.handler(&tool_name) { Some(handler) => handler, None => { let message = unsupported_tool_call_message(&invocation.payload, &tool_name); otel.tool_result_with_tags( &display_name, &call_id_owned, log_payload.as_ref(), Duration::ZERO, /*success*/ false, &message, &metric_tags, mcp_server_ref, mcp_server_origin_ref, ); return Err(FunctionCallError::RespondToModel(message)); } }; if !handler.matches_kind(&invocation.payload) { let message = format!("tool {display_name} invoked with incompatible payload"); otel.tool_result_with_tags( &display_name, &call_id_owned, log_payload.as_ref(), Duration::ZERO, /*success*/ false, &message, &metric_tags, mcp_server_ref, mcp_server_origin_ref, ); return Err(FunctionCallError::Fatal(message)); } if let Some(pre_tool_use_payload) = handler.pre_tool_use_payload(&invocation) && let Some(reason) = run_pre_tool_use_hooks( &invocation.session, &invocation.turn, invocation.call_id.clone(), pre_tool_use_payload.command.clone(), ) .await { return Err(FunctionCallError::RespondToModel(format!( "Command blocked by PreToolUse hook: {reason}. Command: {}", pre_tool_use_payload.command ))); } let is_mutating = handler.is_mutating(&invocation).await; let response_cell = tokio::sync::Mutex::new(None); let invocation_for_tool = invocation.clone(); let started = Instant::now(); let result = otel .log_tool_result_with_tags( &display_name, &call_id_owned, log_payload.as_ref(), &metric_tags, mcp_server_ref, mcp_server_origin_ref, || { let handler = handler.clone(); let response_cell = &response_cell; async move { if is_mutating { tracing::trace!("waiting for tool gate"); invocation_for_tool.turn.tool_call_gate.wait_ready().await; tracing::trace!("tool gate released"); } match handler.handle_any(invocation_for_tool).await { Ok(result) => { let preview = result.result.log_preview(); let success = result.result.success_for_logging(); let mut guard = response_cell.lock().await; *guard = Some(result); Ok((preview, success)) } Err(err) => Err(err), } } }, ) .await; let duration = started.elapsed(); let (output_preview, success) = match &result { Ok((preview, success)) => (preview.clone(), *success), Err(err) => (err.to_string(), false), }; emit_metric_for_tool_read(&invocation, success).await; let post_tool_use_payload = if success { let guard = response_cell.lock().await; guard.as_ref().and_then(|result| { handler.post_tool_use_payload( &result.call_id, &result.payload, result.result.as_ref(), ) }) } else { None }; let post_tool_use_outcome = if let Some(post_tool_use_payload) = post_tool_use_payload { Some( run_post_tool_use_hooks( &invocation.session, &invocation.turn, invocation.call_id.clone(), post_tool_use_payload.command, post_tool_use_payload.tool_response, ) .await, ) } else { None }; // Deprecated: this is the legacy AfterToolUse hook. Prefer the new PostToolUse let hook_abort_error = dispatch_after_tool_use_hook(AfterToolUseHookDispatch { invocation: &invocation, output_preview, success, executed: true, duration, mutating: is_mutating, }) .await; if let Some(err) = hook_abort_error { return Err(err); } if let Some(outcome) = &post_tool_use_outcome { record_additional_contexts( &invocation.session, &invocation.turn, outcome.additional_contexts.clone(), ) .await; let replacement_text = if outcome.should_stop { Some( outcome .feedback_message .clone() .or_else(|| outcome.stop_reason.clone()) .unwrap_or_else(|| "PostToolUse hook stopped execution".to_string()), ) } else { outcome.feedback_message.clone() }; if let Some(replacement_text) = replacement_text { let mut guard = response_cell.lock().await; if let Some(result) = guard.as_mut() { result.result = Box::new(FunctionToolOutput::from_text( replacement_text, /*success*/ None, )); } } } match result { Ok(_) => { let mut guard = response_cell.lock().await; let result = guard.take().ok_or_else(|| { FunctionCallError::Fatal("tool produced no output".to_string()) })?; Ok(result) } Err(err) => Err(err), } } } pub struct ToolRegistryBuilder { handlers: HashMap>, specs: Vec, } impl ToolRegistryBuilder { pub fn new() -> Self { Self { handlers: HashMap::new(), specs: Vec::new(), } } pub fn push_spec(&mut self, spec: ToolSpec) { self.push_spec_with_parallel_support(spec, /*supports_parallel_tool_calls*/ false); } pub fn push_spec_with_parallel_support( &mut self, spec: ToolSpec, supports_parallel_tool_calls: bool, ) { self.specs .push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls)); } pub fn register_handler(&mut self, name: impl Into, handler: Arc) where H: ToolHandler + 'static, { let name = name.into(); let display_name = name.display(); let handler: Arc = handler; if self.handlers.insert(name, handler).is_some() { warn!("overwriting handler for tool {display_name}"); } } // TODO(jif) for dynamic tools. // pub fn register_many(&mut self, names: I, handler: Arc) // where // I: IntoIterator, // I::Item: Into, // { // for name in names { // let name = name.into(); // if self // .handlers // .insert(name.clone(), handler.clone()) // .is_some() // { // warn!("overwriting handler for tool {name}"); // } // } // } pub fn build(self) -> (Vec, ToolRegistry) { let registry = ToolRegistry::new(self.handlers); (self.specs, registry) } } fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> String { let tool_name = tool_name.display(); match payload { ToolPayload::Custom { .. } => format!("unsupported custom tool call: {tool_name}"), _ => format!("unsupported call: {tool_name}"), } } fn sandbox_policy_tag(policy: &SandboxPolicy) -> &'static str { match policy { SandboxPolicy::ReadOnly { .. } => "read-only", SandboxPolicy::WorkspaceWrite { .. } => "workspace-write", SandboxPolicy::DangerFullAccess => "danger-full-access", SandboxPolicy::ExternalSandbox { .. } => "external-sandbox", } } // Hooks use a separate wire-facing input type so hook payload JSON stays stable // and decoupled from core's internal tool runtime representation. impl From<&ToolPayload> for HookToolInput { fn from(payload: &ToolPayload) -> Self { match payload { ToolPayload::Function { arguments } => HookToolInput::Function { arguments: arguments.clone(), }, ToolPayload::ToolSearch { arguments } => HookToolInput::Function { arguments: serde_json::json!({ "query": arguments.query, "limit": arguments.limit, }) .to_string(), }, ToolPayload::Custom { input } => HookToolInput::Custom { input: input.clone(), }, ToolPayload::LocalShell { params } => HookToolInput::LocalShell { params: HookToolInputLocalShell { command: params.command.clone(), workdir: params.workdir.clone(), timeout_ms: params.timeout_ms, sandbox_permissions: params.sandbox_permissions, prefix_rule: params.prefix_rule.clone(), justification: params.justification.clone(), }, }, ToolPayload::Mcp { server, tool, raw_arguments, } => HookToolInput::Mcp { server: server.clone(), tool: tool.clone(), arguments: raw_arguments.clone(), }, } } } fn hook_tool_kind(tool_input: &HookToolInput) -> HookToolKind { match tool_input { HookToolInput::Function { .. } => HookToolKind::Function, HookToolInput::Custom { .. } => HookToolKind::Custom, HookToolInput::LocalShell { .. } => HookToolKind::LocalShell, HookToolInput::Mcp { .. } => HookToolKind::Mcp, } } struct AfterToolUseHookDispatch<'a> { invocation: &'a ToolInvocation, output_preview: String, success: bool, executed: bool, duration: Duration, mutating: bool, } async fn dispatch_after_tool_use_hook( dispatch: AfterToolUseHookDispatch<'_>, ) -> Option { let AfterToolUseHookDispatch { invocation, .. } = dispatch; let session = invocation.session.as_ref(); let turn = invocation.turn.as_ref(); let tool_input = HookToolInput::from(&invocation.payload); let hook_outcomes = session .hooks() .dispatch(HookPayload { session_id: session.conversation_id, cwd: turn.cwd.clone(), client: turn.app_server_client_name.clone(), triggered_at: chrono::Utc::now(), hook_event: HookEvent::AfterToolUse { event: HookEventAfterToolUse { turn_id: turn.sub_id.clone(), call_id: invocation.call_id.clone(), tool_name: invocation.tool_name.display(), tool_kind: hook_tool_kind(&tool_input), tool_input, executed: dispatch.executed, success: dispatch.success, duration_ms: u64::try_from(dispatch.duration.as_millis()).unwrap_or(u64::MAX), mutating: dispatch.mutating, sandbox: sandbox_tag(&turn.sandbox_policy, turn.windows_sandbox_level) .to_string(), sandbox_policy: sandbox_policy_tag(&turn.sandbox_policy).to_string(), output_preview: dispatch.output_preview.clone(), }, }, }) .await; for hook_outcome in hook_outcomes { let hook_name = hook_outcome.hook_name; match hook_outcome.result { HookResult::Success => {} HookResult::FailedContinue(error) => { warn!( call_id = %invocation.call_id, tool_name = %invocation.tool_name.display(), hook_name = %hook_name, error = %error, "after_tool_use hook failed; continuing" ); } HookResult::FailedAbort(error) => { warn!( call_id = %invocation.call_id, tool_name = %invocation.tool_name.display(), hook_name = %hook_name, error = %error, "after_tool_use hook failed; aborting operation" ); return Some(FunctionCallError::Fatal(format!( "after_tool_use hook '{hook_name}' failed and aborted operation: {error}" ))); } } } None } #[cfg(test)] #[path = "registry_tests.rs"] mod tests;