Files
codex/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs
T
Michael BolinandGitHub 986c60467b windows-sandbox: pass workspace roots to runner (#24108)
## Why

#23813 switches the Windows sandbox runner path to `PermissionProfile`,
but it still left one runtime anchor for resolving symbolic
`:workspace_roots` entries. That is not enough once a turn has multiple
effective workspace roots: exact entries and deny globs under
`:workspace_roots` need to be materialized for every runtime root before
the command runner chooses token mode or builds ACL plans.

## What Changed

- Replaces the Windows runner/setup `permission_profile_cwd` plumbing
with `workspace_roots: Vec<AbsolutePathBuf>`.
- Resolves Windows-local `PermissionProfile` data with
`materialize_project_roots_with_workspace_roots(...)` instead of the
single-cwd helper.
- Threads `Config::effective_workspace_roots()` through core execution,
unified exec, TUI setup/read-grant flows, app-server setup, app-server
`command/exec`, and `debug sandbox` on Windows.
- Preserves those workspace roots through the zsh-fork escalation
executor instead of rebuilding them from `sandbox_policy_cwd`.
- Makes `ExecRequest::new(...)` and the remaining
`build_exec_request(...)` helper path take
`windows_sandbox_workspace_roots` explicitly so new call sites cannot
silently fall back to `vec![cwd]`.
- Clarifies the `debug sandbox` non-Windows comment: remaining
cwd-dependent resolution still uses `sandbox_policy_cwd`, while
`:workspace_roots` entries are already materialized from config roots.
- Updates elevated runner IPC `SpawnRequest` to send `workspace_roots`
and bumps the framed IPC protocol version to `3` for the payload shape
change.
- Adds Windows-local resolver coverage for expanding exact and glob
`:workspace_roots` entries across multiple roots, plus core helper
coverage proving explicit roots are preserved.

## Verification

- `cargo check -p codex-windows-sandbox -p codex-core -p codex-tui -p
codex-cli -p codex-app-server`
- `cargo test -p codex-windows-sandbox`
- `cargo test -p codex-core windows_sandbox`
- `cargo test -p codex-core unix_escalation`
- `cargo test -p codex-app-server windows_sandbox`
- `cargo test -p codex-tui windows_sandbox`
- `cargo test -p codex-cli debug_sandbox`
- `just test -p codex-core unified_exec`
- `just test -p codex-core
build_exec_request_preserves_windows_workspace_roots`
- `env -u CODEX_NETWORK_PROXY_ACTIVE -u
CODEX_NETWORK_ALLOW_LOCAL_BINDING just test -p codex-app-server --lib
command_exec`
- `just test -p codex-windows-sandbox`
- `just test -p codex-exec sandbox`
- `just fix -p codex-core -p codex-app-server -p codex-windows-sandbox`

A local macOS cross-check with `cargo check --target
x86_64-pc-windows-msvc ...` did not reach crate Rust code because native
dependencies require Windows SDK headers (`windows.h` / `assert.h`) in
this environment; Windows CI remains the real target validation.

Two local targeted filters compile but do not run assertions on macOS:
`env -u CODEX_NETWORK_PROXY_ACTIVE -u CODEX_NETWORK_ALLOW_LOCAL_BINDING
just test -p codex-app-server --lib command_exec_processor` matched zero
tests, and `just test -p codex-linux-sandbox landlock` matched zero
tests because the landlock suite is Linux-only.
2026-05-28 15:26:55 -07:00

479 lines
17 KiB
Rust

use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
/// Windows-local view of the runtime permission profile.
///
/// Most Windows sandbox code needs resolved runtime permissions plus a few
/// Windows-specific path conventions, not the user/config-facing
/// `PermissionProfile` enum itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedWindowsSandboxPermissions {
file_system: FileSystemSandboxPolicy,
network: NetworkSandboxPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WindowsWritableRoot {
pub(crate) root: PathBuf,
pub(crate) read_only_subpaths: Vec<PathBuf>,
}
/// Restricted-token family needed to enforce a Windows permission profile.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowsSandboxTokenMode {
ReadOnlyCapability,
WritableRootsCapability,
}
/// Chooses the restricted-token family needed for a managed permission profile.
pub fn token_mode_for_permission_profile(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<WindowsSandboxTokenMode> {
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
if permissions.file_system.has_full_disk_write_access() {
anyhow::bail!(
"permission profile requests full-disk filesystem writes, which cannot be enforced by the Windows sandbox"
);
}
if permissions.writable_roots_for_cwd(cwd, env_map).is_empty() {
Ok(WindowsSandboxTokenMode::ReadOnlyCapability)
} else {
Ok(WindowsSandboxTokenMode::WritableRootsCapability)
}
}
impl ResolvedWindowsSandboxPermissions {
pub fn try_from_permission_profile(permission_profile: &PermissionProfile) -> Result<Self> {
if !matches!(permission_profile, PermissionProfile::Managed { .. }) {
anyhow::bail!(
"only managed permission profiles can be enforced by the Windows sandbox"
);
}
let (file_system, network) = permission_profile.to_runtime_permissions();
if !matches!(file_system.kind, FileSystemSandboxKind::Restricted) {
anyhow::bail!(
"only restricted managed filesystem permissions can be enforced by the Windows sandbox"
);
}
Ok(Self {
file_system,
network,
})
}
/// Resolves a managed permission profile and binds symbolic `:workspace_roots`
/// entries to the workspace roots supplied by the caller.
pub fn try_from_permission_profile_for_workspace_roots(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
) -> Result<Self> {
let mut permissions = Self::try_from_permission_profile(permission_profile)?;
permissions.file_system = permissions
.file_system
.materialize_project_roots_with_workspace_roots(workspace_roots);
Ok(permissions)
}
pub(crate) fn should_apply_network_block(&self) -> bool {
!self.network.is_enabled()
}
pub(crate) fn network_policy(&self) -> NetworkSandboxPolicy {
self.network
}
pub(crate) fn is_enforceable_by_windows_sandbox(&self) -> bool {
matches!(self.file_system.kind, FileSystemSandboxKind::Restricted)
}
pub(crate) fn has_full_disk_read_access(&self) -> bool {
self.file_system.has_full_disk_read_access()
}
pub(crate) fn include_platform_defaults(&self) -> bool {
self.file_system.include_platform_defaults()
}
pub(crate) fn readable_roots_for_cwd(&self, cwd: &Path) -> Vec<PathBuf> {
self.file_system
.get_readable_roots_with_cwd(cwd)
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect()
}
pub(crate) fn uses_write_capabilities_for_cwd(
&self,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> bool {
!self.writable_roots_for_cwd(cwd, env_map).is_empty()
}
pub(crate) fn writable_roots_for_cwd(
&self,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Vec<WindowsWritableRoot> {
let mut file_system = self.file_system.clone();
file_system
.entries
.retain(|FileSystemSandboxEntry { path, .. }| {
!matches!(
path,
FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Tmpdir
| codex_protocol::permissions::FileSystemSpecialPath::SlashTmp,
}
)
});
let mut roots = file_system
.get_writable_roots_with_cwd(cwd)
.into_iter()
.map(|root| WindowsWritableRoot {
root: root.root.into_path_buf(),
read_only_subpaths: root
.read_only_subpaths
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect(),
})
.collect::<Vec<_>>();
if self.has_writable_tmpdir_entry() {
roots.extend(windows_temp_env_roots(env_map).into_iter().map(|root| {
WindowsWritableRoot {
root,
read_only_subpaths: Vec::new(),
}
}));
}
roots
}
fn has_writable_tmpdir_entry(&self) -> bool {
self.file_system
.entries
.iter()
.any(|FileSystemSandboxEntry { path, access }| {
matches!(
path,
FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Tmpdir,
}
) && access.can_write()
})
}
}
fn windows_temp_env_roots(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
["TEMP", "TMP"]
.into_iter()
.filter_map(|key| {
env_map
.get(key)
.map(|value| PathBuf::from(value.as_str()))
.or_else(|| std::env::var_os(key).map(PathBuf::from))
})
.filter(|path| path.is_absolute())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::project_roots_glob_pattern;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
#[test]
fn permission_profile_workspace_write_uses_windows_temp_env_vars() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
let temp_dir = tmp.path().join("temp");
std::fs::create_dir_all(&cwd).expect("create cwd");
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
let mut env_map = HashMap::new();
env_map.insert("TEMP".to_string(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".to_string(), temp_dir.to_string_lossy().to_string());
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
&PermissionProfile::workspace_write(),
)
.expect("managed permission profile");
let roots = permissions
.writable_roots_for_cwd(&cwd, &env_map)
.into_iter()
.map(|root| root.root)
.collect::<std::collections::HashSet<_>>();
let expected_roots = [
temp_dir,
dunce::canonicalize(&cwd).expect("canonicalize cwd"),
]
.into_iter()
.collect::<std::collections::HashSet<_>>();
assert_eq!(expected_roots, roots);
}
#[test]
fn permission_profile_workspace_root_uses_runtime_workspace_roots() {
let tmp = TempDir::new().expect("tempdir");
let workspace_root = tmp.path().join("workspace");
let command_cwd = workspace_root.join("subdir");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
.expect("managed permission profile");
let roots = permissions
.writable_roots_for_cwd(&command_cwd, &HashMap::new())
.into_iter()
.map(|root| root.root)
.collect::<Vec<_>>();
assert_eq!(
roots,
vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")]
);
}
#[test]
fn permission_profile_workspace_roots_expand_all_runtime_workspace_roots() {
let tmp = TempDir::new().expect("tempdir");
let first = AbsolutePathBuf::from_absolute_path(tmp.path().join("first"))
.expect("absolute first root");
let second = AbsolutePathBuf::from_absolute_path(tmp.path().join("second"))
.expect("absolute second root");
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(Some(".git".into())),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
},
access: FileSystemAccessMode::Deny,
},
],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
&[first.clone(), second.clone()],
)
.expect("managed permission profile");
assert_eq!(
permissions.file_system,
FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: first.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: second.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: first.join(".git"),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: second.join(".git"),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
first.as_path(),
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::Deny,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
second.as_path(),
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::Deny,
},
])
);
}
#[test]
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let workspace_roots = workspace_roots_for(cwd.as_path());
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::read_only(),
workspace_roots.as_slice(),
&cwd,
&HashMap::new(),
)
.expect("token mode");
assert_eq!(WindowsSandboxTokenMode::ReadOnlyCapability, token_mode);
}
#[test]
fn token_mode_for_profile_with_writable_roots_uses_write_capabilities() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let workspace_roots = workspace_roots_for(cwd.as_path());
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::workspace_write(),
workspace_roots.as_slice(),
&cwd,
&HashMap::new(),
)
.expect("token mode");
assert_eq!(WindowsSandboxTokenMode::WritableRootsCapability, token_mode);
}
#[test]
fn permission_profile_rejects_disabled_profiles() {
let err = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
&PermissionProfile::Disabled,
)
.expect_err("disabled profile should not resolve for sandbox enforcement");
assert!(
err.to_string()
.contains("only managed permission profiles can be enforced")
);
}
#[test]
fn permission_profile_rejects_unrestricted_managed_filesystem() {
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
};
let err =
ResolvedWindowsSandboxPermissions::try_from_permission_profile(&permission_profile)
.expect_err("unrestricted profile should not resolve for sandbox enforcement");
assert!(
err.to_string()
.contains("only restricted managed filesystem permissions can be enforced")
);
}
#[test]
fn token_mode_rejects_full_disk_write_entries() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let workspace_roots = workspace_roots_for(cwd.as_path());
let err = token_mode_for_permission_profile(
&permission_profile,
workspace_roots.as_slice(),
&cwd,
&HashMap::new(),
)
.expect_err("full disk writes should not resolve to a token mode");
assert!(
err.to_string()
.contains("full-disk filesystem writes, which cannot be enforced")
);
}
}