Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
544 changes: 22 additions & 522 deletions codex-rs/core/src/tools/spec.rs

Large diffs are not rendered by default.

197 changes: 16 additions & 181 deletions codex-rs/core/src/tools/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ use crate::shell::ShellType;
use crate::tools::ToolRouter;
use crate::tools::router::ToolRouterParams;
use codex_app_server_protocol::AppInfo;
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
use codex_tools::AdditionalProperties;
use codex_tools::CommandToolOptions;
use codex_tools::ConfiguredToolSpec;
use codex_tools::FreeformTool;
use codex_tools::ResponsesApiWebSearchFilters;
use codex_tools::ResponsesApiWebSearchUserLocation;
use codex_tools::ViewImageToolOptions;
use codex_tools::create_exec_command_tool;
use codex_tools::create_request_permissions_tool;
use codex_tools::create_view_image_tool;
use codex_tools::create_write_stdin_tool;
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
Expand Down Expand Up @@ -54,10 +61,6 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab
}))
}

fn windows_shell_safety_description() -> String {
format!("\n\n{}", super::windows_destructive_filesystem_guidance())
}

fn search_capable_model_info() -> ModelInfo {
let config = test_config();
let mut model_info =
Expand Down Expand Up @@ -298,9 +301,10 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
// Build expected from the same helpers used by the builder.
let mut expected: BTreeMap<String, ToolSpec> = BTreeMap::from([]);
for spec in [
create_exec_command_tool(
/*allow_login_shell*/ true, /*exec_permission_approvals_enabled*/ false,
),
create_exec_command_tool(CommandToolOptions {
allow_login_shell: true,
exec_permission_approvals_enabled: false,
}),
create_write_stdin_tool(),
PLAN_TOOL.clone(),
create_request_user_input_tool(CollaborationModesConfig::default()),
Expand All @@ -312,7 +316,9 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
search_context_size: None,
search_content_types: None,
},
create_view_image_tool(config.can_request_original_image_detail),
create_view_image_tool(ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
}),
] {
expected.insert(spec.name().to_string(), spec);
}
Expand Down Expand Up @@ -340,7 +346,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
}

if config.exec_permission_approvals_enabled {
let spec = create_request_permissions_tool();
let spec = create_request_permissions_tool(request_permissions_tool_description());
expected.insert(spec.name().to_string(), spec);
}

Expand Down Expand Up @@ -804,7 +810,7 @@ fn request_permissions_requires_feature_flag() {
let request_permissions_tool = find_tool(&tools, "request_permissions");
assert_eq!(
request_permissions_tool.spec,
create_request_permissions_tool()
create_request_permissions_tool(request_permissions_tool_description())
);
}

Expand Down Expand Up @@ -2640,177 +2646,6 @@ fn test_mcp_tool_anyof_defaults_to_string() {
);
}

#[test]
fn test_shell_tool() {
let tool = super::create_shell_tool(/*exec_permission_approvals_enabled*/ false);
let ToolSpec::Function(ResponsesApiTool {
description, name, ..
}) = &tool
else {
panic!("expected function tool");
};
assert_eq!(name, "shell");

let expected = if cfg!(windows) {
r#"Runs a Powershell command (Windows) and returns its output. Arguments to `shell` will be passed to CreateProcessW(). Most commands should be prefixed with ["powershell.exe", "-Command"].

Examples of valid command strings:

- ls -a (show hidden): ["powershell.exe", "-Command", "Get-ChildItem -Force"]
- recursive find by name: ["powershell.exe", "-Command", "Get-ChildItem -Recurse -Filter *.py"]
- recursive grep: ["powershell.exe", "-Command", "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"]
- ps aux | grep python: ["powershell.exe", "-Command", "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"]
- setting an env var: ["powershell.exe", "-Command", "$env:FOO='bar'; echo $env:FOO"]
- running an inline Python script: ["powershell.exe", "-Command", "@'\\nprint('Hello, world!')\\n'@ | python -"]"#
.to_string()
+ &windows_shell_safety_description()
} else {
r#"Runs a shell command and returns its output.
- The arguments to `shell` will be passed to execvp(). Most terminal commands should be prefixed with ["bash", "-lc"].
- Always set the `workdir` param when using the shell function. Do not use `cd` unless absolutely necessary."#
.to_string()
};
assert_eq!(description, &expected);
}

#[test]
fn test_exec_command_tool_windows_description_includes_shell_safety_guidance() {
let tool = super::create_exec_command_tool(
/*allow_login_shell*/ true, /*exec_permission_approvals_enabled*/ false,
);
let ToolSpec::Function(ResponsesApiTool {
description, name, ..
}) = &tool
else {
panic!("expected function tool");
};
assert_eq!(name, "exec_command");

let expected = if cfg!(windows) {
format!(
"Runs a command in a PTY, returning output or a session ID for ongoing interaction.{}",
windows_shell_safety_description()
)
} else {
"Runs a command in a PTY, returning output or a session ID for ongoing interaction."
.to_string()
};
assert_eq!(description, &expected);
}

#[test]
fn shell_tool_with_request_permission_includes_additional_permissions() {
let tool = super::create_shell_tool(/*exec_permission_approvals_enabled*/ true);
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
panic!("expected function tool");
};
let JsonSchema::Object { properties, .. } = parameters else {
panic!("expected object parameters");
};

assert!(properties.contains_key("additional_permissions"));

let Some(JsonSchema::String {
description: Some(description),
}) = properties.get("sandbox_permissions")
else {
panic!("expected sandbox_permissions description");
};
assert!(description.contains("with_additional_permissions"));
assert!(description.contains("filesystem or network permissions"));

let Some(JsonSchema::Object {
properties: additional_properties,
..
}) = properties.get("additional_permissions")
else {
panic!("expected additional_permissions schema");
};
assert!(additional_properties.contains_key("network"));
assert!(additional_properties.contains_key("file_system"));
assert!(!additional_properties.contains_key("macos"));
}

#[test]
fn request_permissions_tool_includes_full_permission_schema() {
let tool = super::create_request_permissions_tool();
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) = tool else {
panic!("expected function tool");
};
let JsonSchema::Object { properties, .. } = parameters else {
panic!("expected object parameters");
};
let Some(JsonSchema::Object {
properties: permission_properties,
additional_properties,
..
}) = properties.get("permissions")
else {
panic!("expected permissions object");
};

assert_eq!(additional_properties, &Some(false.into()));
assert!(permission_properties.contains_key("network"));
assert!(permission_properties.contains_key("file_system"));
assert!(!permission_properties.contains_key("macos"));

let Some(JsonSchema::Object {
properties: network_properties,
additional_properties,
..
}) = permission_properties.get("network")
else {
panic!("expected network object");
};
assert_eq!(additional_properties, &Some(false.into()));
assert!(network_properties.contains_key("enabled"));

let Some(JsonSchema::Object {
properties: file_system_properties,
additional_properties,
..
}) = permission_properties.get("file_system")
else {
panic!("expected file_system object");
};
assert_eq!(additional_properties, &Some(false.into()));
assert!(file_system_properties.contains_key("read"));
assert!(file_system_properties.contains_key("write"));
}

#[test]
fn test_shell_command_tool() {
let tool = super::create_shell_command_tool(
/*allow_login_shell*/ true, /*exec_permission_approvals_enabled*/ false,
);
let ToolSpec::Function(ResponsesApiTool {
description, name, ..
}) = &tool
else {
panic!("expected function tool");
};
assert_eq!(name, "shell_command");

let expected = if cfg!(windows) {
r#"Runs a Powershell command (Windows) and returns its output.

Examples of valid command strings:

- ls -a (show hidden): "Get-ChildItem -Force"
- recursive find by name: "Get-ChildItem -Recurse -Filter *.py"
- recursive grep: "Get-ChildItem -Path C:\\myrepo -Recurse | Select-String -Pattern 'TODO' -CaseSensitive"
- ps aux | grep python: "Get-Process | Where-Object { $_.ProcessName -like '*python*' }"
- setting an env var: "$env:FOO='bar'; echo $env:FOO"
- running an inline Python script: "@'\\nprint('Hello, world!')\\n'@ | python -""#
.to_string()
+ &windows_shell_safety_description()
} else {
r#"Runs a shell command and returns its output.
- Always set the `workdir` param when using the shell_command function. Do not use `cd` unless absolutely necessary."#.to_string()
};
assert_eq!(description, &expected);
}

#[test]
fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() {
let config = test_config();
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ schema and Responses API tool primitives that no longer need to live in
- `ResponsesApiNamespace`
- `ResponsesApiNamespaceTool`
- code-mode `ToolSpec` adapters
- local host tool spec builders for shell/exec/request-permissions/view-image
- `parse_tool_input_schema()`
- `parse_dynamic_tool()`
- `parse_mcp_tool()`
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@
mod code_mode;
mod dynamic_tool;
mod json_schema;
mod local_tool;
mod mcp_tool;
mod responses_api;
mod tool_definition;
mod tool_spec;
mod view_image;

pub use code_mode::augment_tool_spec_for_code_mode;
pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use dynamic_tool::parse_dynamic_tool;
pub use json_schema::AdditionalProperties;
pub use json_schema::JsonSchema;
pub use json_schema::parse_tool_input_schema;
pub use local_tool::CommandToolOptions;
pub use local_tool::ShellToolOptions;
pub use local_tool::create_exec_command_tool;
pub use local_tool::create_request_permissions_tool;
pub use local_tool::create_shell_command_tool;
pub use local_tool::create_shell_tool;
pub use local_tool::create_write_stdin_tool;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool;
pub use responses_api::FreeformTool;
Expand All @@ -33,3 +42,5 @@ pub use tool_spec::ResponsesApiWebSearchFilters;
pub use tool_spec::ResponsesApiWebSearchUserLocation;
pub use tool_spec::ToolSpec;
pub use tool_spec::create_tools_json_for_responses_api;
pub use view_image::ViewImageToolOptions;
pub use view_image::create_view_image_tool;
Loading
Loading