Compare commits

...

16 Commits

Author SHA1 Message Date
ashwinnathan-openai
27d20b3e54 fix 2026-03-20 13:10:24 -07:00
ashwinnathan-openai
1945812d7c readme 2026-03-20 12:28:19 -07:00
ashwinnathan-openai
c6b0cf680b changes 2026-03-20 12:28:19 -07:00
ashwinnathan-openai
df316191fa fmt 2026-03-20 12:28:19 -07:00
ashwinnathan-openai
ae038b9b88 web search resolution 2026-03-20 12:28:19 -07:00
ashwinnathan-openai
4b9f85e15b schema 2026-03-20 12:28:19 -07:00
ashwinnathan-openai
37679d2a6c comments 2026-03-20 12:28:15 -07:00
ashwinnathan-openai
42d3e1b45f tests 2026-03-20 12:27:59 -07:00
ashwinnathan-openai
c857d458ad update 2026-03-20 12:27:58 -07:00
ashwinnathan-openai
861ac203d8 tests 2026-03-20 12:27:58 -07:00
ashwinnathan-openai
eb50e97cf1 web search mode 2026-03-20 12:27:58 -07:00
ashwinnathan-openai
b3b0ce3645 remove execution mode 2026-03-20 12:27:58 -07:00
ashwinnathan-openai
6cfd0fd5e9 fix 2026-03-20 12:27:53 -07:00
ashwinnathan-openai
400d1a5f57 enabled 2026-03-20 12:26:35 -07:00
ashwinnathan-openai
7b0f953a6c tests 2026-03-20 12:26:35 -07:00
ashwinnathan-openai
33fa66ae1e builtin config 2026-03-20 12:26:31 -07:00
20 changed files with 1639 additions and 267 deletions

View File

@@ -13784,9 +13784,117 @@
],
"type": "object"
},
"ToolFeatureConfig": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ToolsV2": {
"properties": {
"agent_jobs": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"agents": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"disable_defaults": {
"type": [
"boolean",
"null"
]
},
"document_generation": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"filesystem": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"image_generation": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"javascript": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"planning": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"shell": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"user_input": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"view_image": {
"description": "Legacy enablement for the `view_image` capability.",
"type": [
"boolean",
"null"
@@ -13795,7 +13903,7 @@
"web_search": {
"anyOf": [
{
"$ref": "#/definitions/v2/WebSearchToolConfig"
"$ref": "#/definitions/v2/WebSearchFeatureConfig"
},
{
"type": "null"
@@ -14417,6 +14525,46 @@
],
"type": "string"
},
"WebSearchFeatureConfig": {
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"context_size": {
"anyOf": [
{
"$ref": "#/definitions/v2/WebSearchContextSize"
},
{
"type": "null"
}
]
},
"enabled": {
"type": [
"boolean",
"null"
]
},
"location": {
"anyOf": [
{
"$ref": "#/definitions/v2/WebSearchLocation"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"WebSearchLocation": {
"additionalProperties": false,
"properties": {
@@ -14455,41 +14603,6 @@
],
"type": "string"
},
"WebSearchToolConfig": {
"additionalProperties": false,
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"context_size": {
"anyOf": [
{
"$ref": "#/definitions/v2/WebSearchContextSize"
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"$ref": "#/definitions/v2/WebSearchLocation"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"WindowsSandboxSetupCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {

View File

@@ -11544,9 +11544,117 @@
],
"type": "object"
},
"ToolFeatureConfig": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ToolsV2": {
"properties": {
"agent_jobs": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"agents": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"disable_defaults": {
"type": [
"boolean",
"null"
]
},
"document_generation": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"filesystem": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"image_generation": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"javascript": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"planning": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"shell": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"user_input": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"view_image": {
"description": "Legacy enablement for the `view_image` capability.",
"type": [
"boolean",
"null"
@@ -11555,7 +11663,7 @@
"web_search": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchToolConfig"
"$ref": "#/definitions/WebSearchFeatureConfig"
},
{
"type": "null"
@@ -12177,6 +12285,46 @@
],
"type": "string"
},
"WebSearchFeatureConfig": {
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"context_size": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchContextSize"
},
{
"type": "null"
}
]
},
"enabled": {
"type": [
"boolean",
"null"
]
},
"location": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchLocation"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"WebSearchLocation": {
"additionalProperties": false,
"properties": {
@@ -12215,41 +12363,6 @@
],
"type": "string"
},
"WebSearchToolConfig": {
"additionalProperties": false,
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"context_size": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchContextSize"
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchLocation"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"WindowsSandboxSetupCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {

View File

@@ -760,9 +760,117 @@
],
"type": "string"
},
"ToolFeatureConfig": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ToolsV2": {
"properties": {
"agent_jobs": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"agents": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"disable_defaults": {
"type": [
"boolean",
"null"
]
},
"document_generation": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"filesystem": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"image_generation": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"javascript": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"planning": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"shell": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"user_input": {
"anyOf": [
{
"$ref": "#/definitions/ToolFeatureConfig"
},
{
"type": "null"
}
]
},
"view_image": {
"description": "Legacy enablement for the `view_image` capability.",
"type": [
"boolean",
"null"
@@ -771,7 +879,7 @@
"web_search": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchToolConfig"
"$ref": "#/definitions/WebSearchFeatureConfig"
},
{
"type": "null"
@@ -798,6 +906,46 @@
],
"type": "string"
},
"WebSearchFeatureConfig": {
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"context_size": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchContextSize"
},
{
"type": "null"
}
]
},
"enabled": {
"type": [
"boolean",
"null"
]
},
"location": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchLocation"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"WebSearchLocation": {
"additionalProperties": false,
"properties": {
@@ -835,41 +983,6 @@
"live"
],
"type": "string"
},
"WebSearchToolConfig": {
"additionalProperties": false,
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"context_size": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchContextSize"
},
{
"type": "null"
}
]
},
"location": {
"anyOf": [
{
"$ref": "#/definitions/WebSearchLocation"
},
{
"type": "null"
}
]
}
},
"type": "object"
}
},
"properties": {

View File

@@ -1,7 +0,0 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { WebSearchContextSize } from "./WebSearchContextSize";
import type { WebSearchLocation } from "./WebSearchLocation";
export type WebSearchToolConfig = { context_size: WebSearchContextSize | null, allowed_domains: Array<string> | null, location: WebSearchLocation | null, };

View File

@@ -74,5 +74,4 @@ export type { WebSearchAction } from "./WebSearchAction";
export type { WebSearchContextSize } from "./WebSearchContextSize";
export type { WebSearchLocation } from "./WebSearchLocation";
export type { WebSearchMode } from "./WebSearchMode";
export type { WebSearchToolConfig } from "./WebSearchToolConfig";
export * as v2 from "./v2";

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ToolFeatureConfig = { enabled: boolean | null, };

View File

@@ -1,6 +1,11 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { WebSearchToolConfig } from "../WebSearchToolConfig";
import type { ToolFeatureConfig } from "./ToolFeatureConfig";
import type { WebSearchFeatureConfig } from "./WebSearchFeatureConfig";
export type ToolsV2 = { web_search: WebSearchToolConfig | null, view_image: boolean | null, };
export type ToolsV2 = { disable_defaults: boolean | null, shell: ToolFeatureConfig | null, filesystem: ToolFeatureConfig | null, javascript: ToolFeatureConfig | null, agents: ToolFeatureConfig | null, agent_jobs: ToolFeatureConfig | null, planning: ToolFeatureConfig | null, user_input: ToolFeatureConfig | null, web_search: WebSearchFeatureConfig | null, image_generation: ToolFeatureConfig | null, document_generation: ToolFeatureConfig | null,
/**
* Legacy enablement for the `view_image` capability.
*/
view_image: boolean | null, };

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { WebSearchContextSize } from "../WebSearchContextSize";
import type { WebSearchLocation } from "../WebSearchLocation";
export type WebSearchFeatureConfig = { enabled: boolean | null, context_size: WebSearchContextSize | null, allowed_domains: Array<string> | null, location: WebSearchLocation | null, };

View File

@@ -305,6 +305,7 @@ export type { ThreadUnsubscribeParams } from "./ThreadUnsubscribeParams";
export type { ThreadUnsubscribeResponse } from "./ThreadUnsubscribeResponse";
export type { ThreadUnsubscribeStatus } from "./ThreadUnsubscribeStatus";
export type { TokenUsageBreakdown } from "./TokenUsageBreakdown";
export type { ToolFeatureConfig } from "./ToolFeatureConfig";
export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer";
export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption";
export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams";
@@ -328,6 +329,7 @@ export type { TurnSteerParams } from "./TurnSteerParams";
export type { TurnSteerResponse } from "./TurnSteerResponse";
export type { UserInput } from "./UserInput";
export type { WebSearchAction } from "./WebSearchAction";
export type { WebSearchFeatureConfig } from "./WebSearchFeatureConfig";
export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification";
export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type { WindowsSandboxSetupStartParams } from "./WindowsSandboxSetupStartParams";

View File

@@ -533,11 +533,38 @@ pub struct SandboxWorkspaceWrite {
pub exclude_slash_tmp: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub struct ToolFeatureConfig {
pub enabled: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub struct WebSearchFeatureConfig {
pub enabled: Option<bool>,
#[serde(flatten)]
pub config: WebSearchToolConfig,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub struct ToolsV2 {
pub web_search: Option<WebSearchToolConfig>,
pub disable_defaults: Option<bool>,
pub shell: Option<ToolFeatureConfig>,
pub filesystem: Option<ToolFeatureConfig>,
pub javascript: Option<ToolFeatureConfig>,
pub agents: Option<ToolFeatureConfig>,
pub agent_jobs: Option<ToolFeatureConfig>,
pub planning: Option<ToolFeatureConfig>,
pub user_input: Option<ToolFeatureConfig>,
pub web_search: Option<WebSearchFeatureConfig>,
pub image_generation: Option<ToolFeatureConfig>,
pub document_generation: Option<ToolFeatureConfig>,
/// Legacy enablement for the `view_image` capability.
pub view_image: Option<bool>,
}

View File

@@ -19,7 +19,9 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxMode;
use codex_app_server_protocol::ToolFeatureConfig;
use codex_app_server_protocol::ToolsV2;
use codex_app_server_protocol::WebSearchFeatureConfig;
use codex_app_server_protocol::WriteStatus;
use codex_core::config::set_project_trust_level;
use codex_protocol::config_types::TrustLevel;
@@ -97,11 +99,17 @@ async fn config_read_includes_tools() -> Result<()> {
model = "gpt-user"
[tools.web_search]
enabled = true
context_size = "low"
allowed_domains = ["example.com"]
[tools]
disable_defaults = true
view_image = false
[tools.shell]
[tools.filesystem]
"#,
)?;
let codex_home_path = codex_home.path().canonicalize()?;
@@ -131,11 +139,24 @@ view_image = false
assert_eq!(
tools,
ToolsV2 {
web_search: Some(WebSearchToolConfig {
context_size: Some(WebSearchContextSize::Low),
allowed_domains: Some(vec!["example.com".to_string()]),
location: None,
disable_defaults: Some(true),
shell: Some(ToolFeatureConfig { enabled: None }),
filesystem: Some(ToolFeatureConfig { enabled: None }),
javascript: None,
agents: None,
agent_jobs: None,
planning: None,
user_input: None,
web_search: Some(WebSearchFeatureConfig {
enabled: Some(true),
config: WebSearchToolConfig {
context_size: Some(WebSearchContextSize::Low),
allowed_domains: Some(vec!["example.com".to_string()]),
location: None,
},
}),
image_generation: None,
document_generation: None,
view_image: Some(false),
}
);
@@ -163,7 +184,12 @@ view_image = false
file: user_file.clone(),
}
);
assert_eq!(
origins.get("tools.disable_defaults").expect("origin").name,
ConfigLayerSource::User {
file: user_file.clone(),
}
);
let layers = layers.expect("layers present");
assert_layers_user_then_optional_system(&layers, user_file)?;
@@ -203,15 +229,18 @@ location = { country = "US", city = "New York", timezone = "America/New_York" }
assert_eq!(
config.tools.expect("tools present").web_search,
Some(WebSearchToolConfig {
context_size: Some(WebSearchContextSize::High),
allowed_domains: Some(vec!["example.com".to_string()]),
location: Some(WebSearchLocation {
country: Some("US".to_string()),
region: None,
city: Some("New York".to_string()),
timezone: Some("America/New_York".to_string()),
}),
Some(WebSearchFeatureConfig {
enabled: None,
config: WebSearchToolConfig {
context_size: Some(WebSearchContextSize::High),
allowed_domains: Some(vec!["example.com".to_string()]),
location: Some(WebSearchLocation {
country: Some("US".to_string()),
region: None,
city: Some("New York".to_string()),
timezone: Some("America/New_York".to_string()),
}),
},
}),
);

View File

@@ -25,6 +25,7 @@ use codex_protocol::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -153,6 +154,46 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_start_accepts_grouped_tool_overrides() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let req_id = mcp
.send_thread_start_request(ThreadStartParams {
config: Some(HashMap::from([(
"tools".to_string(),
json!({
"disable_defaults": true,
"shell": {},
"filesystem": {},
"web_search": {
"enabled": true,
"context_size": "low",
"allowed_domains": ["example.com"]
}
}),
)])),
..Default::default()
})
.await?;
let resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(resp)?;
assert!(!thread.id.is_empty(), "thread id should not be empty");
Ok(())
}
#[tokio::test]
async fn thread_start_respects_project_config_from_cwd() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;

View File

@@ -1552,6 +1552,15 @@
},
"type": "object"
},
"ToolFeatureToml": {
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"ToolSuggestConfig": {
"additionalProperties": false,
"properties": {
@@ -1591,15 +1600,48 @@
"ToolsToml": {
"additionalProperties": false,
"properties": {
"agent_jobs": {
"$ref": "#/definitions/ToolFeatureToml"
},
"agents": {
"$ref": "#/definitions/ToolFeatureToml"
},
"disable_defaults": {
"type": "boolean"
},
"document_generation": {
"$ref": "#/definitions/ToolFeatureToml"
},
"filesystem": {
"$ref": "#/definitions/ToolFeatureToml"
},
"image_generation": {
"$ref": "#/definitions/ToolFeatureToml"
},
"javascript": {
"$ref": "#/definitions/ToolFeatureToml"
},
"planning": {
"$ref": "#/definitions/ToolFeatureToml"
},
"shell": {
"$ref": "#/definitions/ToolFeatureToml"
},
"user_input": {
"$ref": "#/definitions/ToolFeatureToml"
},
"view_image": {
"default": null,
"description": "Enable the `view_image` tool that lets the agent attach local images.",
"description": "Legacy enablement for the `view_image` capability.",
"type": "boolean"
},
"web_search": {
"allOf": [
"anyOf": [
{
"$ref": "#/definitions/WebSearchToolConfig"
"type": "boolean"
},
{
"$ref": "#/definitions/WebSearchFeatureToml"
}
],
"default": null
@@ -1726,6 +1768,27 @@
],
"type": "string"
},
"WebSearchFeatureToml": {
"additionalProperties": false,
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": "array"
},
"context_size": {
"$ref": "#/definitions/WebSearchContextSize"
},
"enabled": {
"type": "boolean"
},
"location": {
"$ref": "#/definitions/WebSearchLocation"
}
},
"type": "object"
},
"WebSearchLocation": {
"additionalProperties": false,
"properties": {
@@ -1752,24 +1815,6 @@
],
"type": "string"
},
"WebSearchToolConfig": {
"additionalProperties": false,
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": "array"
},
"context_size": {
"$ref": "#/definitions/WebSearchContextSize"
},
"location": {
"$ref": "#/definitions/WebSearchLocation"
}
},
"type": "object"
},
"WindowsSandboxModeToml": {
"enum": [
"elevated",

View File

@@ -889,6 +889,8 @@ impl TurnContext {
sandbox_policy: self.sandbox_policy.get(),
windows_sandbox_level: self.windows_sandbox_level,
})
.with_tool_feature_overrides(config.tool_feature_overrides.clone())
.with_legacy_view_image_override(config.legacy_view_image_override)
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
.with_web_search_config(self.tools_config.web_search_config.clone())
.with_allow_login_shell(self.tools_config.allow_login_shell)
@@ -1327,6 +1329,8 @@ impl Session {
sandbox_policy: session_configuration.sandbox_policy.get(),
windows_sandbox_level: session_configuration.windows_sandbox_level,
})
.with_tool_feature_overrides(per_turn_config.tool_feature_overrides.clone())
.with_legacy_view_image_override(per_turn_config.legacy_view_image_override)
.with_unified_exec_shell_mode_for_session(
user_shell,
shell_zsh_path,
@@ -5192,6 +5196,8 @@ async fn spawn_review_thread(
sandbox_policy: parent_turn_context.sandbox_policy.get(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
})
.with_tool_feature_overrides(config.tool_feature_overrides.clone())
.with_legacy_view_image_override(config.legacy_view_image_override)
.with_unified_exec_shell_mode_for_session(
sess.services.user_shell.as_ref(),
sess.services.shell_zsh_path.as_ref(),

View File

@@ -17,6 +17,7 @@ use assert_matches::assert_matches;
use codex_config::CONFIG_TOML_FILE;
use codex_features::Feature;
use codex_features::FeaturesToml;
use codex_protocol::config_types::WebSearchContextSize;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -196,6 +197,7 @@ web_search = true
Some(ToolsToml {
web_search: None,
view_image: None,
..Default::default()
})
);
}
@@ -215,6 +217,35 @@ web_search = false
Some(ToolsToml {
web_search: None,
view_image: None,
..Default::default()
})
);
}
#[test]
fn tools_feature_tables_deserialize() {
let cfg: ConfigToml = toml::from_str(
r#"
[tools]
disable_defaults = true
[tools.shell]
[tools.filesystem]
enabled = false
"#,
)
.expect("TOML deserialization should succeed");
assert_eq!(
cfg.tools,
Some(ToolsToml {
disable_defaults: Some(true),
shell: Some(ToolFeatureToml { enabled: None }),
filesystem: Some(ToolFeatureToml {
enabled: Some(false),
}),
..Default::default()
})
);
}
@@ -1455,7 +1486,10 @@ fn web_search_mode_defaults_to_none_if_unset() {
let profile = ConfigProfile::default();
let features = Features::with_defaults();
assert_eq!(resolve_web_search_mode(&cfg, &profile, &features), None);
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features, &ToolFeatureOverrides::default()),
None
);
}
#[test]
@@ -1469,7 +1503,7 @@ fn web_search_mode_prefers_profile_over_legacy_flags() {
features.enable(Feature::WebSearchCached);
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features),
resolve_web_search_mode(&cfg, &profile, &features, &ToolFeatureOverrides::default()),
Some(WebSearchMode::Live)
);
}
@@ -1485,11 +1519,154 @@ fn web_search_mode_disabled_overrides_legacy_request() {
features.enable(Feature::WebSearchRequest);
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features),
resolve_web_search_mode(&cfg, &profile, &features, &ToolFeatureOverrides::default()),
Some(WebSearchMode::Disabled)
);
}
#[test]
fn web_search_mode_disable_defaults_disables_tool() {
let cfg = ConfigToml::default();
let profile = ConfigProfile::default();
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchCached);
let overrides = ToolFeatureOverrides {
disable_defaults: true,
..Default::default()
};
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features, &overrides),
Some(WebSearchMode::Disabled)
);
}
#[test]
fn web_search_mode_disable_defaults_ignores_explicit_mode_without_grouped_enable() {
let cfg = ConfigToml {
web_search: Some(WebSearchMode::Cached),
..Default::default()
};
let profile = ConfigProfile::default();
let features = Features::with_defaults();
let overrides = ToolFeatureOverrides {
disable_defaults: true,
..Default::default()
};
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features, &overrides),
Some(WebSearchMode::Disabled)
);
}
#[test]
fn web_search_mode_explicit_disabled_wins_when_feature_enabled() {
let cfg = ConfigToml {
web_search: Some(WebSearchMode::Disabled),
..Default::default()
};
let profile = ConfigProfile::default();
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchCached);
let overrides = ToolFeatureOverrides {
web_search: Some(true),
..Default::default()
};
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features, &overrides),
Some(WebSearchMode::Disabled)
);
}
#[test]
fn resolve_tool_feature_overrides_returns_defaults_when_omitted() {
let cfg: ConfigToml = toml::from_str(
r#"
[tools]
view_image = false
"#,
)
.expect("TOML deserialization should succeed");
assert_eq!(
resolve_tool_feature_overrides(&cfg, &ConfigProfile::default()),
ToolFeatureOverrides::default()
);
}
#[test]
fn resolve_tool_feature_overrides_profile_web_search_config_only_inherits_disabled_state() {
let cfg = ConfigToml {
tools: Some(ToolsToml {
web_search: Some(WebSearchFeatureToml {
enabled: Some(false),
config: WebSearchToolConfig::default(),
}),
..Default::default()
}),
..Default::default()
};
let profile = ConfigProfile {
tools: Some(ToolsToml {
web_search: Some(WebSearchFeatureToml {
enabled: None,
config: WebSearchToolConfig {
context_size: Some(WebSearchContextSize::Low),
..Default::default()
},
}),
..Default::default()
}),
..Default::default()
};
assert_eq!(
resolve_tool_feature_overrides(&cfg, &profile),
ToolFeatureOverrides {
web_search: Some(false),
..Default::default()
}
);
}
#[test]
fn resolve_tool_feature_overrides_web_search_config_only_defaults_to_enabled_when_both_layers_present()
{
let cfg = ConfigToml {
tools: Some(ToolsToml {
web_search: Some(WebSearchFeatureToml {
enabled: None,
config: WebSearchToolConfig::default(),
}),
..Default::default()
}),
..Default::default()
};
let profile = ConfigProfile {
tools: Some(ToolsToml {
web_search: Some(WebSearchFeatureToml {
enabled: None,
config: WebSearchToolConfig {
context_size: Some(WebSearchContextSize::Low),
..Default::default()
},
}),
..Default::default()
}),
..Default::default()
};
assert_eq!(
resolve_tool_feature_overrides(&cfg, &profile),
ToolFeatureOverrides {
web_search: Some(true),
..Default::default()
}
);
}
#[test]
fn web_search_mode_for_turn_uses_preference_for_read_only() {
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
@@ -4331,6 +4508,8 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
include_apply_patch_tool: false,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
tool_feature_overrides: ToolFeatureOverrides::default(),
legacy_view_image_override: None,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -4474,6 +4653,8 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
include_apply_patch_tool: false,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
tool_feature_overrides: ToolFeatureOverrides::default(),
legacy_view_image_override: None,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -4615,6 +4796,8 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
include_apply_patch_tool: false,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
tool_feature_overrides: ToolFeatureOverrides::default(),
legacy_view_image_override: None,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -4742,6 +4925,8 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
include_apply_patch_tool: false,
web_search_mode: Constrained::allow_any(WebSearchMode::Cached),
web_search_config: None,
tool_feature_overrides: ToolFeatureOverrides::default(),
legacy_view_image_override: None,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),

View File

@@ -54,6 +54,7 @@ use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
use crate::protocol::AskForApproval;
use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
use crate::tools::registry::ToolFeatureKey;
use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS;
use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
use crate::windows_sandbox::WindowsSandboxLevelExt;
@@ -546,6 +547,12 @@ pub struct Config {
/// Additional parameters for the web search tool when it is enabled.
pub web_search_config: Option<WebSearchConfig>,
/// Explicit grouped tool feature overrides requested by config.
pub tool_feature_overrides: ToolFeatureOverrides,
/// Legacy `tools.view_image` fallback used when no explicit capability override is present.
pub legacy_view_image_override: Option<bool>,
/// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool,
@@ -600,6 +607,38 @@ pub struct Config {
pub otel: crate::config::types::OtelConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolFeatureOverrides {
pub disable_defaults: bool,
pub shell: Option<bool>,
pub filesystem: Option<bool>,
pub javascript: Option<bool>,
pub agents: Option<bool>,
pub agent_jobs: Option<bool>,
pub planning: Option<bool>,
pub user_input: Option<bool>,
pub web_search: Option<bool>,
pub image_generation: Option<bool>,
pub document_generation: Option<bool>,
}
impl ToolFeatureOverrides {
pub(crate) fn for_feature(&self, feature: ToolFeatureKey) -> Option<bool> {
match feature {
ToolFeatureKey::Shell => self.shell,
ToolFeatureKey::Filesystem => self.filesystem,
ToolFeatureKey::Javascript => self.javascript,
ToolFeatureKey::Agents => self.agents,
ToolFeatureKey::AgentJobs => self.agent_jobs,
ToolFeatureKey::Planning => self.planning,
ToolFeatureKey::UserInput => self.user_input,
ToolFeatureKey::WebSearch => self.web_search,
ToolFeatureKey::ImageGeneration => self.image_generation,
ToolFeatureKey::DocumentGeneration => self.document_generation,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConfigBuilder {
codex_home: Option<PathBuf>,
@@ -1604,42 +1643,82 @@ pub struct RealtimeAudioToml {
pub speaker: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ToolFeatureToml {
pub enabled: Option<bool>,
}
impl ToolFeatureToml {
fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct WebSearchFeatureToml {
pub enabled: Option<bool>,
#[serde(flatten)]
pub config: WebSearchToolConfig,
}
impl WebSearchFeatureToml {
fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ToolsToml {
pub disable_defaults: Option<bool>,
pub shell: Option<ToolFeatureToml>,
pub filesystem: Option<ToolFeatureToml>,
pub javascript: Option<ToolFeatureToml>,
pub agents: Option<ToolFeatureToml>,
pub agent_jobs: Option<ToolFeatureToml>,
pub planning: Option<ToolFeatureToml>,
pub user_input: Option<ToolFeatureToml>,
#[serde(
default,
deserialize_with = "deserialize_optional_web_search_tool_config"
deserialize_with = "deserialize_optional_web_search_feature_config"
)]
pub web_search: Option<WebSearchToolConfig>,
#[schemars(schema_with = "crate::config::schema::web_search_feature_schema")]
pub web_search: Option<WebSearchFeatureToml>,
pub image_generation: Option<ToolFeatureToml>,
pub document_generation: Option<ToolFeatureToml>,
/// Enable the `view_image` tool that lets the agent attach local images.
/// Legacy enablement for the `view_image` capability.
#[serde(default)]
pub view_image: Option<bool>,
}
#[allow(dead_code)]
#[derive(Deserialize)]
#[serde(untagged)]
enum WebSearchToolConfigInput {
enum WebSearchFeatureConfigInput {
Enabled(bool),
Config(WebSearchToolConfig),
Feature(WebSearchFeatureToml),
}
fn deserialize_optional_web_search_tool_config<'de, D>(
fn deserialize_optional_web_search_feature_config<'de, D>(
deserializer: D,
) -> Result<Option<WebSearchToolConfig>, D::Error>
) -> Result<Option<WebSearchFeatureToml>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<WebSearchToolConfigInput>::deserialize(deserializer)?;
let value = Option::<WebSearchFeatureConfigInput>::deserialize(deserializer)?;
Ok(match value {
None => None,
Some(WebSearchToolConfigInput::Enabled(enabled)) => {
// Preserve legacy behavior: accept boolean `tools.web_search = true|false`
// without treating it as a grouped feature override.
Some(WebSearchFeatureConfigInput::Enabled(enabled)) => {
let _ = enabled;
None
}
Some(WebSearchToolConfigInput::Config(config)) => Some(config),
Some(WebSearchFeatureConfigInput::Feature(feature)) => Some(feature),
})
}
@@ -1722,7 +1801,10 @@ pub struct AgentRoleToml {
impl From<ToolsToml> for Tools {
fn from(tools_toml: ToolsToml) -> Self {
Self {
web_search: tools_toml.web_search.is_some().then_some(true),
web_search: tools_toml
.web_search
.as_ref()
.map(WebSearchFeatureToml::is_enabled),
view_image: tools_toml.view_image,
}
}
@@ -2034,31 +2116,42 @@ fn resolve_web_search_mode(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
features: &Features,
tool_feature_overrides: &ToolFeatureOverrides,
) -> Option<WebSearchMode> {
if let Some(mode) = config_profile.web_search.or(config_toml.web_search) {
return Some(mode);
match tool_feature_overrides.web_search {
Some(false) => Some(WebSearchMode::Disabled),
None if tool_feature_overrides.disable_defaults => Some(WebSearchMode::Disabled),
None | Some(true) => config_profile
.web_search
.or(config_toml.web_search)
.or_else(|| {
if features.enabled(Feature::WebSearchCached) {
Some(WebSearchMode::Cached)
} else if features.enabled(Feature::WebSearchRequest) {
Some(WebSearchMode::Live)
} else {
None
}
}),
}
if features.enabled(Feature::WebSearchCached) {
return Some(WebSearchMode::Cached);
}
if features.enabled(Feature::WebSearchRequest) {
return Some(WebSearchMode::Live);
}
None
}
fn resolve_web_search_config(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> Option<WebSearchConfig> {
let base = config_toml
.tools
.as_ref()
.and_then(|tools| tools.web_search.as_ref());
let profile = config_profile
.tools
.as_ref()
.and_then(|tools| tools.web_search.as_ref());
let base = config_toml.tools.as_ref().and_then(|tools| {
tools
.web_search
.as_ref()
.map(|web_search| &web_search.config)
});
let profile = config_profile.tools.as_ref().and_then(|tools| {
tools
.web_search
.as_ref()
.map(|web_search| &web_search.config)
});
match (base, profile) {
(None, None) => None,
@@ -2068,6 +2161,93 @@ fn resolve_web_search_config(
}
}
fn resolve_legacy_view_image_override(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> Option<bool> {
config_profile
.tools
.as_ref()
.and_then(|tools| tools.view_image)
.or(config_profile.tools_view_image)
.or_else(|| {
config_toml
.tools
.as_ref()
.and_then(|tools| tools.view_image)
})
}
fn resolve_tool_feature_overrides(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> ToolFeatureOverrides {
let base = config_toml.tools.as_ref();
let profile = config_profile.tools.as_ref();
let resolve_feature = |profile_feature: Option<&ToolFeatureToml>,
base_feature: Option<&ToolFeatureToml>| {
profile_feature
.map(ToolFeatureToml::is_enabled)
.or_else(|| base_feature.map(ToolFeatureToml::is_enabled))
};
ToolFeatureOverrides {
disable_defaults: profile
.and_then(|tools| tools.disable_defaults)
.or_else(|| base.and_then(|tools| tools.disable_defaults))
.unwrap_or(false),
shell: resolve_feature(
profile.and_then(|tools| tools.shell.as_ref()),
base.and_then(|tools| tools.shell.as_ref()),
),
filesystem: resolve_feature(
profile.and_then(|tools| tools.filesystem.as_ref()),
base.and_then(|tools| tools.filesystem.as_ref()),
),
javascript: resolve_feature(
profile.and_then(|tools| tools.javascript.as_ref()),
base.and_then(|tools| tools.javascript.as_ref()),
),
agents: resolve_feature(
profile.and_then(|tools| tools.agents.as_ref()),
base.and_then(|tools| tools.agents.as_ref()),
),
agent_jobs: resolve_feature(
profile.and_then(|tools| tools.agent_jobs.as_ref()),
base.and_then(|tools| tools.agent_jobs.as_ref()),
),
planning: resolve_feature(
profile.and_then(|tools| tools.planning.as_ref()),
base.and_then(|tools| tools.planning.as_ref()),
),
user_input: resolve_feature(
profile.and_then(|tools| tools.user_input.as_ref()),
base.and_then(|tools| tools.user_input.as_ref()),
),
web_search: match (
profile.and_then(|tools| tools.web_search.as_ref()),
base.and_then(|tools| tools.web_search.as_ref()),
) {
(Some(profile_web_search), Some(base_web_search)) => profile_web_search
.enabled
.or(base_web_search.enabled)
.or(Some(true)),
(Some(profile_web_search), None) => Some(profile_web_search.is_enabled()),
(None, Some(base_web_search)) => Some(base_web_search.is_enabled()),
(None, None) => None,
},
image_generation: resolve_feature(
profile.and_then(|tools| tools.image_generation.as_ref()),
base.and_then(|tools| tools.image_generation.as_ref()),
),
document_generation: resolve_feature(
profile.and_then(|tools| tools.document_generation.as_ref()),
base.and_then(|tools| tools.document_generation.as_ref()),
),
}
}
pub(crate) fn resolve_web_search_mode_for_turn(
web_search_mode: &Constrained<WebSearchMode>,
sandbox_policy: &SandboxPolicy,
@@ -2372,13 +2552,16 @@ impl Config {
);
approval_policy = constrained_approval_policy.value();
}
let tool_feature_overrides = resolve_tool_feature_overrides(&cfg, &config_profile);
let approvals_reviewer = approvals_reviewer_override
.or(config_profile.approvals_reviewer)
.or(cfg.approvals_reviewer)
.unwrap_or(ApprovalsReviewer::User);
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
.unwrap_or(WebSearchMode::Cached);
let web_search_mode =
resolve_web_search_mode(&cfg, &config_profile, &features, &tool_feature_overrides)
.unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
let legacy_view_image_override = resolve_legacy_view_image_override(&cfg, &config_profile);
let agent_roles =
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
@@ -2798,6 +2981,8 @@ impl Config {
include_apply_patch_tool: include_apply_patch_tool_flag,
web_search_mode: constrained_web_search_mode.value,
web_search_config,
tool_feature_overrides,
legacy_view_image_override,
use_experimental_unified_exec_tool,
background_terminal_max_timeout,
ghost_snapshot,

View File

@@ -1,4 +1,5 @@
use crate::config::ConfigToml;
use crate::config::WebSearchFeatureToml;
use crate::config::types::RawMcpServerConfig;
use codex_features::FEATURES;
use codex_features::legacy_feature_keys;
@@ -53,6 +54,21 @@ pub(crate) fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema {
Schema::Object(object)
}
/// Schema for `tools.web_search`, which still accepts a legacy boolean
/// shorthand in addition to the grouped object form.
pub(crate) fn web_search_feature_schema(schema_gen: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
any_of: Some(vec![
schema_gen.subschema_for::<bool>(),
schema_gen.subschema_for::<WebSearchFeatureToml>(),
]),
..Default::default()
})),
..Default::default()
})
}
/// Build the config schema for `config.toml`.
pub fn config_schema() -> RootSchema {
SchemaSettings::draft07()

View File

@@ -8,6 +8,7 @@ use crate::function_tool::FunctionCallError;
use crate::memories::usage::emit_metric_for_tool_read;
use crate::protocol::SandboxPolicy;
use crate::sandbox_tags::sandbox_tag;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
@@ -20,9 +21,212 @@ use codex_hooks::HookToolInput;
use codex_hooks::HookToolInputLocalShell;
use codex_hooks::HookToolKind;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
use codex_utils_readiness::Readiness;
use tracing::warn;
pub(crate) const CONTAINER_EXEC_TOOL_NAME: &str = "container.exec";
pub(crate) const EXEC_COMMAND_TOOL_NAME: &str = "exec_command";
pub(crate) const LOCAL_SHELL_TOOL_NAME: &str = "local_shell";
pub(crate) const SHELL_COMMAND_TOOL_NAME: &str = "shell_command";
pub(crate) const SHELL_TOOL_NAME: &str = "shell";
pub(crate) const WRITE_STDIN_TOOL_NAME: &str = "write_stdin";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
/// Individual built-in tools known to Codex, independent of how they are
/// grouped for config-driven enablement.
pub(crate) enum BuiltinToolKey {
ApplyPatch,
Artifacts,
CloseAgent,
CodeMode,
ContainerExec,
ExecCommand,
GrepFiles,
ImageGeneration,
JsRepl,
JsReplReset,
ListDir,
ListMcpResources,
ListMcpResourceTemplates,
LocalShell,
ReadFile,
ReadMcpResource,
RequestPermissions,
RequestUserInput,
ReportAgentJobResult,
ResumeAgent,
SearchToolBm25,
SendInput,
Shell,
ShellCommand,
SpawnAgent,
SpawnAgentsOnCsv,
TestSyncTool,
UpdatePlan,
ViewImage,
Wait,
WebSearch,
WriteStdin,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
/// Coarse-grained built-in tool capability groups exposed in `config.tools`.
pub(crate) enum ToolFeatureKey {
Shell,
Filesystem,
Javascript,
Agents,
AgentJobs,
Planning,
UserInput,
WebSearch,
ImageGeneration,
DocumentGeneration,
}
impl BuiltinToolKey {
pub(crate) const ALL: [Self; 32] = [
Self::ApplyPatch,
Self::Artifacts,
Self::CloseAgent,
Self::CodeMode,
Self::ContainerExec,
Self::ExecCommand,
Self::GrepFiles,
Self::ImageGeneration,
Self::JsRepl,
Self::JsReplReset,
Self::ListDir,
Self::ListMcpResources,
Self::ListMcpResourceTemplates,
Self::LocalShell,
Self::ReadFile,
Self::ReadMcpResource,
Self::RequestPermissions,
Self::RequestUserInput,
Self::ReportAgentJobResult,
Self::ResumeAgent,
Self::SearchToolBm25,
Self::SendInput,
Self::Shell,
Self::ShellCommand,
Self::SpawnAgent,
Self::SpawnAgentsOnCsv,
Self::TestSyncTool,
Self::UpdatePlan,
Self::ViewImage,
Self::Wait,
Self::WebSearch,
Self::WriteStdin,
];
pub(crate) fn iter() -> impl Iterator<Item = Self> {
Self::ALL.into_iter()
}
pub(crate) const fn invocation_names(self) -> &'static [&'static str] {
match self {
Self::CodeMode => &[PUBLIC_TOOL_NAME],
Self::ContainerExec => &[CONTAINER_EXEC_TOOL_NAME],
Self::ExecCommand => &[EXEC_COMMAND_TOOL_NAME],
Self::LocalShell => &[LOCAL_SHELL_TOOL_NAME],
Self::Shell => &[SHELL_TOOL_NAME],
Self::ShellCommand => &[SHELL_COMMAND_TOOL_NAME],
Self::WriteStdin => &[WRITE_STDIN_TOOL_NAME],
Self::ListMcpResources => &["list_mcp_resources"],
Self::ListMcpResourceTemplates => &["list_mcp_resource_templates"],
Self::ReadMcpResource => &["read_mcp_resource"],
Self::UpdatePlan => &["update_plan"],
Self::JsRepl => &["js_repl"],
Self::JsReplReset => &["js_repl_reset"],
Self::RequestUserInput => &["request_user_input"],
Self::RequestPermissions => &["request_permissions"],
Self::SearchToolBm25 => &["tool_search"],
Self::ApplyPatch => &["apply_patch"],
Self::GrepFiles => &["grep_files"],
Self::ReadFile => &["read_file"],
Self::ListDir => &["list_dir"],
Self::TestSyncTool => &["test_sync_tool"],
Self::WebSearch => &["web_search"],
Self::ImageGeneration => &["image_generation"],
Self::ViewImage => &[VIEW_IMAGE_TOOL_NAME],
Self::Artifacts => &["artifacts"],
Self::SpawnAgent => &["spawn_agent"],
Self::SendInput => &["send_input"],
Self::ResumeAgent => &["resume_agent"],
Self::Wait => &["wait_agent"],
Self::CloseAgent => &["close_agent"],
Self::SpawnAgentsOnCsv => &["spawn_agents_on_csv"],
Self::ReportAgentJobResult => &["report_agent_job_result"],
}
}
}
impl ToolFeatureKey {
const ALL: [Self; 10] = [
Self::Shell,
Self::Filesystem,
Self::Javascript,
Self::Agents,
Self::AgentJobs,
Self::Planning,
Self::UserInput,
Self::WebSearch,
Self::ImageGeneration,
Self::DocumentGeneration,
];
pub(crate) fn iter() -> impl Iterator<Item = Self> {
Self::ALL.into_iter()
}
pub(crate) const fn builtin_tool_keys(self) -> &'static [BuiltinToolKey] {
match self {
Self::Shell => &[
BuiltinToolKey::ContainerExec,
BuiltinToolKey::ExecCommand,
BuiltinToolKey::LocalShell,
BuiltinToolKey::Shell,
BuiltinToolKey::ShellCommand,
BuiltinToolKey::WriteStdin,
],
Self::Filesystem => &[
BuiltinToolKey::ApplyPatch,
BuiltinToolKey::GrepFiles,
BuiltinToolKey::ReadFile,
BuiltinToolKey::ListDir,
BuiltinToolKey::ViewImage,
],
Self::Javascript => &[BuiltinToolKey::JsRepl, BuiltinToolKey::JsReplReset],
Self::Agents => &[
BuiltinToolKey::SpawnAgent,
BuiltinToolKey::SendInput,
BuiltinToolKey::ResumeAgent,
BuiltinToolKey::Wait,
BuiltinToolKey::CloseAgent,
],
Self::AgentJobs => &[
BuiltinToolKey::SpawnAgentsOnCsv,
BuiltinToolKey::ReportAgentJobResult,
],
Self::Planning => &[BuiltinToolKey::UpdatePlan],
Self::UserInput => &[BuiltinToolKey::RequestUserInput],
Self::WebSearch => &[BuiltinToolKey::WebSearch],
Self::ImageGeneration => &[BuiltinToolKey::ImageGeneration],
Self::DocumentGeneration => &[BuiltinToolKey::Artifacts],
}
}
pub(crate) fn for_builtin_tool(tool: BuiltinToolKey) -> Option<Self> {
Self::iter().find(|feature| feature.builtin_tool_keys().contains(&tool))
}
}
pub(crate) fn builtin_tool_key(name: &str) -> Option<BuiltinToolKey> {
BuiltinToolKey::iter().find(|key| key.invocation_names().contains(&name))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ToolKind {
Function,

View File

@@ -3,6 +3,7 @@ use crate::client_common::tools::FreeformToolFormat;
use crate::client_common::tools::ResponsesApiTool;
use crate::client_common::tools::ToolSpec;
use crate::config::AgentRoleConfig;
use crate::config::ToolFeatureOverrides;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_connection_manager::ToolInfo;
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
@@ -31,7 +32,16 @@ use crate::tools::handlers::multi_agents::MAX_WAIT_TIMEOUT_MS;
use crate::tools::handlers::multi_agents::MIN_WAIT_TIMEOUT_MS;
use crate::tools::handlers::request_permissions_tool_description;
use crate::tools::handlers::request_user_input_tool_description;
use crate::tools::registry::BuiltinToolKey;
use crate::tools::registry::CONTAINER_EXEC_TOOL_NAME;
use crate::tools::registry::EXEC_COMMAND_TOOL_NAME;
use crate::tools::registry::LOCAL_SHELL_TOOL_NAME;
use crate::tools::registry::SHELL_COMMAND_TOOL_NAME;
use crate::tools::registry::SHELL_TOOL_NAME;
use crate::tools::registry::ToolFeatureKey;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::registry::WRITE_STDIN_TOOL_NAME;
use crate::tools::registry::builtin_tool_key;
use crate::tools::registry::tool_handler_key;
use codex_features::Feature;
use codex_features::Features;
@@ -269,6 +279,8 @@ pub(crate) struct ToolsConfig {
shell_command_backend: ShellCommandBackendConfig,
pub unified_exec_shell_mode: UnifiedExecShellMode,
pub allow_login_shell: bool,
pub tool_feature_overrides: ToolFeatureOverrides,
pub legacy_view_image_override: Option<bool>,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_mode: Option<WebSearchMode>,
pub web_search_config: Option<WebSearchConfig>,
@@ -403,6 +415,8 @@ impl ToolsConfig {
shell_command_backend,
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
allow_login_shell: true,
tool_feature_overrides: ToolFeatureOverrides::default(),
legacy_view_image_override: None,
apply_patch_tool_type,
web_search_mode: *web_search_mode,
web_search_config: None,
@@ -439,6 +453,22 @@ impl ToolsConfig {
self
}
pub fn with_tool_feature_overrides(
mut self,
tool_feature_overrides: ToolFeatureOverrides,
) -> Self {
self.tool_feature_overrides = tool_feature_overrides;
self
}
pub fn with_legacy_view_image_override(
mut self,
legacy_view_image_override: Option<bool>,
) -> Self {
self.legacy_view_image_override = legacy_view_image_override;
self
}
pub fn with_unified_exec_shell_mode(
mut self,
unified_exec_shell_mode: UnifiedExecShellMode,
@@ -473,6 +503,71 @@ impl ToolsConfig {
nested.code_mode_only_enabled = false;
nested
}
pub fn is_builtin_tool_invocation_enabled(&self, tool_name: &str) -> bool {
let Some(tool) = builtin_tool_key(tool_name) else {
return false;
};
let Some(feature) = ToolFeatureKey::for_builtin_tool(tool) else {
return true;
};
if self.is_feature_explicitly_controlled(feature) {
// In explicit grouped mode (`tools.disable_defaults = true`), the
// `[tools.<feature>]` table is the source of truth for exposure.
// That intentionally bypasses legacy fallback logic such as
// top-level `web_search` mode-based enablement.
return self.is_tool_feature_enabled(feature);
}
match tool {
BuiltinToolKey::ApplyPatch => self.apply_patch_tool_type.is_some(),
BuiltinToolKey::ViewImage => self.legacy_view_image_override.unwrap_or(true),
BuiltinToolKey::WebSearch => self.is_web_search_enabled(),
_ => self.is_tool_feature_enabled(feature),
}
}
fn is_feature_explicitly_controlled(&self, feature: ToolFeatureKey) -> bool {
self.tool_feature_overrides.disable_defaults
|| self.tool_feature_overrides.for_feature(feature).is_some()
}
fn is_web_search_enabled(&self) -> bool {
// The resolved web_search_mode already reflects config, feature defaults,
// and any requirement-driven overrides.
self.web_search_mode
.is_some_and(|mode| mode != WebSearchMode::Disabled)
}
fn is_tool_feature_enabled(&self, feature: ToolFeatureKey) -> bool {
if let Some(enabled) = self.tool_feature_overrides.for_feature(feature) {
return enabled;
}
if self.tool_feature_overrides.disable_defaults {
false
} else {
self.is_tool_feature_enabled_by_default(feature)
}
}
fn is_tool_feature_enabled_by_default(&self, feature: ToolFeatureKey) -> bool {
match feature {
ToolFeatureKey::Shell => self.shell_type != ConfigShellToolType::Disabled,
ToolFeatureKey::Filesystem => self
.experimental_supported_tools
.iter()
.any(|tool| matches!(tool.as_str(), "grep_files" | "read_file" | "list_dir")),
ToolFeatureKey::Javascript => self.js_repl_enabled,
ToolFeatureKey::Agents => self.collab_tools,
ToolFeatureKey::AgentJobs => self.agent_jobs_tools,
ToolFeatureKey::Planning => true,
ToolFeatureKey::UserInput => self.request_user_input,
ToolFeatureKey::WebSearch => self.is_web_search_enabled(),
ToolFeatureKey::ImageGeneration => self.image_gen_tool,
ToolFeatureKey::DocumentGeneration => self.artifact_tools,
}
}
}
fn supports_image_generation(model_info: &ModelInfo) -> bool {
@@ -2331,6 +2426,44 @@ fn push_tool_spec(
}
}
fn push_builtin_tool_spec_if_enabled(
builder: &mut ToolRegistryBuilder,
config: &ToolsConfig,
invocation_name: &str,
spec: ToolSpec,
supports_parallel_tool_calls: bool,
) {
if config.is_builtin_tool_invocation_enabled(invocation_name) {
push_tool_spec(
builder,
spec,
supports_parallel_tool_calls,
config.code_mode_enabled,
);
}
}
fn push_builtin_tool_with_handler_if_enabled<H>(
builder: &mut ToolRegistryBuilder,
config: &ToolsConfig,
invocation_name: &str,
spec: ToolSpec,
supports_parallel_tool_calls: bool,
handler: std::sync::Arc<H>,
) where
H: crate::tools::registry::ToolHandler + 'static,
{
if config.is_builtin_tool_invocation_enabled(invocation_name) {
push_tool_spec(
builder,
spec,
supports_parallel_tool_calls,
config.code_mode_enabled,
);
builder.register_handler(invocation_name, handler);
}
}
pub(crate) fn mcp_tool_to_openai_tool(
fully_qualified_name: String,
tool: rmcp::model::Tool,
@@ -2670,62 +2803,69 @@ pub(crate) fn build_specs_with_discoverable_tools(
match &config.shell_type {
ConfigShellToolType::Default => {
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
SHELL_TOOL_NAME,
create_shell_tool(exec_permission_approvals_enabled),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::Local => {
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
LOCAL_SHELL_TOOL_NAME,
ToolSpec::LocalShell {},
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
ConfigShellToolType::UnifiedExec => {
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
EXEC_COMMAND_TOOL_NAME,
create_exec_command_tool(
config.allow_login_shell,
exec_permission_approvals_enabled,
),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
unified_exec_handler.clone(),
);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
WRITE_STDIN_TOOL_NAME,
create_write_stdin_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
unified_exec_handler,
);
builder.register_handler("exec_command", unified_exec_handler.clone());
builder.register_handler("write_stdin", unified_exec_handler);
}
ConfigShellToolType::Disabled => {
// Do nothing.
}
ConfigShellToolType::ShellCommand => {
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
SHELL_COMMAND_TOOL_NAME,
create_shell_command_tool(
config.allow_login_shell,
exec_permission_approvals_enabled,
),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
);
}
}
if config.shell_type != ConfigShellToolType::Disabled {
if config.shell_type != ConfigShellToolType::Disabled
&& config.is_builtin_tool_invocation_enabled(SHELL_TOOL_NAME)
{
// Always register shell aliases so older prompts remain compatible.
builder.register_handler("shell", shell_handler.clone());
builder.register_handler("container.exec", shell_handler.clone());
builder.register_handler("local_shell", shell_handler);
builder.register_handler("shell_command", shell_command_handler);
builder.register_handler(SHELL_TOOL_NAME, shell_handler.clone());
builder.register_handler(CONTAINER_EXEC_TOOL_NAME, shell_handler.clone());
builder.register_handler(LOCAL_SHELL_TOOL_NAME, shell_handler);
builder.register_handler(SHELL_COMMAND_TOOL_NAME, shell_command_handler);
}
if mcp_tools.is_some() {
@@ -2752,41 +2892,46 @@ pub(crate) fn build_specs_with_discoverable_tools(
builder.register_handler("read_mcp_resource", mcp_resource_handler);
}
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"update_plan",
PLAN_TOOL.clone(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
plan_handler,
);
builder.register_handler("update_plan", plan_handler);
if config.js_repl_enabled {
push_tool_spec(
if config.js_repl_enabled && config.is_builtin_tool_invocation_enabled("js_repl") {
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"js_repl",
create_js_repl_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
js_repl_handler,
);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"js_repl_reset",
create_js_repl_reset_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
js_repl_reset_handler,
);
builder.register_handler("js_repl", js_repl_handler);
builder.register_handler("js_repl_reset", js_repl_reset_handler);
}
if config.request_user_input {
push_tool_spec(
if config.request_user_input && config.is_builtin_tool_invocation_enabled("request_user_input")
{
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"request_user_input",
create_request_user_input_tool(CollaborationModesConfig {
default_mode_request_user_input: config.default_mode_request_user_input,
}),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
request_user_input_handler,
);
builder.register_handler("request_user_input", request_user_input_handler);
}
if config.request_permissions_tool_enabled {
@@ -2831,22 +2976,26 @@ pub(crate) fn build_specs_with_discoverable_tools(
builder.register_handler(TOOL_SUGGEST_TOOL_NAME, tool_suggest_handler);
}
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type
&& config.is_builtin_tool_invocation_enabled("apply_patch")
{
match apply_patch_tool_type {
ApplyPatchToolType::Freeform => {
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
"apply_patch",
create_apply_patch_freeform_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
ApplyPatchToolType::Function => {
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
"apply_patch",
create_apply_patch_json_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
}
@@ -2856,44 +3005,50 @@ pub(crate) fn build_specs_with_discoverable_tools(
if config
.experimental_supported_tools
.contains(&"grep_files".to_string())
&& config.is_builtin_tool_invocation_enabled("grep_files")
{
let grep_files_handler = Arc::new(GrepFilesHandler);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"grep_files",
create_grep_files_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
grep_files_handler,
);
builder.register_handler("grep_files", grep_files_handler);
}
if config
.experimental_supported_tools
.contains(&"read_file".to_string())
&& config.is_builtin_tool_invocation_enabled("read_file")
{
let read_file_handler = Arc::new(ReadFileHandler);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"read_file",
create_read_file_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
read_file_handler,
);
builder.register_handler("read_file", read_file_handler);
}
if config
.experimental_supported_tools
.iter()
.any(|tool| tool == "list_dir")
&& config.is_builtin_tool_invocation_enabled("list_dir")
{
let list_dir_handler = Arc::new(ListDirHandler);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"list_dir",
create_list_dir_tool(),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
list_dir_handler,
);
builder.register_handler("list_dir", list_dir_handler);
}
if config
@@ -2916,7 +3071,9 @@ pub(crate) fn build_specs_with_discoverable_tools(
Some(WebSearchMode::Disabled) | None => None,
};
if let Some(external_web_access) = external_web_access {
if let Some(external_web_access) = external_web_access
&& config.is_builtin_tool_invocation_enabled("web_search")
{
let search_content_types = match config.web_search_tool_type {
WebSearchToolType::Text => None,
WebSearchToolType::TextAndImage => Some(
@@ -2927,8 +3084,10 @@ pub(crate) fn build_specs_with_discoverable_tools(
),
};
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
"web_search",
ToolSpec::WebSearch {
external_web_access: Some(external_web_access),
filters: config
@@ -2946,96 +3105,107 @@ pub(crate) fn build_specs_with_discoverable_tools(
search_content_types,
},
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
if config.image_gen_tool {
push_tool_spec(
push_builtin_tool_spec_if_enabled(
&mut builder,
config,
"image_generation",
ToolSpec::ImageGeneration {
output_format: "png".to_string(),
},
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
);
}
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"view_image",
create_view_image_tool(config.can_request_original_image_detail),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
view_image_handler,
);
builder.register_handler("view_image", view_image_handler);
if config.artifact_tools {
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"artifacts",
create_artifacts_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
artifacts_handler,
);
builder.register_handler("artifacts", artifacts_handler);
}
if config.collab_tools {
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"spawn_agent",
create_spawn_agent_tool(config),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
Arc::new(SpawnAgentHandler),
);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"send_input",
create_send_input_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
Arc::new(SendInputHandler),
);
if !config.multi_agent_v2 {
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"resume_agent",
create_resume_agent_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
Arc::new(ResumeAgentHandler),
);
builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler));
}
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"wait_agent",
create_wait_agent_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
Arc::new(WaitAgentHandler),
);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"close_agent",
create_close_agent_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
Arc::new(CloseAgentHandler),
);
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler));
builder.register_handler("send_input", Arc::new(SendInputHandler));
builder.register_handler("wait_agent", Arc::new(WaitAgentHandler));
builder.register_handler("close_agent", Arc::new(CloseAgentHandler));
}
if config.agent_jobs_tools {
let agent_jobs_handler = Arc::new(BatchJobHandler);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"spawn_agents_on_csv",
create_spawn_agents_on_csv_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
agent_jobs_handler.clone(),
);
builder.register_handler("spawn_agents_on_csv", agent_jobs_handler.clone());
if config.agent_jobs_worker_tools {
push_tool_spec(
if config.agent_jobs_worker_tools
&& config.is_builtin_tool_invocation_enabled("report_agent_job_result")
{
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"report_agent_job_result",
create_report_agent_job_result_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
agent_jobs_handler,
);
builder.register_handler("report_agent_job_result", agent_jobs_handler);
}
}

View File

@@ -1,4 +1,5 @@
use crate::client_common::tools::FreeformTool;
use crate::config::ToolFeatureOverrides;
use crate::config::test_config;
use crate::models_manager::manager::ModelsManager;
use crate::models_manager::model_info::with_config_overrides;
@@ -515,12 +516,52 @@ fn test_build_specs_collab_tools_enabled() {
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
let (tools, registry) = build_specs(&tools_config, None, None, &[]).build();
assert_contains_tool_names(
&tools,
&["spawn_agent", "send_input", "wait_agent", "close_agent"],
);
assert_lacks_tool_name(&tools, "spawn_agents_on_csv");
assert!(registry.has_handler("spawn_agent", None));
assert!(registry.has_handler("send_input", None));
assert!(registry.has_handler("resume_agent", None));
assert!(registry.has_handler("wait_agent", None));
assert!(registry.has_handler("close_agent", None));
}
#[test]
fn test_build_specs_multi_agent_handlers_omitted_when_capability_disabled() {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::Collab);
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_tool_feature_overrides(ToolFeatureOverrides {
disable_defaults: true,
..Default::default()
});
let (tools, registry) = build_specs(&tools_config, None, None, &[]).build();
for tool_name in [
"spawn_agent",
"send_input",
"resume_agent",
"wait_agent",
"close_agent",
] {
assert_lacks_tool_name(&tools, tool_name);
assert!(!registry.has_handler(tool_name, None));
}
}
#[test]
@@ -1136,6 +1177,79 @@ fn web_search_mode_live_sets_external_web_access_true() {
);
}
#[test]
fn web_search_mode_respects_disable_defaults() {
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &search_capable_model_info(),
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Live),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_tool_feature_overrides(ToolFeatureOverrides {
disable_defaults: true,
shell: Some(true),
..Default::default()
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
assert_lacks_tool_name(&tools, "web_search");
}
#[test]
fn disable_defaults_ignores_legacy_view_image_override_without_filesystem_override() {
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &search_capable_model_info(),
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Disabled),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_tool_feature_overrides(ToolFeatureOverrides {
disable_defaults: true,
..Default::default()
})
.with_legacy_view_image_override(Some(true));
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
assert_lacks_tool_name(&tools, "view_image");
}
#[test]
fn explicit_filesystem_override_enables_view_image_with_disable_defaults() {
let features = Features::with_defaults();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &search_capable_model_info(),
available_models: &available_models,
features: &features,
web_search_mode: Some(WebSearchMode::Disabled),
session_source: SessionSource::Cli,
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
})
.with_tool_feature_overrides(ToolFeatureOverrides {
disable_defaults: true,
filesystem: Some(true),
..Default::default()
})
.with_legacy_view_image_override(Some(true));
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
assert_contains_tool_names(&tools, &["view_image"]);
}
#[test]
fn web_search_config_is_forwarded_to_tool_spec() {
let config = test_config();