From e28d2a19b431e3507482c05c2e292aeb9c68bf8c Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Fri, 5 Jun 2026 18:40:43 -0700 Subject: [PATCH 1/2] Enable standalone web search in code mode --- .../app-server/tests/suite/v2/web_search.rs | 7 +- codex-rs/codex-api/src/endpoint/search.rs | 12 +- codex-rs/codex-api/src/search.rs | 3 +- codex-rs/core/tests/suite/code_mode.rs | 111 ++++++++++++++++++ codex-rs/ext/web-search/src/output.rs | 33 +++--- codex-rs/ext/web-search/src/tool.rs | 8 +- 6 files changed, 147 insertions(+), 27 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/web_search.rs b/codex-rs/app-server/tests/suite/v2/web_search.rs index 13c595d1480b..626767132b71 100644 --- a/codex-rs/app-server/tests/suite/v2/web_search.rs +++ b/codex-rs/app-server/tests/suite/v2/web_search.rs @@ -41,7 +41,7 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60); const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); #[tokio::test] -async fn standalone_web_search_round_trips_encrypted_output() -> Result<()> { +async fn standalone_web_search_round_trips_output() -> Result<()> { let call_id = "web-run-1"; let server = responses::start_mock_server().await; mount_search_response(&server).await; @@ -170,8 +170,8 @@ async fn standalone_web_search_round_trips_encrypted_output() -> Result<()> { "type": "function_call_output", "call_id": call_id, "output": [{ - "type": "encrypted_content", - "encrypted_content": "ciphertext", + "type": "input_text", + "text": "Search result", }], }) ); @@ -259,6 +259,7 @@ async fn mount_search_response(server: &MockServer) { .and(path("/api/codex/alpha/search")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "encrypted_output": "ciphertext", + "output": "Search result", }))) .expect(1) .mount(server) diff --git a/codex-rs/codex-api/src/endpoint/search.rs b/codex-rs/codex-api/src/endpoint/search.rs index d01fbfb7808c..143048c9545a 100644 --- a/codex-rs/codex-api/src/endpoint/search.rs +++ b/codex-rs/codex-api/src/endpoint/search.rs @@ -134,10 +134,13 @@ mod tests { } #[tokio::test] - async fn search_posts_typed_request_and_parses_encrypted_output() { + async fn search_posts_typed_request_and_parses_output() { let transport = CapturingTransport::new( - serde_json::to_vec(&json!({"encrypted_output": "ciphertext"})) - .expect("serialize response"), + serde_json::to_vec(&json!({ + "encrypted_output": "ciphertext", + "output": "search result", + })) + .expect("serialize response"), ); let client = SearchClient::new(transport.clone(), provider(), Arc::new(DummyAuth)); @@ -203,7 +206,8 @@ mod tests { assert_eq!( response, SearchResponse { - encrypted_output: "ciphertext".to_string(), + encrypted_output: Some("ciphertext".to_string()), + output: "search result".to_string(), } ); diff --git a/codex-rs/codex-api/src/search.rs b/codex-rs/codex-api/src/search.rs index 061b3ac8c6ca..bae7c8a7da4c 100644 --- a/codex-rs/codex-api/src/search.rs +++ b/codex-rs/codex-api/src/search.rs @@ -280,5 +280,6 @@ pub enum AllowedCaller { #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] pub struct SearchResponse { - pub encrypted_output: String, + pub encrypted_output: Option, + pub output: String, } diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index fca4556972d9..a6047e92bf58 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -6,9 +6,11 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; +use codex_extension_api::ExtensionRegistryBuilder; use codex_features::Feature; use codex_login::CodexAuth; use codex_models_manager::bundled_models_response; +use codex_protocol::config_types::WebSearchMode; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; @@ -17,6 +19,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; +use codex_web_search_extension::install as install_web_search_extension; use core_test_support::apps_test_server::AppsTestServer; use core_test_support::apps_test_server::AppsTestToolLoading; use core_test_support::apps_test_server::DIRECT_CALENDAR_APP_ONLY_TOOL; @@ -45,9 +48,14 @@ use std::collections::HashMap; use std::collections::HashSet; use std::fs; use std::path::Path; +use std::sync::Arc; use std::time::Duration; use std::time::Instant; +use wiremock::Mock; use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; fn custom_tool_output_items(req: &ResponsesRequest, call_id: &str) -> Vec { match req.custom_tool_call_output(call_id).get("output") { @@ -191,6 +199,109 @@ async fn run_code_mode_turn_with_config( Ok((test, second_mock)) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn code_mode_can_call_standalone_web_search() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + Mock::given(method("POST")) + .and(path("/v1/alpha/search")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "encrypted_output": "ciphertext", + "output": "Search result", + }))) + .expect(1) + .mount(&server) + .await; + + responses::mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_custom_tool_call( + "call-1", + "exec", + r#" +const result = await tools.web__run({ + search_query: [{ q: "standalone web search" }], +}); +text(result); +"#, + ), + ev_completed("resp-1"), + ]), + ) + .await; + let follow_up_mock = responses::mount_sse_once( + &server, + sse(vec![ + ev_assistant_message("msg-1", "done"), + ev_completed("resp-2"), + ]), + ) + .await; + + let auth = CodexAuth::from_api_key("dummy"); + let auth_manager = codex_core::test_support::auth_manager_from_auth(auth.clone()); + let mut extension_builder = ExtensionRegistryBuilder::::new(); + install_web_search_extension(&mut extension_builder, auth_manager); + let mut builder = test_codex() + .with_auth(auth) + .with_extensions(Arc::new(extension_builder.build())) + .with_model("test-gpt-5.1-codex") + .with_config(|config| { + config + .features + .enable(Feature::CodeMode) + .expect("code mode should be enabled"); + config + .features + .enable(Feature::StandaloneWebSearch) + .expect("standalone web search should be enabled"); + config + .web_search_mode + .set(WebSearchMode::Live) + .expect("web search mode should be accepted"); + }); + let test = builder.build(&server).await?; + + test.submit_turn("Search the web from code mode").await?; + + let search_request = server + .received_requests() + .await + .expect("received requests should be available") + .into_iter() + .find(|request| request.url.path() == "/v1/alpha/search") + .expect("standalone search request should be sent"); + let search_body = search_request + .body_json::() + .expect("search request body should be JSON"); + assert_eq!( + search_body["model"], + serde_json::json!("test-gpt-5.1-codex") + ); + assert_eq!( + search_body["commands"], + serde_json::json!({ + "search_query": [{"q": "standalone web search"}], + }) + ); + assert_eq!( + search_body["settings"], + serde_json::json!({ + "allowed_callers": ["direct"], + "external_web_access": true, + }) + ); + assert_eq!( + custom_tool_output_last_non_empty_text(&follow_up_mock.single_request(), "call-1"), + Some("Search result".to_string()) + ); + + Ok(()) +} + async fn run_code_mode_turn_with_rmcp( server: &MockServer, prompt: &str, diff --git a/codex-rs/ext/web-search/src/output.rs b/codex-rs/ext/web-search/src/output.rs index 124271c216d5..6c53f7af5dd2 100644 --- a/codex-rs/ext/web-search/src/output.rs +++ b/codex-rs/ext/web-search/src/output.rs @@ -3,20 +3,21 @@ use codex_extension_api::ToolPayload; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; +use serde_json::Value; -pub(crate) struct EncryptedSearchOutput { - encrypted_output: String, +pub(crate) struct SearchOutput { + output: String, } -impl EncryptedSearchOutput { - pub(crate) fn new(encrypted_output: String) -> Self { - Self { encrypted_output } +impl SearchOutput { + pub(crate) fn new(output: String) -> Self { + Self { output } } } -impl ToolOutput for EncryptedSearchOutput { +impl ToolOutput for SearchOutput { fn log_preview(&self) -> String { - "[encrypted standalone web search output]".to_string() + "[standalone web search output]".to_string() } fn success_for_logging(&self) -> bool { @@ -29,12 +30,16 @@ impl ToolOutput for EncryptedSearchOutput { ResponseInputItem::FunctionCallOutput { call_id: call_id.to_string(), output: FunctionCallOutputPayload::from_content_items(vec![ - FunctionCallOutputContentItem::EncryptedContent { - encrypted_content: self.encrypted_output.clone(), + FunctionCallOutputContentItem::InputText { + text: self.output.clone(), }, ]), } } + + fn code_mode_result(&self, _payload: &ToolPayload) -> Value { + Value::String(self.output.clone()) + } } #[cfg(test)] @@ -45,12 +50,12 @@ mod tests { use codex_protocol::models::ResponseInputItem; use pretty_assertions::assert_eq; - use super::EncryptedSearchOutput; + use super::SearchOutput; use super::ToolOutput; #[test] - fn emits_encrypted_function_call_output() { - let output = EncryptedSearchOutput::new("encrypted-search-output".to_string()); + fn emits_plaintext_function_call_output() { + let output = SearchOutput::new("search output".to_string()); assert_eq!( output.to_response_item( @@ -62,8 +67,8 @@ mod tests { ResponseInputItem::FunctionCallOutput { call_id: "call-1".to_string(), output: FunctionCallOutputPayload::from_content_items(vec![ - FunctionCallOutputContentItem::EncryptedContent { - encrypted_content: "encrypted-search-output".to_string(), + FunctionCallOutputContentItem::InputText { + text: "search output".to_string(), }, ]), } diff --git a/codex-rs/ext/web-search/src/tool.rs b/codex-rs/ext/web-search/src/tool.rs index 9a09b73ebb10..35f126dd6baa 100644 --- a/codex-rs/ext/web-search/src/tool.rs +++ b/codex-rs/ext/web-search/src/tool.rs @@ -26,7 +26,7 @@ use http::HeaderMap; use url::Url; use crate::history::recent_input; -use crate::output::EncryptedSearchOutput; +use crate::output::SearchOutput; use crate::schema::commands_schema; pub(crate) const WEB_NAMESPACE: &str = "web"; @@ -67,7 +67,7 @@ impl ToolExecutor for WebSearchTool { } fn exposure(&self) -> ToolExposure { - ToolExposure::DirectModelOnly + ToolExposure::Direct } fn supports_parallel_tool_calls(&self) -> bool { @@ -114,9 +114,7 @@ impl ToolExecutor for WebSearchTool { .emit_completed(web_search_item(&call.call_id, command_action)) .await; - Ok(Box::new(EncryptedSearchOutput::new( - response.encrypted_output, - ))) + Ok(Box::new(SearchOutput::new(response.output))) } } From 02724b98d36c0285a4ded52dd8eeb5cae7dba445 Mon Sep 17 00:00:00 2001 From: Rohit Arunachalam Date: Sat, 6 Jun 2026 12:30:38 -0700 Subject: [PATCH 2/2] Simplify standalone search code mode output --- codex-rs/core/tests/suite/code_mode.rs | 1 - codex-rs/ext/web-search/src/output.rs | 5 ----- 2 files changed, 6 deletions(-) diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index a6047e92bf58..f5f17a09bda6 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -207,7 +207,6 @@ async fn code_mode_can_call_standalone_web_search() -> Result<()> { Mock::given(method("POST")) .and(path("/v1/alpha/search")) .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "encrypted_output": "ciphertext", "output": "Search result", }))) .expect(1) diff --git a/codex-rs/ext/web-search/src/output.rs b/codex-rs/ext/web-search/src/output.rs index 6c53f7af5dd2..799897b62dd1 100644 --- a/codex-rs/ext/web-search/src/output.rs +++ b/codex-rs/ext/web-search/src/output.rs @@ -3,7 +3,6 @@ use codex_extension_api::ToolPayload; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; -use serde_json::Value; pub(crate) struct SearchOutput { output: String, @@ -36,10 +35,6 @@ impl ToolOutput for SearchOutput { ]), } } - - fn code_mode_result(&self, _payload: &ToolPayload) -> Value { - Value::String(self.output.clone()) - } } #[cfg(test)]