mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
• Keep Windows sandbox runner launches working from packaged installs by running the helper from a user-owned runtime location. On some Windows installs, the packaged helper location is difficult to use reliably for sandboxed runner launches even though the binaries are present. This change works around that by copying codex- command-runner.exe into CODEX_HOME/.sandbox-bin/, reusing that copy across launches, and falling back to the existing packaged-path lookup if anything goes wrong. The runtime copy lives in a dedicated directory with tighter ACLs than .sandbox: sandbox users can read and execute the runner there, but they cannot modify it. This keeps the workaround focused on the command runner, leaves the setup helper on its trusted packaged path, and adds logging so it is clear which runner path was selected at launch.
688 lines
22 KiB
Rust
688 lines
22 KiB
Rust
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::ffi::c_void;
|
|
use std::os::windows::process::CommandExt;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use std::process::Stdio;
|
|
|
|
use crate::allow::compute_allow_paths;
|
|
use crate::allow::AllowDenyPaths;
|
|
use crate::helper_materialization::helper_bin_dir;
|
|
use crate::logging::log_note;
|
|
use crate::path_normalization::canonical_path_key;
|
|
use crate::policy::SandboxPolicy;
|
|
use crate::setup_error::clear_setup_error_report;
|
|
use crate::setup_error::failure;
|
|
use crate::setup_error::read_setup_error_report;
|
|
use crate::setup_error::SetupErrorCode;
|
|
use crate::setup_error::SetupFailure;
|
|
use anyhow::anyhow;
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
|
use base64::Engine;
|
|
|
|
use windows_sys::Win32::Foundation::CloseHandle;
|
|
use windows_sys::Win32::Foundation::GetLastError;
|
|
use windows_sys::Win32::Security::AllocateAndInitializeSid;
|
|
use windows_sys::Win32::Security::CheckTokenMembership;
|
|
use windows_sys::Win32::Security::FreeSid;
|
|
use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY;
|
|
|
|
pub const SETUP_VERSION: u32 = 5;
|
|
pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline";
|
|
pub const ONLINE_USERNAME: &str = "CodexSandboxOnline";
|
|
const ERROR_CANCELLED: u32 = 1223;
|
|
const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020;
|
|
const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220;
|
|
const USERPROFILE_READ_ROOT_EXCLUSIONS: &[&str] = &[
|
|
".ssh",
|
|
".gnupg",
|
|
".aws",
|
|
".azure",
|
|
".kube",
|
|
".docker",
|
|
".config",
|
|
".npm",
|
|
".pki",
|
|
".terraform.d",
|
|
];
|
|
|
|
pub fn sandbox_dir(codex_home: &Path) -> PathBuf {
|
|
codex_home.join(".sandbox")
|
|
}
|
|
|
|
pub fn sandbox_bin_dir(codex_home: &Path) -> PathBuf {
|
|
codex_home.join(".sandbox-bin")
|
|
}
|
|
|
|
pub fn sandbox_secrets_dir(codex_home: &Path) -> PathBuf {
|
|
codex_home.join(".sandbox-secrets")
|
|
}
|
|
|
|
pub fn setup_marker_path(codex_home: &Path) -> PathBuf {
|
|
sandbox_dir(codex_home).join("setup_marker.json")
|
|
}
|
|
|
|
pub fn sandbox_users_path(codex_home: &Path) -> PathBuf {
|
|
sandbox_secrets_dir(codex_home).join("sandbox_users.json")
|
|
}
|
|
|
|
pub fn run_setup_refresh(
|
|
policy: &SandboxPolicy,
|
|
policy_cwd: &Path,
|
|
command_cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
codex_home: &Path,
|
|
) -> Result<()> {
|
|
run_setup_refresh_inner(
|
|
policy,
|
|
policy_cwd,
|
|
command_cwd,
|
|
env_map,
|
|
codex_home,
|
|
None,
|
|
None,
|
|
)
|
|
}
|
|
|
|
pub fn run_setup_refresh_with_extra_read_roots(
|
|
policy: &SandboxPolicy,
|
|
policy_cwd: &Path,
|
|
command_cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
codex_home: &Path,
|
|
extra_read_roots: Vec<PathBuf>,
|
|
) -> Result<()> {
|
|
let mut read_roots = gather_read_roots(command_cwd, policy, codex_home);
|
|
read_roots.extend(extra_read_roots);
|
|
run_setup_refresh_inner(
|
|
policy,
|
|
policy_cwd,
|
|
command_cwd,
|
|
env_map,
|
|
codex_home,
|
|
Some(read_roots),
|
|
Some(Vec::new()),
|
|
)
|
|
}
|
|
|
|
fn run_setup_refresh_inner(
|
|
policy: &SandboxPolicy,
|
|
policy_cwd: &Path,
|
|
command_cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
codex_home: &Path,
|
|
read_roots_override: Option<Vec<PathBuf>>,
|
|
write_roots_override: Option<Vec<PathBuf>>,
|
|
) -> Result<()> {
|
|
// Skip in danger-full-access.
|
|
if matches!(
|
|
policy,
|
|
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
|
|
) {
|
|
return Ok(());
|
|
}
|
|
let (read_roots, write_roots) = build_payload_roots(
|
|
policy,
|
|
policy_cwd,
|
|
command_cwd,
|
|
env_map,
|
|
codex_home,
|
|
read_roots_override,
|
|
write_roots_override,
|
|
);
|
|
let payload = ElevationPayload {
|
|
version: SETUP_VERSION,
|
|
offline_username: OFFLINE_USERNAME.to_string(),
|
|
online_username: ONLINE_USERNAME.to_string(),
|
|
codex_home: codex_home.to_path_buf(),
|
|
command_cwd: command_cwd.to_path_buf(),
|
|
read_roots,
|
|
write_roots,
|
|
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
|
refresh_only: true,
|
|
};
|
|
let json = serde_json::to_vec(&payload)?;
|
|
let b64 = BASE64_STANDARD.encode(json);
|
|
let exe = find_setup_exe();
|
|
// Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC.
|
|
let mut cmd = Command::new(&exe);
|
|
cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null());
|
|
let cwd = std::env::current_dir().unwrap_or_else(|_| codex_home.to_path_buf());
|
|
log_note(
|
|
&format!(
|
|
"setup refresh: spawning {} (cwd={}, payload_len={})",
|
|
exe.display(),
|
|
cwd.display(),
|
|
b64.len()
|
|
),
|
|
Some(&sandbox_dir(codex_home)),
|
|
);
|
|
let status = cmd
|
|
.status()
|
|
.map_err(|e| {
|
|
log_note(
|
|
&format!("setup refresh: failed to spawn {}: {e}", exe.display()),
|
|
Some(&sandbox_dir(codex_home)),
|
|
);
|
|
e
|
|
})
|
|
.context("spawn setup refresh")?;
|
|
if !status.success() {
|
|
log_note(
|
|
&format!("setup refresh: exited with status {status:?}"),
|
|
Some(&sandbox_dir(codex_home)),
|
|
);
|
|
return Err(anyhow!("setup refresh failed with status {}", status));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct SetupMarker {
|
|
pub version: u32,
|
|
pub offline_username: String,
|
|
pub online_username: String,
|
|
#[serde(default)]
|
|
pub created_at: Option<String>,
|
|
}
|
|
|
|
impl SetupMarker {
|
|
pub fn version_matches(&self) -> bool {
|
|
self.version == SETUP_VERSION
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct SandboxUserRecord {
|
|
pub username: String,
|
|
/// DPAPI-encrypted password blob, base64 encoded.
|
|
pub password: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct SandboxUsersFile {
|
|
pub version: u32,
|
|
pub offline: SandboxUserRecord,
|
|
pub online: SandboxUserRecord,
|
|
}
|
|
|
|
impl SandboxUsersFile {
|
|
pub fn version_matches(&self) -> bool {
|
|
self.version == SETUP_VERSION
|
|
}
|
|
}
|
|
|
|
fn is_elevated() -> Result<bool> {
|
|
unsafe {
|
|
let mut administrators_group: *mut c_void = std::ptr::null_mut();
|
|
let ok = AllocateAndInitializeSid(
|
|
&SECURITY_NT_AUTHORITY,
|
|
2,
|
|
SECURITY_BUILTIN_DOMAIN_RID,
|
|
DOMAIN_ALIAS_RID_ADMINS,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
&mut administrators_group,
|
|
);
|
|
if ok == 0 {
|
|
return Err(anyhow!(
|
|
"AllocateAndInitializeSid failed: {}",
|
|
GetLastError()
|
|
));
|
|
}
|
|
let mut is_member = 0i32;
|
|
let check = CheckTokenMembership(0, administrators_group, &mut is_member as *mut _);
|
|
FreeSid(administrators_group as *mut _);
|
|
if check == 0 {
|
|
return Err(anyhow!("CheckTokenMembership failed: {}", GetLastError()));
|
|
}
|
|
Ok(is_member != 0)
|
|
}
|
|
}
|
|
|
|
fn canonical_existing(paths: &[PathBuf]) -> Vec<PathBuf> {
|
|
paths
|
|
.iter()
|
|
.filter_map(|p| {
|
|
if !p.exists() {
|
|
return None;
|
|
}
|
|
Some(dunce::canonicalize(p).unwrap_or_else(|_| p.clone()))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn profile_read_roots(user_profile: &Path) -> Vec<PathBuf> {
|
|
let entries = match std::fs::read_dir(user_profile) {
|
|
Ok(entries) => entries,
|
|
Err(_) => return vec![user_profile.to_path_buf()],
|
|
};
|
|
|
|
entries
|
|
.filter_map(Result::ok)
|
|
.map(|entry| (entry.file_name(), entry.path()))
|
|
.filter(|(name, _)| {
|
|
let name = name.to_string_lossy();
|
|
!USERPROFILE_READ_ROOT_EXCLUSIONS
|
|
.iter()
|
|
.any(|excluded| name.eq_ignore_ascii_case(excluded))
|
|
})
|
|
.map(|(_, path)| path)
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn gather_read_roots(
|
|
command_cwd: &Path,
|
|
policy: &SandboxPolicy,
|
|
codex_home: &Path,
|
|
) -> Vec<PathBuf> {
|
|
let mut roots: Vec<PathBuf> = Vec::new();
|
|
if let Ok(exe) = std::env::current_exe() {
|
|
if let Some(dir) = exe.parent() {
|
|
roots.push(dir.to_path_buf());
|
|
}
|
|
}
|
|
let helper_dir = helper_bin_dir(codex_home);
|
|
let _ = std::fs::create_dir_all(&helper_dir);
|
|
roots.push(helper_dir);
|
|
for p in [
|
|
PathBuf::from(r"C:\Windows"),
|
|
PathBuf::from(r"C:\Program Files"),
|
|
PathBuf::from(r"C:\Program Files (x86)"),
|
|
PathBuf::from(r"C:\ProgramData"),
|
|
] {
|
|
roots.push(p);
|
|
}
|
|
if let Ok(up) = std::env::var("USERPROFILE") {
|
|
roots.extend(profile_read_roots(Path::new(&up)));
|
|
}
|
|
roots.push(command_cwd.to_path_buf());
|
|
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
|
|
for root in writable_roots {
|
|
roots.push(root.to_path_buf());
|
|
}
|
|
}
|
|
canonical_existing(&roots)
|
|
}
|
|
|
|
pub(crate) fn gather_write_roots(
|
|
policy: &SandboxPolicy,
|
|
policy_cwd: &Path,
|
|
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 mut dedup: HashSet<PathBuf> = HashSet::new();
|
|
let mut out: Vec<PathBuf> = Vec::new();
|
|
for r in canonical_existing(&roots) {
|
|
if dedup.insert(r.clone()) {
|
|
out.push(r);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct ElevationPayload {
|
|
version: u32,
|
|
offline_username: String,
|
|
online_username: String,
|
|
codex_home: PathBuf,
|
|
command_cwd: PathBuf,
|
|
read_roots: Vec<PathBuf>,
|
|
write_roots: Vec<PathBuf>,
|
|
real_user: String,
|
|
#[serde(default)]
|
|
refresh_only: bool,
|
|
}
|
|
|
|
fn quote_arg(arg: &str) -> String {
|
|
let needs = arg.is_empty()
|
|
|| arg
|
|
.chars()
|
|
.any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"'));
|
|
if !needs {
|
|
return arg.to_string();
|
|
}
|
|
let mut out = String::from("\"");
|
|
let mut bs = 0;
|
|
for ch in arg.chars() {
|
|
match ch {
|
|
'\\' => {
|
|
bs += 1;
|
|
}
|
|
'"' => {
|
|
out.push_str(&"\\".repeat(bs * 2 + 1));
|
|
out.push('"');
|
|
bs = 0;
|
|
}
|
|
_ => {
|
|
if bs > 0 {
|
|
out.push_str(&"\\".repeat(bs));
|
|
bs = 0;
|
|
}
|
|
out.push(ch);
|
|
}
|
|
}
|
|
}
|
|
if bs > 0 {
|
|
out.push_str(&"\\".repeat(bs * 2));
|
|
}
|
|
out.push('"');
|
|
out
|
|
}
|
|
|
|
fn find_setup_exe() -> PathBuf {
|
|
if let Ok(exe) = std::env::current_exe() {
|
|
if let Some(dir) = exe.parent() {
|
|
let candidate = dir.join("codex-windows-sandbox-setup.exe");
|
|
if candidate.exists() {
|
|
return candidate;
|
|
}
|
|
}
|
|
}
|
|
PathBuf::from("codex-windows-sandbox-setup.exe")
|
|
}
|
|
|
|
fn report_helper_failure(
|
|
codex_home: &Path,
|
|
cleared_report: bool,
|
|
exit_code: Option<i32>,
|
|
) -> anyhow::Error {
|
|
let exit_detail = format!("setup helper exited with status {exit_code:?}");
|
|
if !cleared_report {
|
|
return failure(SetupErrorCode::OrchestratorHelperExitNonzero, exit_detail);
|
|
}
|
|
match read_setup_error_report(codex_home) {
|
|
Ok(Some(report)) => anyhow::Error::new(SetupFailure::from_report(report)),
|
|
Ok(None) => failure(SetupErrorCode::OrchestratorHelperExitNonzero, exit_detail),
|
|
Err(err) => failure(
|
|
SetupErrorCode::OrchestratorHelperReportReadFailed,
|
|
format!("{exit_detail}; failed to read setup_error.json: {err}"),
|
|
),
|
|
}
|
|
}
|
|
|
|
fn run_setup_exe(
|
|
payload: &ElevationPayload,
|
|
needs_elevation: bool,
|
|
codex_home: &Path,
|
|
) -> Result<()> {
|
|
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
|
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
|
use windows_sys::Win32::System::Threading::INFINITE;
|
|
use windows_sys::Win32::UI::Shell::ShellExecuteExW;
|
|
use windows_sys::Win32::UI::Shell::SEE_MASK_NOCLOSEPROCESS;
|
|
use windows_sys::Win32::UI::Shell::SHELLEXECUTEINFOW;
|
|
let exe = find_setup_exe();
|
|
let payload_json = serde_json::to_string(payload).map_err(|err| {
|
|
failure(
|
|
SetupErrorCode::OrchestratorPayloadSerializeFailed,
|
|
format!("failed to serialize elevation payload: {err}"),
|
|
)
|
|
})?;
|
|
let payload_b64 = BASE64_STANDARD.encode(payload_json.as_bytes());
|
|
let cleared_report = match clear_setup_error_report(codex_home) {
|
|
Ok(()) => true,
|
|
Err(err) => {
|
|
log_note(
|
|
&format!(
|
|
"setup orchestrator: failed to clear setup_error.json before launch: {err}"
|
|
),
|
|
Some(&sandbox_dir(codex_home)),
|
|
);
|
|
false
|
|
}
|
|
};
|
|
|
|
if !needs_elevation {
|
|
let status = Command::new(&exe)
|
|
.arg(&payload_b64)
|
|
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.map_err(|err| {
|
|
failure(
|
|
SetupErrorCode::OrchestratorHelperLaunchFailed,
|
|
format!("failed to launch setup helper (non-elevated): {err}"),
|
|
)
|
|
})?;
|
|
if !status.success() {
|
|
return Err(report_helper_failure(
|
|
codex_home,
|
|
cleared_report,
|
|
status.code(),
|
|
));
|
|
}
|
|
if let Err(err) = clear_setup_error_report(codex_home) {
|
|
log_note(
|
|
&format!(
|
|
"setup orchestrator: failed to clear setup_error.json after success: {err}"
|
|
),
|
|
Some(&sandbox_dir(codex_home)),
|
|
);
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let exe_w = crate::winutil::to_wide(&exe);
|
|
let params = quote_arg(&payload_b64);
|
|
let params_w = crate::winutil::to_wide(params);
|
|
let verb_w = crate::winutil::to_wide("runas");
|
|
let mut sei: SHELLEXECUTEINFOW = unsafe { std::mem::zeroed() };
|
|
sei.cbSize = std::mem::size_of::<SHELLEXECUTEINFOW>() as u32;
|
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
|
|
sei.lpVerb = verb_w.as_ptr();
|
|
sei.lpFile = exe_w.as_ptr();
|
|
sei.lpParameters = params_w.as_ptr();
|
|
// Hide the window for the elevated helper.
|
|
sei.nShow = 0; // SW_HIDE
|
|
let ok = unsafe { ShellExecuteExW(&mut sei) };
|
|
if ok == 0 || sei.hProcess == 0 {
|
|
let last_error = unsafe { GetLastError() };
|
|
let code = if last_error == ERROR_CANCELLED {
|
|
SetupErrorCode::OrchestratorHelperLaunchCanceled
|
|
} else {
|
|
SetupErrorCode::OrchestratorHelperLaunchFailed
|
|
};
|
|
return Err(failure(
|
|
code,
|
|
format!("ShellExecuteExW failed to launch setup helper: {last_error}"),
|
|
));
|
|
}
|
|
unsafe {
|
|
WaitForSingleObject(sei.hProcess, INFINITE);
|
|
let mut code: u32 = 1;
|
|
GetExitCodeProcess(sei.hProcess, &mut code);
|
|
CloseHandle(sei.hProcess);
|
|
if code != 0 {
|
|
return Err(report_helper_failure(
|
|
codex_home,
|
|
cleared_report,
|
|
Some(code as i32),
|
|
));
|
|
}
|
|
}
|
|
if let Err(err) = clear_setup_error_report(codex_home) {
|
|
log_note(
|
|
&format!("setup orchestrator: failed to clear setup_error.json after success: {err}"),
|
|
Some(&sandbox_dir(codex_home)),
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn run_elevated_setup(
|
|
policy: &SandboxPolicy,
|
|
policy_cwd: &Path,
|
|
command_cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
codex_home: &Path,
|
|
read_roots_override: Option<Vec<PathBuf>>,
|
|
write_roots_override: Option<Vec<PathBuf>>,
|
|
) -> Result<()> {
|
|
// Ensure the shared sandbox directory exists before we send it to the elevated helper.
|
|
let sbx_dir = sandbox_dir(codex_home);
|
|
std::fs::create_dir_all(&sbx_dir).map_err(|err| {
|
|
failure(
|
|
SetupErrorCode::OrchestratorSandboxDirCreateFailed,
|
|
format!("failed to create sandbox dir {}: {err}", sbx_dir.display()),
|
|
)
|
|
})?;
|
|
let (read_roots, write_roots) = build_payload_roots(
|
|
policy,
|
|
policy_cwd,
|
|
command_cwd,
|
|
env_map,
|
|
codex_home,
|
|
read_roots_override,
|
|
write_roots_override,
|
|
);
|
|
let payload = ElevationPayload {
|
|
version: SETUP_VERSION,
|
|
offline_username: OFFLINE_USERNAME.to_string(),
|
|
online_username: ONLINE_USERNAME.to_string(),
|
|
codex_home: codex_home.to_path_buf(),
|
|
command_cwd: command_cwd.to_path_buf(),
|
|
read_roots,
|
|
write_roots,
|
|
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
|
refresh_only: false,
|
|
};
|
|
let needs_elevation = !is_elevated().map_err(|err| {
|
|
failure(
|
|
SetupErrorCode::OrchestratorElevationCheckFailed,
|
|
format!("failed to determine elevation state: {err}"),
|
|
)
|
|
})?;
|
|
run_setup_exe(&payload, needs_elevation, codex_home)
|
|
}
|
|
|
|
fn build_payload_roots(
|
|
policy: &SandboxPolicy,
|
|
policy_cwd: &Path,
|
|
command_cwd: &Path,
|
|
env_map: &HashMap<String, String>,
|
|
codex_home: &Path,
|
|
read_roots_override: Option<Vec<PathBuf>>,
|
|
write_roots_override: Option<Vec<PathBuf>>,
|
|
) -> (Vec<PathBuf>, Vec<PathBuf>) {
|
|
let write_roots = if let Some(roots) = write_roots_override {
|
|
canonical_existing(&roots)
|
|
} else {
|
|
gather_write_roots(policy, policy_cwd, command_cwd, env_map)
|
|
};
|
|
let write_roots = filter_sensitive_write_roots(write_roots, codex_home);
|
|
let mut read_roots = if let Some(roots) = read_roots_override {
|
|
canonical_existing(&roots)
|
|
} else {
|
|
gather_read_roots(command_cwd, policy, codex_home)
|
|
};
|
|
let write_root_set: HashSet<PathBuf> = write_roots.iter().cloned().collect();
|
|
read_roots.retain(|root| !write_root_set.contains(root));
|
|
(read_roots, write_roots)
|
|
}
|
|
|
|
fn filter_sensitive_write_roots(mut roots: Vec<PathBuf>, codex_home: &Path) -> Vec<PathBuf> {
|
|
// Never grant capability write access to CODEX_HOME or anything under CODEX_HOME/.sandbox,
|
|
// CODEX_HOME/.sandbox-bin, or CODEX_HOME/.sandbox-secrets. These locations contain sandbox
|
|
// control/state and helper binaries and must remain tamper-resistant.
|
|
let codex_home_key = canonical_path_key(codex_home);
|
|
let sbx_dir_key = canonical_path_key(&sandbox_dir(codex_home));
|
|
let sbx_dir_prefix = format!("{}/", sbx_dir_key.trim_end_matches('/'));
|
|
let sbx_bin_dir_key = canonical_path_key(&sandbox_bin_dir(codex_home));
|
|
let sbx_bin_dir_prefix = format!("{}/", sbx_bin_dir_key.trim_end_matches('/'));
|
|
let secrets_dir_key = canonical_path_key(&sandbox_secrets_dir(codex_home));
|
|
let secrets_dir_prefix = format!("{}/", secrets_dir_key.trim_end_matches('/'));
|
|
|
|
roots.retain(|root| {
|
|
let key = canonical_path_key(root);
|
|
key != codex_home_key
|
|
&& key != sbx_dir_key
|
|
&& !key.starts_with(&sbx_dir_prefix)
|
|
&& key != sbx_bin_dir_key
|
|
&& !key.starts_with(&sbx_bin_dir_prefix)
|
|
&& key != secrets_dir_key
|
|
&& !key.starts_with(&secrets_dir_prefix)
|
|
});
|
|
roots
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::gather_read_roots;
|
|
use super::profile_read_roots;
|
|
use crate::helper_materialization::helper_bin_dir;
|
|
use crate::policy::SandboxPolicy;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashSet;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn profile_read_roots_excludes_configured_top_level_entries() {
|
|
let tmp = TempDir::new().expect("tempdir");
|
|
let user_profile = tmp.path();
|
|
let allowed_dir = user_profile.join("Documents");
|
|
let allowed_file = user_profile.join(".gitconfig");
|
|
let excluded_dir = user_profile.join(".ssh");
|
|
let excluded_case_variant = user_profile.join(".AWS");
|
|
|
|
fs::create_dir_all(&allowed_dir).expect("create allowed dir");
|
|
fs::write(&allowed_file, "safe").expect("create allowed file");
|
|
fs::create_dir_all(&excluded_dir).expect("create excluded dir");
|
|
fs::create_dir_all(&excluded_case_variant).expect("create excluded case variant");
|
|
|
|
let roots = profile_read_roots(user_profile);
|
|
let actual: HashSet<PathBuf> = roots.into_iter().collect();
|
|
let expected: HashSet<PathBuf> = [allowed_dir, allowed_file].into_iter().collect();
|
|
|
|
assert_eq!(expected, actual);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_read_roots_falls_back_to_profile_root_when_enumeration_fails() {
|
|
let tmp = TempDir::new().expect("tempdir");
|
|
let missing_profile = tmp.path().join("missing-user-profile");
|
|
|
|
let roots = profile_read_roots(&missing_profile);
|
|
|
|
assert_eq!(vec![missing_profile], roots);
|
|
}
|
|
|
|
#[test]
|
|
fn gather_read_roots_includes_helper_bin_dir() {
|
|
let tmp = TempDir::new().expect("tempdir");
|
|
let codex_home = tmp.path().join("codex-home");
|
|
let command_cwd = tmp.path().join("workspace");
|
|
fs::create_dir_all(&command_cwd).expect("create workspace");
|
|
let policy = SandboxPolicy::new_read_only_policy();
|
|
|
|
let roots = gather_read_roots(&command_cwd, &policy, &codex_home);
|
|
let expected =
|
|
dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir");
|
|
|
|
assert!(roots.contains(&expected));
|
|
}
|
|
}
|