mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
builtin config
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user