From 7e15e6db9ea5dab04937f7e3b060b5cf199c60f0 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 11 May 2026 14:39:21 +0200 Subject: [PATCH] [codex] default unknown contributed tools to mutating (#22143) ## Summary - make the shared `ToolExecutor::is_mutating` default conservative by returning `true` - update the trait docs to say read-only tools should opt out explicitly - add a regression test covering the default behavior ## Why Hosts use this signal for serialization and approval policy. Treating unknown contributed tools as read-only lets a write-capable tool accidentally bypass mutating-tool safeguards if it forgets to override the hook. ## Validation - not run, per request --- codex-rs/tool-api/src/bundle.rs | 52 +++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/codex-rs/tool-api/src/bundle.rs b/codex-rs/tool-api/src/bundle.rs index ba24e1e2d..9c03184c6 100644 --- a/codex-rs/tool-api/src/bundle.rs +++ b/codex-rs/tool-api/src/bundle.rs @@ -72,8 +72,56 @@ pub trait ToolExecutor: Send + Sync { /// Returns whether the call may mutate user state. /// /// Hosts can use this conservative signal for serialization or approval - /// policy. Context-free read tools should keep the default. + /// policy. Read-only tools should override this default. fn is_mutating<'a>(&'a self, _call: &'a ToolCall) -> BoolFuture<'a> { - Box::pin(async { false }) + Box::pin(async { true }) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::task::Context; + use std::task::Poll; + use std::task::Wake; + use std::task::Waker; + + use super::*; + use crate::JsonToolOutput; + use crate::ToolInput; + + struct DefaultMutatingExecutor; + + impl ToolExecutor<()> for DefaultMutatingExecutor { + fn execute<'a>(&'a self, _call: ToolCall<()>) -> ToolFuture<'a> { + Box::pin(async { + Ok(Box::new(JsonToolOutput::new(serde_json::json!(null))) as Box) + }) + } + } + + struct NoopWaker; + + impl Wake for NoopWaker { + fn wake(self: Arc) {} + } + + #[test] + fn contributed_tools_default_to_mutating() { + let call = ToolCall { + context: (), + call_id: "call-default-mutating".to_string(), + input: ToolInput::Function { + arguments: "{}".to_string(), + }, + }; + let mut future = DefaultMutatingExecutor.is_mutating(&call); + let waker = Waker::from(Arc::new(NoopWaker)); + let mut context = Context::from_waker(&waker); + + assert!(matches!( + future.as_mut().poll(&mut context), + Poll::Ready(true) + )); } }