Codex Handbook
core/src/tools/registry.rs 740 lines
use std::collections::HashMap;use std::sync::Arc;use std::sync::atomic::AtomicBool;use std::sync::atomic::Ordering;use std::time::Duration;use crate::function_tool::FunctionCallError;use crate::hook_runtime::PreToolUseHookResult;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::memory_usage::emit_metric_for_tool_read;use crate::sandbox_tags::permission_profile_policy_tag;use crate::sandbox_tags::permission_profile_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 crate::tools::flat_tool_name;use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;use crate::tools::hook_names::HookToolName;use crate::tools::lifecycle::notify_tool_finish;use crate::tools::lifecycle::notify_tool_start;use crate::tools::tool_dispatch_trace::ToolDispatchTrace;use crate::util::error_or_panic;use codex_extension_api::ToolCallOutcome;use codex_protocol::models::FunctionCallOutputPayload;use codex_protocol::models::ResponseInputItem;use codex_protocol::protocol::EventMsg;use codex_rollout::state_db;use codex_tools::ToolName;use codex_tools::ToolSearchInfo;use codex_tools::ToolSpec;use futures::future::BoxFuture;use serde_json::Value;pub(crate) type ToolTelemetryTags = Vec<(&'static str, String)>;pub use codex_tools::ToolExecutor;pub use codex_tools::ToolExposure;/// Typed runtime contract for locally executed tools.////// Implementers provide the shared `ToolExecutor` behavior plus optional/// core-owned metadata for hooks, telemetry, tool search, and argument diffs.pub(crate) trait CoreToolRuntime: ToolExecutor<ToolInvocation> {    fn matches_kind(&self, payload: &ToolPayload) -> bool {        matches!(            payload,            ToolPayload::Function { .. } | ToolPayload::ToolSearch { .. }        )    }    /// Whether cancellation should let the handler finish teardown before the    /// host returns an aborted tool response.    fn waits_for_runtime_cancellation(&self) -> bool {        false    }    fn telemetry_tags<'a>(        &'a self,        _invocation: &'a ToolInvocation,    ) -> BoxFuture<'a, ToolTelemetryTags> {        Box::pin(async { Vec::new() })    }    fn post_tool_use_payload(        &self,        invocation: &ToolInvocation,        result: &dyn ToolOutput,    ) -> Option<PostToolUsePayload> {        let ToolPayload::Function { arguments } = &invocation.payload else {            return None;        };        Some(PostToolUsePayload {            tool_name: function_hook_tool_name(invocation),            tool_use_id: result.post_tool_use_id(&invocation.call_id),            tool_input: result                .post_tool_use_input(&invocation.payload)                .unwrap_or_else(|| function_hook_tool_input(arguments)),            tool_response: result                .post_tool_use_response(&invocation.call_id, &invocation.payload)                .or_else(|| {                    // Most function tools can expose their model-facing output                    // as the hook response. Outputs with a more stable hook                    // contract should override post_tool_use_response above.                    let ResponseInputItem::FunctionCallOutput {                        output: FunctionCallOutputPayload { body, .. },                        ..                    } = result.to_response_item(&invocation.call_id, &invocation.payload)                    else {                        return None;                    };                    serde_json::to_value(body).ok()                })?,        })    }    fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {        let ToolPayload::Function { arguments } = &invocation.payload else {            return None;        };        Some(PreToolUsePayload {            tool_name: function_hook_tool_name(invocation),            tool_input: function_hook_tool_input(arguments),        })    }    /// Rebuilds a tool invocation from hook-facing `tool_input`.    ///    /// Tools that opt into input-rewriting hooks should invert the same stable    /// hook contract they expose from `pre_tool_use_payload`.    fn with_updated_hook_input(        &self,        invocation: ToolInvocation,        updated_input: Value,    ) -> Result<ToolInvocation, FunctionCallError> {        let ToolPayload::Function { .. } = &invocation.payload else {            return Err(FunctionCallError::RespondToModel(                "hook input rewrite received unsupported function tool payload".to_string(),            ));        };        let arguments = serde_json::to_string(&updated_input).map_err(|err| {            FunctionCallError::RespondToModel(format!(                "failed to serialize rewritten {} arguments: {err}",                flat_tool_name(&invocation.tool_name)            ))        })?;        Ok(ToolInvocation {            payload: ToolPayload::Function { arguments },            ..invocation        })    }    /// Creates an optional consumer for streamed tool argument diffs.    fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {        None    }}/// 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<EventMsg>;    /// Finish consuming argument diffs before the tool call completes.    fn finish(&mut self) -> Result<Option<EventMsg>, FunctionCallError> {        Ok(None)    }}pub(crate) struct AnyToolResult {    pub(crate) call_id: String,    pub(crate) payload: ToolPayload,    pub(crate) result: Box<dyn ToolOutput>,    pub(crate) post_tool_use_payload: Option<PostToolUsePayload>,}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)    }}struct PostToolUseFeedbackOutput {    original: Box<dyn ToolOutput>,    model_visible: FunctionToolOutput,}impl ToolOutput for PostToolUseFeedbackOutput {    fn log_preview(&self) -> String {        self.original.log_preview()    }    fn success_for_logging(&self) -> bool {        self.original.success_for_logging()    }    fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {        self.model_visible.to_response_item(call_id, payload)    }    fn code_mode_result(&self, payload: &ToolPayload) -> Value {        self.original.code_mode_result(payload)    }}#[derive(Debug, Clone, PartialEq, Eq)]pub(crate) struct PreToolUsePayload {    /// Hook-facing tool name model.    ///    /// The canonical name is serialized to hook stdin, while aliases are used    /// only for matcher compatibility.    pub(crate) tool_name: HookToolName,    /// Tool-specific input exposed at `tool_input`.    ///    /// Shell-like tools use `{ "command": ... }`; MCP tools use their resolved    /// JSON arguments.    pub(crate) tool_input: Value,}#[derive(Debug, Clone, PartialEq)]pub(crate) struct PostToolUsePayload {    /// Hook-facing tool name model.    ///    /// The canonical name is serialized to hook stdin, while aliases are used    /// only for matcher compatibility.    pub(crate) tool_name: HookToolName,    /// The originating tool-use id exposed at `tool_use_id`.    pub(crate) tool_use_id: String,    /// Tool-specific input exposed at `tool_input`.    pub(crate) tool_input: Value,    /// Tool result exposed at `tool_response`.    pub(crate) tool_response: Value,}pub(crate) fn override_tool_exposure(    handler: Arc<dyn CoreToolRuntime>,    exposure: ToolExposure,) -> Arc<dyn CoreToolRuntime> {    if handler.exposure() == exposure {        return handler;    }    Arc::new(ExposureOverride { handler, exposure })}struct ExposureOverride {    handler: Arc<dyn CoreToolRuntime>,    exposure: ToolExposure,}impl ToolExecutor<ToolInvocation> for ExposureOverride {    fn tool_name(&self) -> ToolName {        self.handler.tool_name()    }    fn spec(&self) -> ToolSpec {        self.handler.spec()    }    fn exposure(&self) -> ToolExposure {        self.exposure    }    fn supports_parallel_tool_calls(&self) -> bool {        self.exposure != ToolExposure::Hidden && self.handler.supports_parallel_tool_calls()    }    fn search_info(&self) -> Option<ToolSearchInfo> {        self.handler.search_info()    }    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {        self.handler.handle(invocation)    }}impl CoreToolRuntime for ExposureOverride {    fn matches_kind(&self, payload: &ToolPayload) -> bool {        self.handler.matches_kind(payload)    }    fn waits_for_runtime_cancellation(&self) -> bool {        self.handler.waits_for_runtime_cancellation()    }    fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {        self.handler.pre_tool_use_payload(invocation)    }    fn post_tool_use_payload(        &self,        invocation: &ToolInvocation,        result: &dyn ToolOutput,    ) -> Option<PostToolUsePayload> {        self.handler.post_tool_use_payload(invocation, result)    }    fn with_updated_hook_input(        &self,        invocation: ToolInvocation,        updated_input: Value,    ) -> Result<ToolInvocation, FunctionCallError> {        self.handler            .with_updated_hook_input(invocation, updated_input)    }    fn telemetry_tags<'a>(        &'a self,        invocation: &'a ToolInvocation,    ) -> BoxFuture<'a, ToolTelemetryTags> {        self.handler.telemetry_tags(invocation)    }    fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {        self.handler.create_diff_consumer()    }}pub struct ToolRegistry {    tools: HashMap<ToolName, Arc<dyn CoreToolRuntime>>,}impl ToolRegistry {    fn new(tools: HashMap<ToolName, Arc<dyn CoreToolRuntime>>) -> Self {        Self { tools }    }    pub(crate) fn from_tools(tools: impl IntoIterator<Item = Arc<dyn CoreToolRuntime>>) -> Self {        let mut tools_by_name = HashMap::new();        for tool in tools {            let name = tool.tool_name();            if tools_by_name.contains_key(&name) {                error_or_panic(format!("tool {name} already registered"));                continue;            }            tools_by_name.insert(name, tool);        }        Self::new(tools_by_name)    }    #[cfg(test)]    pub(crate) fn empty_for_test() -> Self {        Self::new(HashMap::new())    }    #[cfg(test)]    pub(crate) fn with_handler_for_test<T>(handler: Arc<T>) -> Self    where        T: CoreToolRuntime + 'static,    {        let name = handler.tool_name();        Self::new(HashMap::from([(name, handler as Arc<dyn CoreToolRuntime>)]))    }    fn tool(&self, name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {        self.tools.get(name).map(Arc::clone)    }    #[cfg(test)]    pub(crate) fn tool_names_for_test(&self) -> Vec<ToolName> {        let mut names = self.tools.keys().cloned().collect::<Vec<_>>();        names.sort();        names    }    #[cfg(test)]    pub(crate) fn tool_exposure(&self, name: &ToolName) -> Option<ToolExposure> {        self.tools.get(name).map(|tool| tool.exposure())    }    pub(crate) fn create_diff_consumer(        &self,        name: &ToolName,    ) -> Option<Box<dyn ToolArgumentDiffConsumer>> {        self.tool(name)?.create_diff_consumer()    }    pub(crate) fn supports_parallel_tool_calls(&self, name: &ToolName) -> Option<bool> {        let tool = self.tool(name)?;        Some(tool.supports_parallel_tool_calls())    }    pub(crate) fn waits_for_runtime_cancellation(&self, name: &ToolName) -> Option<bool> {        let tool = self.tool(name)?;        Some(tool.waits_for_runtime_cancellation())    }    #[allow(dead_code)]    pub(crate) async fn dispatch_any(        &self,        invocation: ToolInvocation,    ) -> Result<AnyToolResult, FunctionCallError> {        self.dispatch_any_with_terminal_outcome(invocation, /*terminal_outcome_reached*/ None)            .await    }    #[expect(        clippy::await_holding_invalid_type,        reason = "tool dispatch must keep active-turn accounting atomic"    )]    pub(crate) async fn dispatch_any_with_terminal_outcome(        &self,        mut invocation: ToolInvocation,        terminal_outcome_reached: Option<Arc<AtomicBool>>,    ) -> Result<AnyToolResult, FunctionCallError> {        let tool_name = invocation.tool_name.clone();        let tool_name_flat = flat_tool_name(&tool_name);        let call_id_owned = invocation.call_id.clone();        let otel = invocation.turn.session_telemetry.clone();        let base_tool_result_tags = [            (                "sandbox",                permission_profile_sandbox_tag(                    &invocation.turn.permission_profile,                    invocation.turn.windows_sandbox_level,                    invocation.turn.network.is_some(),                ),            ),            (                "sandbox_policy",                permission_profile_policy_tag(                    &invocation.turn.permission_profile,                    #[allow(deprecated)]                    invocation.turn.cwd.as_path(),                ),            ),        ];        {            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 dispatch_trace = ToolDispatchTrace::start(&invocation);        let tool = match self.tool(&tool_name) {            Some(tool) => tool,            None => {                let message = unsupported_tool_call_message(&invocation.payload, &tool_name);                let log_payload = invocation.payload.log_payload();                otel.tool_result_with_tags(                    tool_name_flat.as_ref(),                    &call_id_owned,                    log_payload.as_ref(),                    Duration::ZERO,                    /*success*/ false,                    &message,                    &base_tool_result_tags,                    /*extra_trace_fields*/ &[],                );                let err = FunctionCallError::RespondToModel(message);                dispatch_trace.record_failed(&err);                return Err(err);            }        };        let telemetry_tags = tool.telemetry_tags(&invocation).await;        let mut tool_result_tags =            Vec::with_capacity(base_tool_result_tags.len() + telemetry_tags.len());        let mut extra_trace_fields = Vec::new();        tool_result_tags.extend_from_slice(&base_tool_result_tags);        for (key, value) in &telemetry_tags {            if matches!(*key, "mcp_server" | "mcp_server_origin") {                extra_trace_fields.push((*key, value.as_str()));            } else {                tool_result_tags.push((*key, value.as_str()));            }        }        if !tool.matches_kind(&invocation.payload) {            let message = format!("tool {tool_name} invoked with incompatible payload");            let log_payload = invocation.payload.log_payload();            otel.tool_result_with_tags(                tool_name_flat.as_ref(),                &call_id_owned,                log_payload.as_ref(),                Duration::ZERO,                /*success*/ false,                &message,                &tool_result_tags,                &extra_trace_fields,            );            let err = FunctionCallError::Fatal(message);            dispatch_trace.record_failed(&err);            return Err(err);        }        notify_tool_start(&invocation).await;        if let Some(pre_tool_use_payload) = tool.pre_tool_use_payload(&invocation) {            match run_pre_tool_use_hooks(                &invocation.session,                &invocation.turn,                invocation.call_id.clone(),                &pre_tool_use_payload.tool_name,                &pre_tool_use_payload.tool_input,            )            .await            {                PreToolUseHookResult::Blocked(message) => {                    let err = FunctionCallError::RespondToModel(message);                    dispatch_trace.record_failed(&err);                    notify_tool_finish_if_unclaimed(                        &invocation,                        terminal_outcome_reached.as_deref(),                        ToolCallOutcome::Blocked,                    )                    .await;                    return Err(err);                }                PreToolUseHookResult::Continue {                    updated_input: Some(updated_input),                } => match tool.with_updated_hook_input(invocation.clone(), updated_input) {                    Ok(updated_invocation) => {                        invocation = updated_invocation;                    }                    Err(err) => {                        dispatch_trace.record_failed(&err);                        notify_tool_finish_if_unclaimed(                            &invocation,                            terminal_outcome_reached.as_deref(),                            ToolCallOutcome::Failed {                                handler_executed: false,                            },                        )                        .await;                        return Err(err);                    }                },                PreToolUseHookResult::Continue {                    updated_input: None,                } => {}            }        }        let response_cell = tokio::sync::Mutex::new(None);        let invocation_for_tool = invocation.clone();        let log_payload = invocation.payload.log_payload();        let result = otel            .log_tool_result_with_tags(                tool_name_flat.as_ref(),                &call_id_owned,                log_payload.as_ref(),                &tool_result_tags,                &extra_trace_fields,                || {                    let tool = tool.clone();                    let response_cell = &response_cell;                    async move {                        match handle_any_tool(tool.as_ref(), 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 success = match &result {            Ok((_, success)) => *success,            Err(_) => false,        };        emit_metric_for_tool_read(&invocation, success);        let post_tool_use_payload = if success {            let guard = response_cell.lock().await;            guard                .as_ref()                .and_then(|result| result.post_tool_use_payload.clone())        } 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,                    post_tool_use_payload.tool_use_id,                    post_tool_use_payload.tool_name.name().to_string(),                    post_tool_use_payload.tool_name.matcher_aliases().to_vec(),                    post_tool_use_payload.tool_input,                    post_tool_use_payload.tool_response,                )                .await,            )        } else {            None        };        if let Some(outcome) = &post_tool_use_outcome {            record_additional_contexts(                &invocation.session,                &invocation.turn,                outcome.additional_contexts.clone(),            )            .await;        }        // A PostToolUse block rejects the result, not the already-completed tool execution.        let lifecycle_outcome = match &result {            Ok(_) => {                let guard = response_cell.lock().await;                match guard.as_ref() {                    Some(result) => ToolCallOutcome::Completed {                        success: result.result.success_for_logging(),                    },                    None => ToolCallOutcome::Failed {                        handler_executed: true,                    },                }            }            Err(_) => ToolCallOutcome::Failed {                handler_executed: true,            },        };        notify_tool_finish_if_unclaimed(            &invocation,            terminal_outcome_reached.as_deref(),            lifecycle_outcome,        )        .await;        match result {            Ok(_) => {                let mut guard = response_cell.lock().await;                let mut result = guard.take().ok_or_else(|| {                    FunctionCallError::Fatal("tool produced no output".to_string())                })?;                if let Some(outcome) = post_tool_use_outcome {                    if outcome.should_block {                        let message = outcome.feedback_message.unwrap_or_else(|| {                            "PostToolUse hook blocked the tool result".to_string()                        });                        let err = FunctionCallError::RespondToModel(message);                        dispatch_trace.record_failed(&err);                        return Err(err);                    }                    if let Some(feedback_message) = outcome.feedback_message {                        result.result = Box::new(PostToolUseFeedbackOutput {                            original: result.result,                            model_visible: FunctionToolOutput::from_text(                                feedback_message,                                /*success*/ None,                            ),                        });                    }                }                dispatch_trace.record_completed(                    &invocation,                    &result.call_id,                    &result.payload,                    result.result.as_ref(),                );                Ok(result)            }            Err(err) => {                dispatch_trace.record_failed(&err);                Err(err)            }        }    }}async fn notify_tool_finish_if_unclaimed(    invocation: &ToolInvocation,    terminal_outcome_reached: Option<&AtomicBool>,    outcome: ToolCallOutcome,) -> bool {    if terminal_outcome_reached.is_some_and(|reached| reached.swap(true, Ordering::AcqRel)) {        return false;    }    notify_tool_finish(invocation, outcome).await;    true}async fn handle_any_tool(    tool: &dyn CoreToolRuntime,    invocation: ToolInvocation,) -> Result<AnyToolResult, FunctionCallError> {    let call_id = invocation.call_id.clone();    let payload = invocation.payload.clone();    let output = tool.handle(invocation.clone()).await?;    if output.contains_external_context()        && invocation.turn.config.memories.disable_on_external_context    {        state_db::mark_thread_memory_mode_polluted(            invocation.session.services.state_db.as_deref(),            invocation.session.thread_id,            "tool_output",        )        .await;    }    let post_tool_use_payload =        CoreToolRuntime::post_tool_use_payload(tool, &invocation, output.as_ref());    Ok(AnyToolResult {        call_id,        payload,        result: output,        post_tool_use_payload,    })}fn function_hook_tool_name(invocation: &ToolInvocation) -> HookToolName {    if invocation.tool_name.name == "spawn_agent"        && matches!(            invocation.tool_name.namespace.as_deref(),            None | Some(MULTI_AGENT_V1_NAMESPACE)        )    {        return HookToolName::spawn_agent();    }    HookToolName::new(flat_tool_name(&invocation.tool_name).into_owned())}fn function_hook_tool_input(arguments: &str) -> Value {    if arguments.trim().is_empty() {        return Value::Object(serde_json::Map::new());    }    serde_json::from_str(arguments).unwrap_or_else(|_| Value::String(arguments.to_string()))}fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &ToolName) -> String {    match payload {        ToolPayload::Custom { .. } => format!("unsupported custom tool call: {tool_name}"),        _ => format!("unsupported call: {tool_name}"),    }}#[cfg(test)]#[path = "registry_tests.rs"]mod tests;