mirror of
https://github.com/openai/codex.git
synced 2026-05-16 19:51:15 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56238d2fa2 | ||
|
|
86e11d7697 |
@@ -1,4 +1,5 @@
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use dunce::canonicalize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -11,9 +12,19 @@ pub struct AllowDenyPaths {
|
||||
pub deny: HashSet<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn compute_allow_paths(
|
||||
pub(crate) fn compute_allow_paths(
|
||||
policy: &SandboxPolicy,
|
||||
policy_cwd: &Path,
|
||||
_policy_cwd: &Path,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> AllowDenyPaths {
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, command_cwd);
|
||||
compute_allow_paths_for_permissions(&permissions, command_cwd, env_map)
|
||||
}
|
||||
|
||||
pub(crate) fn compute_allow_paths_for_permissions(
|
||||
permissions: &ResolvedWindowsSandboxPermissions,
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> AllowDenyPaths {
|
||||
@@ -30,65 +41,15 @@ pub fn compute_allow_paths(
|
||||
deny.insert(p);
|
||||
}
|
||||
};
|
||||
let include_tmp_env_vars = matches!(
|
||||
policy,
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
exclude_tmpdir_env_var: false,
|
||||
..
|
||||
}
|
||||
);
|
||||
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
let add_writable_root =
|
||||
|root: PathBuf,
|
||||
policy_cwd: &Path,
|
||||
add_allow: &mut dyn FnMut(PathBuf),
|
||||
add_deny: &mut dyn FnMut(PathBuf)| {
|
||||
let candidate = if root.is_absolute() {
|
||||
root
|
||||
} else {
|
||||
policy_cwd.join(root)
|
||||
};
|
||||
let canonical = canonicalize(&candidate).unwrap_or(candidate);
|
||||
add_allow(canonical.clone());
|
||||
|
||||
for protected_subdir in [".git", ".codex", ".agents"] {
|
||||
let protected_entry = canonical.join(protected_subdir);
|
||||
if protected_entry.exists() {
|
||||
add_deny(protected_entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
add_writable_root(
|
||||
command_cwd.to_path_buf(),
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
|
||||
for root in writable_roots {
|
||||
add_writable_root(
|
||||
root.clone().into(),
|
||||
policy_cwd,
|
||||
&mut add_allow_path,
|
||||
&mut add_deny_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if include_tmp_env_vars {
|
||||
for key in ["TEMP", "TMP"] {
|
||||
if let Some(v) = env_map.get(key) {
|
||||
let abs = PathBuf::from(v);
|
||||
add_allow_path(abs);
|
||||
} else if let Ok(v) = std::env::var(key) {
|
||||
let abs = PathBuf::from(v);
|
||||
add_allow_path(abs);
|
||||
}
|
||||
for writable_root in permissions.writable_roots_for_cwd(command_cwd, env_map) {
|
||||
let canonical = canonicalize(&writable_root.root).unwrap_or(writable_root.root);
|
||||
add_allow_path(canonical);
|
||||
for read_only_subpath in writable_root.read_only_subpaths {
|
||||
add_deny_path(read_only_subpath);
|
||||
}
|
||||
}
|
||||
|
||||
AllowDenyPaths { allow, deny }
|
||||
}
|
||||
|
||||
@@ -146,6 +107,7 @@ mod tests {
|
||||
};
|
||||
let mut env_map = HashMap::new();
|
||||
env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
|
||||
env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
|
||||
|
||||
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
|
||||
|
||||
@@ -162,6 +124,59 @@ mod tests {
|
||||
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn includes_tmp_env_vars_when_requested() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let command_cwd = tmp.path().join("workspace");
|
||||
let temp_dir = tmp.path().join("temp");
|
||||
let _ = fs::create_dir_all(&command_cwd);
|
||||
let _ = fs::create_dir_all(&temp_dir);
|
||||
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
let mut env_map = HashMap::new();
|
||||
env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
|
||||
env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
|
||||
|
||||
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
|
||||
|
||||
let expected_allow: HashSet<PathBuf> = [
|
||||
dunce::canonicalize(&command_cwd).unwrap(),
|
||||
dunce::canonicalize(&temp_dir).unwrap(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
assert_eq!(expected_allow, paths.allow);
|
||||
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unix_slash_tmp_for_windows_allow_roots() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
let command_cwd = tmp.path().join("workspace");
|
||||
let _ = fs::create_dir_all(&command_cwd);
|
||||
|
||||
let policy = SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: false,
|
||||
};
|
||||
|
||||
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
|
||||
let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
assert_eq!(expected_allow, paths.allow);
|
||||
assert!(paths.deny.is_empty(), "no deny paths expected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn denies_git_dir_inside_writable_root() {
|
||||
let tmp = TempDir::new().expect("tempdir");
|
||||
|
||||
@@ -22,11 +22,11 @@ use codex_windows_sandbox::OutputPayload;
|
||||
use codex_windows_sandbox::OutputStream;
|
||||
use codex_windows_sandbox::PipeSpawnHandles;
|
||||
use codex_windows_sandbox::ResizePayload;
|
||||
use codex_windows_sandbox::SandboxPolicy;
|
||||
use codex_windows_sandbox::SpawnReady;
|
||||
use codex_windows_sandbox::SpawnRequest;
|
||||
use codex_windows_sandbox::StderrMode;
|
||||
use codex_windows_sandbox::StdinMode;
|
||||
use codex_windows_sandbox::WindowsSandboxTokenMode;
|
||||
use codex_windows_sandbox::allow_null_device;
|
||||
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_caps_and_user_from;
|
||||
@@ -35,11 +35,11 @@ use codex_windows_sandbox::encode_bytes;
|
||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||
use codex_windows_sandbox::hide_current_user_profile_dir;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::parse_policy;
|
||||
use codex_windows_sandbox::read_frame;
|
||||
use codex_windows_sandbox::read_handle_loop;
|
||||
use codex_windows_sandbox::spawn_process_with_pipes;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::token_mode_for_permission_profile;
|
||||
use codex_windows_sandbox::write_frame;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::File;
|
||||
@@ -235,7 +235,12 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
|
||||
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
|
||||
let log_dir = req.codex_home.clone();
|
||||
hide_current_user_profile_dir(req.codex_home.as_path());
|
||||
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
|
||||
let token_mode = token_mode_for_permission_profile(
|
||||
&req.permission_profile,
|
||||
&req.permission_profile_cwd,
|
||||
&req.env,
|
||||
)
|
||||
.context("resolve permission profile token mode")?;
|
||||
let mut cap_psids: Vec<LocalSid> = Vec::new();
|
||||
for sid in &req.cap_sids {
|
||||
cap_psids.push(
|
||||
@@ -253,16 +258,13 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
|
||||
let cap_psid_ptrs: Vec<*mut _> = cap_psids.iter().map(LocalSid::as_ptr).collect();
|
||||
let base = OwnedWinHandle::new(unsafe { get_current_token_for_restriction()? });
|
||||
let h_token = OwnedWinHandle::new(unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly { .. } => {
|
||||
match token_mode {
|
||||
WindowsSandboxTokenMode::ReadOnlyCapability => {
|
||||
create_readonly_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
WindowsSandboxTokenMode::WriteCapabilityRoots => {
|
||||
create_workspace_write_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}?);
|
||||
unsafe {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -55,8 +56,8 @@ pub struct SpawnRequest {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub env: HashMap<String, String>,
|
||||
pub policy_json_or_preset: String,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
pub permission_profile: PermissionProfile,
|
||||
pub permission_profile_cwd: PathBuf,
|
||||
pub codex_home: PathBuf,
|
||||
pub real_codex_home: PathBuf,
|
||||
pub cap_sids: Vec<String>,
|
||||
@@ -164,6 +165,7 @@ pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn framed_round_trip() {
|
||||
@@ -189,4 +191,43 @@ mod tests {
|
||||
other => panic!("unexpected message: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_request_serializes_permission_profile() {
|
||||
let msg = FramedMessage {
|
||||
version: 1,
|
||||
message: Message::SpawnRequest {
|
||||
payload: Box::new(SpawnRequest {
|
||||
command: vec!["cmd.exe".to_string(), "/c".to_string(), "ver".to_string()],
|
||||
cwd: PathBuf::from(r"C:\workspace"),
|
||||
env: HashMap::new(),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
permission_profile_cwd: PathBuf::from(r"C:\workspace"),
|
||||
codex_home: PathBuf::from(r"C:\codex"),
|
||||
real_codex_home: PathBuf::from(r"C:\Users\codex"),
|
||||
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
|
||||
timeout_ms: Some(1000),
|
||||
tty: false,
|
||||
stdin_open: false,
|
||||
use_private_desktop: false,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_value(&msg).expect("serialize");
|
||||
assert_eq!("spawn_request", encoded["type"]);
|
||||
assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]);
|
||||
assert_eq!(None, encoded["payload"].get("policy_json_or_preset"));
|
||||
assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd"));
|
||||
|
||||
let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize");
|
||||
let Message::SpawnRequest { payload } = decoded.message else {
|
||||
panic!("unexpected message");
|
||||
};
|
||||
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
|
||||
assert_eq!(
|
||||
PathBuf::from(r"C:\workspace"),
|
||||
payload.permission_profile_cwd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ mod windows_impl {
|
||||
use crate::setup::effective_write_roots_for_setup;
|
||||
use crate::token::LocalSid;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -144,12 +145,14 @@ mod windows_impl {
|
||||
}
|
||||
|
||||
(|| -> Result<CaptureResult> {
|
||||
let permission_profile =
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
|
||||
let spawn_request = SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: env_map.clone(),
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
permission_profile,
|
||||
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids,
|
||||
|
||||
@@ -38,6 +38,8 @@ mod policy;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod process;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod resolved_permissions;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod token;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wfp;
|
||||
@@ -195,6 +197,10 @@ pub use process::read_handle_loop;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use process::spawn_process_with_pipes;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use resolved_permissions::WindowsSandboxTokenMode;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use resolved_permissions::token_mode_for_permission_profile;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SETUP_VERSION;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SandboxSetupRequest;
|
||||
|
||||
298
codex-rs/windows-sandbox-rs/src/resolved_permissions.rs
Normal file
298
codex-rs/windows-sandbox-rs/src/resolved_permissions.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
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_protocol::protocol::SandboxPolicy;
|
||||
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(crate) 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,
|
||||
WriteCapabilityRoots,
|
||||
}
|
||||
|
||||
/// Chooses the restricted-token family needed for a managed permission profile.
|
||||
pub fn token_mode_for_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Result<WindowsSandboxTokenMode> {
|
||||
let permissions =
|
||||
ResolvedWindowsSandboxPermissions::try_from_permission_profile(permission_profile)?;
|
||||
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::WriteCapabilityRoots)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolvedWindowsSandboxPermissions {
|
||||
pub(crate) fn from_legacy_policy(policy: &SandboxPolicy) -> Self {
|
||||
Self {
|
||||
file_system: FileSystemSandboxPolicy::from(policy),
|
||||
network: NetworkSandboxPolicy::from(policy),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self {
|
||||
Self {
|
||||
file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd),
|
||||
network: NetworkSandboxPolicy::from(policy),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn should_apply_network_block(&self) -> bool {
|
||||
!self.network.is_enabled()
|
||||
}
|
||||
|
||||
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 pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[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 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 token_mode = token_mode_for_permission_profile(
|
||||
&PermissionProfile::read_only(),
|
||||
&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 token_mode = token_mode_for_permission_profile(
|
||||
&PermissionProfile::workspace_write(),
|
||||
&cwd,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.expect("token mode");
|
||||
|
||||
assert_eq!(WindowsSandboxTokenMode::WriteCapabilityRoots, 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 err = token_mode_for_permission_profile(&permission_profile, &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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -395,14 +395,9 @@ pub(crate) fn gather_write_roots(
|
||||
command_cwd: &Path,
|
||||
env_map: &HashMap<String, String>,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut roots: Vec<PathBuf> = Vec::new();
|
||||
// Always include the command CWD for workspace-write.
|
||||
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
roots.push(command_cwd.to_path_buf());
|
||||
}
|
||||
let AllowDenyPaths { allow, .. } =
|
||||
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
|
||||
roots.extend(allow);
|
||||
let roots: Vec<PathBuf> = allow.into_iter().collect();
|
||||
let mut dedup: HashSet<PathBuf> = HashSet::new();
|
||||
let mut out: Vec<PathBuf> = Vec::new();
|
||||
for r in canonical_existing(&roots) {
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::logging::log_start;
|
||||
use crate::path_normalization::canonicalize_path;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::policy::parse_policy;
|
||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||
use crate::sandbox_utils::inject_git_safe_directory;
|
||||
use crate::setup::effective_write_roots_for_setup;
|
||||
@@ -74,7 +75,7 @@ pub(crate) struct LegacyAclSids<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
|
||||
!policy.has_full_network_access()
|
||||
ResolvedWindowsSandboxPermissions::from_legacy_policy(policy).should_apply_network_block()
|
||||
}
|
||||
|
||||
fn prepare_spawn_context_common(
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::ipc_framed::SpawnRequest;
|
||||
use crate::runner_client::spawn_runner_transport;
|
||||
use crate::spawn_prep::prepare_elevated_spawn_context;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use codex_utils_pty::SpawnedProcess;
|
||||
@@ -60,12 +61,16 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
|
||||
&deny_write_paths_override,
|
||||
)?;
|
||||
|
||||
let permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&elevated.common.policy,
|
||||
sandbox_policy_cwd,
|
||||
);
|
||||
let spawn_request = SpawnRequest {
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: env_map.clone(),
|
||||
policy_json_or_preset: policy_json_or_preset.to_string(),
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
permission_profile,
|
||||
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: elevated.common.sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sids: elevated.cap_sids.clone(),
|
||||
|
||||
Reference in New Issue
Block a user