mirror of
https://github.com/openai/codex.git
synced 2026-05-02 10:26:45 +00:00
## Why `core/src/tools/spec.rs` still bundled a set of pure local-host tool builders with the orchestration that actually decides when those tools are exposed and which handlers back them. That made `codex-core` responsible for JSON/tool-shape construction that does not depend on session state, and it kept the `codex-tools` migration from taking a meaningfully larger bite out of `spec.rs`. This PR moves that reusable spec-building layer into `codex-tools` while leaving feature gating, handler registration, and runtime-coupled descriptions in `codex-core`. ## What changed - added `codex-rs/tools/src/local_tool.rs` for the pure builders for `exec_command`, `write_stdin`, `shell`, `shell_command`, and `request_permissions` - added `codex-rs/tools/src/view_image.rs` for the `view_image` tool spec and output schema so the extracted modules stay right-sized - rewired `codex-rs/core/src/tools/spec.rs` to call those extracted builders instead of constructing these specs inline - kept the `request_permissions` description source in `codex-core`, with `codex-tools` taking the description as input so the crate boundary does not grow a dependency on handler/runtime code - moved the direct constructor coverage for this slice from `codex-rs/core/src/tools/spec_tests.rs` into `codex-rs/tools/src/local_tool_tests.rs` and `codex-rs/tools/src/view_image_tests.rs` - updated `codex-rs/tools/README.md` to reflect that `codex-tools` now owns this local-host spec layer ## Test plan - `CARGO_TARGET_DIR=/tmp/codex-tools-local-host cargo test -p codex-tools` - `CARGO_TARGET_DIR=/tmp/codex-core-local-tools cargo test -p codex-core --lib tools::spec::` - `just argument-comment-lint` ## References - #15923 - #15928 - #15944 - #15953 - #16031 - #16047 - #16129 - #16132
443 lines
16 KiB
Rust
443 lines
16 KiB
Rust
use super::*;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeMap;
|
|
|
|
fn windows_shell_safety_description() -> String {
|
|
format!("\n\n{}", windows_destructive_filesystem_guidance())
|
|
}
|
|
|
|
#[test]
|
|
fn shell_tool_matches_expected_spec() {
|
|
let tool = create_shell_tool(ShellToolOptions {
|
|
exec_permission_approvals_enabled: false,
|
|
});
|
|
|
|
let description = 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()
|
|
};
|
|
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"command".to_string(),
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
description: Some("The command to execute".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"workdir".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("The working directory to execute the command in".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"timeout_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some("The timeout for the command in milliseconds".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"sandbox_permissions".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"justification".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
r#"Only set if sandbox_permissions is \"require_escalated\".
|
|
Request approval from the user to run this command outside the sandbox.
|
|
Phrased as a simple question that summarizes the purpose of the
|
|
command as it relates to the task at hand - e.g. 'Do you want to
|
|
fetch and pull the latest version of this git branch?'"#
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"prefix_rule".to_string(),
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
description: Some(
|
|
r#"Only specify when sandbox_permissions is `require_escalated`.
|
|
Suggest a prefix command pattern that will allow you to fulfill similar requests from the user in the future.
|
|
Should be a short but reasonable prefix, e.g. [\"git\", \"pull\"] or [\"uv\", \"run\"] or [\"pytest\"]."#
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
assert_eq!(
|
|
tool,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "shell".to_string(),
|
|
description,
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["command".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exec_command_tool_matches_expected_spec() {
|
|
let tool = create_exec_command_tool(CommandToolOptions {
|
|
allow_login_shell: true,
|
|
exec_permission_approvals_enabled: false,
|
|
});
|
|
|
|
let description = 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()
|
|
};
|
|
|
|
let mut properties = BTreeMap::from([
|
|
(
|
|
"cmd".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Shell command to execute.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"workdir".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional working directory to run the command in; defaults to the turn cwd."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"shell".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Shell binary to launch. Defaults to the user's default shell.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"tty".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"Whether to allocate a TTY for the command. Defaults to false (plain pipes); set to true to open a PTY and access TTY process."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"yield_time_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"How long to wait (in milliseconds) for output before yielding.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"max_output_tokens".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Maximum number of tokens to return. Excess output will be truncated."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"login".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"Whether to run the shell with -l/-i semantics. Defaults to true.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
properties.extend(create_approval_parameters(
|
|
/*exec_permission_approvals_enabled*/ false,
|
|
));
|
|
|
|
assert_eq!(
|
|
tool,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "exec_command".to_string(),
|
|
description,
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["cmd".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(unified_exec_output_schema()),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn write_stdin_tool_matches_expected_spec() {
|
|
let tool = create_write_stdin_tool();
|
|
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"session_id".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some("Identifier of the running unified exec session.".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"chars".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("Bytes to write to stdin (may be empty to poll).".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"yield_time_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"How long to wait (in milliseconds) for output before yielding.".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"max_output_tokens".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some(
|
|
"Maximum number of tokens to return. Excess output will be truncated."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
|
|
assert_eq!(
|
|
tool,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "write_stdin".to_string(),
|
|
description:
|
|
"Writes characters to an existing unified exec session and returns recent output."
|
|
.to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["session_id".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: Some(unified_exec_output_schema()),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_tool_with_request_permission_includes_additional_permissions() {
|
|
let tool = create_shell_tool(ShellToolOptions {
|
|
exec_permission_approvals_enabled: true,
|
|
});
|
|
|
|
let mut properties = BTreeMap::from([
|
|
(
|
|
"command".to_string(),
|
|
JsonSchema::Array {
|
|
items: Box::new(JsonSchema::String { description: None }),
|
|
description: Some("The command to execute".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"workdir".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("The working directory to execute the command in".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"timeout_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some("The timeout for the command in milliseconds".to_string()),
|
|
},
|
|
),
|
|
]);
|
|
properties.extend(create_approval_parameters(
|
|
/*exec_permission_approvals_enabled*/ true,
|
|
));
|
|
|
|
let description = if cfg!(windows) {
|
|
format!(
|
|
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 -"]
|
|
|
|
{}"#,
|
|
windows_destructive_filesystem_guidance()
|
|
)
|
|
} 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!(
|
|
tool,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "shell".to_string(),
|
|
description,
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["command".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_tool_includes_full_permission_schema() {
|
|
let tool =
|
|
create_request_permissions_tool("Request extra permissions for this turn.".to_string());
|
|
|
|
let properties = BTreeMap::from([
|
|
(
|
|
"reason".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"Optional short explanation for why additional permissions are needed."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
("permissions".to_string(), permission_profile_schema()),
|
|
]);
|
|
|
|
assert_eq!(
|
|
tool,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "request_permissions".to_string(),
|
|
description: "Request extra permissions for this turn.".to_string(),
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["permissions".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shell_command_tool_matches_expected_spec() {
|
|
let tool = create_shell_command_tool(CommandToolOptions {
|
|
allow_login_shell: true,
|
|
exec_permission_approvals_enabled: false,
|
|
});
|
|
|
|
let description = 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()
|
|
};
|
|
|
|
let mut properties = BTreeMap::from([
|
|
(
|
|
"command".to_string(),
|
|
JsonSchema::String {
|
|
description: Some(
|
|
"The shell script to execute in the user's default shell".to_string(),
|
|
),
|
|
},
|
|
),
|
|
(
|
|
"workdir".to_string(),
|
|
JsonSchema::String {
|
|
description: Some("The working directory to execute the command in".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"timeout_ms".to_string(),
|
|
JsonSchema::Number {
|
|
description: Some("The timeout for the command in milliseconds".to_string()),
|
|
},
|
|
),
|
|
(
|
|
"login".to_string(),
|
|
JsonSchema::Boolean {
|
|
description: Some(
|
|
"Whether to run the shell with login shell semantics. Defaults to true."
|
|
.to_string(),
|
|
),
|
|
},
|
|
),
|
|
]);
|
|
properties.extend(create_approval_parameters(
|
|
/*exec_permission_approvals_enabled*/ false,
|
|
));
|
|
|
|
assert_eq!(
|
|
tool,
|
|
ToolSpec::Function(ResponsesApiTool {
|
|
name: "shell_command".to_string(),
|
|
description,
|
|
strict: false,
|
|
defer_loading: None,
|
|
parameters: JsonSchema::Object {
|
|
properties,
|
|
required: Some(vec!["command".to_string()]),
|
|
additional_properties: Some(false.into()),
|
|
},
|
|
output_schema: None,
|
|
})
|
|
);
|
|
}
|