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
46 changes: 35 additions & 11 deletions codex-rs/core/src/tools/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,12 @@ fn create_request_permissions_schema() -> JsonSchema {
}
}

fn windows_destructive_filesystem_guidance() -> &'static str {
r#"Windows safety rules:
- Do not compose destructive filesystem commands across shells. Do not enumerate paths in PowerShell and then pass them to `cmd /c`, batch builtins, or another shell for deletion or moving. Use one shell end-to-end, prefer native PowerShell cmdlets such as `Remove-Item` / `Move-Item` with `-LiteralPath`, and avoid string-built shell commands for file operations.
- Before any recursive delete or move on Windows, verify the resolved absolute target paths stay within the intended workspace or explicitly named target directory. Never issue a recursive delete or move against a computed path if the final target has not been checked."#
}

fn create_approval_parameters(
exec_permission_approvals_enabled: bool,
) -> BTreeMap<String, JsonSchema> {
Expand Down Expand Up @@ -709,9 +715,15 @@ fn create_exec_command_tool(

ToolSpec::Function(ResponsesApiTool {
name: "exec_command".to_string(),
description:
description: if cfg!(windows) {
format!(
"Runs a command in a PTY, returning output or a session ID for ongoing interaction.\n\n{}",
windows_destructive_filesystem_guidance()
)
} else {
"Runs a command in a PTY, returning output or a session ID for ongoing interaction."
.to_string(),
.to_string()
},
strict: false,
defer_loading: None,
parameters: JsonSchema::Object {
Expand Down Expand Up @@ -848,22 +860,28 @@ fn create_shell_tool(exec_permission_approvals_enabled: bool) -> ToolSpec {
exec_permission_approvals_enabled,
));

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"].
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*' }"]
- 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 -"]"#
- 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();
.to_string()
};

ToolSpec::Function(ResponsesApiTool {
name: "shell".to_string(),
Expand Down Expand Up @@ -921,20 +939,26 @@ fn create_shell_command_tool(
));

let description = if cfg!(windows) {
r#"Runs a Powershell command (Windows) and returns its output.
format!(
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*' }"
- 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 -"#
- running an inline Python script: "@'\\nprint('Hello, world!')\\n'@ | python -"

{}"#,
windows_destructive_filesystem_guidance()
)
} 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();
.to_string()
};

ToolSpec::Function(ResponsesApiTool {
name: "shell_command".to_string(),
Expand Down
42 changes: 37 additions & 5 deletions codex-rs/core/src/tools/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ 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 @@ -2363,7 +2367,7 @@ fn test_shell_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"].
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:

Expand All @@ -2373,11 +2377,37 @@ Examples of valid command strings:
- 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 -"]"#
} else {
r#"Runs a shell command and returns its output.
.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();
.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(true, 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);
}

Expand Down Expand Up @@ -2482,7 +2512,9 @@ Examples of valid command strings:
- 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()
- 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()
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec-server/src/server/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub(crate) struct ExecServerFileSystem {
impl Default for ExecServerFileSystem {
fn default() -> Self {
Self {
file_system: Arc::new(Environment.get_filesystem()),
file_system: Arc::new(Environment::default().get_filesystem()),
}
}
}
Expand Down
Loading