From f69f07b028eae41c28f30bbdc26c901b1d688f1e Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:10:52 -0700 Subject: [PATCH] put workspace roots in the environment context (#3375) to keep the tool description constant when the writable roots change. --- codex-rs/core/src/environment_context.rs | 96 +++++++++++++++++++++ codex-rs/core/src/openai_tools.rs | 22 ++--- codex-rs/core/tests/suite/prompt_caching.rs | 10 ++- 3 files changed, 112 insertions(+), 16 deletions(-) diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index 0a8b09d1..1d755841 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -26,6 +26,7 @@ pub(crate) struct EnvironmentContext { pub approval_policy: Option, pub sandbox_mode: Option, pub network_access: Option, + pub writable_roots: Option>, pub shell: Option, } @@ -57,6 +58,16 @@ impl EnvironmentContext { } None => None, }, + writable_roots: match sandbox_policy { + Some(SandboxPolicy::WorkspaceWrite { writable_roots, .. }) => { + if writable_roots.is_empty() { + None + } else { + Some(writable_roots.clone()) + } + } + _ => None, + }, shell, } } @@ -72,6 +83,7 @@ impl EnvironmentContext { /// ... /// ... /// ... + /// ... /// ... /// ... /// @@ -94,6 +106,16 @@ impl EnvironmentContext { " {network_access}" )); } + if let Some(writable_roots) = self.writable_roots { + lines.push(" ".to_string()); + for writable_root in writable_roots { + lines.push(format!( + " {}", + writable_root.to_string_lossy() + )); + } + lines.push(" ".to_string()); + } if let Some(shell) = self.shell && let Some(shell_name) = shell.name() { @@ -115,3 +137,77 @@ impl From for ResponseItem { } } } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + fn workspace_write_policy(writable_roots: Vec<&str>, network_access: bool) -> SandboxPolicy { + SandboxPolicy::WorkspaceWrite { + writable_roots: writable_roots.into_iter().map(PathBuf::from).collect(), + network_access, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + } + } + + #[test] + fn serialize_workspace_write_environment_context() { + let context = EnvironmentContext::new( + Some(PathBuf::from("/repo")), + Some(AskForApproval::OnRequest), + Some(workspace_write_policy(vec!["/repo", "/tmp"], false)), + None, + ); + + let expected = r#" + /repo + on-request + workspace-write + restricted + + /repo + /tmp + +"#; + + assert_eq!(context.serialize_to_xml(), expected); + } + + #[test] + fn serialize_read_only_environment_context() { + let context = EnvironmentContext::new( + None, + Some(AskForApproval::Never), + Some(SandboxPolicy::ReadOnly), + None, + ); + + let expected = r#" + never + read-only + restricted +"#; + + assert_eq!(context.serialize_to_xml(), expected); + } + + #[test] + fn serialize_full_access_environment_context() { + let context = EnvironmentContext::new( + None, + Some(AskForApproval::OnFailure), + Some(SandboxPolicy::DangerFullAccess), + None, + ); + + let expected = r#" + on-failure + danger-full-access + enabled +"#; + + assert_eq!(context.serialize_to_xml(), expected); + } +} diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 72237028..9521e4ee 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -240,17 +240,20 @@ fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool { let description = match sandbox_policy { SandboxPolicy::WorkspaceWrite { network_access, - writable_roots, .. } => { + let network_line = if !network_access { + "\n - Commands that require network access" + } else { + "" + }; + format!( r#" The shell tool is used to execute shell commands. - When invoking the shell tool, your call will be running in a sandbox, and some shell commands will require escalated privileges: - Types of actions that require escalated privileges: - - Writing files other than those in the writable roots - - writable roots: -{}{} + - Writing files other than those in the writable roots (see the environment context for the allowed directories){network_line} - Examples of commands that require escalated privileges: - git commit - npm install or pnpm install @@ -259,12 +262,6 @@ The shell tool is used to execute shell commands. - When invoking a command that will require escalated privileges: - Provide the with_escalated_permissions parameter with the boolean value true - Include a short, 1 sentence explanation for why we need to run with_escalated_permissions in the justification parameter."#, - writable_roots.iter().map(|wr| format!(" - {}", wr.to_string_lossy())).collect::>().join("\n"), - if !network_access { - "\n - Commands that require network access\n" - } else { - "" - } ) } SandboxPolicy::DangerFullAccess => { @@ -1105,11 +1102,8 @@ mod tests { The shell tool is used to execute shell commands. - When invoking the shell tool, your call will be running in a sandbox, and some shell commands will require escalated privileges: - Types of actions that require escalated privileges: - - Writing files other than those in the writable roots - - writable roots: - - workspace + - Writing files other than those in the writable roots (see the environment context for the allowed directories) - Commands that require network access - - Examples of commands that require escalated privileges: - git commit - npm install or pnpm install diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 4625841a..5ac3da60 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -426,11 +426,17 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() { // After overriding the turn context, the environment context should be emitted again // reflecting the new approval policy and sandbox settings. Omit cwd because it did // not change. - let expected_env_text_2 = r#" + let expected_env_text_2 = format!( + r#" never workspace-write enabled -"#; + + {} + +"#, + writable.path().to_string_lossy() + ); let expected_env_msg_2 = serde_json::json!({ "type": "message", "role": "user",