builtin config

This commit is contained in:
ashwinnathan-openai
2026-03-20 12:26:31 -07:00
parent 1350477150
commit 33fa66ae1e
20 changed files with 987 additions and 92 deletions
@@ -13784,9 +13784,38 @@
],
"type": "object"
},
"ToolExecutionMode": {
"enum": [
"auto",
"manual"
],
"type": "string"
},
"ToolsV2": {
"properties": {
"capabilities": {
"additionalProperties": {
"type": "boolean"
},
"description": "Additive capability overrides keyed by canonical capability name.",
"type": [
"object",
"null"
]
},
"execution_mode": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolExecutionMode"
},
{
"type": "null"
}
],
"description": "Execution policy for approval-capable tools."
},
"view_image": {
"description": "Legacy enablement for the `view_image` capability.",
"type": [
"boolean",
"null"
@@ -13800,7 +13829,8 @@
{
"type": "null"
}
]
],
"description": "Per-tool web search configuration. Presence alone does not enable the capability."
}
},
"type": "object"
@@ -11544,9 +11544,38 @@
],
"type": "object"
},
"ToolExecutionMode": {
"enum": [
"auto",
"manual"
],
"type": "string"
},
"ToolsV2": {
"properties": {
"capabilities": {
"additionalProperties": {
"type": "boolean"
},
"description": "Additive capability overrides keyed by canonical capability name.",
"type": [
"object",
"null"
]
},
"execution_mode": {
"anyOf": [
{
"$ref": "#/definitions/ToolExecutionMode"
},
{
"type": "null"
}
],
"description": "Execution policy for approval-capable tools."
},
"view_image": {
"description": "Legacy enablement for the `view_image` capability.",
"type": [
"boolean",
"null"
@@ -11560,7 +11589,8 @@
{
"type": "null"
}
]
],
"description": "Per-tool web search configuration. Presence alone does not enable the capability."
}
},
"type": "object"
@@ -760,9 +760,38 @@
],
"type": "string"
},
"ToolExecutionMode": {
"enum": [
"auto",
"manual"
],
"type": "string"
},
"ToolsV2": {
"properties": {
"capabilities": {
"additionalProperties": {
"type": "boolean"
},
"description": "Additive capability overrides keyed by canonical capability name.",
"type": [
"object",
"null"
]
},
"execution_mode": {
"anyOf": [
{
"$ref": "#/definitions/ToolExecutionMode"
},
{
"type": "null"
}
],
"description": "Execution policy for approval-capable tools."
},
"view_image": {
"description": "Legacy enablement for the `view_image` capability.",
"type": [
"boolean",
"null"
@@ -776,7 +805,8 @@
{
"type": "null"
}
]
],
"description": "Per-tool web search configuration. Presence alone does not enable the capability."
}
},
"type": "object"
@@ -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 ToolExecutionMode = "auto" | "manual";
@@ -2,5 +2,22 @@
// 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 { ToolExecutionMode } from "./ToolExecutionMode";
export type ToolsV2 = { web_search: WebSearchToolConfig | null, view_image: boolean | null, };
export type ToolsV2 = {
/**
* Per-tool web search configuration. Presence alone does not enable the capability.
*/
web_search: WebSearchToolConfig | null,
/**
* Legacy enablement for the `view_image` capability.
*/
view_image: boolean | null,
/**
* Additive capability overrides keyed by canonical capability name.
*/
capabilities: { [key in string]?: boolean } | null,
/**
* Execution policy for approval-capable tools.
*/
execution_mode: ToolExecutionMode | null, };
@@ -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 { ToolExecutionMode } from "./ToolExecutionMode";
export type { ToolRequestUserInputAnswer } from "./ToolRequestUserInputAnswer";
export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption";
export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams";
@@ -533,12 +533,28 @@ 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 enum ToolExecutionMode {
Auto,
Manual,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub struct ToolsV2 {
/// Per-tool web search configuration. Presence alone does not enable the capability.
pub web_search: Option<WebSearchToolConfig>,
/// Legacy enablement for the `view_image` capability.
pub view_image: Option<bool>,
/// Additive capability overrides keyed by canonical capability name.
#[serde(alias = "builtin_features")]
pub capabilities: Option<std::collections::BTreeMap<String, bool>>,
/// Execution policy for approval-capable tools.
#[serde(alias = "builtin_execution_mode")]
pub execution_mode: Option<ToolExecutionMode>,
}
#[derive(Serialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -30,6 +30,7 @@ use codex_protocol::openai_models::ReasoningEffort;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -102,6 +103,11 @@ allowed_domains = ["example.com"]
[tools]
view_image = false
execution_mode = "manual"
[tools.capabilities]
command_execution = true
apply_patch = false
"#,
)?;
let codex_home_path = codex_home.path().canonicalize()?;
@@ -137,6 +143,11 @@ view_image = false
location: None,
}),
view_image: Some(false),
capabilities: Some(BTreeMap::from([
("command_execution".to_string(), true),
("apply_patch".to_string(), false),
])),
execution_mode: Some(codex_app_server_protocol::ToolExecutionMode::Manual),
}
);
assert_eq!(
@@ -163,6 +174,30 @@ view_image = false
file: user_file.clone(),
}
);
assert_eq!(
origins.get("tools.execution_mode").expect("origin").name,
ConfigLayerSource::User {
file: user_file.clone(),
}
);
assert_eq!(
origins
.get("tools.capabilities.command_execution")
.expect("origin")
.name,
ConfigLayerSource::User {
file: user_file.clone(),
}
);
assert_eq!(
origins
.get("tools.capabilities.apply_patch")
.expect("origin")
.name,
ConfigLayerSource::User {
file: user_file.clone(),
}
);
let layers = layers.expect("layers present");
assert_layers_user_then_optional_system(&layers, user_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,49 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn thread_start_accepts_tool_capability_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!({
"execution_mode": "manual",
"capabilities": {
"command_execution": true,
"apply_patch": true,
"web_search": true,
"view_image": false
},
"web_search": {
"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;
+25 -1
View File
@@ -1552,6 +1552,13 @@
},
"type": "object"
},
"ToolExecutionMode": {
"enum": [
"auto",
"manual"
],
"type": "string"
},
"ToolSuggestConfig": {
"additionalProperties": false,
"properties": {
@@ -1591,9 +1598,26 @@
"ToolsToml": {
"additionalProperties": false,
"properties": {
"capabilities": {
"additionalProperties": {
"type": "boolean"
},
"default": null,
"description": "Additive capability overrides keyed by canonical capability name.",
"type": "object"
},
"execution_mode": {
"allOf": [
{
"$ref": "#/definitions/ToolExecutionMode"
}
],
"default": null,
"description": "Execution policy for approval-capable tools."
},
"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": {
+12 -3
View File
@@ -38,6 +38,8 @@ pub(crate) async fn apply_patch(
file_system_sandbox_policy: &FileSystemSandboxPolicy,
action: ApplyPatchAction,
) -> InternalApplyPatchInvocation {
let manual_tool_approval_required = turn_context.tools_config.requires_manual_tool_approval();
match assess_patch_safety(
&action,
turn_context.approval_policy.value(),
@@ -52,9 +54,16 @@ pub(crate) async fn apply_patch(
} => InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec {
action,
auto_approved: !user_explicitly_approved,
exec_approval_requirement: ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
exec_approval_requirement: if manual_tool_approval_required {
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
}
} else {
ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
}
},
}),
SafetyCheck::AskUser => {
+9
View File
@@ -889,6 +889,9 @@ impl TurnContext {
sandbox_policy: self.sandbox_policy.get(),
windows_sandbox_level: self.windows_sandbox_level,
})
.with_tool_capability_overrides(config.tool_capability_overrides.clone())
.with_legacy_view_image_override(config.legacy_view_image_override)
.with_execution_mode(config.tool_execution_mode)
.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 +1330,9 @@ impl Session {
sandbox_policy: session_configuration.sandbox_policy.get(),
windows_sandbox_level: session_configuration.windows_sandbox_level,
})
.with_tool_capability_overrides(per_turn_config.tool_capability_overrides.clone())
.with_legacy_view_image_override(per_turn_config.legacy_view_image_override)
.with_execution_mode(per_turn_config.tool_execution_mode)
.with_unified_exec_shell_mode_for_session(
user_shell,
shell_zsh_path,
@@ -5192,6 +5198,9 @@ async fn spawn_review_thread(
sandbox_policy: parent_turn_context.sandbox_policy.get(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
})
.with_tool_capability_overrides(config.tool_capability_overrides.clone())
.with_legacy_view_image_override(config.legacy_view_image_override)
.with_execution_mode(config.tool_execution_mode)
.with_unified_exec_shell_mode_for_session(
sess.services.user_shell.as_ref(),
sess.services.shell_zsh_path.as_ref(),
+76 -3
View File
@@ -196,6 +196,8 @@ web_search = true
Some(ToolsToml {
web_search: None,
view_image: None,
capabilities: None,
execution_mode: None,
})
);
}
@@ -215,6 +217,36 @@ web_search = false
Some(ToolsToml {
web_search: None,
view_image: None,
capabilities: None,
execution_mode: None,
})
);
}
#[test]
fn tools_capabilities_deserialize_with_execution_mode() {
let cfg: ConfigToml = toml::from_str(
r#"
[tools]
execution_mode = "manual"
[tools.capabilities]
command_execution = true
apply_patch = false
"#,
)
.expect("TOML deserialization should succeed");
assert_eq!(
cfg.tools,
Some(ToolsToml {
web_search: None,
view_image: None,
capabilities: Some(BTreeMap::from([
("command_execution".to_string(), true),
("apply_patch".to_string(), false),
])),
execution_mode: Some(ToolExecutionMode::Manual),
})
);
}
@@ -1455,7 +1487,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, None),
None
);
}
#[test]
@@ -1469,7 +1504,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, None),
Some(WebSearchMode::Live)
);
}
@@ -1485,11 +1520,41 @@ 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, None),
Some(WebSearchMode::Disabled)
);
}
#[test]
fn web_search_mode_capability_override_false_disables_tool() {
let cfg = ConfigToml::default();
let profile = ConfigProfile::default();
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchCached);
let capabilities = BTreeMap::from([("web_search".to_string(), false)]);
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features, Some(&capabilities)),
Some(WebSearchMode::Disabled)
);
}
#[test]
fn tool_capability_overrides_include_legacy_view_image_fallback() {
let cfg: ConfigToml = toml::from_str(
r#"
[tools]
view_image = false
"#,
)
.expect("TOML deserialization should succeed");
assert_eq!(
resolve_tool_capability_overrides(&cfg, &ConfigProfile::default()),
Ok(Some(BTreeMap::from([("view_image".to_string(), false)])))
);
}
#[test]
fn web_search_mode_for_turn_uses_preference_for_read_only() {
let web_search_mode = Constrained::allow_any(WebSearchMode::Cached);
@@ -4331,6 +4396,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_capability_overrides: None,
tool_execution_mode: ToolExecutionMode::Auto,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -4474,6 +4541,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_capability_overrides: None,
tool_execution_mode: ToolExecutionMode::Auto,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -4615,6 +4684,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_capability_overrides: None,
tool_execution_mode: ToolExecutionMode::Auto,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
@@ -4742,6 +4813,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_capability_overrides: None,
tool_execution_mode: ToolExecutionMode::Auto,
use_experimental_unified_exec_tool: !cfg!(windows),
background_terminal_max_timeout: DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS,
ghost_snapshot: GhostSnapshotConfig::default(),
+133 -12
View File
@@ -546,6 +546,15 @@ pub struct Config {
/// Additional parameters for the web search tool when it is enabled.
pub web_search_config: Option<WebSearchConfig>,
/// Optional capability overrides requested by config.
pub tool_capability_overrides: Option<BTreeMap<String, bool>>,
/// Legacy `tools.view_image` fallback used when no explicit capability override is present.
pub legacy_view_image_override: Option<bool>,
/// Execution policy for approval-capable tools.
pub tool_execution_mode: ToolExecutionMode,
/// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool,
@@ -1613,9 +1622,24 @@ pub struct ToolsToml {
)]
pub web_search: Option<WebSearchToolConfig>,
/// 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>,
/// Additive capability overrides keyed by canonical capability name.
#[serde(default, alias = "builtin_features")]
pub capabilities: Option<std::collections::BTreeMap<String, bool>>,
/// Execution policy for approval-capable tools.
#[serde(default, alias = "builtin_execution_mode")]
pub execution_mode: Option<ToolExecutionMode>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ToolExecutionMode {
Auto,
Manual,
}
#[derive(Deserialize)]
@@ -2034,17 +2058,26 @@ fn resolve_web_search_mode(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
features: &Features,
tool_capability_overrides: Option<&BTreeMap<String, bool>>,
) -> Option<WebSearchMode> {
if let Some(mode) = config_profile.web_search.or(config_toml.web_search) {
return Some(mode);
let explicit_mode = || config_profile.web_search.or(config_toml.web_search);
let feature_default_mode = || {
if features.enabled(Feature::WebSearchCached) {
Some(WebSearchMode::Cached)
} else if features.enabled(Feature::WebSearchRequest) {
Some(WebSearchMode::Live)
} else {
None
}
};
match tool_capability_overrides.and_then(|overrides| overrides.get("web_search")) {
Some(false) => Some(WebSearchMode::Disabled),
Some(true) => explicit_mode()
.filter(|mode| *mode != WebSearchMode::Disabled)
.or_else(feature_default_mode),
None => explicit_mode().or_else(feature_default_mode),
}
if features.enabled(Feature::WebSearchCached) {
return Some(WebSearchMode::Cached);
}
if features.enabled(Feature::WebSearchRequest) {
return Some(WebSearchMode::Live);
}
None
}
fn resolve_web_search_config(
@@ -2068,6 +2101,77 @@ fn resolve_web_search_config(
}
}
fn resolve_tool_capability_overrides(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> std::result::Result<Option<BTreeMap<String, bool>>, String> {
let base_capabilities = config_toml
.tools
.as_ref()
.and_then(|tools| tools.capabilities.as_ref());
let profile_capabilities = config_profile
.tools
.as_ref()
.and_then(|tools| tools.capabilities.as_ref());
let capabilities = match (base_capabilities, profile_capabilities) {
(None, None) => None,
(Some(base), None) => Some(base.clone()),
(None, Some(profile)) => Some(profile.clone()),
(Some(base), Some(profile)) => {
let mut merged = base.clone();
merged.extend(profile.clone());
Some(merged)
}
};
if capabilities.as_ref().is_none_or(BTreeMap::is_empty) {
return Ok(None);
}
let declared_capability_names = capabilities
.as_ref()
.map(|capabilities| capabilities.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default();
crate::tools::spec::validate_tool_capability_names(&declared_capability_names)?;
Ok(capabilities)
}
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_execution_mode(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
) -> ToolExecutionMode {
config_profile
.tools
.as_ref()
.and_then(|tools| tools.execution_mode)
.or_else(|| {
config_toml
.tools
.as_ref()
.and_then(|tools| tools.execution_mode)
})
.unwrap_or(ToolExecutionMode::Auto)
}
pub(crate) fn resolve_web_search_mode_for_turn(
web_search_mode: &Constrained<WebSearchMode>,
sandbox_policy: &SandboxPolicy,
@@ -2372,13 +2476,27 @@ impl Config {
);
approval_policy = constrained_approval_policy.value();
}
let tool_capability_overrides = resolve_tool_capability_overrides(&cfg, &config_profile)
.map_err(|err| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid tools.capabilities configuration: {err}"),
)
})?;
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_capability_overrides.as_ref(),
)
.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 tool_execution_mode = resolve_tool_execution_mode(&cfg, &config_profile);
let agent_roles =
agent_roles::load_agent_roles(&cfg, &config_layer_stack, &mut startup_warnings)?;
@@ -2798,6 +2916,9 @@ impl Config {
include_apply_patch_tool: include_apply_patch_tool_flag,
web_search_mode: constrained_web_search_mode.value,
web_search_config,
tool_capability_overrides,
legacy_view_image_override,
tool_execution_mode,
use_experimental_unified_exec_tool,
background_terminal_max_timeout,
ghost_snapshot,
@@ -444,6 +444,11 @@ impl ShellHandler {
prefix_rule,
})
.await;
let exec_approval_requirement = if turn.tools_config.requires_manual_tool_approval() {
exec_approval_requirement.force_manual_approval()
} else {
exec_approval_requirement
};
let req = ShellRequest {
command: exec_params.command.clone(),
+206
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,214 @@ 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;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum BuiltinToolKey {
CodeMode,
ExecCommand,
WriteStdin,
ListMcpResources,
ListMcpResourceTemplates,
ReadMcpResource,
UpdatePlan,
JsRepl,
JsReplReset,
RequestUserInput,
RequestPermissions,
SearchToolBm25,
ApplyPatch,
GrepFiles,
ReadFile,
ListDir,
TestSyncTool,
WebSearch,
ImageGeneration,
ViewImage,
Artifacts,
SpawnAgent,
SendInput,
ResumeAgent,
Wait,
CloseAgent,
SpawnAgentsOnCsv,
ReportAgentJobResult,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum ToolCapabilityKey {
CommandExecution,
JavascriptExecution,
FilesystemInspection,
MultiAgent,
AgentJobs,
ApplyPatch,
UpdatePlan,
RequestUserInput,
WebSearch,
ImageGeneration,
ViewImage,
Artifacts,
}
impl BuiltinToolKey {
pub(crate) const ALL: [Self; 28] = [
Self::CodeMode,
Self::ExecCommand,
Self::WriteStdin,
Self::ListMcpResources,
Self::ListMcpResourceTemplates,
Self::ReadMcpResource,
Self::UpdatePlan,
Self::JsRepl,
Self::JsReplReset,
Self::RequestUserInput,
Self::RequestPermissions,
Self::SearchToolBm25,
Self::ApplyPatch,
Self::GrepFiles,
Self::ReadFile,
Self::ListDir,
Self::TestSyncTool,
Self::WebSearch,
Self::ImageGeneration,
Self::ViewImage,
Self::Artifacts,
Self::SpawnAgent,
Self::SendInput,
Self::ResumeAgent,
Self::Wait,
Self::CloseAgent,
Self::SpawnAgentsOnCsv,
Self::ReportAgentJobResult,
];
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::ExecCommand => &[
"exec_command",
"shell",
"container.exec",
"local_shell",
"shell_command",
],
Self::WriteStdin => &["write_stdin"],
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"],
Self::CloseAgent => &["close_agent"],
Self::SpawnAgentsOnCsv => &["spawn_agents_on_csv"],
Self::ReportAgentJobResult => &["report_agent_job_result"],
}
}
}
impl ToolCapabilityKey {
const ALL: [Self; 12] = [
Self::CommandExecution,
Self::JavascriptExecution,
Self::FilesystemInspection,
Self::MultiAgent,
Self::AgentJobs,
Self::ApplyPatch,
Self::UpdatePlan,
Self::RequestUserInput,
Self::WebSearch,
Self::ImageGeneration,
Self::ViewImage,
Self::Artifacts,
];
pub(crate) fn iter() -> impl Iterator<Item = Self> {
Self::ALL.into_iter()
}
pub(crate) const fn capability_name(self) -> &'static str {
match self {
Self::CommandExecution => "command_execution",
Self::JavascriptExecution => "javascript_execution",
Self::FilesystemInspection => "filesystem_inspection",
Self::MultiAgent => "multi_agent",
Self::AgentJobs => "agent_jobs",
Self::ApplyPatch => "apply_patch",
Self::UpdatePlan => "update_plan",
Self::RequestUserInput => "request_user_input",
Self::WebSearch => "web_search",
Self::ImageGeneration => "image_generation",
Self::ViewImage => "view_image",
Self::Artifacts => "artifacts",
}
}
pub(crate) const fn builtin_tool_keys(self) -> &'static [BuiltinToolKey] {
match self {
Self::CommandExecution => &[BuiltinToolKey::ExecCommand, BuiltinToolKey::WriteStdin],
Self::JavascriptExecution => &[BuiltinToolKey::JsRepl, BuiltinToolKey::JsReplReset],
Self::FilesystemInspection => &[
BuiltinToolKey::GrepFiles,
BuiltinToolKey::ReadFile,
BuiltinToolKey::ListDir,
],
Self::MultiAgent => &[
BuiltinToolKey::SpawnAgent,
BuiltinToolKey::SendInput,
BuiltinToolKey::ResumeAgent,
BuiltinToolKey::Wait,
BuiltinToolKey::CloseAgent,
],
Self::AgentJobs => &[
BuiltinToolKey::SpawnAgentsOnCsv,
BuiltinToolKey::ReportAgentJobResult,
],
Self::ApplyPatch => &[BuiltinToolKey::ApplyPatch],
Self::UpdatePlan => &[BuiltinToolKey::UpdatePlan],
Self::RequestUserInput => &[BuiltinToolKey::RequestUserInput],
Self::WebSearch => &[BuiltinToolKey::WebSearch],
Self::ImageGeneration => &[BuiltinToolKey::ImageGeneration],
Self::ViewImage => &[BuiltinToolKey::ViewImage],
Self::Artifacts => &[BuiltinToolKey::Artifacts],
}
}
pub(crate) fn for_builtin_tool(tool: BuiltinToolKey) -> Option<Self> {
Self::iter().find(|capability| capability.builtin_tool_keys().contains(&tool))
}
}
pub(crate) fn builtin_tool_key(name: &str) -> Option<BuiltinToolKey> {
BuiltinToolKey::iter().find(|key| key.invocation_names().contains(&name))
}
pub(crate) fn tool_capability_key(name: &str) -> Option<ToolCapabilityKey> {
ToolCapabilityKey::iter().find(|key| key.capability_name() == name)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ToolKind {
Function,
+13
View File
@@ -157,6 +157,19 @@ impl ExecApprovalRequirement {
_ => None,
}
}
pub fn force_manual_approval(self) -> Self {
match self {
Self::Skip {
proposed_execpolicy_amendment,
..
} => Self::NeedsApproval {
reason: None,
proposed_execpolicy_amendment,
},
other => other,
}
}
}
/// - Never, OnFailure: do not ask
@@ -108,3 +108,33 @@ fn guardian_bypasses_sandbox_for_explicit_escalation_on_first_attempt() {
SandboxOverride::BypassSandboxFirstAttempt
);
}
#[test]
fn force_manual_approval_converts_skip_to_needs_approval() {
let requirement = ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
};
assert_eq!(
requirement.force_manual_approval(),
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
}
);
}
#[test]
fn force_manual_approval_preserves_forbidden() {
let requirement = ExecApprovalRequirement::Forbidden {
reason: "denied".to_string(),
};
assert_eq!(
requirement.force_manual_approval(),
ExecApprovalRequirement::Forbidden {
reason: "denied".to_string(),
}
);
}
+260 -69
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::ToolExecutionMode;
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,9 @@ 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::ToolCapabilityKey;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::registry::builtin_tool_key;
use crate::tools::registry::tool_handler_key;
use codex_features::Feature;
use codex_features::Features;
@@ -269,6 +272,9 @@ pub(crate) struct ToolsConfig {
shell_command_backend: ShellCommandBackendConfig,
pub unified_exec_shell_mode: UnifiedExecShellMode,
pub allow_login_shell: bool,
pub tool_capability_overrides: Option<BTreeMap<String, bool>>,
pub legacy_view_image_override: Option<bool>,
pub execution_mode: ToolExecutionMode,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_mode: Option<WebSearchMode>,
pub web_search_config: Option<WebSearchConfig>,
@@ -403,6 +409,9 @@ impl ToolsConfig {
shell_command_backend,
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
allow_login_shell: true,
tool_capability_overrides: None,
legacy_view_image_override: None,
execution_mode: ToolExecutionMode::Auto,
apply_patch_tool_type,
web_search_mode: *web_search_mode,
web_search_config: None,
@@ -439,6 +448,27 @@ impl ToolsConfig {
self
}
pub fn with_tool_capability_overrides(
mut self,
tool_capability_overrides: Option<BTreeMap<String, bool>>,
) -> Self {
self.tool_capability_overrides = tool_capability_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_execution_mode(mut self, execution_mode: ToolExecutionMode) -> Self {
self.execution_mode = execution_mode;
self
}
pub fn with_unified_exec_shell_mode(
mut self,
unified_exec_shell_mode: UnifiedExecShellMode,
@@ -473,6 +503,68 @@ 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(capability) = ToolCapabilityKey::for_builtin_tool(tool) else {
return true;
};
self.is_tool_capability_enabled(capability)
}
pub fn requires_manual_tool_approval(&self) -> bool {
matches!(self.execution_mode, ToolExecutionMode::Manual)
}
fn is_tool_capability_enabled(&self, capability: ToolCapabilityKey) -> bool {
if let Some(enabled) = self
.tool_capability_overrides
.as_ref()
.and_then(|overrides| overrides.get(capability.capability_name()))
{
return *enabled;
}
match capability {
ToolCapabilityKey::CommandExecution => self.shell_type != ConfigShellToolType::Disabled,
ToolCapabilityKey::JavascriptExecution => self.js_repl_enabled,
ToolCapabilityKey::FilesystemInspection => self
.experimental_supported_tools
.iter()
.any(|tool| matches!(tool.as_str(), "grep_files" | "read_file" | "list_dir")),
ToolCapabilityKey::MultiAgent => self.collab_tools,
ToolCapabilityKey::AgentJobs => self.agent_jobs_tools,
ToolCapabilityKey::ApplyPatch => self.apply_patch_tool_type.is_some(),
ToolCapabilityKey::UpdatePlan => true,
ToolCapabilityKey::RequestUserInput => self.request_user_input,
ToolCapabilityKey::WebSearch => self
.web_search_mode
.is_some_and(|mode| mode != WebSearchMode::Disabled),
ToolCapabilityKey::ImageGeneration => self.image_gen_tool,
ToolCapabilityKey::ViewImage => self.legacy_view_image_override.unwrap_or(true),
ToolCapabilityKey::Artifacts => self.artifact_tools,
}
}
}
pub fn validate_tool_capability_names(requested_capabilities: &[String]) -> Result<(), String> {
let known_names = ToolCapabilityKey::iter()
.map(ToolCapabilityKey::capability_name)
.collect::<std::collections::BTreeSet<_>>();
let invalid_names = requested_capabilities
.iter()
.filter(|name| !known_names.contains(name.as_str()))
.cloned()
.collect::<Vec<_>>();
if !invalid_names.is_empty() {
return Err(format!(
"unknown tool capability name(s): {}",
invalid_names.join(", ")
));
}
Ok(())
}
fn supports_image_generation(model_info: &ModelInfo) -> bool {
@@ -2331,6 +2423,45 @@ 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_name: &str,
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(handler_name, handler);
}
}
pub(crate) fn mcp_tool_to_openai_tool(
fully_qualified_name: String,
tool: rmcp::model::Tool,
@@ -2670,57 +2801,66 @@ 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,
"exec_command",
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",
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",
create_exec_command_tool(
config.allow_login_shell,
exec_permission_approvals_enabled,
),
/*supports_parallel_tool_calls*/ true,
config.code_mode_enabled,
"exec_command",
unified_exec_handler.clone(),
);
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"write_stdin",
create_write_stdin_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
"write_stdin",
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",
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("exec_command")
{
// Always register shell aliases so older prompts remain compatible.
builder.register_handler("shell", shell_handler.clone());
builder.register_handler("container.exec", shell_handler.clone());
@@ -2752,41 +2892,50 @@ 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,
"update_plan",
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",
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",
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",
request_user_input_handler,
);
builder.register_handler("request_user_input", request_user_input_handler);
}
if config.request_permissions_tool_enabled {
@@ -2831,22 +2980,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 +3009,53 @@ 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",
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",
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",
list_dir_handler,
);
builder.register_handler("list_dir", list_dir_handler);
}
if config
@@ -2916,7 +3078,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 +3091,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,72 +3112,91 @@ 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",
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",
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,
"spawn_agent",
multi_agent_handler.clone(),
);
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,
"send_input",
multi_agent_handler.clone(),
);
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,
"resume_agent",
multi_agent_handler.clone(),
);
builder.register_handler("resume_agent", Arc::new(ResumeAgentHandler));
}
push_tool_spec(
push_builtin_tool_with_handler_if_enabled(
&mut builder,
config,
"wait",
create_wait_agent_tool(),
/*supports_parallel_tool_calls*/ false,
config.code_mode_enabled,
"wait",
multi_agent_handler.clone(),
);
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,
"close_agent",
multi_agent_handler,
);
builder.register_handler("spawn_agent", Arc::new(SpawnAgentHandler));
builder.register_handler("send_input", Arc::new(SendInputHandler));
@@ -3021,21 +3206,27 @@ pub(crate) fn build_specs_with_discoverable_tools(
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,
"spawn_agents_on_csv",
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,
"report_agent_job_result",
agent_jobs_handler,
);
builder.register_handler("report_agent_job_result", agent_jobs_handler);
}
}
@@ -609,6 +609,12 @@ impl UnifiedExecProcessManager {
prefix_rule: request.prefix_rule.clone(),
})
.await;
let exec_approval_requirement = if context.turn.tools_config.requires_manual_tool_approval()
{
exec_approval_requirement.force_manual_approval()
} else {
exec_approval_requirement
};
let req = UnifiedExecToolRequest {
command: request.command.clone(),
cwd,