mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
elevated setup smoke test.
This commit is contained in:
@@ -24,14 +24,14 @@ use windows::Win32::System::Com::COINIT_APARTMENTTHREADED;
|
||||
use codex_windows_sandbox::SetupErrorCode;
|
||||
use codex_windows_sandbox::SetupFailure;
|
||||
|
||||
// This is the stable identifier we use to find/update the rule idempotently.
|
||||
// It intentionally does not change between installs.
|
||||
const OFFLINE_BLOCK_RULE_NAME: &str = "codex_sandbox_offline_block_outbound";
|
||||
|
||||
// Friendly text shown in the firewall UI.
|
||||
const OFFLINE_BLOCK_RULE_FRIENDLY: &str = "Codex Sandbox Offline - Block Outbound";
|
||||
|
||||
pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Result<()> {
|
||||
pub fn ensure_offline_outbound_block(
|
||||
offline_sid: &str,
|
||||
offline_block_rule_name: &str,
|
||||
log: &mut File,
|
||||
) -> Result<()> {
|
||||
let local_user_spec = format!("O:LSD:(A;;CC;;;{offline_sid})");
|
||||
|
||||
let hr = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
|
||||
@@ -61,7 +61,7 @@ pub fn ensure_offline_outbound_block(offline_sid: &str, log: &mut File) -> Resul
|
||||
// Block all outbound IP protocols for this user.
|
||||
ensure_block_rule(
|
||||
&rules,
|
||||
OFFLINE_BLOCK_RULE_NAME,
|
||||
offline_block_rule_name,
|
||||
OFFLINE_BLOCK_RULE_FRIENDLY,
|
||||
NET_FW_IP_PROTOCOL_ANY.0,
|
||||
&local_user_spec,
|
||||
|
||||
@@ -81,12 +81,16 @@ pub use process::create_process_as_user;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_elevated_setup;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_elevated_setup_with_identity_overrides;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::run_setup_refresh;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::sandbox_dir;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::sandbox_secrets_dir;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SetupIdentityOverrides;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup::SETUP_VERSION;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use setup_error::extract_failure as extract_setup_failure;
|
||||
|
||||
@@ -78,6 +78,8 @@ struct Payload {
|
||||
version: u32,
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
#[serde(default = "default_offline_block_rule_name")]
|
||||
offline_block_rule_name: String,
|
||||
codex_home: PathBuf,
|
||||
command_cwd: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
@@ -97,6 +99,10 @@ enum SetupMode {
|
||||
ReadAclsOnly,
|
||||
}
|
||||
|
||||
fn default_offline_block_rule_name() -> String {
|
||||
"codex_sandbox_offline_block_outbound".to_string()
|
||||
}
|
||||
|
||||
fn log_line(log: &mut File, msg: &str) -> Result<()> {
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
writeln!(log, "[{ts}] {msg}").map_err(|err| {
|
||||
@@ -572,7 +578,11 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
};
|
||||
let mut refresh_errors: Vec<String> = Vec::new();
|
||||
if !refresh_only {
|
||||
let firewall_result = firewall::ensure_offline_outbound_block(&offline_sid_str, log);
|
||||
let firewall_result = firewall::ensure_offline_outbound_block(
|
||||
&offline_sid_str,
|
||||
&payload.offline_block_rule_name,
|
||||
log,
|
||||
);
|
||||
if let Err(err) = firewall_result {
|
||||
if extract_setup_failure(&err).is_some() {
|
||||
return Err(err);
|
||||
|
||||
@@ -35,10 +35,18 @@ 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";
|
||||
pub const OFFLINE_BLOCK_RULE_NAME: &str = "codex_sandbox_offline_block_outbound";
|
||||
const ERROR_CANCELLED: u32 = 1223;
|
||||
const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020;
|
||||
const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SetupIdentityOverrides {
|
||||
pub offline_username: Option<String>,
|
||||
pub online_username: Option<String>,
|
||||
pub offline_block_rule_name: Option<String>,
|
||||
}
|
||||
|
||||
pub fn sandbox_dir(codex_home: &Path) -> PathBuf {
|
||||
codex_home.join(".sandbox")
|
||||
}
|
||||
@@ -82,6 +90,7 @@ pub fn run_setup_refresh(
|
||||
version: SETUP_VERSION,
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
offline_block_rule_name: OFFLINE_BLOCK_RULE_NAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
command_cwd: command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
@@ -260,6 +269,7 @@ struct ElevationPayload {
|
||||
version: u32,
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
offline_block_rule_name: String,
|
||||
codex_home: PathBuf,
|
||||
command_cwd: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
@@ -455,6 +465,28 @@ pub fn run_elevated_setup(
|
||||
codex_home: &Path,
|
||||
read_roots_override: Option<Vec<PathBuf>>,
|
||||
write_roots_override: Option<Vec<PathBuf>>,
|
||||
) -> Result<()> {
|
||||
run_elevated_setup_with_identity_overrides(
|
||||
policy,
|
||||
policy_cwd,
|
||||
command_cwd,
|
||||
env_map,
|
||||
codex_home,
|
||||
read_roots_override,
|
||||
write_roots_override,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn run_elevated_setup_with_identity_overrides(
|
||||
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>>,
|
||||
identity_overrides: Option<&SetupIdentityOverrides>,
|
||||
) -> Result<()> {
|
||||
// Ensure the shared sandbox directory exists before we send it to the elevated helper.
|
||||
let sbx_dir = sandbox_dir(codex_home);
|
||||
@@ -475,8 +507,15 @@ pub fn run_elevated_setup(
|
||||
);
|
||||
let payload = ElevationPayload {
|
||||
version: SETUP_VERSION,
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
offline_username: identity_overrides
|
||||
.and_then(|o| o.offline_username.clone())
|
||||
.unwrap_or_else(|| OFFLINE_USERNAME.to_string()),
|
||||
online_username: identity_overrides
|
||||
.and_then(|o| o.online_username.clone())
|
||||
.unwrap_or_else(|| ONLINE_USERNAME.to_string()),
|
||||
offline_block_rule_name: identity_overrides
|
||||
.and_then(|o| o.offline_block_rule_name.clone())
|
||||
.unwrap_or_else(|| OFFLINE_BLOCK_RULE_NAME.to_string()),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
command_cwd: command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
|
||||
3
codex-rs/windows-sandbox-rs/tests/all.rs
Normal file
3
codex-rs/windows-sandbox-rs/tests/all.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod suite;
|
||||
193
codex-rs/windows-sandbox-rs/tests/suite/elevated_setup_smoke.rs
Normal file
193
codex-rs/windows-sandbox-rs/tests/suite/elevated_setup_smoke.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_windows_sandbox::run_elevated_setup_with_identity_overrides;
|
||||
use codex_windows_sandbox::sandbox_setup_is_complete;
|
||||
use codex_windows_sandbox::SetupIdentityOverrides;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
struct CleanupGuard {
|
||||
usernames: Vec<String>,
|
||||
firewall_rule_name: String,
|
||||
}
|
||||
|
||||
impl Drop for CleanupGuard {
|
||||
fn drop(&mut self) {
|
||||
cleanup_global_sandbox_artifacts(&self.usernames, &self.firewall_rule_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SandboxUsersFile {
|
||||
offline: SandboxUserRecord,
|
||||
online: SandboxUserRecord,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SandboxUserRecord {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevated_setup_creates_both_sandbox_users() -> Result<()> {
|
||||
let suffix = unique_suffix();
|
||||
let offline_username = format!("CdxSbxOf{suffix}");
|
||||
let online_username = format!("CdxSbxOn{suffix}");
|
||||
let firewall_rule_name = format!("codex_sbx_test_{suffix}");
|
||||
|
||||
let pre_users = vec![offline_username.clone(), online_username.clone()];
|
||||
cleanup_global_sandbox_artifacts(&pre_users, &firewall_rule_name);
|
||||
|
||||
let mut guard = CleanupGuard {
|
||||
usernames: Vec::new(),
|
||||
firewall_rule_name: firewall_rule_name.clone(),
|
||||
};
|
||||
|
||||
ensure_setup_helper_on_path()?;
|
||||
|
||||
let temp = tempfile::tempdir().context("create temp dir")?;
|
||||
let codex_home = temp.path().join("codex-home");
|
||||
let workspace = temp.path().join("workspace");
|
||||
std::fs::create_dir_all(&codex_home).context("create codex_home")?;
|
||||
std::fs::create_dir_all(&workspace).context("create workspace")?;
|
||||
|
||||
let policy = SandboxPolicy::ReadOnly;
|
||||
let env_map = HashMap::new();
|
||||
let overrides = SetupIdentityOverrides {
|
||||
offline_username: Some(offline_username),
|
||||
online_username: Some(online_username),
|
||||
offline_block_rule_name: Some(firewall_rule_name),
|
||||
};
|
||||
run_elevated_setup_with_identity_overrides(
|
||||
&policy,
|
||||
workspace.as_path(),
|
||||
workspace.as_path(),
|
||||
&env_map,
|
||||
codex_home.as_path(),
|
||||
None,
|
||||
None,
|
||||
Some(&overrides),
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
sandbox_setup_is_complete(codex_home.as_path()),
|
||||
"sandbox setup should be complete after elevated setup runs"
|
||||
);
|
||||
|
||||
let users = read_sandbox_users_file(codex_home.as_path())?;
|
||||
guard.usernames = vec![users.offline.username.clone(), users.online.username.clone()];
|
||||
assert_local_user_exists(&users.offline.username)?;
|
||||
assert_local_user_exists(&users.online.username)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_sandbox_users_file(codex_home: &Path) -> Result<SandboxUsersFile> {
|
||||
let path = codex_home
|
||||
.join(".sandbox-secrets")
|
||||
.join("sandbox_users.json");
|
||||
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
let users: SandboxUsersFile = serde_json::from_slice(&bytes)
|
||||
.with_context(|| format!("parse {}", path.display()))?;
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
fn assert_local_user_exists(username: &str) -> Result<()> {
|
||||
let status = Command::new("net")
|
||||
.args(["user", username])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.with_context(|| format!("query local user {username}"))?;
|
||||
if status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
anyhow::bail!("expected local user {username} to exist")
|
||||
}
|
||||
|
||||
fn ensure_setup_helper_on_path() -> Result<()> {
|
||||
if let Some(helper) = helper_from_env() {
|
||||
let helper_dir = Path::new(&helper)
|
||||
.parent()
|
||||
.context("setup helper path has no parent dir")?;
|
||||
prepend_to_path(helper_dir)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(helper_dir) = helper_dir_from_target_layout() {
|
||||
prepend_to_path(&helper_dir)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
anyhow::bail!(
|
||||
"setup helper path was not found via CARGO_BIN_EXE_* or target/debug layout"
|
||||
)
|
||||
}
|
||||
|
||||
fn helper_from_env() -> Option<String> {
|
||||
for key in [
|
||||
"CARGO_BIN_EXE_codex-windows-sandbox-setup",
|
||||
"CARGO_BIN_EXE_codex_windows_sandbox_setup",
|
||||
] {
|
||||
if let Ok(path) = std::env::var(key) {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn helper_dir_from_target_layout() -> Option<PathBuf> {
|
||||
let current_exe = std::env::current_exe().ok()?;
|
||||
let deps_dir = current_exe.parent()?;
|
||||
let target_debug_dir = deps_dir.parent()?;
|
||||
let helper = target_debug_dir.join("codex-windows-sandbox-setup.exe");
|
||||
if helper.is_file() {
|
||||
return Some(target_debug_dir.to_path_buf());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn prepend_to_path(path: &Path) -> Result<()> {
|
||||
let existing = std::env::var("PATH").unwrap_or_default();
|
||||
let mut parts = vec![path.display().to_string()];
|
||||
if !existing.is_empty() {
|
||||
parts.push(existing);
|
||||
}
|
||||
let joined = parts.join(";");
|
||||
std::env::set_var("PATH", joined);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_global_sandbox_artifacts(usernames: &[String], firewall_rule_name: &str) {
|
||||
for user in usernames {
|
||||
let _ = Command::new("net")
|
||||
.args(["user", user, "/delete"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
let cmd = format!(
|
||||
"Remove-NetFirewallRule -Name '{firewall_rule_name}' -ErrorAction SilentlyContinue"
|
||||
);
|
||||
let _ = Command::new("powershell")
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", &cmd])
|
||||
.status();
|
||||
}
|
||||
|
||||
fn unique_suffix() -> String {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let pid = std::process::id() as u128;
|
||||
format!("{:08x}", ((nanos ^ pid) & 0xffff_ffff) as u32)
|
||||
}
|
||||
1
codex-rs/windows-sandbox-rs/tests/suite/mod.rs
Normal file
1
codex-rs/windows-sandbox-rs/tests/suite/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
mod elevated_setup_smoke;
|
||||
Reference in New Issue
Block a user