mirror of
https://github.com/openai/codex.git
synced 2026-09-08 15:50:34 +00:00
Handle Windows sandbox mapped-drive workspaces
This commit is contained in:
@@ -80,6 +80,7 @@ features = [
|
||||
"Win32_Security_Cryptography",
|
||||
"Win32_Security_Authentication_Identity",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_NetworkManagement_WNet",
|
||||
"Win32_System_StationsAndDesktops",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_UI_Shell",
|
||||
|
||||
@@ -38,6 +38,7 @@ mod windows_impl {
|
||||
use crate::logging::log_note;
|
||||
use crate::logging::log_start;
|
||||
use crate::logging::log_success;
|
||||
use crate::path_normalization::normalize_command_cwd;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::token::convert_string_sid_to_sid;
|
||||
@@ -241,11 +242,12 @@ mod windows_impl {
|
||||
write_roots_override,
|
||||
deny_write_paths_override,
|
||||
} = request;
|
||||
let normalized_cwd = normalize_command_cwd(cwd);
|
||||
let policy = parse_policy(policy_json_or_preset)?;
|
||||
normalize_null_device_env(&mut env_map);
|
||||
ensure_non_interactive_pager(&mut env_map);
|
||||
inherit_path_env(&mut env_map);
|
||||
inject_git_safe_directory(&mut env_map, cwd, None);
|
||||
inject_git_safe_directory(&mut env_map, &normalized_cwd, None);
|
||||
// Use a temp-based log dir that the sandbox user can write.
|
||||
let sandbox_base = codex_home.join(".sandbox");
|
||||
ensure_codex_home_exists(&sandbox_base)?;
|
||||
@@ -255,7 +257,7 @@ mod windows_impl {
|
||||
let sandbox_creds = require_logon_sandbox_creds(
|
||||
&policy,
|
||||
sandbox_policy_cwd,
|
||||
cwd,
|
||||
&normalized_cwd,
|
||||
&env_map,
|
||||
codex_home,
|
||||
read_roots_override,
|
||||
@@ -289,7 +291,7 @@ mod windows_impl {
|
||||
psid,
|
||||
vec![
|
||||
caps.workspace,
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, &normalized_cwd)?,
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -321,7 +323,7 @@ mod windows_impl {
|
||||
);
|
||||
let mut cmdline_vec: Vec<u16> = to_wide(&runner_full_cmd);
|
||||
let exe_w: Vec<u16> = to_wide(&runner_cmdline);
|
||||
let cwd_w: Vec<u16> = to_wide(cwd);
|
||||
let cwd_w: Vec<u16> = to_wide(sandbox_base.as_path());
|
||||
|
||||
// Minimal CPWL launch: inherit env, no desktop override, no handle inheritance.
|
||||
let env_block: Option<Vec<u16>> = None;
|
||||
@@ -339,7 +341,7 @@ mod windows_impl {
|
||||
"runner launch: exe={} cmdline={} cwd={}",
|
||||
runner_exe.display(),
|
||||
runner_full_cmd,
|
||||
cwd.display()
|
||||
sandbox_base.display()
|
||||
),
|
||||
logs_base_dir,
|
||||
);
|
||||
@@ -413,7 +415,7 @@ mod windows_impl {
|
||||
message: Message::SpawnRequest {
|
||||
payload: Box::new(SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
cwd: normalized_cwd.clone(),
|
||||
env: env_map.clone(),
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Storage::FileSystem::DRIVE_REMOTE;
|
||||
use windows_sys::Win32::Storage::FileSystem::GetDriveTypeW;
|
||||
|
||||
use crate::winutil::to_wide;
|
||||
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
|
||||
use windows_sys::Win32::Foundation::NO_ERROR;
|
||||
use windows_sys::Win32::NetworkManagement::WNet::WNetGetConnectionW;
|
||||
|
||||
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
||||
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
||||
@@ -17,41 +17,54 @@ pub fn canonical_path_key(path: &Path) -> String {
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
pub fn ensure_windows_sandbox_local_path(path: &Path, context: &str) -> Result<()> {
|
||||
let raw = path.to_string_lossy();
|
||||
let normalized = normalize_windows_device_path(&raw).unwrap_or_else(|| raw.into_owned());
|
||||
if normalized.starts_with(r"\\") {
|
||||
anyhow::bail!(
|
||||
"windows sandbox does not support {context} on UNC or mapped network paths: {}. Use a local drive workspace or run without the Windows sandbox for this session.",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(root) = windows_drive_root(&normalized) {
|
||||
let drive_type = unsafe { GetDriveTypeW(to_wide(&root).as_ptr()) };
|
||||
if drive_type == DRIVE_REMOTE {
|
||||
anyhow::bail!(
|
||||
"windows sandbox does not support {context} on mapped network drives: {}. Use the underlying local drive path or run without the Windows sandbox for this session.",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub fn normalize_command_cwd(path: &Path) -> PathBuf {
|
||||
let simplified = dunce::simplified(path).to_path_buf();
|
||||
normalize_mapped_drive_path_with(&simplified, mapped_drive_remote_root).unwrap_or(simplified)
|
||||
}
|
||||
|
||||
fn windows_drive_root(path: &str) -> Option<String> {
|
||||
let bytes = path.as_bytes();
|
||||
if bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
|
||||
return Some(format!("{}:\\", path[..1].to_ascii_uppercase()));
|
||||
fn normalize_mapped_drive_path_with<F>(path: &Path, resolve_remote_root: F) -> Option<PathBuf>
|
||||
where
|
||||
F: Fn(&str) -> Option<String>,
|
||||
{
|
||||
let raw = path.to_string_lossy();
|
||||
let bytes = raw.as_bytes();
|
||||
if bytes.len() < 2 || !bytes[0].is_ascii_alphabetic() || bytes[1] != b':' {
|
||||
return None;
|
||||
}
|
||||
|
||||
let drive = raw[..2].to_ascii_uppercase();
|
||||
let remote_root = resolve_remote_root(&drive)?;
|
||||
let suffix = raw[2..].trim_start_matches(['\\', '/']);
|
||||
let mut normalized = PathBuf::from(remote_root);
|
||||
if !suffix.is_empty() {
|
||||
normalized.push(suffix);
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
|
||||
fn mapped_drive_remote_root(drive: &str) -> Option<String> {
|
||||
let drive_wide = to_wide(drive);
|
||||
let mut len = 260u32;
|
||||
|
||||
loop {
|
||||
let mut buf = vec![0u16; len as usize];
|
||||
let status = unsafe { WNetGetConnectionW(drive_wide.as_ptr(), buf.as_mut_ptr(), &mut len) };
|
||||
match status {
|
||||
NO_ERROR => {
|
||||
let end = buf.iter().position(|ch| *ch == 0).unwrap_or(buf.len());
|
||||
return String::from_utf16(&buf[..end]).ok();
|
||||
}
|
||||
ERROR_MORE_DATA => continue,
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::canonical_path_key;
|
||||
use super::windows_drive_root;
|
||||
use super::normalize_command_cwd;
|
||||
use super::normalize_mapped_drive_path_with;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -67,9 +80,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_drive_root_extracts_drive_prefix() {
|
||||
assert_eq!(windows_drive_root(r"l:\cs-web"), Some(r"L:\".to_string()));
|
||||
assert_eq!(windows_drive_root(r"C:/repo"), Some(r"C:\".to_string()));
|
||||
assert_eq!(windows_drive_root(r"\\video1\node\cs-web"), None);
|
||||
fn mapped_drive_paths_expand_to_unc_roots() {
|
||||
let path = Path::new(r"L:\cs-web\context");
|
||||
let normalized = normalize_mapped_drive_path_with(path, |drive| {
|
||||
(drive == "L:").then(|| r"\\video1\node".to_string())
|
||||
});
|
||||
assert_eq!(
|
||||
normalized,
|
||||
Some(PathBuf::from(r"\\video1\node\cs-web\context"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_paths_are_left_alone() {
|
||||
let path = Path::new(r"C:\Users\Dev\Repo");
|
||||
assert_eq!(
|
||||
normalize_command_cwd(path),
|
||||
PathBuf::from(r"C:\Users\Dev\Repo")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::allow::compute_allow_paths;
|
||||
use crate::helper_materialization::helper_bin_dir;
|
||||
use crate::logging::log_note;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use crate::path_normalization::ensure_windows_sandbox_local_path;
|
||||
use crate::path_normalization::normalize_command_cwd;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::setup_error::SetupErrorCode;
|
||||
use crate::setup_error::SetupFailure;
|
||||
@@ -166,24 +166,30 @@ fn run_setup_refresh_inner(
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
|
||||
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
|
||||
ensure_windows_sandbox_local_path(request.command_cwd, "sandbox working directory")?;
|
||||
for root in &write_roots {
|
||||
ensure_windows_sandbox_local_path(root, "sandbox writable root")?;
|
||||
}
|
||||
for path in &deny_write_paths {
|
||||
ensure_windows_sandbox_local_path(path, "sandbox protected path")?;
|
||||
}
|
||||
let network_identity =
|
||||
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
|
||||
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
|
||||
let normalized_command_cwd = normalize_command_cwd(request.command_cwd);
|
||||
let normalized_request = SandboxSetupRequest {
|
||||
policy: request.policy,
|
||||
policy_cwd: request.policy_cwd,
|
||||
command_cwd: &normalized_command_cwd,
|
||||
env_map: request.env_map,
|
||||
codex_home: request.codex_home,
|
||||
proxy_enforced: request.proxy_enforced,
|
||||
};
|
||||
let (read_roots, write_roots) = build_payload_roots(&normalized_request, &overrides);
|
||||
let deny_write_paths =
|
||||
build_payload_deny_write_paths(&normalized_request, overrides.deny_write_paths);
|
||||
let network_identity = SandboxNetworkIdentity::from_policy(
|
||||
normalized_request.policy,
|
||||
normalized_request.proxy_enforced,
|
||||
);
|
||||
let offline_proxy_settings =
|
||||
offline_proxy_settings_from_env(normalized_request.env_map, network_identity);
|
||||
let payload = ElevationPayload {
|
||||
version: SETUP_VERSION,
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: request.codex_home.to_path_buf(),
|
||||
command_cwd: request.command_cwd.to_path_buf(),
|
||||
codex_home: normalized_request.codex_home.to_path_buf(),
|
||||
command_cwd: normalized_request.command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
write_roots,
|
||||
deny_write_paths,
|
||||
@@ -206,14 +212,14 @@ fn run_setup_refresh_inner(
|
||||
cwd.display(),
|
||||
b64.len()
|
||||
),
|
||||
Some(&sandbox_dir(request.codex_home)),
|
||||
Some(&sandbox_dir(normalized_request.codex_home)),
|
||||
);
|
||||
let status = cmd
|
||||
.status()
|
||||
.map_err(|e| {
|
||||
log_note(
|
||||
&format!("setup refresh: failed to spawn {}: {e}", exe.display()),
|
||||
Some(&sandbox_dir(request.codex_home)),
|
||||
Some(&sandbox_dir(normalized_request.codex_home)),
|
||||
);
|
||||
e
|
||||
})
|
||||
@@ -221,7 +227,7 @@ fn run_setup_refresh_inner(
|
||||
if !status.success() {
|
||||
log_note(
|
||||
&format!("setup refresh: exited with status {status:?}"),
|
||||
Some(&sandbox_dir(request.codex_home)),
|
||||
Some(&sandbox_dir(normalized_request.codex_home)),
|
||||
);
|
||||
return Err(anyhow!("setup refresh failed with status {status}"));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::identity::SandboxCreds;
|
||||
use crate::identity::require_logon_sandbox_creds;
|
||||
use crate::logging::log_start;
|
||||
use crate::path_normalization::canonicalize_path;
|
||||
use crate::path_normalization::ensure_windows_sandbox_local_path;
|
||||
use crate::path_normalization::normalize_command_cwd;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||
@@ -104,7 +104,7 @@ fn prepare_spawn_context_common(
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
|
||||
ensure_windows_sandbox_local_path(cwd, "sandbox working directory")?;
|
||||
let normalized_cwd = normalize_command_cwd(cwd);
|
||||
|
||||
normalize_null_device_env(env_map);
|
||||
ensure_non_interactive_pager(env_map);
|
||||
@@ -112,7 +112,7 @@ fn prepare_spawn_context_common(
|
||||
inherit_path_env(env_map);
|
||||
}
|
||||
if add_git_safe_directory {
|
||||
inject_git_safe_directory(env_map, cwd);
|
||||
inject_git_safe_directory(env_map, &normalized_cwd);
|
||||
}
|
||||
|
||||
ensure_codex_home_exists(codex_home)?;
|
||||
@@ -125,7 +125,7 @@ fn prepare_spawn_context_common(
|
||||
|
||||
Ok(SpawnContext {
|
||||
policy,
|
||||
current_dir: cwd.to_path_buf(),
|
||||
current_dir: normalized_cwd,
|
||||
sandbox_base,
|
||||
logs_base_dir,
|
||||
is_workspace_write,
|
||||
@@ -171,7 +171,8 @@ pub(crate) fn prepare_legacy_session_security(
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let psid_generic = LocalSid::from_string(&caps.workspace)?;
|
||||
let workspace_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
|
||||
let workspace_sid =
|
||||
workspace_cap_sid_for_cwd(codex_home, &normalize_command_cwd(cwd))?;
|
||||
let psid_workspace = LocalSid::from_string(&workspace_sid)?;
|
||||
let base = get_current_token_for_restriction()?;
|
||||
let h_token = create_workspace_write_token_with_caps_from(
|
||||
@@ -285,12 +286,6 @@ pub(crate) fn prepare_elevated_spawn_context(
|
||||
);
|
||||
let write_roots: Vec<PathBuf> = allow.into_iter().collect();
|
||||
let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();
|
||||
for root in &write_roots {
|
||||
ensure_windows_sandbox_local_path(root, "sandbox writable root")?;
|
||||
}
|
||||
for path in &deny_write_paths {
|
||||
ensure_windows_sandbox_local_path(path, "sandbox protected path")?;
|
||||
}
|
||||
let write_roots_override = if common.is_workspace_write {
|
||||
Some(write_roots.as_slice())
|
||||
} else {
|
||||
@@ -299,7 +294,7 @@ pub(crate) fn prepare_elevated_spawn_context(
|
||||
let sandbox_creds = require_logon_sandbox_creds(
|
||||
&common.policy,
|
||||
sandbox_policy_cwd,
|
||||
cwd,
|
||||
&common.current_dir,
|
||||
env_map,
|
||||
codex_home,
|
||||
/*read_roots_override*/ None,
|
||||
@@ -314,7 +309,7 @@ pub(crate) fn prepare_elevated_spawn_context(
|
||||
vec![caps.readonly.clone()],
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let cap_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
|
||||
let cap_sid = workspace_cap_sid_for_cwd(codex_home, &common.current_dir)?;
|
||||
(
|
||||
LocalSid::from_string(&caps.workspace)?,
|
||||
vec![caps.workspace.clone(), cap_sid],
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
|
||||
let spawn_request = SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
cwd: elevated.common.current_dir.clone(),
|
||||
env: env_map.clone(),
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
@@ -55,12 +55,16 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
use_private_desktop,
|
||||
};
|
||||
let codex_home = codex_home.to_path_buf();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let runner_cwd = elevated.common.sandbox_base.clone();
|
||||
let sandbox_creds = elevated.sandbox_creds.clone();
|
||||
let logs_base_dir = elevated.common.logs_base_dir.clone();
|
||||
let transport = tokio::task::spawn_blocking(move || -> Result<_> {
|
||||
let mut transport =
|
||||
spawn_runner_transport(&codex_home, &cwd, &sandbox_creds, logs_base_dir.as_deref())?;
|
||||
let mut transport = spawn_runner_transport(
|
||||
&codex_home,
|
||||
&runner_cwd,
|
||||
&sandbox_creds,
|
||||
logs_base_dir.as_deref(),
|
||||
)?;
|
||||
transport.send_spawn_request(spawn_request)?;
|
||||
transport.read_spawn_ready()?;
|
||||
Ok(transport)
|
||||
|
||||
@@ -300,7 +300,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
if !common.policy.has_full_disk_read_access() {
|
||||
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
|
||||
}
|
||||
let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?;
|
||||
let security =
|
||||
prepare_legacy_session_security(&common.policy, codex_home, &common.current_dir)?;
|
||||
allow_null_device_for_workspace_write(common.is_workspace_write);
|
||||
|
||||
let persist_aces = common.is_workspace_write;
|
||||
@@ -333,7 +334,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
|
||||
} = match spawn_legacy_process(
|
||||
security.h_token,
|
||||
&command,
|
||||
cwd,
|
||||
&common.current_dir,
|
||||
&env_map,
|
||||
use_private_desktop,
|
||||
tty,
|
||||
|
||||
Reference in New Issue
Block a user