change-web-search

This commit is contained in:
Ahmed Ibrahim
2025-10-15 15:55:50 -07:00
parent 7a1af53f03
commit b02f9bdcd4
21 changed files with 357 additions and 114 deletions

View File

@@ -10,12 +10,12 @@ use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::Verbosity;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::DisabledTool;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::default_disabled_tools;
use paste::paste;
use serde::Deserialize;
use serde::Serialize;
@@ -551,8 +551,8 @@ pub struct SendUserTurnParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<ReasoningEffort>,
pub summary: ReasoningSummary,
#[serde(default = "default_disabled_tools")]
pub disabled_tools: Vec<String>,
#[serde(default = "DisabledTool::defaults")]
pub disabled_tools: Vec<DisabledTool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, TS)]

View File

@@ -27,10 +27,10 @@ use codex_core::protocol_config_types::ReasoningEffort;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::protocol::DisabledTool;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::InputMessageKind;
use codex_protocol::protocol::default_disabled_tools;
use pretty_assertions::assert_eq;
use std::env;
use tempfile::TempDir;
@@ -346,7 +346,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() {
model: "mock-model".to_string(),
effort: Some(ReasoningEffort::Medium),
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.expect("send sendUserTurn");
@@ -485,7 +485,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() {
model: model.clone(),
effort: Some(ReasoningEffort::Medium),
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.expect("send first sendUserTurn");
@@ -516,7 +516,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() {
model: model.clone(),
effort: Some(ReasoningEffort::Medium),
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.expect("send second sendUserTurn");

View File

@@ -239,8 +239,7 @@ impl ModelClient {
instructions: &full_instructions,
input: &input_with_instructions,
tools: &tools_json,
allowed_tools: prompt.allowed_tools.as_slice(),
tool_choice: "auto",
tool_choice: prompt.tool_choice(),
parallel_tool_calls: prompt.parallel_tool_calls,
reasoning,
store: azure_workaround,

View File

@@ -43,7 +43,7 @@ pub struct Prompt {
pub output_schema: Option<Value>,
/// The set of tools that are allowed to be used by the model.
pub(crate) allowed_tools: Vec<Value>,
pub(crate) allowed_tools: Option<Vec<Value>>,
}
impl Prompt {
@@ -88,10 +88,17 @@ impl Prompt {
input
}
}
fn json_slice_is_empty(values: &[Value]) -> bool {
values.is_empty()
pub(crate) fn tool_choice(&self) -> ToolChoice<'_> {
match &self.allowed_tools {
Some(tools) => ToolChoice::AllowedTools(AllowedToolsChoice {
choice_type: "allowed_tools",
mode: "auto",
tools,
}),
None => ToolChoice::Auto("auto"),
}
}
}
fn reserialize_shell_outputs(items: &mut [ResponseItem]) {
@@ -275,9 +282,7 @@ pub(crate) struct ResponsesApiRequest<'a> {
// separate enum for serialization.
pub(crate) input: &'a Vec<ResponseItem>,
pub(crate) tools: &'a [serde_json::Value],
#[serde(skip_serializing_if = "json_slice_is_empty")]
pub(crate) allowed_tools: &'a [serde_json::Value],
pub(crate) tool_choice: &'static str,
pub(crate) tool_choice: ToolChoice<'a>,
pub(crate) parallel_tool_calls: bool,
pub(crate) reasoning: Option<Reasoning>,
pub(crate) store: bool,
@@ -289,6 +294,21 @@ pub(crate) struct ResponsesApiRequest<'a> {
pub(crate) text: Option<TextControls>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub(crate) enum ToolChoice<'a> {
Auto(&'static str),
AllowedTools(AllowedToolsChoice<'a>),
}
#[derive(Debug, Serialize)]
pub(crate) struct AllowedToolsChoice<'a> {
#[serde(rename = "type")]
pub(crate) choice_type: &'static str,
pub(crate) mode: &'static str,
pub(crate) tools: &'a [Value],
}
pub(crate) mod tools {
use crate::openai_tools::JsonSchema;
use serde::Deserialize;
@@ -466,8 +486,7 @@ mod tests {
instructions: "i",
input: &input,
tools: &tools,
allowed_tools: &[],
tool_choice: "auto",
tool_choice: ToolChoice::Auto("auto"),
parallel_tool_calls: true,
reasoning: None,
store: false,
@@ -508,8 +527,7 @@ mod tests {
instructions: "i",
input: &input,
tools: &tools,
allowed_tools: &[],
tool_choice: "auto",
tool_choice: ToolChoice::Auto("auto"),
parallel_tool_calls: true,
reasoning: None,
store: false,
@@ -545,8 +563,7 @@ mod tests {
instructions: "i",
input: &input,
tools: &tools,
allowed_tools: &[],
tool_choice: "auto",
tool_choice: ToolChoice::Auto("auto"),
parallel_tool_calls: true,
reasoning: None,
store: false,
@@ -573,8 +590,11 @@ mod tests {
instructions: "i",
input: &input,
tools: &tools,
allowed_tools: allowed.as_slice(),
tool_choice: "auto",
tool_choice: ToolChoice::AllowedTools(AllowedToolsChoice {
choice_type: "allowed_tools",
mode: "auto",
tools: allowed.as_slice(),
}),
parallel_tool_calls: true,
reasoning: None,
store: false,
@@ -585,17 +605,21 @@ mod tests {
};
let v = serde_json::to_value(&req).expect("json");
let choice = v.get("tool_choice").expect("tool_choice field");
assert_eq!(
v.get("allowed_tools")
.and_then(|val| val.as_array())
.map(std::vec::Vec::len),
Some(1)
choice.get("type"),
Some(&serde_json::Value::String("allowed_tools".into()))
);
let first = v
.get("allowed_tools")
assert_eq!(
choice.get("mode"),
Some(&serde_json::Value::String("auto".into()))
);
let tools_array = choice
.get("tools")
.and_then(|val| val.as_array())
.and_then(|arr| arr.first())
.expect("allowed tool entry");
.expect("tools array");
assert_eq!(tools_array.len(), 1);
let first = &tools_array[0];
assert_eq!(
first.get("type"),
Some(&serde_json::Value::String("function".into()))

View File

@@ -16,6 +16,7 @@ use async_channel::Sender;
use codex_apply_patch::ApplyPatchAction;
use codex_protocol::ConversationId;
use codex_protocol::protocol::ConversationPathResponseEvent;
use codex_protocol::protocol::DisabledTool;
use codex_protocol::protocol::ExitedReviewModeEvent;
use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::RolloutItem;
@@ -23,7 +24,6 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TaskStartedEvent;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::default_disabled_tools;
use futures::future::BoxFuture;
use futures::prelude::*;
use futures::stream::FuturesOrdered;
@@ -265,7 +265,7 @@ pub(crate) struct TurnContext {
pub(crate) tools_config: ToolsConfig,
pub(crate) is_review_mode: bool,
pub(crate) final_output_json_schema: Option<Value>,
pub(crate) disabled_tools: Vec<String>,
pub(crate) disabled_tools: Vec<DisabledTool>,
}
impl TurnContext {
@@ -461,7 +461,7 @@ impl Session {
cwd,
is_review_mode: false,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
};
let services = SessionServices {
mcp_connection_manager,
@@ -1211,7 +1211,7 @@ async fn submission_loop(
cwd: new_cwd.clone(),
is_review_mode: false,
final_output_json_schema: None,
disabled_tools: disabled_tools.unwrap_or_else(default_disabled_tools),
disabled_tools: disabled_tools.unwrap_or_else(DisabledTool::defaults),
};
// Install the new persistent context for subsequent tasks/turns.
@@ -1586,7 +1586,7 @@ async fn spawn_review_thread(
cwd: parent_turn_context.cwd.clone(),
is_review_mode: true,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
};
// Seed the child task with the review prompt as the initial user message.
@@ -1953,20 +1953,29 @@ async fn run_turn(
.get_model_family()
.supports_parallel_tool_calls;
let parallel_tool_calls = model_supports_parallel;
let allowed_tools = router
.allowed_tools()
.into_iter()
.filter(|tool| {
tool.get("name")
.and_then(|val| val.as_str())
.is_none_or(|name| {
!turn_context
.disabled_tools
.iter()
.any(|disabled| disabled == name)
})
})
.collect::<Vec<_>>();
let mut allowed_tools = Vec::new();
let mut restricted_tool_choice = false;
for tool in router.allowed_tools() {
let is_disabled = tool
.get("name")
.and_then(|val| val.as_str())
.is_some_and(|name| {
turn_context
.disabled_tools
.iter()
.any(|disabled| disabled.matches_tool_name(name))
});
if is_disabled {
restricted_tool_choice = true;
continue;
}
allowed_tools.push(tool);
}
let allowed_tools = if restricted_tool_choice {
Some(allowed_tools)
} else {
None
};
let prompt = Prompt {
input,
tools: router.specs(),
@@ -2781,7 +2790,7 @@ mod tests {
tools_config,
is_review_mode: false,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
};
let services = SessionServices {
mcp_connection_manager: McpConnectionManager::default(),
@@ -2850,7 +2859,7 @@ mod tests {
tools_config,
is_review_mode: false,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
});
let services = SessionServices {
mcp_connection_manager: McpConnectionManager::default(),

View File

@@ -245,6 +245,6 @@ pub const FEATURES: &[FeatureSpec] = &[
id: Feature::WebSearchRequest,
key: "web_search_request",
stage: Stage::Stable,
default_enabled: false,
default_enabled: true,
},
];

View File

@@ -124,7 +124,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
.unwrap_or_default()
.to_string();
let tool_calls = requests[0]["tools"].clone();
let allowed_tools = requests[0]["allowed_tools"].clone();
let tool_choice = requests[0]["tool_choice"].clone();
let prompt_cache_key = requests[0]["prompt_cache_key"]
.as_str()
.unwrap_or_default()
@@ -171,8 +171,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
}
],
"tools": tool_calls,
"allowed_tools": allowed_tools,
"tool_choice": "auto",
"tool_choice": tool_choice,
"parallel_tool_calls": false,
"reasoning": {
"summary": "auto"
@@ -241,8 +240,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
}
],
"tools": [],
"allowed_tools": allowed_tools,
"tool_choice": "auto",
"tool_choice": tool_choice,
"parallel_tool_calls": false,
"reasoning": {
"summary": "auto"
@@ -307,8 +305,7 @@ SUMMARY_ONLY_CONTEXT"
}
],
"tools": tool_calls,
"allowed_tools": allowed_tools,
"tool_choice": "auto",
"tool_choice": tool_choice,
"parallel_tool_calls": false,
"reasoning": {
"summary": "auto"
@@ -393,8 +390,7 @@ SUMMARY_ONLY_CONTEXT"
}
],
"tools": tool_calls,
"allowed_tools": allowed_tools,
"tool_choice": "auto",
"tool_choice": tool_choice,
"parallel_tool_calls": false,
"reasoning": {
"summary": "auto"
@@ -479,8 +475,7 @@ SUMMARY_ONLY_CONTEXT"
}
],
"tools": tool_calls,
"allowed_tools": allowed_tools,
"tool_choice": "auto",
"tool_choice": tool_choice,
"parallel_tool_calls": false,
"reasoning": {
"summary": "auto"

View File

@@ -3,11 +3,11 @@
use anyhow::Result;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -160,7 +160,7 @@ async fn submit_turn(test: &TestCodex, prompt: &str) -> Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -1,11 +1,11 @@
#![cfg(not(target_os = "windows"))]
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
@@ -85,7 +85,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -1,11 +1,11 @@
#![cfg(not(target_os = "windows"))]
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -77,7 +77,7 @@ async fn list_dir_tool_returns_entries() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -183,7 +183,7 @@ async fn list_dir_tool_depth_one_omits_children() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -296,7 +296,7 @@ async fn list_dir_tool_depth_two_includes_children_only() -> anyhow::Result<()>
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -412,7 +412,7 @@ async fn list_dir_tool_depth_three_includes_grandchildren() -> anyhow::Result<()
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -8,11 +8,11 @@ use codex_core::config::OPENAI_DEFAULT_MODEL;
use codex_core::features::Feature;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_core::protocol_config_types::ReasoningEffort;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_core::shell::Shell;
@@ -579,7 +579,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() {
effort: Some(ReasoningEffort::High),
summary: ReasoningSummary::Detailed,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.unwrap();
@@ -691,7 +691,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() {
effort: default_effort,
summary: default_summary,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.unwrap();
@@ -709,7 +709,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() {
effort: default_effort,
summary: default_summary,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.unwrap();
@@ -807,7 +807,7 @@ async fn send_user_turn_with_changes_sends_environment_context() {
effort: default_effort,
summary: default_summary,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.unwrap();
@@ -825,7 +825,7 @@ async fn send_user_turn_with_changes_sends_environment_context() {
effort: Some(ReasoningEffort::High),
summary: ReasoningSummary::Detailed,
final_output_json_schema: None,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await
.unwrap();

View File

@@ -1,11 +1,11 @@
#![cfg(not(target_os = "windows"))]
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -75,7 +75,7 @@ async fn read_file_tool_returns_requested_lines() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -12,11 +12,11 @@ use codex_core::config_types::McpServerTransportConfig;
use codex_core::features::Feature;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses;
use core_test_support::responses::mount_sse_once_match;
@@ -111,7 +111,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -261,7 +261,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -443,7 +443,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -4,11 +4,11 @@ use anyhow::Result;
use codex_core::features::Feature;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::assert_regex_match;
use core_test_support::responses::ev_apply_patch_function_call;
@@ -46,7 +46,7 @@ async fn submit_turn(test: &TestCodex, prompt: &str, sandbox_policy: SandboxPoli
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -6,11 +6,11 @@ use assert_matches::assert_matches;
use codex_core::features::Feature;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::plan_tool::StepStatus;
use core_test_support::assert_regex_match;
@@ -85,7 +85,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()>
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -155,7 +155,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -239,7 +239,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -338,7 +338,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -440,7 +440,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -6,11 +6,11 @@ use std::time::Instant;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -39,7 +39,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -5,11 +5,12 @@ use anyhow::Result;
use codex_core::features::Feature;
use codex_core::model_family::find_family_for_model;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::DisabledToolKind;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::assert_regex_match;
use core_test_support::responses::ev_assistant_message;
@@ -49,7 +50,7 @@ async fn submit_turn(
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -410,6 +411,163 @@ async fn shell_timeout_includes_timeout_prefix_and_metadata() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_allowed_when_other_tool_disabled() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mock = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-1"),
ev_assistant_message("msg-1", "done"),
ev_completed("resp-1"),
]),
)
.await;
let mut builder = test_codex().with_config(|config| {
config.model = "gpt-5".to_string();
config.model_family = find_family_for_model("gpt-5").expect("gpt-5 family");
config.features.enable(Feature::WebSearchRequest);
});
let test = builder.build(&server).await?;
test.codex
.submit(Op::UserTurn {
items: vec![InputItem::Text {
text: "hello codex".into(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: test.session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: vec![DisabledToolKind::ViewImage.into()],
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TaskComplete(_))
})
.await;
let body = mock.single_request().body_json();
let choice = body
.get("tool_choice")
.expect("tool_choice field should be present");
assert!(
choice.is_object(),
"expected tool_choice to be an object when tools are restricted: {choice:?}"
);
assert_eq!(
choice.get("type").and_then(Value::as_str),
Some("allowed_tools")
);
assert_eq!(choice.get("mode").and_then(Value::as_str), Some("auto"));
let allowed = choice
.get("tools")
.and_then(Value::as_array)
.cloned()
.expect("allowed tools array");
assert!(
allowed
.iter()
.any(|tool| tool.get("name").and_then(Value::as_str) == Some("web_search")),
"expected web_search to remain allowed: {allowed:?}"
);
assert!(
!allowed
.iter()
.any(|tool| tool.get("name").and_then(Value::as_str) == Some("view_image")),
"expected view_image to be excluded: {allowed:?}"
);
let tools = tool_names(&body);
assert!(
tools.iter().any(|name| name == "web_search"),
"expected tools list to include web_search: {tools:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_enables_web_search() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mock = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-1"),
ev_assistant_message("msg-1", "done"),
ev_completed("resp-1"),
]),
)
.await;
let mut builder = test_codex().with_config(|config| {
config.model = "gpt-5".to_string();
config.model_family = find_family_for_model("gpt-5").expect("gpt-5 family");
config.features.enable(Feature::WebSearchRequest);
});
let test = builder.build(&server).await?;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
sandbox_policy: None,
model: None,
effort: None,
summary: None,
disabled_tools: Some(vec![]),
})
.await?;
test.codex
.submit(Op::UserTurn {
items: vec![InputItem::Text {
text: "hello after override".into(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: test.session_configured.model.clone(),
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: vec![],
})
.await?;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TaskComplete(_))
})
.await;
let body = mock.single_request().body_json();
let choice = body
.get("tool_choice")
.expect("tool_choice field should be present");
assert_eq!(
choice.as_str(),
Some("auto"),
"expected unrestricted tool choice to be auto: {choice:?}"
);
let tools = tool_names(&body);
assert!(
tools.iter().any(|name| name == "web_search"),
"expected tools list to include web_search: {tools:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_spawn_failure_truncates_exec_error() -> Result<()> {
skip_if_no_network!(Ok(()));

View File

@@ -5,11 +5,11 @@ use std::collections::HashMap;
use anyhow::Result;
use codex_core::features::Feature;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -129,7 +129,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -266,7 +266,7 @@ PY
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -373,7 +373,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -3,11 +3,11 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::default_disabled_tools;
use codex_protocol::config_types::ReasoningSummary;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -101,7 +101,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -201,7 +201,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
@@ -267,7 +267,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
model: session_model,
effort: None,
summary: ReasoningSummary::Auto,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;

View File

@@ -19,13 +19,13 @@ use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::git_info::get_git_repo_root;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::DisabledTool;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SessionSource;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::default_disabled_tools;
use codex_ollama::DEFAULT_OSS_MODEL;
use codex_protocol::config_types::SandboxMode;
use event_processor_with_human_output::EventProcessorWithHumanOutput;
@@ -349,7 +349,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
effort: default_effort,
summary: default_summary,
final_output_json_schema: output_schema,
disabled_tools: default_disabled_tools(),
disabled_tools: DisabledTool::defaults(),
})
.await?;
info!("Sent prompt with event ID: {initial_prompt_task_id}");

View File

@@ -37,9 +37,67 @@ pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = "<environment_context>";
pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = "</environment_context>";
pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:";
/// Default disabled tools used when clients do not explicitly supply one.
pub fn default_disabled_tools() -> Vec<String> {
vec!["web_search".to_string()]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
pub enum DisabledToolKind {
WebSearch,
ViewImage,
UnifiedExec,
UpdatePlan,
Shell,
ApplyPatch,
GrepFiles,
ReadFile,
ListDir,
LocalShell,
}
impl DisabledToolKind {
pub fn raw_name(self) -> &'static str {
match self {
DisabledToolKind::WebSearch => "web_search",
DisabledToolKind::ViewImage => "view_image",
DisabledToolKind::UnifiedExec => "unified_exec",
DisabledToolKind::UpdatePlan => "update_plan",
DisabledToolKind::Shell => "shell",
DisabledToolKind::ApplyPatch => "apply_patch",
DisabledToolKind::GrepFiles => "grep_files",
DisabledToolKind::ReadFile => "read_file",
DisabledToolKind::ListDir => "list_dir",
DisabledToolKind::LocalShell => "local_shell",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(untagged)]
pub enum DisabledTool {
Known(DisabledToolKind),
Other(String),
}
impl From<DisabledToolKind> for DisabledTool {
fn from(kind: DisabledToolKind) -> Self {
DisabledTool::Known(kind)
}
}
impl DisabledTool {
/// Default disabled tools used when clients do not explicitly supply one.
pub fn defaults() -> Vec<Self> {
vec![DisabledToolKind::WebSearch.into()]
}
pub fn raw_name(&self) -> &str {
match self {
DisabledTool::Known(kind) => kind.raw_name(),
DisabledTool::Other(name) => name.as_str(),
}
}
pub fn matches_tool_name(&self, tool_name: &str) -> bool {
self.raw_name() == tool_name
}
}
/// Submission Queue Entry - requests from user
@@ -96,8 +154,8 @@ pub enum Op {
// The JSON schema to use for the final assistant message
final_output_json_schema: Option<Value>,
// disables tools
#[serde(default = "default_disabled_tools")]
disabled_tools: Vec<String>,
#[serde(default = "DisabledTool::defaults")]
disabled_tools: Vec<DisabledTool>,
},
/// Override parts of the persistent turn context for subsequent turns.
@@ -136,7 +194,7 @@ pub enum Op {
/// Updated disabled tools.
#[serde(skip_serializing_if = "Option::is_none")]
disabled_tools: Option<Vec<String>>,
disabled_tools: Option<Vec<DisabledTool>>,
},
/// Approve a command execution