From d4d2b293b446b019e4a14e26d4d16bfc6d682f8f Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Thu, 27 Aug 2026 18:07:12 +0000 Subject: [PATCH] Propagate executor home directories into sandbox contexts (#41204) ## What changed - Report the executor's user home in environment metadata. - Cache the reported home on selected turn environments and include it in filesystem sandbox contexts, including `apply_patch` requests. - Preserve compatibility with executors that do not report a home directory. ## Testing - Cover local, remote, inherited, serialized, and `apply_patch` sandbox contexts. GitOrigin-RevId: ea65e68e33a80bc9bcfc7c53b1486ab944699470 --- codex-rs/core/src/environment_selection.rs | 35 +++++++++++++++---- codex-rs/core/src/session/tests.rs | 1 + codex-rs/core/src/session/turn_context.rs | 5 +++ .../core/src/tools/runtimes/apply_patch.rs | 1 + .../src/tools/runtimes/apply_patch_tests.rs | 5 ++- codex-rs/core/src/tools/sandboxing.rs | 1 + codex-rs/core/src/tools/sandboxing_tests.rs | 1 + codex-rs/exec-server-protocol/src/protocol.rs | 20 ++++++++--- .../exec-server/tests/environment_config.rs | 4 +++ codex-rs/file-system/src/lib.rs | 5 +++ codex-rs/protocol/src/permissions.rs | 7 ++++ 11 files changed, 73 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 465aaf4fec..d6804c02ae 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -137,6 +137,7 @@ type PendingConfigurationResult = Result; struct ResolvedEnvironment { environment: Arc, shell: Option, + user_home_dir: Option, temporary_directories: Option>, shell_snapshot: ShellSnapshotTask, shell_snapshot_v2_supported: bool, @@ -257,6 +258,7 @@ impl ThreadEnvironments { futures::future::ready(Ok(ResolvedEnvironment { environment: environment.environment, shell: environment.shell, + user_home_dir: environment.user_home_dir, temporary_directories: environment.temporary_directories, shell_snapshot: environment.shell_snapshot, shell_snapshot_v2_supported: environment.shell_snapshot_v2_supported, @@ -623,10 +625,10 @@ impl ThreadEnvironments { }; // Resolve the attachment only after both prerequisites are ready. let ((), installed_config) = tokio::try_join!(connection_ready, configuration_ready)?; - let (shell, temporary_directories, shell_snapshot_v2_supported) = if environment.is_remote() - { + let (shell, user_home_dir, temporary_dirs, snapshot_v2) = if environment.is_remote() { match environment.info().await { Ok(info) => { + let user_home_dir = info.user_home_dir; let temporary_directories = info.temporary_directories; let shell_snapshot_v2_supported = info.capabilities.shell_snapshot_v2; let shell = match Shell::from_environment_shell_info(info.shell) { @@ -638,16 +640,22 @@ impl ThreadEnvironments { None } }; - (shell, temporary_directories, shell_snapshot_v2_supported) + ( + shell, + user_home_dir, + temporary_directories, + shell_snapshot_v2_supported, + ) } Err(err) => { tracing::warn!("failed to get info for environment `{environment_id}`: {err}"); - (None, None, false) + (None, None, None, false) } } } else { ( Some(local_shell), + PathUri::from_host_native_path("~").ok(), Some(EnvironmentInfo::local_temporary_directories()), cfg!(unix), ) @@ -662,9 +670,10 @@ impl ThreadEnvironments { Ok(ResolvedEnvironment { environment, shell, - temporary_directories, + user_home_dir, + temporary_directories: temporary_dirs, shell_snapshot: task, - shell_snapshot_v2_supported, + shell_snapshot_v2_supported: snapshot_v2, installed_config, }) } @@ -742,6 +751,7 @@ impl TurnEnvironmentState { turn_environment.shell_snapshot = environment.shell_snapshot; turn_environment.shell_snapshot_v2_supported = environment.shell_snapshot_v2_supported; + turn_environment.user_home_dir = environment.user_home_dir; turn_environment.temporary_directories = environment.temporary_directories; Some(Self::Ready(turn_environment)) } @@ -1018,6 +1028,7 @@ mod tests { "id": info["id"], "result": { "shell": { "name": "zsh", "path": "/bin/zsh" }, + "userHomeDir": "file:///home/remote", "temporaryDirectories": ["file:///tmp/remote"], } }) @@ -1145,6 +1156,7 @@ url = "ws://127.0.0.1:8765" let snapshot = turn_environments.snapshot().await; let environment = snapshot.primary().expect("local environment"); + let expected_user_home_dir = PathUri::from_host_native_path("~").ok(); let expected_temporary_directories = EnvironmentInfo::local_temporary_directories(); assert_eq!(environment.shell.as_ref(), Some(&local_shell)); @@ -1157,6 +1169,7 @@ url = "ws://127.0.0.1:8765" FileSystemSandboxPolicyContext { cwd: environment.cwd(), workspace_roots: &[], + user_home_dir: expected_user_home_dir.as_ref(), temporary_directories: Some(expected_temporary_directories.as_slice()), } ); @@ -1472,6 +1485,7 @@ url = "ws://127.0.0.1:8765" ); assert_eq!(attached.to_selections(), vec![remote, local]); let environment = attached.primary().expect("remote environment"); + let expected_user_home_dir = PathUri::parse("file:///home/remote").expect("remote home"); let expected_temporary_directories = [PathUri::parse("file:///tmp/remote").expect("remote temporary directory")]; assert_eq!( @@ -1482,6 +1496,7 @@ url = "ws://127.0.0.1:8765" FileSystemSandboxPolicyContext { cwd: environment.cwd(), workspace_roots: &[], + user_home_dir: Some(&expected_user_home_dir), temporary_directories: Some(expected_temporary_directories.as_slice()), } ); @@ -1707,6 +1722,8 @@ url = "ws://127.0.0.1:8765" Arc::clone(&inherited_environment), /*shell*/ None, ); + inherited.user_home_dir = + Some(PathUri::parse("file:///home/inherited").expect("home directory")); inherited.temporary_directories = Some(vec![ PathUri::parse("file:///tmp/inherited").expect("temporary directory"), ]); @@ -1752,6 +1769,12 @@ url = "ws://127.0.0.1:8765" let inherited = snapshot.primary().expect("inherited environment"); assert!(Arc::ptr_eq(&inherited.environment, &inherited_environment)); assert_eq!(inherited.config(), &child_config); + assert_eq!( + inherited + .sandbox_context(/*additional_permissions*/ None) + .user_home_dir, + Some(PathUri::parse("file:///home/inherited").expect("home directory")), + ); assert_eq!( inherited .sandbox_context(/*additional_permissions*/ None) diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index a42d748a9c..b334571814 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -9297,6 +9297,7 @@ async fn capability_discovery_uses_environment_permission_profile() { permissions: environment.permission_profile().clone().into(), cwd: Some(environment.cwd().clone()), workspace_roots: environment.workspace_roots().to_vec(), + user_home_dir: environment.user_home_dir.clone(), temporary_directories: environment.temporary_directories.clone(), windows_sandbox_level: WindowsSandboxLevel::Elevated, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 93c9732f25..7a4184e9f1 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -48,6 +48,8 @@ pub(crate) struct TurnEnvironment { pub(crate) selection: TurnEnvironmentSelection, pub(crate) config_origin: EnvironmentConfigOrigin, pub(crate) environment: Arc, + /// Cached from the selected executor; `None` means it did not report one. + pub(crate) user_home_dir: Option, /// Cached from the selected executor; `None` means it did not report them. pub(crate) temporary_directories: Option>, pub(crate) shell: Option, @@ -67,6 +69,7 @@ impl TurnEnvironment { selection, config_origin, environment, + user_home_dir: None, temporary_directories: None, shell, shell_snapshot: futures::future::ready(None).boxed().shared(), @@ -131,6 +134,7 @@ impl TurnEnvironment { permissions: permissions.into(), cwd: Some(self.cwd().clone()), workspace_roots: self.workspace_roots().to_vec(), + user_home_dir: self.user_home_dir.clone(), temporary_directories: self.temporary_directories.clone(), windows_sandbox_level: executor_windows_sandbox_level( config.windows_sandbox_level, @@ -165,6 +169,7 @@ impl std::fmt::Debug for TurnEnvironment { .field("environment", &self.environment) .field("cwd", &self.selection.cwd) .field("workspace_roots", &self.config().workspace_roots) + .field("user_home_dir", &self.user_home_dir) .field("temporary_directories", &self.temporary_directories) .field("shell", &self.shell) .field("config", self.config()) diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 7df1946569..723e6eb034 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -100,6 +100,7 @@ impl ApplyPatchRuntime { permissions: permissions.into(), cwd: Some(attempt.sandbox_cwd.clone()), workspace_roots: attempt.workspace_roots.to_vec(), + user_home_dir: req.turn_environment.user_home_dir.clone(), temporary_directories: None, windows_sandbox_level: executor_windows_sandbox_level( attempt.windows_sandbox_level, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 6effb9591d..35470229b4 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -288,7 +288,7 @@ async fn file_system_sandbox_context_respects_sandbox_request() { let path = std::env::temp_dir() .join("apply-patch-runtime-none.txt") .abs(); - let req = ApplyPatchRequest { + let mut req = ApplyPatchRequest { turn_environment: test_turn_environment(codex_exec_server::LOCAL_ENVIRONMENT_ID), action: ApplyPatchAction::new_add_for_test( &PathUri::from_abs_path(&path), @@ -329,6 +329,8 @@ async fn file_system_sandbox_context_respects_sandbox_request() { ); let cwd = PathUri::parse("file:///C:/workspace").expect("Windows workspace URI"); + let user_home_dir = PathUri::parse("file:///C:/Users/remote").expect("Windows home URI"); + req.turn_environment.user_home_dir = Some(user_home_dir.clone()); let permissions = PermissionProfile::workspace_write(); let attempt = SandboxAttempt { sandbox_requested: true, @@ -345,6 +347,7 @@ async fn file_system_sandbox_context_respects_sandbox_request() { permissions: permissions.into(), cwd: Some(cwd.clone()), workspace_roots: vec![cwd], + user_home_dir: Some(user_home_dir), temporary_directories: None, windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 195cf54e96..615b917413 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -513,6 +513,7 @@ impl<'a> SandboxAttempt<'a> { permissions: exec_server_permissions.into(), cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()), workspace_roots: self.workspace_roots.to_vec(), + user_home_dir: None, temporary_directories: None, windows_sandbox_level: executor_windows_sandbox_level( self.windows_sandbox_level, diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index ba63e270eb..0a9e1fe114 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -370,6 +370,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { permissions: exec_server_permissions.clone().into(), cwd: Some(cwd_uri.clone()), workspace_roots: vec![cwd_uri.clone()], + user_home_dir: None, temporary_directories: None, windows_sandbox_level: if cfg!(windows) { codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 95213a63d9..5cc016fe70 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -97,6 +97,9 @@ pub struct EnvironmentInfo { /// Working directory inherited by the exec-server process. #[serde(default)] pub cwd: Option, + /// Executor user home used to expand `~` in path-bearing values. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_home_dir: Option, /// Executor-local default directories for resolving `:tmpdir`, when reported. /// On Windows, a command's `TEMP` or `TMP` overrides take precedence. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -209,6 +212,7 @@ impl EnvironmentInfo { Self { shell: codex_shell_command::shell_detect::default_user_shell().into(), cwd: cwd.and_then(|cwd| PathUri::from_host_native_path(cwd).ok()), + user_home_dir: PathUri::from_host_native_path("~").ok(), temporary_directories: Some(temporary_directories), temp_dir, capabilities: EnvironmentCapabilities { @@ -973,6 +977,7 @@ mod tests { path: "/bin/zsh".to_string(), }, cwd: None, + user_home_dir: None, temporary_directories: None, temp_dir: None, capabilities: EnvironmentCapabilities::default(), @@ -1006,6 +1011,7 @@ mod tests { let expected = serde_json::json!({ "shell": { "name": "powershell", "path": "powershell.exe" }, "cwd": null, + "userHomeDir": "file:///C:/Users/remote", "temporaryDirectories": ["file:///C:/Temp", "file:///D:/Temp"], "capabilities": { "networkProxyLaunch": false, @@ -1050,10 +1056,9 @@ mod tests { .collect::>(); expected.dedup(); - assert_eq!( - EnvironmentInfo::local().temporary_directories, - Some(expected) - ); + let info = EnvironmentInfo::local(); + assert_eq!(info.temporary_directories, Some(expected)); + assert_eq!(info.user_home_dir, PathUri::from_host_native_path("~").ok()); } #[cfg(unix)] @@ -1147,11 +1152,16 @@ mod tests { file_system, network: NetworkSandboxPolicy::Restricted, }; - let sandbox = + let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone()); + sandbox.user_home_dir = Some(cwd.clone()); let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox"); + assert_eq!( + serialized["userHomeDir"], + serde_json::json!(cwd.to_string()) + ); assert_eq!( serialized["permissions"]["file_system"]["entries"][0]["path"]["path"], serde_json::json!(cwd.to_string()) diff --git a/codex-rs/exec-server/tests/environment_config.rs b/codex-rs/exec-server/tests/environment_config.rs index 2dff179253..5df1bea910 100644 --- a/codex-rs/exec-server/tests/environment_config.rs +++ b/codex-rs/exec-server/tests/environment_config.rs @@ -46,6 +46,10 @@ unselected = "do not return" let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?; let environment_info = environment.info().await?; assert!(environment_info.capabilities.environment_config_read); + assert_eq!( + environment_info.user_home_dir, + dirs::home_dir().and_then(|home_dir| PathUri::from_host_native_path(home_dir).ok()), + ); let response = environment .read_environment_config(EnvironmentConfigReadParams { diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index a921bbd1c5..255ce2bcdc 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -333,6 +333,9 @@ pub struct FileSystemSandboxContext { pub cwd: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub workspace_roots: Vec, + /// Executor-local user home used to resolve home-relative policy paths. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_home_dir: Option, /// Executor-local default directories used to resolve `:tmpdir` policy entries. #[serde(default, skip_serializing_if = "Option::is_none")] pub temporary_directories: Option>, @@ -380,6 +383,7 @@ impl FileSystemSandboxContext { permissions: permissions.into(), cwd, workspace_roots, + user_home_dir: None, temporary_directories: None, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -405,6 +409,7 @@ impl FileSystemSandboxContext { Some(FileSystemSandboxPolicyContext { cwd: self.cwd.as_ref()?, workspace_roots: &self.workspace_roots, + user_home_dir: self.user_home_dir.as_ref(), temporary_directories: self.temporary_directories.as_deref(), }) } diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index 83e53099cf..19a42b80dc 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -263,6 +263,7 @@ pub struct RawFileSystemSandboxPolicy { pub struct FileSystemSandboxPolicyContext<'a> { pub cwd: &'a PathUri, pub workspace_roots: &'a [PathUri], + pub user_home_dir: Option<&'a PathUri>, pub temporary_directories: Option<&'a [PathUri]>, } @@ -1784,10 +1785,12 @@ fn with_local_policy_context( let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok()?; let path = PathUri::from(resolve_candidate_path(path, cwd.as_path())?); let cwd = PathUri::from(cwd); + let user_home_dir = PathUri::from_host_native_path("~").ok(); let temporary_directories = local_temporary_directories(); let context = FileSystemSandboxPolicyContext { cwd: &cwd, workspace_roots: std::slice::from_ref(&cwd), + user_home_dir: user_home_dir.as_ref(), temporary_directories: Some(&temporary_directories), }; Some(evaluate(&path, &context)) @@ -2451,6 +2454,7 @@ mod tests { let context = FileSystemSandboxPolicyContext { cwd: &cwd, workspace_roots: &workspace_roots, + user_home_dir: None, temporary_directories: Some(&temporary_directories), }; let policy = FileSystemSandboxPolicy::restricted(vec![ @@ -2530,6 +2534,7 @@ mod tests { let context = FileSystemSandboxPolicyContext { cwd: &cwd, workspace_roots: std::slice::from_ref(&cwd), + user_home_dir: None, temporary_directories: None, }; let candidate = path("file:///workspace/private/secret/key"); @@ -2575,6 +2580,7 @@ mod tests { let context = FileSystemSandboxPolicyContext { cwd: &cwd, workspace_roots: &[], + user_home_dir: None, temporary_directories: None, }; let policy = FileSystemSandboxPolicy::restricted(vec![ @@ -2605,6 +2611,7 @@ mod tests { let opaque_context = FileSystemSandboxPolicyContext { cwd: &opaque, workspace_roots: std::slice::from_ref(&opaque), + user_home_dir: None, temporary_directories: None, }; let opaque_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry::new(