Windows Sandbox - Alpha version (#4905)
- Added the new codex-windows-sandbox crate that builds both a library
entry point (run_windows_sandbox_capture) and a CLI executable to launch
commands inside a Windows restricted-token sandbox, including ACL
management, capability SID provisioning, network lockdown, and output
capture
(windows-sandbox-rs/src/lib.rs:167, windows-sandbox-rs/src/main.rs:54).
- Introduced the experimental WindowsSandbox feature flag and wiring so
Windows builds can opt into the sandbox:
SandboxType::WindowsRestrictedToken, the in-process execution path, and
platform sandbox selection now honor the flag (core/src/features.rs:47,
core/src/config.rs:1224, core/src/safety.rs:19,
core/src/sandboxing/mod.rs:69, core/src/exec.rs:79,
core/src/exec.rs:172).
- Updated workspace metadata to include the new crate and its
Windows-specific dependencies so the core crate can link against it
(codex-rs/
Cargo.toml:91, core/Cargo.toml:86).
- Added a PowerShell bootstrap script that installs the Windows
toolchain, required CLI utilities, and builds the workspace to ease
development
on the platform (scripts/setup-windows.ps1:1).
- Landed a Python smoke-test suite that exercises
read-only/workspace-write policies, ACL behavior, and network denial for
the Windows sandbox
binary (windows-sandbox-rs/sandbox_smoketests.py:1).
This commit is contained in:
14
codex-rs/Cargo.lock
generated
14
codex-rs/Cargo.lock
generated
@@ -983,6 +983,7 @@ dependencies = [
|
|||||||
"codex-rmcp-client",
|
"codex-rmcp-client",
|
||||||
"codex-stdio-to-uds",
|
"codex-stdio-to-uds",
|
||||||
"codex-tui",
|
"codex-tui",
|
||||||
|
"codex-windows-sandbox",
|
||||||
"ctor 0.5.0",
|
"ctor 0.5.0",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
"predicates",
|
"predicates",
|
||||||
@@ -1072,6 +1073,7 @@ dependencies = [
|
|||||||
"codex-utils-readiness",
|
"codex-utils-readiness",
|
||||||
"codex-utils-string",
|
"codex-utils-string",
|
||||||
"codex-utils-tokenizer",
|
"codex-utils-tokenizer",
|
||||||
|
"codex-windows-sandbox",
|
||||||
"core-foundation 0.9.4",
|
"core-foundation 0.9.4",
|
||||||
"core_test_support",
|
"core_test_support",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -1551,6 +1553,18 @@ dependencies = [
|
|||||||
"tiktoken-rs",
|
"tiktoken-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codex-windows-sandbox"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dirs-next",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color-eyre"
|
name = "color-eyre"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ codex-utils-pty = { path = "utils/pty" }
|
|||||||
codex-utils-readiness = { path = "utils/readiness" }
|
codex-utils-readiness = { path = "utils/readiness" }
|
||||||
codex-utils-string = { path = "utils/string" }
|
codex-utils-string = { path = "utils/string" }
|
||||||
codex-utils-tokenizer = { path = "utils/tokenizer" }
|
codex-utils-tokenizer = { path = "utils/tokenizer" }
|
||||||
|
codex-windows-sandbox = { path = "windows-sandbox" }
|
||||||
core_test_support = { path = "core/tests/common" }
|
core_test_support = { path = "core/tests/common" }
|
||||||
mcp-types = { path = "mcp-types" }
|
mcp-types = { path = "mcp-types" }
|
||||||
mcp_test_support = { path = "mcp-server/tests/common" }
|
mcp_test_support = { path = "mcp-server/tests/common" }
|
||||||
@@ -210,6 +211,7 @@ walkdir = "2.5.0"
|
|||||||
webbrowser = "1.0"
|
webbrowser = "1.0"
|
||||||
which = "6"
|
which = "6"
|
||||||
wildmatch = "2.5.0"
|
wildmatch = "2.5.0"
|
||||||
|
|
||||||
wiremock = "0.6"
|
wiremock = "0.6"
|
||||||
zeroize = "1.8.1"
|
zeroize = "1.8.1"
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ codex sandbox macos [--full-auto] [COMMAND]...
|
|||||||
# Linux
|
# Linux
|
||||||
codex sandbox linux [--full-auto] [COMMAND]...
|
codex sandbox linux [--full-auto] [COMMAND]...
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
codex sandbox windows [--full-auto] [COMMAND]...
|
||||||
|
|
||||||
# Legacy aliases
|
# Legacy aliases
|
||||||
codex debug seatbelt [--full-auto] [COMMAND]...
|
codex debug seatbelt [--full-auto] [COMMAND]...
|
||||||
codex debug landlock [--full-auto] [COMMAND]...
|
codex debug landlock [--full-auto] [COMMAND]...
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ tokio = { workspace = true, features = [
|
|||||||
"signal",
|
"signal",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = { workspace = true }
|
assert_cmd = { workspace = true }
|
||||||
assert_matches = { workspace = true }
|
assert_matches = { workspace = true }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use codex_protocol::config_types::SandboxMode;
|
|||||||
|
|
||||||
use crate::LandlockCommand;
|
use crate::LandlockCommand;
|
||||||
use crate::SeatbeltCommand;
|
use crate::SeatbeltCommand;
|
||||||
|
use crate::WindowsCommand;
|
||||||
use crate::exit_status::handle_exit_status;
|
use crate::exit_status::handle_exit_status;
|
||||||
|
|
||||||
pub async fn run_command_under_seatbelt(
|
pub async fn run_command_under_seatbelt(
|
||||||
@@ -51,9 +52,29 @@ pub async fn run_command_under_landlock(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run_command_under_windows(
|
||||||
|
command: WindowsCommand,
|
||||||
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let WindowsCommand {
|
||||||
|
full_auto,
|
||||||
|
config_overrides,
|
||||||
|
command,
|
||||||
|
} = command;
|
||||||
|
run_command_under_sandbox(
|
||||||
|
full_auto,
|
||||||
|
command,
|
||||||
|
config_overrides,
|
||||||
|
codex_linux_sandbox_exe,
|
||||||
|
SandboxType::Windows,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
enum SandboxType {
|
enum SandboxType {
|
||||||
Seatbelt,
|
Seatbelt,
|
||||||
Landlock,
|
Landlock,
|
||||||
|
Windows,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_command_under_sandbox(
|
async fn run_command_under_sandbox(
|
||||||
@@ -87,6 +108,63 @@ async fn run_command_under_sandbox(
|
|||||||
let stdio_policy = StdioPolicy::Inherit;
|
let stdio_policy = StdioPolicy::Inherit;
|
||||||
let env = create_env(&config.shell_environment_policy);
|
let env = create_env(&config.shell_environment_policy);
|
||||||
|
|
||||||
|
// Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
|
||||||
|
if let SandboxType::Windows = sandbox_type {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use codex_windows_sandbox::run_windows_sandbox_capture;
|
||||||
|
|
||||||
|
let policy_str = match &config.sandbox_policy {
|
||||||
|
codex_core::protocol::SandboxPolicy::DangerFullAccess => "workspace-write",
|
||||||
|
codex_core::protocol::SandboxPolicy::ReadOnly => "read-only",
|
||||||
|
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
||||||
|
};
|
||||||
|
|
||||||
|
let sandbox_cwd = sandbox_policy_cwd.clone();
|
||||||
|
let cwd_clone = cwd.clone();
|
||||||
|
let env_map = env.clone();
|
||||||
|
let command_vec = command.clone();
|
||||||
|
let res = tokio::task::spawn_blocking(move || {
|
||||||
|
run_windows_sandbox_capture(
|
||||||
|
policy_str,
|
||||||
|
&sandbox_cwd,
|
||||||
|
command_vec,
|
||||||
|
&cwd_clone,
|
||||||
|
env_map,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let capture = match res {
|
||||||
|
Ok(Ok(v)) => v,
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
eprintln!("windows sandbox failed: {err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(join_err) => {
|
||||||
|
eprintln!("windows sandbox join error: {join_err}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !capture.stdout.is_empty() {
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = std::io::stdout().write_all(&capture.stdout);
|
||||||
|
}
|
||||||
|
if !capture.stderr.is_empty() {
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = std::io::stderr().write_all(&capture.stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(capture.exit_code);
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
anyhow::bail!("Windows sandbox is only available on Windows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut child = match sandbox_type {
|
let mut child = match sandbox_type {
|
||||||
SandboxType::Seatbelt => {
|
SandboxType::Seatbelt => {
|
||||||
spawn_command_under_seatbelt(
|
spawn_command_under_seatbelt(
|
||||||
@@ -115,6 +193,9 @@ async fn run_command_under_sandbox(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
}
|
}
|
||||||
|
SandboxType::Windows => {
|
||||||
|
unreachable!("Windows sandbox should have been handled above");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let status = child.wait().await?;
|
let status = child.wait().await?;
|
||||||
|
|
||||||
|
|||||||
@@ -32,3 +32,17 @@ pub struct LandlockCommand {
|
|||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct WindowsCommand {
|
||||||
|
/// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR)
|
||||||
|
#[arg(long = "full-auto", default_value_t = false)]
|
||||||
|
pub full_auto: bool,
|
||||||
|
|
||||||
|
#[clap(skip)]
|
||||||
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
|
/// Full command args to run under Windows restricted token sandbox.
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
pub command: Vec<String>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use codex_chatgpt::apply_command::ApplyCommand;
|
|||||||
use codex_chatgpt::apply_command::run_apply_command;
|
use codex_chatgpt::apply_command::run_apply_command;
|
||||||
use codex_cli::LandlockCommand;
|
use codex_cli::LandlockCommand;
|
||||||
use codex_cli::SeatbeltCommand;
|
use codex_cli::SeatbeltCommand;
|
||||||
|
use codex_cli::WindowsCommand;
|
||||||
use codex_cli::login::read_api_key_from_stdin;
|
use codex_cli::login::read_api_key_from_stdin;
|
||||||
use codex_cli::login::run_login_status;
|
use codex_cli::login::run_login_status;
|
||||||
use codex_cli::login::run_login_with_api_key;
|
use codex_cli::login::run_login_with_api_key;
|
||||||
@@ -151,6 +152,9 @@ enum SandboxCommand {
|
|||||||
/// Run a command under Landlock+seccomp (Linux only).
|
/// Run a command under Landlock+seccomp (Linux only).
|
||||||
#[clap(visible_alias = "landlock")]
|
#[clap(visible_alias = "landlock")]
|
||||||
Linux(LandlockCommand),
|
Linux(LandlockCommand),
|
||||||
|
|
||||||
|
/// Run a command under Windows restricted token (Windows only).
|
||||||
|
Windows(WindowsCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -472,6 +476,17 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
SandboxCommand::Windows(mut windows_cli) => {
|
||||||
|
prepend_config_flags(
|
||||||
|
&mut windows_cli.config_overrides,
|
||||||
|
root_config_overrides.clone(),
|
||||||
|
);
|
||||||
|
codex_cli::debug_sandbox::run_command_under_windows(
|
||||||
|
windows_cli,
|
||||||
|
codex_linux_sandbox_exe,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Some(Subcommand::Apply(mut apply_cli)) => {
|
Some(Subcommand::Apply(mut apply_cli)) => {
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
@@ -497,7 +512,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
||||||
let cli_kv_overrides = root_config_overrides
|
let cli_kv_overrides = root_config_overrides
|
||||||
.parse_overrides()
|
.parse_overrides()
|
||||||
.map_err(|e| anyhow::anyhow!(e))?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
|
|
||||||
// Thread through relevant top-level flags (at minimum, `--profile`).
|
// Thread through relevant top-level flags (at minimum, `--profile`).
|
||||||
// Also honor `--search` since it maps to a feature toggle.
|
// Also honor `--search` since it maps to a feature toggle.
|
||||||
|
|||||||
@@ -196,7 +196,9 @@ impl McpCli {
|
|||||||
|
|
||||||
async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> {
|
async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Result<()> {
|
||||||
// Validate any provided overrides even though they are not currently applied.
|
// Validate any provided overrides even though they are not currently applied.
|
||||||
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
|
let overrides = config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
@@ -310,7 +312,9 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Result<()> {
|
async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Result<()> {
|
||||||
config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
|
config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
|
|
||||||
let RemoveArgs { name } = remove_args;
|
let RemoveArgs { name } = remove_args;
|
||||||
|
|
||||||
@@ -341,7 +345,9 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) -> Result<()> {
|
async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) -> Result<()> {
|
||||||
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
|
let overrides = config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
@@ -380,7 +386,9 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) -> Result<()> {
|
async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) -> Result<()> {
|
||||||
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
|
let overrides = config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
@@ -407,7 +415,9 @@ async fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutAr
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> {
|
async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> {
|
||||||
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
|
let overrides = config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
@@ -662,7 +672,9 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Result<()> {
|
async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Result<()> {
|
||||||
let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?;
|
let overrides = config_overrides
|
||||||
|
.parse_overrides()
|
||||||
|
.map_err(anyhow::Error::msg)?;
|
||||||
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default())
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ tree-sitter-bash = { workspace = true }
|
|||||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
wildmatch = { workspace = true }
|
wildmatch = { workspace = true }
|
||||||
|
codex_windows_sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
|||||||
@@ -769,6 +769,8 @@ impl ConfigToml {
|
|||||||
let mut forced_auto_mode_downgraded_on_windows = false;
|
let mut forced_auto_mode_downgraded_on_windows = false;
|
||||||
if cfg!(target_os = "windows")
|
if cfg!(target_os = "windows")
|
||||||
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
|
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
|
||||||
|
// If the experimental Windows sandbox is enabled, do not force a downgrade.
|
||||||
|
&& crate::safety::get_platform_sandbox().is_none()
|
||||||
{
|
{
|
||||||
sandbox_policy = SandboxPolicy::new_read_only_policy();
|
sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||||
forced_auto_mode_downgraded_on_windows = true;
|
forced_auto_mode_downgraded_on_windows = true;
|
||||||
@@ -900,6 +902,10 @@ impl Config {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
|
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
crate::safety::set_windows_sandbox_enabled(features.enabled(Feature::WindowsSandbox));
|
||||||
|
}
|
||||||
|
|
||||||
let resolved_cwd = {
|
let resolved_cwd = {
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ pub enum SandboxType {
|
|||||||
|
|
||||||
/// Only available on Linux.
|
/// Only available on Linux.
|
||||||
LinuxSeccomp,
|
LinuxSeccomp,
|
||||||
|
|
||||||
|
/// Only available on Windows.
|
||||||
|
WindowsRestrictedToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -158,11 +161,79 @@ pub(crate) async fn execute_exec_env(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let raw_output_result = exec(params, sandbox_policy, stdout_stream).await;
|
let raw_output_result = exec(params, sandbox, sandbox_policy, stdout_stream).await;
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
finalize_exec_result(raw_output_result, sandbox, duration)
|
finalize_exec_result(raw_output_result, sandbox, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn exec_windows_sandbox(
|
||||||
|
params: ExecParams,
|
||||||
|
sandbox_policy: &SandboxPolicy,
|
||||||
|
) -> Result<RawExecToolCallOutput> {
|
||||||
|
use codex_windows_sandbox::run_windows_sandbox_capture;
|
||||||
|
|
||||||
|
let ExecParams {
|
||||||
|
command,
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
timeout_ms,
|
||||||
|
..
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
let policy_str = match sandbox_policy {
|
||||||
|
SandboxPolicy::DangerFullAccess => "workspace-write",
|
||||||
|
SandboxPolicy::ReadOnly => "read-only",
|
||||||
|
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
|
||||||
|
};
|
||||||
|
|
||||||
|
let sandbox_cwd = cwd.clone();
|
||||||
|
let spawn_res = tokio::task::spawn_blocking(move || {
|
||||||
|
run_windows_sandbox_capture(policy_str, &sandbox_cwd, command, &cwd, env, timeout_ms)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let capture = match spawn_res {
|
||||||
|
Ok(Ok(v)) => v,
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
return Err(CodexErr::Io(io::Error::other(format!(
|
||||||
|
"windows sandbox: {err}"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
Err(join_err) => {
|
||||||
|
return Err(CodexErr::Io(io::Error::other(format!(
|
||||||
|
"windows sandbox join error: {join_err}"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let exit_status = synthetic_exit_status(capture.exit_code);
|
||||||
|
let stdout = StreamOutput {
|
||||||
|
text: capture.stdout,
|
||||||
|
truncated_after_lines: None,
|
||||||
|
};
|
||||||
|
let stderr = StreamOutput {
|
||||||
|
text: capture.stderr,
|
||||||
|
truncated_after_lines: None,
|
||||||
|
};
|
||||||
|
// Best-effort aggregate: stdout then stderr
|
||||||
|
let mut aggregated = Vec::with_capacity(stdout.text.len() + stderr.text.len());
|
||||||
|
append_all(&mut aggregated, &stdout.text);
|
||||||
|
append_all(&mut aggregated, &stderr.text);
|
||||||
|
let aggregated_output = StreamOutput {
|
||||||
|
text: aggregated,
|
||||||
|
truncated_after_lines: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(RawExecToolCallOutput {
|
||||||
|
exit_status,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
aggregated_output,
|
||||||
|
timed_out: capture.timed_out,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn finalize_exec_result(
|
fn finalize_exec_result(
|
||||||
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
|
raw_output_result: std::result::Result<RawExecToolCallOutput, CodexErr>,
|
||||||
sandbox_type: SandboxType,
|
sandbox_type: SandboxType,
|
||||||
@@ -347,11 +418,17 @@ pub struct ExecToolCallOutput {
|
|||||||
pub timed_out: bool,
|
pub timed_out: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(target_os = "windows"), allow(unused_variables))]
|
||||||
async fn exec(
|
async fn exec(
|
||||||
params: ExecParams,
|
params: ExecParams,
|
||||||
|
sandbox: SandboxType,
|
||||||
sandbox_policy: &SandboxPolicy,
|
sandbox_policy: &SandboxPolicy,
|
||||||
stdout_stream: Option<StdoutStream>,
|
stdout_stream: Option<StdoutStream>,
|
||||||
) -> Result<RawExecToolCallOutput> {
|
) -> Result<RawExecToolCallOutput> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if sandbox == SandboxType::WindowsRestrictedToken {
|
||||||
|
return exec_windows_sandbox(params, sandbox_policy).await;
|
||||||
|
}
|
||||||
let timeout = params.timeout_duration();
|
let timeout = params.timeout_duration();
|
||||||
let ExecParams {
|
let ExecParams {
|
||||||
command,
|
command,
|
||||||
@@ -525,8 +602,9 @@ fn synthetic_exit_status(code: i32) -> ExitStatus {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn synthetic_exit_status(code: i32) -> ExitStatus {
|
fn synthetic_exit_status(code: i32) -> ExitStatus {
|
||||||
use std::os::windows::process::ExitStatusExt;
|
use std::os::windows::process::ExitStatusExt;
|
||||||
#[expect(clippy::unwrap_used)]
|
// On Windows the raw status is a u32. Use a direct cast to avoid
|
||||||
std::process::ExitStatus::from_raw(code.try_into().unwrap())
|
// panicking on negative i32 values produced by prior narrowing casts.
|
||||||
|
std::process::ExitStatus::from_raw(code as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ pub enum Feature {
|
|||||||
SandboxCommandAssessment,
|
SandboxCommandAssessment,
|
||||||
/// Create a ghost commit at each turn.
|
/// Create a ghost commit at each turn.
|
||||||
GhostCommit,
|
GhostCommit,
|
||||||
|
/// Enable Windows sandbox (restricted token) on Windows.
|
||||||
|
WindowsSandbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Feature {
|
impl Feature {
|
||||||
@@ -292,4 +294,10 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
stage: Stage::Experimental,
|
stage: Stage::Experimental,
|
||||||
default_enabled: false,
|
default_enabled: false,
|
||||||
},
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::WindowsSandbox,
|
||||||
|
key: "enable_experimental_windows_sandbox",
|
||||||
|
stage: Stage::Experimental,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ use crate::exec::SandboxType;
|
|||||||
use crate::protocol::AskForApproval;
|
use crate::protocol::AskForApproval;
|
||||||
use crate::protocol::SandboxPolicy;
|
use crate::protocol::SandboxPolicy;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
static WINDOWS_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn set_windows_sandbox_enabled(enabled: bool) {
|
||||||
|
WINDOWS_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn set_windows_sandbox_enabled(_enabled: bool) {}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum SafetyCheck {
|
pub enum SafetyCheck {
|
||||||
AutoApprove {
|
AutoApprove {
|
||||||
@@ -84,6 +101,14 @@ pub fn get_platform_sandbox() -> Option<SandboxType> {
|
|||||||
Some(SandboxType::MacosSeatbelt)
|
Some(SandboxType::MacosSeatbelt)
|
||||||
} else if cfg!(target_os = "linux") {
|
} else if cfg!(target_os = "linux") {
|
||||||
Some(SandboxType::LinuxSeccomp)
|
Some(SandboxType::LinuxSeccomp)
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) {
|
||||||
|
return Some(SandboxType::WindowsRestrictedToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,25 +74,13 @@ impl SandboxManager {
|
|||||||
match pref {
|
match pref {
|
||||||
SandboxablePreference::Forbid => SandboxType::None,
|
SandboxablePreference::Forbid => SandboxType::None,
|
||||||
SandboxablePreference::Require => {
|
SandboxablePreference::Require => {
|
||||||
#[cfg(target_os = "macos")]
|
// Require a platform sandbox when available; on Windows this
|
||||||
{
|
// respects the enable_experimental_windows_sandbox feature.
|
||||||
return SandboxType::MacosSeatbelt;
|
crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None)
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
return SandboxType::LinuxSeccomp;
|
|
||||||
}
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
SandboxType::None
|
|
||||||
}
|
}
|
||||||
SandboxablePreference::Auto => match policy {
|
SandboxablePreference::Auto => match policy {
|
||||||
SandboxPolicy::DangerFullAccess => SandboxType::None,
|
SandboxPolicy::DangerFullAccess => SandboxType::None,
|
||||||
#[cfg(target_os = "macos")]
|
_ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None),
|
||||||
_ => SandboxType::MacosSeatbelt,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
_ => SandboxType::LinuxSeccomp,
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
|
||||||
_ => SandboxType::None,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +131,14 @@ impl SandboxManager {
|
|||||||
Some("codex-linux-sandbox".to_string()),
|
Some("codex-linux-sandbox".to_string()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// On Windows, the restricted token sandbox executes in-process via the
|
||||||
|
// codex-windows-sandbox crate. We leave the command unchanged here and
|
||||||
|
// branch during execution based on the sandbox type.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None),
|
||||||
|
// When building for non-Windows targets, this variant is never constructed.
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
SandboxType::WindowsRestrictedToken => (command, HashMap::new(), None),
|
||||||
};
|
};
|
||||||
|
|
||||||
env.extend(sandbox_env);
|
env.extend(sandbox_env);
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ impl ToolOrchestrator {
|
|||||||
if tool.wants_escalated_first_attempt(req) {
|
if tool.wants_escalated_first_attempt(req) {
|
||||||
initial_sandbox = crate::exec::SandboxType::None;
|
initial_sandbox = crate::exec::SandboxType::None;
|
||||||
}
|
}
|
||||||
|
// Platform-specific flag gating is handled by SandboxManager::select_initial
|
||||||
|
// via crate::safety::get_platform_sandbox().
|
||||||
let initial_attempt = SandboxAttempt {
|
let initial_attempt = SandboxAttempt {
|
||||||
sandbox: initial_sandbox,
|
sandbox: initial_sandbox,
|
||||||
policy: &turn_ctx.sandbox_policy,
|
policy: &turn_ctx.sandbox_policy,
|
||||||
|
|||||||
246
codex-rs/scripts/setup-windows.ps1
Normal file
246
codex-rs/scripts/setup-windows.ps1
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<#
|
||||||
|
Setup script for building codex-rs on Windows.
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Installs Rust toolchain (via winget rustup) and required components
|
||||||
|
- Installs Visual Studio 2022 Build Tools (MSVC + Windows SDK)
|
||||||
|
- Installs helpful CLIs used by the repo: git, ripgrep (rg), just, cmake
|
||||||
|
- Installs cargo-insta (for snapshot tests) via cargo
|
||||||
|
- Ensures PATH contains Cargo bin for the current session
|
||||||
|
- Builds the workspace (cargo build)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
- Right-click PowerShell and "Run as Administrator" (VS Build Tools require elevation)
|
||||||
|
- From the repo root (codex-rs), run:
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/setup-windows.ps1
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Requires winget (Windows Package Manager). Most modern Windows 10/11 have it preinstalled.
|
||||||
|
- The script is re-runnable; winget/cargo will skip/reinstall as appropriate.
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch] $SkipBuild
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Ensure-Command($Name) {
|
||||||
|
$exists = Get-Command $Name -ErrorAction SilentlyContinue
|
||||||
|
return $null -ne $exists
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-CargoBinToPath() {
|
||||||
|
$cargoBin = Join-Path $env:USERPROFILE ".cargo\bin"
|
||||||
|
if (Test-Path $cargoBin) {
|
||||||
|
if (-not ($env:Path.Split(';') -contains $cargoBin)) {
|
||||||
|
$env:Path = "$env:Path;$cargoBin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-UserPathContains([string] $Segment) {
|
||||||
|
try {
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
if ($null -eq $userPath) { $userPath = '' }
|
||||||
|
$parts = $userPath.Split(';') | Where-Object { $_ -ne '' }
|
||||||
|
if (-not ($parts -contains $Segment)) {
|
||||||
|
$newPath = if ($userPath) { "$userPath;$Segment" } else { $Segment }
|
||||||
|
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-UserEnvVar([string] $Name, [string] $Value) {
|
||||||
|
try { [Environment]::SetEnvironmentVariable($Name, $Value, 'User') } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-VSComponents([string[]]$Components) {
|
||||||
|
$vsInstaller = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe"
|
||||||
|
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||||
|
if (-not (Test-Path $vsInstaller) -or -not (Test-Path $vswhere)) { return }
|
||||||
|
|
||||||
|
$instPath = & $vswhere -latest -products * -version "[17.0,18.0)" -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath 2>$null
|
||||||
|
if (-not $instPath) {
|
||||||
|
# 2022 instance may be present without VC Tools; pick BuildTools 2022 and add components
|
||||||
|
$instPath = & $vswhere -latest -products Microsoft.VisualStudio.Product.BuildTools -version "[17.0,18.0)" -property installationPath 2>$null
|
||||||
|
}
|
||||||
|
if (-not $instPath) {
|
||||||
|
$instPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath 2>$null
|
||||||
|
}
|
||||||
|
if (-not $instPath) {
|
||||||
|
$default2022 = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools'
|
||||||
|
if (Test-Path $default2022) { $instPath = $default2022 }
|
||||||
|
}
|
||||||
|
if (-not $instPath) { return }
|
||||||
|
|
||||||
|
$vsDevCmd = Join-Path $instPath 'Common7\Tools\VsDevCmd.bat'
|
||||||
|
$verb = if (Test-Path $vsDevCmd) { 'modify' } else { 'install' }
|
||||||
|
$args = @($verb, '--installPath', $instPath, '--quiet', '--norestart', '--nocache')
|
||||||
|
if ($verb -eq 'install') { $args += @('--productId', 'Microsoft.VisualStudio.Product.BuildTools') }
|
||||||
|
foreach ($c in $Components) { $args += @('--add', $c) }
|
||||||
|
Write-Host "-- Ensuring VS components installed: $($Components -join ', ')" -ForegroundColor DarkCyan
|
||||||
|
& $vsInstaller @args | Out-Host
|
||||||
|
}
|
||||||
|
|
||||||
|
function Enter-VsDevShell() {
|
||||||
|
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||||
|
if (-not (Test-Path $vswhere)) { return }
|
||||||
|
|
||||||
|
$instPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null
|
||||||
|
if (-not $instPath) {
|
||||||
|
# Try ARM64 components
|
||||||
|
$instPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64 -property installationPath 2>$null
|
||||||
|
}
|
||||||
|
if (-not $instPath) { return }
|
||||||
|
|
||||||
|
$vsDevCmd = Join-Path $instPath 'Common7\Tools\VsDevCmd.bat'
|
||||||
|
if (-not (Test-Path $vsDevCmd)) { return }
|
||||||
|
|
||||||
|
# Prefer ARM64 on ARM machines, otherwise x64
|
||||||
|
$arch = if ($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64' -or $env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' }
|
||||||
|
$devCmdStr = ('"{0}" -no_logo -arch={1} -host_arch={1} & set' -f $vsDevCmd, $arch)
|
||||||
|
$envLines = & cmd.exe /c $devCmdStr
|
||||||
|
foreach ($line in $envLines) {
|
||||||
|
if ($line -match '^(.*?)=(.*)$') {
|
||||||
|
$name = $matches[1]
|
||||||
|
$value = $matches[2]
|
||||||
|
try { [Environment]::SetEnvironmentVariable($name, $value, 'Process') } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Installing prerequisites via winget (may take a while)" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Accept agreements up-front for non-interactive installs
|
||||||
|
$WingetArgs = @('--accept-package-agreements', '--accept-source-agreements', '-e')
|
||||||
|
|
||||||
|
if (-not (Ensure-Command 'winget')) {
|
||||||
|
throw "winget is required. Please update to the latest Windows 10/11 or install winget."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1) Visual Studio 2022 Build Tools (MSVC toolchain + Windows SDK)
|
||||||
|
# The VC Tools workload brings the required MSVC toolchains; include recommended components to pick up a Windows SDK.
|
||||||
|
Write-Host "-- Installing Visual Studio Build Tools (VC Tools workload + ARM64 toolchains)" -ForegroundColor DarkCyan
|
||||||
|
$vsOverride = @(
|
||||||
|
'--quiet', '--wait', '--norestart', '--nocache',
|
||||||
|
'--add', 'Microsoft.VisualStudio.Workload.VCTools',
|
||||||
|
'--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64',
|
||||||
|
'--add', 'Microsoft.VisualStudio.Component.VC.Tools.ARM64EC',
|
||||||
|
'--add', 'Microsoft.VisualStudio.Component.Windows11SDK.22000'
|
||||||
|
) -join ' '
|
||||||
|
winget install @WingetArgs --id Microsoft.VisualStudio.2022.BuildTools --override $vsOverride | Out-Host
|
||||||
|
|
||||||
|
# Ensure required VC components even if winget doesn't modify the instance
|
||||||
|
$isArm64 = ($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64' -or $env:PROCESSOR_ARCHITECTURE -eq 'ARM64')
|
||||||
|
$components = @(
|
||||||
|
'Microsoft.VisualStudio.Workload.VCTools',
|
||||||
|
'Microsoft.VisualStudio.Component.VC.Tools.ARM64',
|
||||||
|
'Microsoft.VisualStudio.Component.VC.Tools.ARM64EC',
|
||||||
|
'Microsoft.VisualStudio.Component.Windows11SDK.22000'
|
||||||
|
)
|
||||||
|
Ensure-VSComponents -Components $components
|
||||||
|
|
||||||
|
# 2) Rustup
|
||||||
|
Write-Host "-- Installing rustup" -ForegroundColor DarkCyan
|
||||||
|
winget install @WingetArgs --id Rustlang.Rustup | Out-Host
|
||||||
|
|
||||||
|
# Make cargo available in this session
|
||||||
|
Add-CargoBinToPath
|
||||||
|
|
||||||
|
# 3) Git (often present, but ensure installed)
|
||||||
|
Write-Host "-- Installing Git" -ForegroundColor DarkCyan
|
||||||
|
winget install @WingetArgs --id Git.Git | Out-Host
|
||||||
|
|
||||||
|
# 4) ripgrep (rg)
|
||||||
|
Write-Host "-- Installing ripgrep (rg)" -ForegroundColor DarkCyan
|
||||||
|
winget install @WingetArgs --id BurntSushi.ripgrep.MSVC | Out-Host
|
||||||
|
|
||||||
|
# 5) just
|
||||||
|
Write-Host "-- Installing just" -ForegroundColor DarkCyan
|
||||||
|
winget install @WingetArgs --id Casey.Just | Out-Host
|
||||||
|
|
||||||
|
# 6) cmake (commonly needed by native crates)
|
||||||
|
Write-Host "-- Installing CMake" -ForegroundColor DarkCyan
|
||||||
|
winget install @WingetArgs --id Kitware.CMake | Out-Host
|
||||||
|
|
||||||
|
# Ensure cargo is available after rustup install
|
||||||
|
Add-CargoBinToPath
|
||||||
|
if (-not (Ensure-Command 'cargo')) {
|
||||||
|
# Some shells need a re-login; attempt to source cargo.env if present
|
||||||
|
$cargoEnv = Join-Path $env:USERPROFILE ".cargo\env"
|
||||||
|
if (Test-Path $cargoEnv) { . $cargoEnv }
|
||||||
|
Add-CargoBinToPath
|
||||||
|
}
|
||||||
|
if (-not (Ensure-Command 'cargo')) {
|
||||||
|
throw "cargo not found in PATH after rustup install. Please open a new terminal and re-run the script."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Configuring Rust toolchain per rust-toolchain.toml" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Pin to the workspace toolchain and install components
|
||||||
|
$toolchain = '1.90.0'
|
||||||
|
& rustup toolchain install $toolchain --profile minimal | Out-Host
|
||||||
|
& rustup default $toolchain | Out-Host
|
||||||
|
& rustup component add clippy rustfmt rust-src --toolchain $toolchain | Out-Host
|
||||||
|
|
||||||
|
# 6.5) LLVM/Clang (some crates/bindgen require clang/libclang)
|
||||||
|
function Add-LLVMToPath() {
|
||||||
|
$llvmBin = 'C:\\Program Files\\LLVM\\bin'
|
||||||
|
if (Test-Path $llvmBin) {
|
||||||
|
if (-not ($env:Path.Split(';') -contains $llvmBin)) {
|
||||||
|
$env:Path = "$env:Path;$llvmBin"
|
||||||
|
}
|
||||||
|
if (-not $env:LIBCLANG_PATH) {
|
||||||
|
$env:LIBCLANG_PATH = $llvmBin
|
||||||
|
}
|
||||||
|
Ensure-UserPathContains $llvmBin
|
||||||
|
Ensure-UserEnvVar -Name 'LIBCLANG_PATH' -Value $llvmBin
|
||||||
|
|
||||||
|
$clang = Join-Path $llvmBin 'clang.exe'
|
||||||
|
$clangxx = Join-Path $llvmBin 'clang++.exe'
|
||||||
|
if (Test-Path $clang) {
|
||||||
|
$env:CC = $clang
|
||||||
|
Ensure-UserEnvVar -Name 'CC' -Value $clang
|
||||||
|
}
|
||||||
|
if (Test-Path $clangxx) {
|
||||||
|
$env:CXX = $clangxx
|
||||||
|
Ensure-UserEnvVar -Name 'CXX' -Value $clangxx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "-- Installing LLVM/Clang" -ForegroundColor DarkCyan
|
||||||
|
winget install @WingetArgs --id LLVM.LLVM | Out-Host
|
||||||
|
Add-LLVMToPath
|
||||||
|
|
||||||
|
# 7) cargo-insta (used by snapshot tests)
|
||||||
|
# Ensure MSVC linker is available before building/cargo-install by entering VS dev shell
|
||||||
|
Enter-VsDevShell
|
||||||
|
$hasLink = $false
|
||||||
|
try { & where.exe link | Out-Null; $hasLink = $true } catch {}
|
||||||
|
if ($hasLink) {
|
||||||
|
Write-Host "-- Installing cargo-insta" -ForegroundColor DarkCyan
|
||||||
|
& cargo install cargo-insta --locked | Out-Host
|
||||||
|
} else {
|
||||||
|
Write-Host "-- Skipping cargo-insta for now (MSVC linker not found yet)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($SkipBuild) {
|
||||||
|
Write-Host "==> Skipping cargo build (SkipBuild specified)" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Building workspace (cargo build)" -ForegroundColor Cyan
|
||||||
|
pushd "$PSScriptRoot\.." | Out-Null
|
||||||
|
try {
|
||||||
|
# Clear RUSTFLAGS if coming from constrained environments
|
||||||
|
$env:RUSTFLAGS = ''
|
||||||
|
Enter-VsDevShell
|
||||||
|
& cargo build
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
popd | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "==> Build complete" -ForegroundColor Green
|
||||||
@@ -1862,7 +1862,10 @@ impl ChatWidget {
|
|||||||
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
||||||
let name = preset.label.to_string();
|
let name = preset.label.to_string();
|
||||||
let description_text = preset.description;
|
let description_text = preset.description;
|
||||||
let description = if cfg!(target_os = "windows") && preset.id == "auto" {
|
let description = if cfg!(target_os = "windows")
|
||||||
|
&& preset.id == "auto"
|
||||||
|
&& codex_core::get_platform_sandbox().is_none()
|
||||||
|
{
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"{description_text}\nRequires Windows Subsystem for Linux (WSL). Show installation instructions..."
|
"{description_text}\nRequires Windows Subsystem for Linux (WSL). Show installation instructions..."
|
||||||
))
|
))
|
||||||
@@ -1882,7 +1885,10 @@ impl ChatWidget {
|
|||||||
preset: preset_clone.clone(),
|
preset: preset_clone.clone(),
|
||||||
});
|
});
|
||||||
})]
|
})]
|
||||||
} else if cfg!(target_os = "windows") && preset.id == "auto" {
|
} else if cfg!(target_os = "windows")
|
||||||
|
&& preset.id == "auto"
|
||||||
|
&& codex_core::get_platform_sandbox().is_none()
|
||||||
|
{
|
||||||
vec![Box::new(|tx| {
|
vec![Box::new(|tx| {
|
||||||
tx.send(AppEvent::ShowWindowsAutoModeInstructions);
|
tx.send(AppEvent::ShowWindowsAutoModeInstructions);
|
||||||
})]
|
})]
|
||||||
|
|||||||
@@ -1424,7 +1424,7 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
|||||||
let args_str = invocation
|
let args_str = invocation
|
||||||
.arguments
|
.arguments
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|v| {
|
.map(|v: &serde_json::Value| {
|
||||||
// Use compact form to keep things short but readable.
|
// Use compact form to keep things short but readable.
|
||||||
serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
|
serde_json::to_string(v).unwrap_or_else(|_| v.to_string())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ pub(crate) fn compose_account_display(config: &Config) -> Option<StatusAccountDi
|
|||||||
if let Some(tokens) = auth.tokens.as_ref() {
|
if let Some(tokens) = auth.tokens.as_ref() {
|
||||||
let info = &tokens.id_token;
|
let info = &tokens.id_token;
|
||||||
let email = info.email.clone();
|
let email = info.email.clone();
|
||||||
let plan = info.get_chatgpt_plan_type().map(|plan| title_case(&plan));
|
let plan = info.get_chatgpt_plan_type().as_deref().map(title_case);
|
||||||
return Some(StatusAccountDisplay::ChatGpt { email, plan });
|
return Some(StatusAccountDisplay::ChatGpt { email, plan });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
369
codex-rs/windows-sandbox-rs/Cargo.lock
generated
Normal file
369
codex-rs/windows-sandbox-rs/Cargo.lock
generated
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codex-windows-sandbox"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dirs-next",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-next"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"dirs-sys-next",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys-next"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"redox_users",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"libredox",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.145"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_gnullvm",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
43
codex-rs/windows-sandbox-rs/Cargo.toml
Normal file
43
codex-rs/windows-sandbox-rs/Cargo.toml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[package]
|
||||||
|
name = "codex-windows-sandbox"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "codex_windows_sandbox"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
[dependencies.rand]
|
||||||
|
version = "0.8"
|
||||||
|
default-features = false
|
||||||
|
features = ["std", "small_rng"]
|
||||||
|
[dependencies.dirs-next]
|
||||||
|
version = "2.0"
|
||||||
|
[dependencies.windows-sys]
|
||||||
|
version = "0.52"
|
||||||
|
features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_System_Diagnostics_Debug",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Security_Authorization",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_JobObjects",
|
||||||
|
"Win32_System_SystemServices",
|
||||||
|
"Win32_System_Environment",
|
||||||
|
"Win32_System_Pipes",
|
||||||
|
"Win32_System_WindowsProgramming",
|
||||||
|
"Win32_System_IO",
|
||||||
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Kernel",
|
||||||
|
"Win32_System_Console",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
"Win32_System_Diagnostics_ToolHelp",
|
||||||
|
"Win32_Networking_WinSock",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Com",
|
||||||
|
"Win32_Security_Authentication_Identity",
|
||||||
|
]
|
||||||
306
codex-rs/windows-sandbox-rs/sandbox_smoketests.py
Normal file
306
codex-rs/windows-sandbox-rs/sandbox_smoketests.py
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# sandbox_smoketests.py
|
||||||
|
# Run a suite of smoke tests against the Windows sandbox via the Codex CLI
|
||||||
|
# Requires: Python 3.8+ on Windows. No pip requirements.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
def _resolve_codex_cmd() -> List[str]:
|
||||||
|
"""Resolve the Codex CLI to invoke `codex sandbox windows`.
|
||||||
|
|
||||||
|
Prefer `codex` on PATH; if not found, try common local build locations.
|
||||||
|
Returns the argv prefix to run Codex.
|
||||||
|
"""
|
||||||
|
# 1) Prefer PATH
|
||||||
|
try:
|
||||||
|
cp = subprocess.run(["where", "codex"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
||||||
|
if cp.returncode == 0:
|
||||||
|
for line in cp.stdout.splitlines():
|
||||||
|
p = Path(line.strip())
|
||||||
|
if p.exists():
|
||||||
|
return [str(p)]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2) Try workspace targets
|
||||||
|
root = Path(__file__).parent
|
||||||
|
ws_root = root.parent
|
||||||
|
cargo_target = os.environ.get("CARGO_TARGET_DIR")
|
||||||
|
candidates = [
|
||||||
|
ws_root / "target" / "release" / "codex.exe",
|
||||||
|
ws_root / "target" / "debug" / "codex.exe",
|
||||||
|
]
|
||||||
|
if cargo_target:
|
||||||
|
candidates.extend([
|
||||||
|
Path(cargo_target) / "release" / "codex.exe",
|
||||||
|
Path(cargo_target) / "debug" / "codex.exe",
|
||||||
|
])
|
||||||
|
for p in candidates:
|
||||||
|
if p.exists():
|
||||||
|
return [str(p)]
|
||||||
|
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"Codex CLI not found. Build it first, e.g.\n"
|
||||||
|
" cargo build -p codex-cli --release\n"
|
||||||
|
"or for debug:\n"
|
||||||
|
" cargo build -p codex-cli\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEX_CMD = _resolve_codex_cmd()
|
||||||
|
TIMEOUT_SEC = 20
|
||||||
|
|
||||||
|
WS_ROOT = Path(os.environ["USERPROFILE"]) / "sbx_ws_tests"
|
||||||
|
OUTSIDE = Path(os.environ["USERPROFILE"]) / "sbx_ws_outside" # outside CWD for deny checks
|
||||||
|
|
||||||
|
ENV_BASE = {} # extend if needed
|
||||||
|
|
||||||
|
class CaseResult:
|
||||||
|
def __init__(self, name: str, ok: bool, detail: str = ""):
|
||||||
|
self.name, self.ok, self.detail = name, ok, detail
|
||||||
|
|
||||||
|
def run_sbx(policy: str, cmd_argv: List[str], cwd: Path, env_extra: Optional[dict] = None) -> Tuple[int, str, str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(ENV_BASE)
|
||||||
|
if env_extra:
|
||||||
|
env.update(env_extra)
|
||||||
|
# Map policy to codex CLI flags
|
||||||
|
# read-only => default; workspace-write => --full-auto
|
||||||
|
if policy not in ("read-only", "workspace-write"):
|
||||||
|
raise ValueError(f"unknown policy: {policy}")
|
||||||
|
policy_flags: List[str] = ["--full-auto"] if policy == "workspace-write" else []
|
||||||
|
|
||||||
|
argv = [*CODEX_CMD, "sandbox", "windows", *policy_flags, "--", *cmd_argv]
|
||||||
|
print(cmd_argv)
|
||||||
|
cp = subprocess.run(argv, cwd=str(cwd), env=env,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
timeout=TIMEOUT_SEC, text=True)
|
||||||
|
return cp.returncode, cp.stdout, cp.stderr
|
||||||
|
|
||||||
|
def have(cmd: str) -> bool:
|
||||||
|
try:
|
||||||
|
cp = subprocess.run(["where", cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
||||||
|
return cp.returncode == 0 and any(Path(p.strip()).exists() for p in cp.stdout.splitlines())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def make_dir_clean(p: Path) -> None:
|
||||||
|
if p.exists():
|
||||||
|
shutil.rmtree(p, ignore_errors=True)
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def write_file(p: Path, content: str = "x") -> None:
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
|
def remove_if_exists(p: Path) -> None:
|
||||||
|
try:
|
||||||
|
if p.is_dir(): shutil.rmtree(p, ignore_errors=True)
|
||||||
|
elif p.exists(): p.unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def assert_exists(p: Path) -> bool:
|
||||||
|
return p.exists()
|
||||||
|
|
||||||
|
def assert_not_exists(p: Path) -> bool:
|
||||||
|
return not p.exists()
|
||||||
|
|
||||||
|
def summarize(results: List[CaseResult]) -> int:
|
||||||
|
ok = sum(1 for r in results if r.ok)
|
||||||
|
total = len(results)
|
||||||
|
print("\n" + "=" * 72)
|
||||||
|
print(f"Sandbox smoke tests: {ok}/{total} passed")
|
||||||
|
for r in results:
|
||||||
|
print(f"[{'PASS' if r.ok else 'FAIL'}] {r.name}" + (f" :: {r.detail.strip()}" if r.detail and not r.ok else ""))
|
||||||
|
print("=" * 72)
|
||||||
|
return 0 if ok == total else 1
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
results: List[CaseResult] = []
|
||||||
|
make_dir_clean(WS_ROOT)
|
||||||
|
OUTSIDE.mkdir(exist_ok=True)
|
||||||
|
# Environment probe: some hosts allow TEMP writes even under read-only
|
||||||
|
# tokens due to ACLs and restricted SID semantics. Detect and adapt tests.
|
||||||
|
probe_rc, _, _ = run_sbx(
|
||||||
|
"read-only",
|
||||||
|
["cmd", "/c", "echo probe > %TEMP%\\sbx_ro_probe.txt"],
|
||||||
|
WS_ROOT,
|
||||||
|
)
|
||||||
|
ro_temp_denied = probe_rc != 0
|
||||||
|
|
||||||
|
def add(name: str, ok: bool, detail: str = ""):
|
||||||
|
print('running', name)
|
||||||
|
results.append(CaseResult(name, ok, detail))
|
||||||
|
|
||||||
|
# 1. RO: deny write in CWD
|
||||||
|
target = WS_ROOT / "ro_should_fail.txt"
|
||||||
|
remove_if_exists(target)
|
||||||
|
rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo nope > ro_should_fail.txt"], WS_ROOT)
|
||||||
|
add("RO: write in CWD denied", rc != 0 and assert_not_exists(target), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 2. WS: allow write in CWD
|
||||||
|
target = WS_ROOT / "ws_ok.txt"
|
||||||
|
remove_if_exists(target)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo ok > ws_ok.txt"], WS_ROOT)
|
||||||
|
add("WS: write in CWD allowed", rc == 0 and assert_exists(target), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 3. WS: deny write outside workspace
|
||||||
|
outside_file = OUTSIDE / "blocked.txt"
|
||||||
|
remove_if_exists(outside_file)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", f"echo nope > {outside_file}"], WS_ROOT)
|
||||||
|
add("WS: write outside workspace denied", rc != 0 and assert_not_exists(outside_file), f"rc={rc}")
|
||||||
|
|
||||||
|
# 4. WS: allow TEMP write
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo tempok > %TEMP%\\ws_temp_ok.txt"], WS_ROOT)
|
||||||
|
add("WS: TEMP write allowed", rc == 0, f"rc={rc}")
|
||||||
|
|
||||||
|
# 5. RO: deny TEMP write
|
||||||
|
rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo tempno > %TEMP%\\ro_temp_fail.txt"], WS_ROOT)
|
||||||
|
if ro_temp_denied:
|
||||||
|
add("RO: TEMP write denied", rc != 0, f"rc={rc}")
|
||||||
|
else:
|
||||||
|
add("RO: TEMP write denied (skipped on this host)", True)
|
||||||
|
|
||||||
|
# 6. WS: append OK in CWD
|
||||||
|
target = WS_ROOT / "append.txt"
|
||||||
|
remove_if_exists(target); write_file(target, "line1\n")
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo line2 >> append.txt"], WS_ROOT)
|
||||||
|
add("WS: append allowed", rc == 0 and target.read_text().strip().endswith("line2"), f"rc={rc}")
|
||||||
|
|
||||||
|
# 7. RO: append denied
|
||||||
|
target = WS_ROOT / "ro_append.txt"
|
||||||
|
write_file(target, "line1\n")
|
||||||
|
rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo line2 >> ro_append.txt"], WS_ROOT)
|
||||||
|
add("RO: append denied", rc != 0 and target.read_text() == "line1\n", f"rc={rc}")
|
||||||
|
|
||||||
|
# 8. WS: PowerShell Set-Content in CWD (OK)
|
||||||
|
target = WS_ROOT / "ps_ok.txt"
|
||||||
|
remove_if_exists(target)
|
||||||
|
rc, out, err = run_sbx("workspace-write",
|
||||||
|
["powershell", "-NoLogo", "-NoProfile", "-Command",
|
||||||
|
"Set-Content -LiteralPath ps_ok.txt -Value 'hello' -Encoding ASCII"], WS_ROOT)
|
||||||
|
add("WS: PowerShell Set-Content allowed", rc == 0 and assert_exists(target), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 9. RO: PowerShell Set-Content denied
|
||||||
|
target = WS_ROOT / "ps_ro_fail.txt"
|
||||||
|
remove_if_exists(target)
|
||||||
|
rc, out, err = run_sbx("read-only",
|
||||||
|
["powershell", "-NoLogo", "-NoProfile", "-Command",
|
||||||
|
"Set-Content -LiteralPath ps_ro_fail.txt -Value 'x'"], WS_ROOT)
|
||||||
|
add("RO: PowerShell Set-Content denied", rc != 0 and assert_not_exists(target), f"rc={rc}")
|
||||||
|
|
||||||
|
# 10. WS: mkdir and write (OK)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "mkdir sub && echo hi > sub\\in_sub.txt"], WS_ROOT)
|
||||||
|
add("WS: mkdir+write allowed", rc == 0 and (WS_ROOT / "sub/in_sub.txt").exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# 11. WS: rename (EXPECTED SUCCESS on this host)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "echo x > r.txt & ren r.txt r2.txt"], WS_ROOT)
|
||||||
|
add("WS: rename succeeds (expected on this host)", rc == 0 and (WS_ROOT / "r2.txt").exists(), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 12. WS: delete (EXPECTED SUCCESS on this host)
|
||||||
|
target = WS_ROOT / "delme.txt"; write_file(target, "x")
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "del /q delme.txt"], WS_ROOT)
|
||||||
|
add("WS: delete succeeds (expected on this host)", rc == 0 and not target.exists(), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 13. RO: python tries to write (denied)
|
||||||
|
pyfile = WS_ROOT / "py_should_fail.txt"; remove_if_exists(pyfile)
|
||||||
|
rc, out, err = run_sbx("read-only", ["python", "-c", "open('py_should_fail.txt','w').write('x')"], WS_ROOT)
|
||||||
|
add("RO: python file write denied", rc != 0 and assert_not_exists(pyfile), f"rc={rc}")
|
||||||
|
|
||||||
|
# 14. WS: python writes file (OK)
|
||||||
|
pyfile = WS_ROOT / "py_ok.txt"; remove_if_exists(pyfile)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["python", "-c", "open('py_ok.txt','w').write('x')"], WS_ROOT)
|
||||||
|
add("WS: python file write allowed", rc == 0 and assert_exists(pyfile), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 15. WS: curl network blocked (short timeout)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["curl", "--connect-timeout", "1", "--max-time", "2", "https://example.com"], WS_ROOT)
|
||||||
|
add("WS: curl network blocked", rc != 0, f"rc={rc}")
|
||||||
|
|
||||||
|
# 16. WS: iwr network blocked (HTTP)
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["powershell", "-NoLogo", "-NoProfile", "-Command",
|
||||||
|
"try { iwr http://neverssl.com -TimeoutSec 2 } catch { exit 1 }"], WS_ROOT)
|
||||||
|
add("WS: iwr network blocked", rc != 0, f"rc={rc}")
|
||||||
|
|
||||||
|
# 17. RO: deny TEMP writes via PowerShell
|
||||||
|
rc, out, err = run_sbx("read-only",
|
||||||
|
["powershell", "-NoLogo", "-NoProfile", "-Command",
|
||||||
|
"Set-Content -LiteralPath $env:TEMP\\ro_tmpfail.txt -Value 'x'"], WS_ROOT)
|
||||||
|
if ro_temp_denied:
|
||||||
|
add("RO: TEMP write denied (PS)", rc != 0, f"rc={rc}")
|
||||||
|
else:
|
||||||
|
add("RO: TEMP write denied (PS, skipped)", True)
|
||||||
|
|
||||||
|
# 18. WS: curl version check — don't rely on stub, just succeed
|
||||||
|
if have("curl"):
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "curl --version"], WS_ROOT)
|
||||||
|
add("WS: curl present (version prints)", rc == 0, f"rc={rc}, err={err}")
|
||||||
|
else:
|
||||||
|
add("WS: curl present (optional, skipped)", True)
|
||||||
|
|
||||||
|
# 19. Optional: ripgrep version
|
||||||
|
if have("rg"):
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["cmd", "/c", "rg --version"], WS_ROOT)
|
||||||
|
add("WS: rg --version (optional)", rc == 0, f"rc={rc}, err={err}")
|
||||||
|
else:
|
||||||
|
add("WS: rg --version (optional, skipped)", True)
|
||||||
|
|
||||||
|
# 20. Optional: git --version
|
||||||
|
if have("git"):
|
||||||
|
rc, out, err = run_sbx("workspace-write", ["git", "--version"], WS_ROOT)
|
||||||
|
add("WS: git --version (optional)", rc == 0, f"rc={rc}, err={err}")
|
||||||
|
else:
|
||||||
|
add("WS: git --version (optional, skipped)", True)
|
||||||
|
|
||||||
|
# 21–23. JSON policy: allow only .\allowed — note CWD is still allowed by current impl
|
||||||
|
(WS_ROOT / "allowed").mkdir(exist_ok=True)
|
||||||
|
(WS_ROOT / "blocked").mkdir(exist_ok=True)
|
||||||
|
policy_json = '{"mode":"workspace-write","workspace_roots":[".\\\\allowed"]}'
|
||||||
|
|
||||||
|
# Allowed: inside .\allowed (OK)
|
||||||
|
rc, out, err = run_sbx(policy_json, ["cmd", "/c", "echo ok > allowed\\in_allowed.txt"], WS_ROOT)
|
||||||
|
add("JSON WS: write in allowed/ OK", rc == 0 and (WS_ROOT / "allowed/in_allowed.txt").exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# Outside CWD (deny)
|
||||||
|
json_outside = OUTSIDE / "json_blocked.txt"; remove_if_exists(json_outside)
|
||||||
|
rc, out, err = run_sbx(policy_json, ["cmd", "/c", f"echo nope > {json_outside}"], WS_ROOT)
|
||||||
|
add("JSON WS: write outside allowed/ denied", rc != 0 and not json_outside.exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# CWD is still allowed by current sandbox (documented behavior)
|
||||||
|
rc, out, err = run_sbx(policy_json, ["cmd", "/c", "echo ok > cwd_ok_under_json.txt"], WS_ROOT)
|
||||||
|
add("JSON WS: write in CWD allowed (by design)", rc == 0 and (WS_ROOT / "cwd_ok_under_json.txt").exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# 24. WS: PS bytes write (OK)
|
||||||
|
rc, out, err = run_sbx("workspace-write",
|
||||||
|
["powershell", "-NoLogo", "-NoProfile", "-Command",
|
||||||
|
"[IO.File]::WriteAllBytes('bytes_ok.bin',[byte[]](0..255))"], WS_ROOT)
|
||||||
|
add("WS: PS bytes write allowed", rc == 0 and (WS_ROOT / "bytes_ok.bin").exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# 25. RO: PS bytes write denied
|
||||||
|
rc, out, err = run_sbx("read-only",
|
||||||
|
["powershell", "-NoLogo", "-NoProfile", "-Command",
|
||||||
|
"[IO.File]::WriteAllBytes('bytes_fail.bin',[byte[]](0..10))"], WS_ROOT)
|
||||||
|
add("RO: PS bytes write denied", rc != 0 and not (WS_ROOT / "bytes_fail.bin").exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# 26. WS: deep mkdir and write (OK)
|
||||||
|
rc, out, err = run_sbx("workspace-write",
|
||||||
|
["cmd", "/c", "mkdir deep\\nest && echo ok > deep\\nest\\f.txt"], WS_ROOT)
|
||||||
|
add("WS: deep mkdir+write allowed", rc == 0 and (WS_ROOT / "deep/nest/f.txt").exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
# 27. WS: move (EXPECTED SUCCESS on this host)
|
||||||
|
rc, out, err = run_sbx("workspace-write",
|
||||||
|
["cmd", "/c", "echo x > m1.txt & move /y m1.txt m2.txt"], WS_ROOT)
|
||||||
|
add("WS: move succeeds (expected on this host)", rc == 0 and (WS_ROOT / "m2.txt").exists(), f"rc={rc}, err={err}")
|
||||||
|
|
||||||
|
# 28. RO: cmd redirection denied
|
||||||
|
target = WS_ROOT / "cmd_ro.txt"; remove_if_exists(target)
|
||||||
|
rc, out, err = run_sbx("read-only", ["cmd", "/c", "echo nope > cmd_ro.txt"], WS_ROOT)
|
||||||
|
add("RO: cmd redirection denied", rc != 0 and not target.exists(), f"rc={rc}")
|
||||||
|
|
||||||
|
return summarize(results)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
286
codex-rs/windows-sandbox-rs/src/acl.rs
Normal file
286
codex-rs/windows-sandbox-rs/src/acl.rs
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
use crate::winutil::to_wide;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::path::Path;
|
||||||
|
use windows_sys::Win32::Foundation::CloseHandle;
|
||||||
|
use windows_sys::Win32::Foundation::LocalFree;
|
||||||
|
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||||
|
use windows_sys::Win32::Foundation::HLOCAL;
|
||||||
|
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||||
|
use windows_sys::Win32::Security::AclSizeInformation;
|
||||||
|
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
|
||||||
|
use windows_sys::Win32::Security::Authorization::GetSecurityInfo;
|
||||||
|
use windows_sys::Win32::Security::Authorization::SetEntriesInAclW;
|
||||||
|
use windows_sys::Win32::Security::Authorization::SetNamedSecurityInfoW;
|
||||||
|
use windows_sys::Win32::Security::Authorization::SetSecurityInfo;
|
||||||
|
use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W;
|
||||||
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||||
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||||
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||||
|
use windows_sys::Win32::Security::EqualSid;
|
||||||
|
use windows_sys::Win32::Security::GetAce;
|
||||||
|
use windows_sys::Win32::Security::GetAclInformation;
|
||||||
|
use windows_sys::Win32::Security::ACCESS_ALLOWED_ACE;
|
||||||
|
use windows_sys::Win32::Security::ACE_HEADER;
|
||||||
|
use windows_sys::Win32::Security::ACL;
|
||||||
|
use windows_sys::Win32::Security::ACL_SIZE_INFORMATION;
|
||||||
|
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_EXECUTE;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_WRITE;
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING;
|
||||||
|
const SE_KERNEL_OBJECT: u32 = 6;
|
||||||
|
const INHERIT_ONLY_ACE: u8 = 0x08;
|
||||||
|
|
||||||
|
pub unsafe fn dacl_has_write_allow_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -> bool {
|
||||||
|
if p_dacl.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut info: ACL_SIZE_INFORMATION = std::mem::zeroed();
|
||||||
|
let ok = GetAclInformation(
|
||||||
|
p_dacl as *const ACL,
|
||||||
|
&mut info as *mut _ as *mut c_void,
|
||||||
|
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
|
||||||
|
AclSizeInformation,
|
||||||
|
);
|
||||||
|
if ok == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let count = info.AceCount as usize;
|
||||||
|
for i in 0..count {
|
||||||
|
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||||
|
if GetAce(p_dacl as *const ACL, i as u32, &mut p_ace) == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hdr = &*(p_ace as *const ACE_HEADER);
|
||||||
|
if hdr.AceType != 0 {
|
||||||
|
continue; // ACCESS_ALLOWED_ACE_TYPE
|
||||||
|
}
|
||||||
|
// Ignore ACEs that are inherit-only (do not apply to the current object)
|
||||||
|
if (hdr.AceFlags & INHERIT_ONLY_ACE) != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ace = &*(p_ace as *const ACCESS_ALLOWED_ACE);
|
||||||
|
let mask = ace.Mask;
|
||||||
|
let base = p_ace as usize;
|
||||||
|
let sid_ptr =
|
||||||
|
(base + std::mem::size_of::<ACE_HEADER>() + std::mem::size_of::<u32>()) as *mut c_void;
|
||||||
|
let eq = EqualSid(sid_ptr, psid);
|
||||||
|
if eq != 0 && (mask & FILE_GENERIC_WRITE) != 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute effective rights for a trustee SID against a DACL and decide if write is effectively allowed.
|
||||||
|
// This accounts for deny ACEs and ordering; falls back to a conservative per-ACE scan if the API fails.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub unsafe fn dacl_effective_allows_write(p_dacl: *mut ACL, psid: *mut c_void) -> bool {
|
||||||
|
if p_dacl.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
use windows_sys::Win32::Security::Authorization::GetEffectiveRightsFromAclW;
|
||||||
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID;
|
||||||
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN;
|
||||||
|
use windows_sys::Win32::Security::Authorization::TRUSTEE_W;
|
||||||
|
|
||||||
|
let trustee = TRUSTEE_W {
|
||||||
|
pMultipleTrustee: std::ptr::null_mut(),
|
||||||
|
MultipleTrusteeOperation: 0,
|
||||||
|
TrusteeForm: TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||||
|
ptstrName: psid as *mut u16,
|
||||||
|
};
|
||||||
|
let mut access: u32 = 0;
|
||||||
|
let ok = GetEffectiveRightsFromAclW(p_dacl, &trustee, &mut access);
|
||||||
|
if ok != 0 {
|
||||||
|
// Check for generic or specific write bits
|
||||||
|
let write_bits = FILE_GENERIC_WRITE
|
||||||
|
| windows_sys::Win32::Storage::FileSystem::FILE_WRITE_DATA
|
||||||
|
| windows_sys::Win32::Storage::FileSystem::FILE_APPEND_DATA
|
||||||
|
| windows_sys::Win32::Storage::FileSystem::FILE_WRITE_EA
|
||||||
|
| windows_sys::Win32::Storage::FileSystem::FILE_WRITE_ATTRIBUTES;
|
||||||
|
return (access & write_bits) != 0;
|
||||||
|
}
|
||||||
|
// Fallback: simple allow ACE scan (already ignores inherit-only)
|
||||||
|
dacl_has_write_allow_for_sid(p_dacl, psid)
|
||||||
|
}
|
||||||
|
pub unsafe fn add_allow_ace(path: &Path, psid: *mut c_void) -> Result<bool> {
|
||||||
|
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||||
|
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code = GetNamedSecurityInfoW(
|
||||||
|
to_wide(path).as_ptr(),
|
||||||
|
1,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_sd,
|
||||||
|
);
|
||||||
|
if code != ERROR_SUCCESS {
|
||||||
|
return Err(anyhow!("GetNamedSecurityInfoW failed: {}", code));
|
||||||
|
}
|
||||||
|
let mut added = false;
|
||||||
|
if !dacl_has_write_allow_for_sid(p_dacl, psid) {
|
||||||
|
let trustee = TRUSTEE_W {
|
||||||
|
pMultipleTrustee: std::ptr::null_mut(),
|
||||||
|
MultipleTrusteeOperation: 0,
|
||||||
|
TrusteeForm: TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||||
|
ptstrName: psid as *mut u16,
|
||||||
|
};
|
||||||
|
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||||
|
explicit.grfAccessPermissions =
|
||||||
|
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
|
||||||
|
explicit.grfAccessMode = 2; // SET_ACCESS
|
||||||
|
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||||
|
explicit.Trustee = trustee;
|
||||||
|
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
|
||||||
|
if code2 == ERROR_SUCCESS {
|
||||||
|
let code3 = SetNamedSecurityInfoW(
|
||||||
|
to_wide(path).as_ptr() as *mut u16,
|
||||||
|
1,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
p_new_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
if code3 == ERROR_SUCCESS {
|
||||||
|
added = true;
|
||||||
|
}
|
||||||
|
if !p_new_dacl.is_null() {
|
||||||
|
LocalFree(p_new_dacl as HLOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !p_sd.is_null() {
|
||||||
|
LocalFree(p_sd as HLOCAL);
|
||||||
|
}
|
||||||
|
Ok(added)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn revoke_ace(path: &Path, psid: *mut c_void) {
|
||||||
|
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||||
|
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code = GetNamedSecurityInfoW(
|
||||||
|
to_wide(path).as_ptr(),
|
||||||
|
1,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_sd,
|
||||||
|
);
|
||||||
|
if code != ERROR_SUCCESS {
|
||||||
|
if !p_sd.is_null() {
|
||||||
|
LocalFree(p_sd as HLOCAL);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let trustee = TRUSTEE_W {
|
||||||
|
pMultipleTrustee: std::ptr::null_mut(),
|
||||||
|
MultipleTrusteeOperation: 0,
|
||||||
|
TrusteeForm: TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||||
|
ptstrName: psid as *mut u16,
|
||||||
|
};
|
||||||
|
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||||
|
explicit.grfAccessPermissions = 0;
|
||||||
|
explicit.grfAccessMode = 4; // REVOKE_ACCESS
|
||||||
|
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||||
|
explicit.Trustee = trustee;
|
||||||
|
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
|
||||||
|
if code2 == ERROR_SUCCESS {
|
||||||
|
let _ = SetNamedSecurityInfoW(
|
||||||
|
to_wide(path).as_ptr() as *mut u16,
|
||||||
|
1,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
p_new_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
if !p_new_dacl.is_null() {
|
||||||
|
LocalFree(p_new_dacl as HLOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !p_sd.is_null() {
|
||||||
|
LocalFree(p_sd as HLOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn allow_null_device(psid: *mut c_void) {
|
||||||
|
let desired = 0x00020000 | 0x00040000; // READ_CONTROL | WRITE_DAC
|
||||||
|
let h = CreateFileW(
|
||||||
|
to_wide(r"\\\\.\\NUL").as_ptr(),
|
||||||
|
desired,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if h == 0 || h == INVALID_HANDLE_VALUE {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||||
|
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code = GetSecurityInfo(
|
||||||
|
h,
|
||||||
|
SE_KERNEL_OBJECT as i32,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_sd,
|
||||||
|
);
|
||||||
|
if code == ERROR_SUCCESS {
|
||||||
|
let trustee = TRUSTEE_W {
|
||||||
|
pMultipleTrustee: std::ptr::null_mut(),
|
||||||
|
MultipleTrusteeOperation: 0,
|
||||||
|
TrusteeForm: TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: TRUSTEE_IS_UNKNOWN,
|
||||||
|
ptstrName: psid as *mut u16,
|
||||||
|
};
|
||||||
|
let mut explicit: EXPLICIT_ACCESS_W = std::mem::zeroed();
|
||||||
|
explicit.grfAccessPermissions =
|
||||||
|
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE;
|
||||||
|
explicit.grfAccessMode = 2; // SET_ACCESS
|
||||||
|
explicit.grfInheritance = 0;
|
||||||
|
explicit.Trustee = trustee;
|
||||||
|
let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code2 = SetEntriesInAclW(1, &explicit, p_dacl, &mut p_new_dacl);
|
||||||
|
if code2 == ERROR_SUCCESS {
|
||||||
|
let _ = SetSecurityInfo(
|
||||||
|
h,
|
||||||
|
SE_KERNEL_OBJECT as i32,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
p_new_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
if !p_new_dacl.is_null() {
|
||||||
|
LocalFree(p_new_dacl as HLOCAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !p_sd.is_null() {
|
||||||
|
LocalFree(p_sd as HLOCAL);
|
||||||
|
}
|
||||||
|
CloseHandle(h);
|
||||||
|
}
|
||||||
|
const CONTAINER_INHERIT_ACE: u32 = 0x2;
|
||||||
|
const OBJECT_INHERIT_ACE: u32 = 0x1;
|
||||||
37
codex-rs/windows-sandbox-rs/src/allow.rs
Normal file
37
codex-rs/windows-sandbox-rs/src/allow.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use crate::policy::SandboxMode;
|
||||||
|
use crate::policy::SandboxPolicy;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn compute_allow_paths(
|
||||||
|
policy: &SandboxPolicy,
|
||||||
|
_policy_cwd: &Path,
|
||||||
|
command_cwd: &Path,
|
||||||
|
env_map: &HashMap<String, String>,
|
||||||
|
) -> Vec<PathBuf> {
|
||||||
|
let mut allow: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
if matches!(policy.0, SandboxMode::WorkspaceWrite) {
|
||||||
|
let abs = command_cwd.to_path_buf();
|
||||||
|
if seen.insert(abs.to_string_lossy().to_string()) && abs.exists() {
|
||||||
|
allow.push(abs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matches!(policy.0, SandboxMode::ReadOnly) {
|
||||||
|
for key in ["TEMP", "TMP"] {
|
||||||
|
if let Some(v) = env_map.get(key) {
|
||||||
|
let abs = PathBuf::from(v);
|
||||||
|
if seen.insert(abs.to_string_lossy().to_string()) && abs.exists() {
|
||||||
|
allow.push(abs);
|
||||||
|
}
|
||||||
|
} else if let Ok(v) = std::env::var(key) {
|
||||||
|
let abs = PathBuf::from(v);
|
||||||
|
if seen.insert(abs.to_string_lossy().to_string()) && abs.exists() {
|
||||||
|
allow.push(abs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allow
|
||||||
|
}
|
||||||
147
codex-rs/windows-sandbox-rs/src/audit.rs
Normal file
147
codex-rs/windows-sandbox-rs/src/audit.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use crate::acl::dacl_effective_allows_write;
|
||||||
|
use crate::token::world_sid;
|
||||||
|
use crate::winutil::to_wide;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
use windows_sys::Win32::Foundation::LocalFree;
|
||||||
|
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
|
||||||
|
use windows_sys::Win32::Foundation::HLOCAL;
|
||||||
|
use windows_sys::Win32::Security::Authorization::GetNamedSecurityInfoW;
|
||||||
|
use windows_sys::Win32::Security::ACL;
|
||||||
|
use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION;
|
||||||
|
|
||||||
|
fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf) {
|
||||||
|
if let Ok(abs) = p.canonicalize() {
|
||||||
|
if set.insert(abs.clone()) {
|
||||||
|
out.push(abs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gather_candidates(cwd: &Path, env: &std::collections::HashMap<String, String>) -> Vec<PathBuf> {
|
||||||
|
let mut set: HashSet<PathBuf> = HashSet::new();
|
||||||
|
let mut out: Vec<PathBuf> = Vec::new();
|
||||||
|
// Core roots
|
||||||
|
for p in [
|
||||||
|
PathBuf::from("C:/"),
|
||||||
|
PathBuf::from("C:/Windows"),
|
||||||
|
PathBuf::from("C:/ProgramData"),
|
||||||
|
] {
|
||||||
|
unique_push(&mut set, &mut out, p);
|
||||||
|
}
|
||||||
|
// User roots
|
||||||
|
if let Some(up) = std::env::var_os("USERPROFILE") {
|
||||||
|
unique_push(&mut set, &mut out, PathBuf::from(up));
|
||||||
|
}
|
||||||
|
if let Some(pubp) = std::env::var_os("PUBLIC") {
|
||||||
|
unique_push(&mut set, &mut out, PathBuf::from(pubp));
|
||||||
|
}
|
||||||
|
// CWD
|
||||||
|
unique_push(&mut set, &mut out, cwd.to_path_buf());
|
||||||
|
// TEMP/TMP
|
||||||
|
for k in ["TEMP", "TMP"] {
|
||||||
|
if let Some(v) = env.get(k).cloned().or_else(|| std::env::var(k).ok()) {
|
||||||
|
unique_push(&mut set, &mut out, PathBuf::from(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// PATH entries
|
||||||
|
if let Some(path) = env
|
||||||
|
.get("PATH")
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| std::env::var("PATH").ok())
|
||||||
|
{
|
||||||
|
for part in path.split(std::path::MAIN_SEPARATOR) {
|
||||||
|
if !part.is_empty() {
|
||||||
|
unique_push(&mut set, &mut out, PathBuf::from(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
|
||||||
|
let mut p_sd: *mut c_void = std::ptr::null_mut();
|
||||||
|
let mut p_dacl: *mut ACL = std::ptr::null_mut();
|
||||||
|
let code = GetNamedSecurityInfoW(
|
||||||
|
to_wide(path).as_ptr(),
|
||||||
|
1,
|
||||||
|
DACL_SECURITY_INFORMATION,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut p_sd,
|
||||||
|
);
|
||||||
|
if code != ERROR_SUCCESS {
|
||||||
|
if !p_sd.is_null() {
|
||||||
|
LocalFree(p_sd as HLOCAL);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let mut world = world_sid()?;
|
||||||
|
let psid_world = world.as_mut_ptr() as *mut c_void;
|
||||||
|
let has = dacl_effective_allows_write(p_dacl, psid_world);
|
||||||
|
if !p_sd.is_null() {
|
||||||
|
LocalFree(p_sd as HLOCAL);
|
||||||
|
}
|
||||||
|
Ok(has)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn audit_everyone_writable(
|
||||||
|
cwd: &Path,
|
||||||
|
env: &std::collections::HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut flagged: Vec<PathBuf> = Vec::new();
|
||||||
|
let mut checked = 0usize;
|
||||||
|
let candidates = gather_candidates(cwd, env);
|
||||||
|
for root in candidates {
|
||||||
|
if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
checked += 1;
|
||||||
|
if unsafe { path_has_world_write_allow(&root)? } {
|
||||||
|
flagged.push(root.clone());
|
||||||
|
}
|
||||||
|
// one level down best-effort
|
||||||
|
if let Ok(read) = std::fs::read_dir(&root) {
|
||||||
|
for ent in read.flatten().take(50) {
|
||||||
|
let p = ent.path();
|
||||||
|
if start.elapsed() > Duration::from_secs(5) || checked > 5000 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Skip reparse points (symlinks/junctions) to avoid auditing link ACLs
|
||||||
|
let ft = match ent.file_type() {
|
||||||
|
Ok(ft) => ft,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if ft.is_symlink() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ft.is_dir() {
|
||||||
|
checked += 1;
|
||||||
|
if unsafe { path_has_world_write_allow(&p)? } {
|
||||||
|
flagged.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !flagged.is_empty() {
|
||||||
|
let mut list = String::new();
|
||||||
|
for p in flagged {
|
||||||
|
list.push_str(&format!("\n - {}", p.display()));
|
||||||
|
}
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Refusing to run: found directories writable by Everyone: {}",
|
||||||
|
list
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
50
codex-rs/windows-sandbox-rs/src/cap.rs
Normal file
50
codex-rs/windows-sandbox-rs/src/cap.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use rand::rngs::SmallRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct CapSids {
|
||||||
|
pub workspace: String,
|
||||||
|
pub readonly: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cap_sid_file(policy_cwd: &Path) -> PathBuf {
|
||||||
|
policy_cwd.join(".codex").join("cap_sid")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_random_cap_sid_string() -> String {
|
||||||
|
let mut rng = SmallRng::from_entropy();
|
||||||
|
let a = rng.next_u32();
|
||||||
|
let b = rng.next_u32();
|
||||||
|
let c = rng.next_u32();
|
||||||
|
let d = rng.next_u32();
|
||||||
|
format!("S-1-5-21-{}-{}-{}-{}", a, b, c, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_or_create_cap_sids(policy_cwd: &Path) -> CapSids {
|
||||||
|
let path = cap_sid_file(policy_cwd);
|
||||||
|
if path.exists() {
|
||||||
|
if let Ok(txt) = fs::read_to_string(&path) {
|
||||||
|
let t = txt.trim();
|
||||||
|
if t.starts_with('{') && t.ends_with('}') {
|
||||||
|
if let Ok(obj) = serde_json::from_str::<CapSids>(t) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
} else if !t.is_empty() {
|
||||||
|
return CapSids {
|
||||||
|
workspace: t.to_string(),
|
||||||
|
readonly: make_random_cap_sid_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CapSids {
|
||||||
|
workspace: make_random_cap_sid_string(),
|
||||||
|
readonly: make_random_cap_sid_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
165
codex-rs/windows-sandbox-rs/src/env.rs
Normal file
165
codex-rs/windows-sandbox-rs/src/env.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::fs::{self};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn normalize_null_device_env(env_map: &mut HashMap<String, String>) {
|
||||||
|
let keys: Vec<String> = env_map.keys().cloned().collect();
|
||||||
|
for k in keys {
|
||||||
|
if let Some(v) = env_map.get(&k).cloned() {
|
||||||
|
let t = v.trim().to_ascii_lowercase();
|
||||||
|
if t == "/dev/null" || t == "\\\\\\\\dev\\\\\\\\null" {
|
||||||
|
env_map.insert(k, "NUL".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_non_interactive_pager(env_map: &mut HashMap<String, String>) {
|
||||||
|
env_map
|
||||||
|
.entry("GIT_PAGER".into())
|
||||||
|
.or_insert_with(|| "more.com".into());
|
||||||
|
env_map
|
||||||
|
.entry("PAGER".into())
|
||||||
|
.or_insert_with(|| "more.com".into());
|
||||||
|
env_map.entry("LESS".into()).or_insert_with(|| "".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepend_path(env_map: &mut HashMap<String, String>, prefix: &str) {
|
||||||
|
let existing = env_map
|
||||||
|
.get("PATH")
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| env::var("PATH").ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let parts: Vec<String> = existing.split(';').map(|s| s.to_string()).collect();
|
||||||
|
if parts
|
||||||
|
.first()
|
||||||
|
.map(|p| p.eq_ignore_ascii_case(prefix))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut new_path = String::new();
|
||||||
|
new_path.push_str(prefix);
|
||||||
|
if !existing.is_empty() {
|
||||||
|
new_path.push(';');
|
||||||
|
new_path.push_str(&existing);
|
||||||
|
}
|
||||||
|
env_map.insert("PATH".into(), new_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reorder_pathext_for_stubs(env_map: &mut HashMap<String, String>) {
|
||||||
|
let default = env_map
|
||||||
|
.get("PATHEXT")
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| env::var("PATHEXT").ok())
|
||||||
|
.unwrap_or(".COM;.EXE;.BAT;.CMD".to_string());
|
||||||
|
let exts: Vec<String> = default
|
||||||
|
.split(';')
|
||||||
|
.filter(|e| !e.is_empty())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
let exts_norm: Vec<String> = exts.iter().map(|e| e.to_ascii_uppercase()).collect();
|
||||||
|
let want = [".BAT", ".CMD"]; // move to front if present
|
||||||
|
let mut front: Vec<String> = Vec::new();
|
||||||
|
for w in want {
|
||||||
|
if let Some(idx) = exts_norm.iter().position(|e| e == w) {
|
||||||
|
front.push(exts[idx].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rest: Vec<String> = exts
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| {
|
||||||
|
let up = &exts_norm[*i];
|
||||||
|
up != ".BAT" && up != ".CMD"
|
||||||
|
})
|
||||||
|
.map(|(_, e)| e)
|
||||||
|
.collect();
|
||||||
|
let mut combined = Vec::new();
|
||||||
|
combined.extend(front);
|
||||||
|
combined.extend(rest);
|
||||||
|
env_map.insert("PATHEXT".into(), combined.join(";"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_denybin(tools: &[&str], denybin_dir: Option<&Path>) -> Result<PathBuf> {
|
||||||
|
let base = match denybin_dir {
|
||||||
|
Some(p) => p.to_path_buf(),
|
||||||
|
None => {
|
||||||
|
let home = dirs_next::home_dir().ok_or_else(|| anyhow::anyhow!("no home dir"))?;
|
||||||
|
home.join(".sbx-denybin")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fs::create_dir_all(&base)?;
|
||||||
|
for tool in tools {
|
||||||
|
for ext in [".bat", ".cmd"] {
|
||||||
|
let path = base.join(format!("{}{}", tool, ext));
|
||||||
|
if !path.exists() {
|
||||||
|
let mut f = File::create(&path)?;
|
||||||
|
f.write_all(b"@echo off\\r\\nexit /b 1\\r\\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_no_network_to_env(env_map: &mut HashMap<String, String>) -> Result<()> {
|
||||||
|
env_map.insert("SBX_NONET_ACTIVE".into(), "1".into());
|
||||||
|
env_map
|
||||||
|
.entry("HTTP_PROXY".into())
|
||||||
|
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||||
|
env_map
|
||||||
|
.entry("HTTPS_PROXY".into())
|
||||||
|
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||||
|
env_map
|
||||||
|
.entry("ALL_PROXY".into())
|
||||||
|
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||||
|
env_map
|
||||||
|
.entry("NO_PROXY".into())
|
||||||
|
.or_insert_with(|| "localhost,127.0.0.1,::1".into());
|
||||||
|
env_map
|
||||||
|
.entry("PIP_NO_INDEX".into())
|
||||||
|
.or_insert_with(|| "1".into());
|
||||||
|
env_map
|
||||||
|
.entry("PIP_DISABLE_PIP_VERSION_CHECK".into())
|
||||||
|
.or_insert_with(|| "1".into());
|
||||||
|
env_map
|
||||||
|
.entry("NPM_CONFIG_OFFLINE".into())
|
||||||
|
.or_insert_with(|| "true".into());
|
||||||
|
env_map
|
||||||
|
.entry("CARGO_NET_OFFLINE".into())
|
||||||
|
.or_insert_with(|| "true".into());
|
||||||
|
env_map
|
||||||
|
.entry("GIT_HTTP_PROXY".into())
|
||||||
|
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||||
|
env_map
|
||||||
|
.entry("GIT_HTTPS_PROXY".into())
|
||||||
|
.or_insert_with(|| "http://127.0.0.1:9".into());
|
||||||
|
env_map
|
||||||
|
.entry("GIT_SSH_COMMAND".into())
|
||||||
|
.or_insert_with(|| "cmd /c exit 1".into());
|
||||||
|
env_map
|
||||||
|
.entry("GIT_ALLOW_PROTOCOLS".into())
|
||||||
|
.or_insert_with(|| "".into());
|
||||||
|
|
||||||
|
// Block interactive network tools that bypass HTTP(S) proxy settings, but
|
||||||
|
// allow curl/wget to run so commands like `curl --version` still succeed.
|
||||||
|
// Network access is disabled via proxy envs above.
|
||||||
|
let base = ensure_denybin(&["ssh", "scp"], None)?;
|
||||||
|
// Clean up any stale stubs from previous runs so real curl/wget can run.
|
||||||
|
for tool in ["curl", "wget"] {
|
||||||
|
for ext in [".bat", ".cmd"] {
|
||||||
|
let p = base.join(format!("{}{}", tool, ext));
|
||||||
|
if p.exists() {
|
||||||
|
let _ = std::fs::remove_file(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prepend_path(env_map, &base.to_string_lossy());
|
||||||
|
reorder_pathext_for_stubs(env_map);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
452
codex-rs/windows-sandbox-rs/src/lib.rs
Normal file
452
codex-rs/windows-sandbox-rs/src/lib.rs
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
macro_rules! windows_modules {
|
||||||
|
($($name:ident),+ $(,)?) => {
|
||||||
|
$(#[cfg(target_os = "windows")] mod $name;)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
windows_modules!(acl, allow, audit, cap, env, logging, policy, token, winutil);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use windows_impl::preflight_audit_everyone_writable;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use windows_impl::run_windows_sandbox_capture;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use windows_impl::CaptureResult;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub use stub::preflight_audit_everyone_writable;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub use stub::run_windows_sandbox_capture;
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub use stub::CaptureResult;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod windows_impl {
|
||||||
|
use super::acl::add_allow_ace;
|
||||||
|
use super::acl::allow_null_device;
|
||||||
|
use super::acl::revoke_ace;
|
||||||
|
use super::allow::compute_allow_paths;
|
||||||
|
use super::audit;
|
||||||
|
use super::cap::cap_sid_file;
|
||||||
|
use super::cap::load_or_create_cap_sids;
|
||||||
|
use super::env::apply_no_network_to_env;
|
||||||
|
use super::env::ensure_non_interactive_pager;
|
||||||
|
use super::env::normalize_null_device_env;
|
||||||
|
use super::logging::debug_log;
|
||||||
|
use super::logging::log_failure;
|
||||||
|
use super::logging::log_start;
|
||||||
|
use super::logging::log_success;
|
||||||
|
use super::policy::SandboxMode;
|
||||||
|
use super::policy::SandboxPolicy;
|
||||||
|
use super::token::convert_string_sid_to_sid;
|
||||||
|
use super::winutil::format_last_error;
|
||||||
|
use super::winutil::to_wide;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::ptr;
|
||||||
|
use windows_sys::Win32::Foundation::CloseHandle;
|
||||||
|
use windows_sys::Win32::Foundation::GetLastError;
|
||||||
|
use windows_sys::Win32::Foundation::SetHandleInformation;
|
||||||
|
use windows_sys::Win32::Foundation::HANDLE;
|
||||||
|
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
|
||||||
|
use windows_sys::Win32::System::Pipes::CreatePipe;
|
||||||
|
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||||||
|
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||||
|
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||||
|
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
||||||
|
use windows_sys::Win32::System::Threading::INFINITE;
|
||||||
|
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||||
|
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||||
|
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
||||||
|
|
||||||
|
type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE));
|
||||||
|
|
||||||
|
fn ensure_dir(p: &Path) -> Result<()> {
|
||||||
|
if let Some(d) = p.parent() {
|
||||||
|
std::fs::create_dir_all(d)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
||||||
|
let mut items: Vec<(String, String)> =
|
||||||
|
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||||
|
items.sort_by(|a, b| {
|
||||||
|
a.0.to_uppercase()
|
||||||
|
.cmp(&b.0.to_uppercase())
|
||||||
|
.then(a.0.cmp(&b.0))
|
||||||
|
});
|
||||||
|
let mut w: Vec<u16> = Vec::new();
|
||||||
|
for (k, v) in items {
|
||||||
|
let mut s = to_wide(format!("{}={}", k, v));
|
||||||
|
s.pop();
|
||||||
|
w.extend_from_slice(&s);
|
||||||
|
w.push(0);
|
||||||
|
}
|
||||||
|
w.push(0);
|
||||||
|
w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote a single Windows command-line argument following the rules used by
|
||||||
|
// CommandLineToArgvW/CRT so that spaces, quotes, and backslashes are preserved.
|
||||||
|
// Reference behavior matches Rust std::process::Command on Windows.
|
||||||
|
fn quote_windows_arg(arg: &str) -> String {
|
||||||
|
let needs_quotes = arg.is_empty()
|
||||||
|
|| arg
|
||||||
|
.chars()
|
||||||
|
.any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"'));
|
||||||
|
if !needs_quotes {
|
||||||
|
return arg.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut quoted = String::with_capacity(arg.len() + 2);
|
||||||
|
quoted.push('"');
|
||||||
|
let mut backslashes = 0;
|
||||||
|
for ch in arg.chars() {
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
backslashes += 1;
|
||||||
|
}
|
||||||
|
'"' => {
|
||||||
|
quoted.push_str(&"\\".repeat(backslashes * 2 + 1));
|
||||||
|
quoted.push('"');
|
||||||
|
backslashes = 0;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if backslashes > 0 {
|
||||||
|
quoted.push_str(&"\\".repeat(backslashes));
|
||||||
|
backslashes = 0;
|
||||||
|
}
|
||||||
|
quoted.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if backslashes > 0 {
|
||||||
|
quoted.push_str(&"\\".repeat(backslashes * 2));
|
||||||
|
}
|
||||||
|
quoted.push('"');
|
||||||
|
quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn setup_stdio_pipes() -> io::Result<PipeHandles> {
|
||||||
|
let mut in_r: HANDLE = 0;
|
||||||
|
let mut in_w: HANDLE = 0;
|
||||||
|
let mut out_r: HANDLE = 0;
|
||||||
|
let mut out_w: HANDLE = 0;
|
||||||
|
let mut err_r: HANDLE = 0;
|
||||||
|
let mut err_w: HANDLE = 0;
|
||||||
|
if CreatePipe(&mut in_r, &mut in_w, ptr::null_mut(), 0) == 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||||
|
}
|
||||||
|
if CreatePipe(&mut out_r, &mut out_w, ptr::null_mut(), 0) == 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||||
|
}
|
||||||
|
if CreatePipe(&mut err_r, &mut err_w, ptr::null_mut(), 0) == 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||||
|
}
|
||||||
|
if SetHandleInformation(in_r, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||||
|
}
|
||||||
|
if SetHandleInformation(out_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||||
|
}
|
||||||
|
if SetHandleInformation(err_w, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||||
|
return Err(io::Error::from_raw_os_error(GetLastError() as i32));
|
||||||
|
}
|
||||||
|
Ok(((in_r, in_w), (out_r, out_w), (err_r, err_w)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CaptureResult {
|
||||||
|
pub exit_code: i32,
|
||||||
|
pub stdout: Vec<u8>,
|
||||||
|
pub stderr: Vec<u8>,
|
||||||
|
pub timed_out: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preflight_audit_everyone_writable(
|
||||||
|
cwd: &Path,
|
||||||
|
env_map: &HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
audit::audit_everyone_writable(cwd, env_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_windows_sandbox_capture(
|
||||||
|
policy_json_or_preset: &str,
|
||||||
|
sandbox_policy_cwd: &Path,
|
||||||
|
command: Vec<String>,
|
||||||
|
cwd: &Path,
|
||||||
|
mut env_map: HashMap<String, String>,
|
||||||
|
timeout_ms: Option<u64>,
|
||||||
|
) -> Result<CaptureResult> {
|
||||||
|
let policy = SandboxPolicy::parse(policy_json_or_preset)?;
|
||||||
|
normalize_null_device_env(&mut env_map);
|
||||||
|
ensure_non_interactive_pager(&mut env_map);
|
||||||
|
apply_no_network_to_env(&mut env_map)?;
|
||||||
|
|
||||||
|
let current_dir = cwd.to_path_buf();
|
||||||
|
// for now, don't fail if we detect world-writable directories
|
||||||
|
// audit::audit_everyone_writable(¤t_dir, &env_map)?;
|
||||||
|
log_start(&command);
|
||||||
|
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
||||||
|
match &policy.0 {
|
||||||
|
SandboxMode::ReadOnly => {
|
||||||
|
let caps = load_or_create_cap_sids(sandbox_policy_cwd);
|
||||||
|
ensure_dir(&cap_sid_file(sandbox_policy_cwd))?;
|
||||||
|
fs::write(
|
||||||
|
cap_sid_file(sandbox_policy_cwd),
|
||||||
|
serde_json::to_string(&caps)?,
|
||||||
|
)?;
|
||||||
|
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
|
||||||
|
super::token::create_readonly_token_with_cap(psid)?
|
||||||
|
}
|
||||||
|
SandboxMode::WorkspaceWrite => {
|
||||||
|
let caps = load_or_create_cap_sids(sandbox_policy_cwd);
|
||||||
|
ensure_dir(&cap_sid_file(sandbox_policy_cwd))?;
|
||||||
|
fs::write(
|
||||||
|
cap_sid_file(sandbox_policy_cwd),
|
||||||
|
serde_json::to_string(&caps)?,
|
||||||
|
)?;
|
||||||
|
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||||
|
super::token::create_workspace_write_token_with_cap(psid)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if matches!(policy.0, SandboxMode::WorkspaceWrite) {
|
||||||
|
if let Ok(base) = super::token::get_current_token_for_restriction() {
|
||||||
|
if let Ok(bytes) = super::token::get_logon_sid_bytes(base) {
|
||||||
|
let mut tmp = bytes.clone();
|
||||||
|
let psid2 = tmp.as_mut_ptr() as *mut c_void;
|
||||||
|
allow_null_device(psid2);
|
||||||
|
}
|
||||||
|
windows_sys::Win32::Foundation::CloseHandle(base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let persist_aces = matches!(policy.0, SandboxMode::WorkspaceWrite);
|
||||||
|
let allow = compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||||
|
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
||||||
|
unsafe {
|
||||||
|
for p in &allow {
|
||||||
|
if let Ok(added) = add_allow_ace(p, psid_to_use) {
|
||||||
|
if added {
|
||||||
|
if persist_aces {
|
||||||
|
if p.is_dir() {
|
||||||
|
// best-effort seeding omitted intentionally
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
guards.push((p.clone(), psid_to_use));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allow_null_device(psid_to_use);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||||
|
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
|
||||||
|
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
|
||||||
|
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||||
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
||||||
|
si.hStdInput = in_r;
|
||||||
|
si.hStdOutput = out_w;
|
||||||
|
si.hStdError = err_w;
|
||||||
|
|
||||||
|
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||||
|
let cmdline_str = command
|
||||||
|
.iter()
|
||||||
|
.map(|a| quote_windows_arg(a))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||||||
|
let env_block = make_env_block(&env_map);
|
||||||
|
let desktop = to_wide("Winsta0\\Default");
|
||||||
|
si.lpDesktop = desktop.as_ptr() as *mut u16;
|
||||||
|
let spawn_res = unsafe {
|
||||||
|
CreateProcessAsUserW(
|
||||||
|
h_token,
|
||||||
|
ptr::null(),
|
||||||
|
cmdline.as_mut_ptr(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
1,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT,
|
||||||
|
env_block.as_ptr() as *mut c_void,
|
||||||
|
to_wide(cwd).as_ptr(),
|
||||||
|
&si,
|
||||||
|
&mut pi,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if spawn_res == 0 {
|
||||||
|
let err = unsafe { GetLastError() } as i32;
|
||||||
|
let dbg = format!(
|
||||||
|
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
|
||||||
|
err,
|
||||||
|
format_last_error(err),
|
||||||
|
cwd.display(),
|
||||||
|
cmdline_str,
|
||||||
|
env_block.len(),
|
||||||
|
si.dwFlags,
|
||||||
|
);
|
||||||
|
debug_log(&dbg);
|
||||||
|
unsafe {
|
||||||
|
CloseHandle(in_r);
|
||||||
|
CloseHandle(in_w);
|
||||||
|
CloseHandle(out_r);
|
||||||
|
CloseHandle(out_w);
|
||||||
|
CloseHandle(err_r);
|
||||||
|
CloseHandle(err_w);
|
||||||
|
CloseHandle(h_token);
|
||||||
|
}
|
||||||
|
return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
CloseHandle(in_r);
|
||||||
|
// Close the parent's stdin write end so the child sees EOF immediately.
|
||||||
|
CloseHandle(in_w);
|
||||||
|
CloseHandle(out_w);
|
||||||
|
CloseHandle(err_w);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx_out, rx_out) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||||
|
let (tx_err, rx_err) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||||
|
let t_out = std::thread::spawn(move || {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut tmp = [0u8; 8192];
|
||||||
|
loop {
|
||||||
|
let mut read_bytes: u32 = 0;
|
||||||
|
let ok = unsafe {
|
||||||
|
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||||
|
out_r,
|
||||||
|
tmp.as_mut_ptr(),
|
||||||
|
tmp.len() as u32,
|
||||||
|
&mut read_bytes,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 || read_bytes == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||||
|
}
|
||||||
|
let _ = tx_out.send(buf);
|
||||||
|
});
|
||||||
|
let t_err = std::thread::spawn(move || {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut tmp = [0u8; 8192];
|
||||||
|
loop {
|
||||||
|
let mut read_bytes: u32 = 0;
|
||||||
|
let ok = unsafe {
|
||||||
|
windows_sys::Win32::Storage::FileSystem::ReadFile(
|
||||||
|
err_r,
|
||||||
|
tmp.as_mut_ptr(),
|
||||||
|
tmp.len() as u32,
|
||||||
|
&mut read_bytes,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ok == 0 || read_bytes == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf.extend_from_slice(&tmp[..read_bytes as usize]);
|
||||||
|
}
|
||||||
|
let _ = tx_err.send(buf);
|
||||||
|
});
|
||||||
|
|
||||||
|
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
|
||||||
|
let res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
|
||||||
|
let timed_out = res == 0x0000_0102;
|
||||||
|
let mut exit_code_u32: u32 = 1;
|
||||||
|
if !timed_out {
|
||||||
|
unsafe {
|
||||||
|
GetExitCodeProcess(pi.hProcess, &mut exit_code_u32);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unsafe {
|
||||||
|
windows_sys::Win32::System::Threading::TerminateProcess(pi.hProcess, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if pi.hThread != 0 {
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
}
|
||||||
|
if pi.hProcess != 0 {
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
}
|
||||||
|
CloseHandle(h_token);
|
||||||
|
}
|
||||||
|
let _ = t_out.join();
|
||||||
|
let _ = t_err.join();
|
||||||
|
let stdout = rx_out.recv().unwrap_or_default();
|
||||||
|
let stderr = rx_err.recv().unwrap_or_default();
|
||||||
|
let exit_code = if timed_out {
|
||||||
|
128 + 64
|
||||||
|
} else {
|
||||||
|
exit_code_u32 as i32
|
||||||
|
};
|
||||||
|
|
||||||
|
if exit_code == 0 {
|
||||||
|
log_success(&command);
|
||||||
|
} else {
|
||||||
|
log_failure(&command, &format!("exit code {}", exit_code));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !persist_aces {
|
||||||
|
unsafe {
|
||||||
|
for (p, sid) in guards {
|
||||||
|
revoke_ace(&p, sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CaptureResult {
|
||||||
|
exit_code,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
timed_out,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
mod stub {
|
||||||
|
use anyhow::bail;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct CaptureResult {
|
||||||
|
pub exit_code: i32,
|
||||||
|
pub stdout: Vec<u8>,
|
||||||
|
pub stderr: Vec<u8>,
|
||||||
|
pub timed_out: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preflight_audit_everyone_writable(
|
||||||
|
_cwd: &Path,
|
||||||
|
_env_map: &HashMap<String, String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
bail!("Windows sandbox is only available on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_windows_sandbox_capture(
|
||||||
|
_policy_json_or_preset: &str,
|
||||||
|
_sandbox_policy_cwd: &Path,
|
||||||
|
_command: Vec<String>,
|
||||||
|
_cwd: &Path,
|
||||||
|
_env_map: HashMap<String, String>,
|
||||||
|
_timeout_ms: Option<u64>,
|
||||||
|
) -> Result<CaptureResult> {
|
||||||
|
bail!("Windows sandbox is only available on Windows")
|
||||||
|
}
|
||||||
|
}
|
||||||
47
codex-rs/windows-sandbox-rs/src/logging.rs
Normal file
47
codex-rs/windows-sandbox-rs/src/logging.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
||||||
|
pub const LOG_FILE_NAME: &str = "sandbox_commands.rust.log";
|
||||||
|
|
||||||
|
fn preview(command: &[String]) -> String {
|
||||||
|
let joined = command.join(" ");
|
||||||
|
if joined.len() <= LOG_COMMAND_PREVIEW_LIMIT {
|
||||||
|
joined
|
||||||
|
} else {
|
||||||
|
joined[..LOG_COMMAND_PREVIEW_LIMIT].to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_line(line: &str) {
|
||||||
|
if let Ok(mut f) = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(LOG_FILE_NAME)
|
||||||
|
{
|
||||||
|
let _ = writeln!(f, "{}", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_start(command: &[String]) {
|
||||||
|
let p = preview(command);
|
||||||
|
append_line(&format!("START: {}", p));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_success(command: &[String]) {
|
||||||
|
let p = preview(command);
|
||||||
|
append_line(&format!("SUCCESS: {}", p));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_failure(command: &[String], detail: &str) {
|
||||||
|
let p = preview(command);
|
||||||
|
append_line(&format!("FAILURE: {} ({})", p, detail));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging helper. Emits only when SBX_DEBUG=1 to avoid noisy logs.
|
||||||
|
pub fn debug_log(msg: &str) {
|
||||||
|
if std::env::var("SBX_DEBUG").ok().as_deref() == Some("1") {
|
||||||
|
append_line(&format!("DEBUG: {}", msg));
|
||||||
|
eprintln!("{}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
codex-rs/windows-sandbox-rs/src/policy.rs
Normal file
36
codex-rs/windows-sandbox-rs/src/policy.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PolicyJson {
|
||||||
|
pub mode: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub workspace_roots: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SandboxMode {
|
||||||
|
ReadOnly,
|
||||||
|
WorkspaceWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SandboxPolicy(pub SandboxMode);
|
||||||
|
|
||||||
|
impl SandboxPolicy {
|
||||||
|
pub fn parse(value: &str) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
"read-only" => Ok(SandboxPolicy(SandboxMode::ReadOnly)),
|
||||||
|
"workspace-write" => Ok(SandboxPolicy(SandboxMode::WorkspaceWrite)),
|
||||||
|
other => {
|
||||||
|
let pj: PolicyJson = serde_json::from_str(other)?;
|
||||||
|
Ok(match pj.mode.as_str() {
|
||||||
|
"read-only" => SandboxPolicy(SandboxMode::ReadOnly),
|
||||||
|
"workspace-write" => SandboxPolicy(SandboxMode::WorkspaceWrite),
|
||||||
|
_ => SandboxPolicy(SandboxMode::ReadOnly),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
codex-rs/windows-sandbox-rs/src/process.rs
Normal file
193
codex-rs/windows-sandbox-rs/src/process.rs
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
use crate::logging;
|
||||||
|
use crate::winutil::format_last_error;
|
||||||
|
use crate::winutil::to_wide;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use std::path::Path;
|
||||||
|
use windows_sys::Win32::Foundation::GetLastError;
|
||||||
|
use windows_sys::Win32::Foundation::SetHandleInformation;
|
||||||
|
use windows_sys::Win32::Foundation::HANDLE;
|
||||||
|
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
|
||||||
|
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
|
||||||
|
use windows_sys::Win32::System::Console::GetStdHandle;
|
||||||
|
use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;
|
||||||
|
use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
|
||||||
|
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
||||||
|
use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject;
|
||||||
|
use windows_sys::Win32::System::JobObjects::CreateJobObjectW;
|
||||||
|
use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation;
|
||||||
|
use windows_sys::Win32::System::JobObjects::SetInformationJobObject;
|
||||||
|
use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION;
|
||||||
|
use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||||
|
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
|
||||||
|
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
|
||||||
|
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||||
|
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
|
||||||
|
use windows_sys::Win32::System::Threading::INFINITE;
|
||||||
|
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
|
||||||
|
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
|
||||||
|
use windows_sys::Win32::System::Threading::STARTUPINFOW;
|
||||||
|
|
||||||
|
pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
|
||||||
|
let mut items: Vec<(String, String)> =
|
||||||
|
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||||
|
items.sort_by(|a, b| {
|
||||||
|
a.0.to_uppercase()
|
||||||
|
.cmp(&b.0.to_uppercase())
|
||||||
|
.then(a.0.cmp(&b.0))
|
||||||
|
});
|
||||||
|
let mut w: Vec<u16> = Vec::new();
|
||||||
|
for (k, v) in items {
|
||||||
|
let mut s = to_wide(format!("{}={}", k, v));
|
||||||
|
s.pop();
|
||||||
|
w.extend_from_slice(&s);
|
||||||
|
w.push(0);
|
||||||
|
}
|
||||||
|
w.push(0);
|
||||||
|
w
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quote_arg(a: &str) -> String {
|
||||||
|
let needs_quote = a.is_empty() || a.chars().any(|ch| ch.is_whitespace() || ch == '"');
|
||||||
|
if !needs_quote {
|
||||||
|
return a.to_string();
|
||||||
|
}
|
||||||
|
let mut out = String::from("\"");
|
||||||
|
let mut bs: usize = 0;
|
||||||
|
for ch in a.chars() {
|
||||||
|
if (ch as u32) == 92 {
|
||||||
|
bs += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ch == '"' {
|
||||||
|
out.push_str(&"\\".repeat(bs * 2 + 1));
|
||||||
|
out.push('"');
|
||||||
|
bs = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if bs > 0 {
|
||||||
|
out.push_str(&"\\".repeat(bs * 2));
|
||||||
|
bs = 0;
|
||||||
|
}
|
||||||
|
out.push(ch);
|
||||||
|
}
|
||||||
|
if bs > 0 {
|
||||||
|
out.push_str(&"\\".repeat(bs * 2));
|
||||||
|
}
|
||||||
|
out.push('"');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
unsafe fn ensure_inheritable_stdio(si: &mut STARTUPINFOW) -> Result<()> {
|
||||||
|
for kind in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
|
||||||
|
let h = GetStdHandle(kind);
|
||||||
|
if h == 0 || h == INVALID_HANDLE_VALUE {
|
||||||
|
return Err(anyhow!("GetStdHandle failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
if SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT) == 0 {
|
||||||
|
return Err(anyhow!("SetHandleInformation failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
si.dwFlags |= STARTF_USESTDHANDLES;
|
||||||
|
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn create_process_as_user(
|
||||||
|
h_token: HANDLE,
|
||||||
|
argv: &[String],
|
||||||
|
cwd: &Path,
|
||||||
|
env_map: &HashMap<String, String>,
|
||||||
|
) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> {
|
||||||
|
let cmdline_str = argv
|
||||||
|
.iter()
|
||||||
|
.map(|a| quote_arg(a))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
|
||||||
|
let env_block = make_env_block(env_map);
|
||||||
|
let mut si: STARTUPINFOW = std::mem::zeroed();
|
||||||
|
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||||
|
// Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED
|
||||||
|
// if lpDesktop is not set when launching with a restricted token.
|
||||||
|
// Point explicitly at the interactive desktop.
|
||||||
|
let desktop = to_wide("Winsta0\\Default");
|
||||||
|
si.lpDesktop = desktop.as_ptr() as *mut u16;
|
||||||
|
ensure_inheritable_stdio(&mut si)?;
|
||||||
|
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
|
||||||
|
let ok = CreateProcessAsUserW(
|
||||||
|
h_token,
|
||||||
|
std::ptr::null(),
|
||||||
|
cmdline.as_mut_ptr(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
1,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT,
|
||||||
|
env_block.as_ptr() as *mut c_void,
|
||||||
|
to_wide(cwd).as_ptr(),
|
||||||
|
&si,
|
||||||
|
&mut pi,
|
||||||
|
);
|
||||||
|
if ok == 0 {
|
||||||
|
let err = GetLastError() as i32;
|
||||||
|
let msg = format!(
|
||||||
|
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}",
|
||||||
|
err,
|
||||||
|
format_last_error(err),
|
||||||
|
cwd.display(),
|
||||||
|
cmdline_str,
|
||||||
|
env_block.len(),
|
||||||
|
si.dwFlags,
|
||||||
|
);
|
||||||
|
logging::debug_log(&msg);
|
||||||
|
return Err(anyhow!("CreateProcessAsUserW failed: {}", err));
|
||||||
|
}
|
||||||
|
Ok((pi, si))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn wait_process_and_exitcode(pi: &PROCESS_INFORMATION) -> Result<i32> {
|
||||||
|
let res = WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
|
if res != 0 {
|
||||||
|
return Err(anyhow!("WaitForSingleObject failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
let mut code: u32 = 0;
|
||||||
|
if GetExitCodeProcess(pi.hProcess, &mut code) == 0 {
|
||||||
|
return Err(anyhow!("GetExitCodeProcess failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
Ok(code as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn create_job_kill_on_close() -> Result<HANDLE> {
|
||||||
|
let h = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
|
||||||
|
if h == 0 {
|
||||||
|
return Err(anyhow!("CreateJobObjectW failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
|
||||||
|
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||||
|
let ok = SetInformationJobObject(
|
||||||
|
h,
|
||||||
|
JobObjectExtendedLimitInformation,
|
||||||
|
&mut limits as *mut _ as *mut c_void,
|
||||||
|
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||||
|
);
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"SetInformationJobObject failed: {}",
|
||||||
|
GetLastError()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn assign_to_job(h_job: HANDLE, h_process: HANDLE) -> Result<()> {
|
||||||
|
if AssignProcessToJobObject(h_job, h_process) == 0 {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"AssignProcessToJobObject failed: {}",
|
||||||
|
GetLastError()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
272
codex-rs/windows-sandbox-rs/src/token.rs
Normal file
272
codex-rs/windows-sandbox-rs/src/token.rs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
use crate::winutil::to_wide;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::ffi::c_void;
|
||||||
|
use windows_sys::Win32::Foundation::CloseHandle;
|
||||||
|
use windows_sys::Win32::Foundation::GetLastError;
|
||||||
|
use windows_sys::Win32::Foundation::HANDLE;
|
||||||
|
use windows_sys::Win32::Foundation::LUID;
|
||||||
|
use windows_sys::Win32::Security::AdjustTokenPrivileges;
|
||||||
|
use windows_sys::Win32::Security::CopySid;
|
||||||
|
use windows_sys::Win32::Security::CreateRestrictedToken;
|
||||||
|
use windows_sys::Win32::Security::CreateWellKnownSid;
|
||||||
|
use windows_sys::Win32::Security::GetLengthSid;
|
||||||
|
use windows_sys::Win32::Security::GetTokenInformation;
|
||||||
|
use windows_sys::Win32::Security::LookupPrivilegeValueW;
|
||||||
|
|
||||||
|
use windows_sys::Win32::Security::TokenGroups;
|
||||||
|
use windows_sys::Win32::Security::SID_AND_ATTRIBUTES;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_ADJUST_DEFAULT;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_ADJUST_PRIVILEGES;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_ADJUST_SESSIONID;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_ASSIGN_PRIMARY;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_DUPLICATE;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_PRIVILEGES;
|
||||||
|
use windows_sys::Win32::Security::TOKEN_QUERY;
|
||||||
|
use windows_sys::Win32::System::Threading::GetCurrentProcess;
|
||||||
|
|
||||||
|
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
||||||
|
const LUA_TOKEN: u32 = 0x04;
|
||||||
|
const WRITE_RESTRICTED: u32 = 0x08;
|
||||||
|
const WIN_WORLD_SID: i32 = 1;
|
||||||
|
const SE_GROUP_LOGON_ID: u32 = 0xC0000000;
|
||||||
|
|
||||||
|
pub unsafe fn world_sid() -> Result<Vec<u8>> {
|
||||||
|
let mut size: u32 = 0;
|
||||||
|
CreateWellKnownSid(
|
||||||
|
WIN_WORLD_SID,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut size,
|
||||||
|
);
|
||||||
|
let mut buf: Vec<u8> = vec![0u8; size as usize];
|
||||||
|
let ok = CreateWellKnownSid(
|
||||||
|
WIN_WORLD_SID,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
buf.as_mut_ptr() as *mut c_void,
|
||||||
|
&mut size,
|
||||||
|
);
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(anyhow!("CreateWellKnownSid failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn convert_string_sid_to_sid(s: &str) -> Option<*mut c_void> {
|
||||||
|
#[link(name = "advapi32")]
|
||||||
|
extern "system" {
|
||||||
|
fn ConvertStringSidToSidW(StringSid: *const u16, Sid: *mut *mut c_void) -> i32;
|
||||||
|
}
|
||||||
|
let mut psid: *mut c_void = std::ptr::null_mut();
|
||||||
|
let ok = unsafe { ConvertStringSidToSidW(to_wide(s).as_ptr(), &mut psid) };
|
||||||
|
if ok != 0 {
|
||||||
|
Some(psid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_current_token_for_restriction() -> Result<HANDLE> {
|
||||||
|
let desired = TOKEN_DUPLICATE
|
||||||
|
| TOKEN_QUERY
|
||||||
|
| TOKEN_ASSIGN_PRIMARY
|
||||||
|
| TOKEN_ADJUST_DEFAULT
|
||||||
|
| TOKEN_ADJUST_SESSIONID
|
||||||
|
| TOKEN_ADJUST_PRIVILEGES;
|
||||||
|
let mut h: HANDLE = 0;
|
||||||
|
#[link(name = "advapi32")]
|
||||||
|
extern "system" {
|
||||||
|
fn OpenProcessToken(
|
||||||
|
ProcessHandle: HANDLE,
|
||||||
|
DesiredAccess: u32,
|
||||||
|
TokenHandle: *mut HANDLE,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
let ok = unsafe { OpenProcessToken(GetCurrentProcess(), desired, &mut h) };
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(anyhow!("OpenProcessToken failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
Ok(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn get_logon_sid_bytes(h_token: HANDLE) -> Result<Vec<u8>> {
|
||||||
|
unsafe fn scan_token_groups_for_logon(h: HANDLE) -> Option<Vec<u8>> {
|
||||||
|
let mut needed: u32 = 0;
|
||||||
|
GetTokenInformation(h, TokenGroups, std::ptr::null_mut(), 0, &mut needed);
|
||||||
|
if needed == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut buf: Vec<u8> = vec![0u8; needed as usize];
|
||||||
|
let ok = GetTokenInformation(
|
||||||
|
h,
|
||||||
|
TokenGroups,
|
||||||
|
buf.as_mut_ptr() as *mut c_void,
|
||||||
|
needed,
|
||||||
|
&mut needed,
|
||||||
|
);
|
||||||
|
if ok == 0 || (needed as usize) < std::mem::size_of::<u32>() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let group_count = std::ptr::read_unaligned(buf.as_ptr() as *const u32) as usize;
|
||||||
|
// TOKEN_GROUPS layout is: DWORD GroupCount; SID_AND_ATTRIBUTES Groups[];
|
||||||
|
// On 64-bit, Groups is aligned to pointer alignment after 4-byte GroupCount.
|
||||||
|
let after_count = unsafe { buf.as_ptr().add(std::mem::size_of::<u32>()) } as usize;
|
||||||
|
let align = std::mem::align_of::<SID_AND_ATTRIBUTES>();
|
||||||
|
let aligned = (after_count + (align - 1)) & !(align - 1);
|
||||||
|
let groups_ptr = aligned as *const SID_AND_ATTRIBUTES;
|
||||||
|
for i in 0..group_count {
|
||||||
|
let entry: SID_AND_ATTRIBUTES = std::ptr::read_unaligned(groups_ptr.add(i));
|
||||||
|
if (entry.Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID {
|
||||||
|
let sid = entry.Sid;
|
||||||
|
let sid_len = GetLengthSid(sid);
|
||||||
|
if sid_len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut out = vec![0u8; sid_len as usize];
|
||||||
|
if CopySid(sid_len, out.as_mut_ptr() as *mut c_void, sid) == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(v) = scan_token_groups_for_logon(h_token) {
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct TOKEN_LINKED_TOKEN {
|
||||||
|
linked_token: HANDLE,
|
||||||
|
}
|
||||||
|
const TOKEN_LINKED_TOKEN_CLASS: i32 = 19; // TokenLinkedToken
|
||||||
|
let mut ln_needed: u32 = 0;
|
||||||
|
GetTokenInformation(
|
||||||
|
h_token,
|
||||||
|
TOKEN_LINKED_TOKEN_CLASS,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
&mut ln_needed,
|
||||||
|
);
|
||||||
|
if ln_needed >= std::mem::size_of::<TOKEN_LINKED_TOKEN>() as u32 {
|
||||||
|
let mut ln_buf: Vec<u8> = vec![0u8; ln_needed as usize];
|
||||||
|
let ok = GetTokenInformation(
|
||||||
|
h_token,
|
||||||
|
TOKEN_LINKED_TOKEN_CLASS,
|
||||||
|
ln_buf.as_mut_ptr() as *mut c_void,
|
||||||
|
ln_needed,
|
||||||
|
&mut ln_needed,
|
||||||
|
);
|
||||||
|
if ok != 0 {
|
||||||
|
let lt: TOKEN_LINKED_TOKEN =
|
||||||
|
std::ptr::read_unaligned(ln_buf.as_ptr() as *const TOKEN_LINKED_TOKEN);
|
||||||
|
if lt.linked_token != 0 {
|
||||||
|
let res = scan_token_groups_for_logon(lt.linked_token);
|
||||||
|
CloseHandle(lt.linked_token);
|
||||||
|
if let Some(v) = res {
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("Logon SID not present on token"))
|
||||||
|
}
|
||||||
|
unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
||||||
|
let mut luid = LUID {
|
||||||
|
LowPart: 0,
|
||||||
|
HighPart: 0,
|
||||||
|
};
|
||||||
|
let ok = LookupPrivilegeValueW(std::ptr::null(), to_wide(name).as_ptr(), &mut luid);
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(anyhow!("LookupPrivilegeValueW failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
let mut tp: TOKEN_PRIVILEGES = std::mem::zeroed();
|
||||||
|
tp.PrivilegeCount = 1;
|
||||||
|
tp.Privileges[0].Luid = luid;
|
||||||
|
tp.Privileges[0].Attributes = 0x00000002; // SE_PRIVILEGE_ENABLED
|
||||||
|
let ok2 = AdjustTokenPrivileges(h_token, 0, &tp, 0, std::ptr::null_mut(), std::ptr::null_mut());
|
||||||
|
if ok2 == 0 {
|
||||||
|
return Err(anyhow!("AdjustTokenPrivileges failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
let err = GetLastError();
|
||||||
|
if err != 0 {
|
||||||
|
return Err(anyhow!("AdjustTokenPrivileges error {}", err));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// removed unused create_write_restricted_token_strict
|
||||||
|
|
||||||
|
pub unsafe fn create_workspace_write_token_with_cap(
|
||||||
|
psid_capability: *mut c_void,
|
||||||
|
) -> Result<(HANDLE, *mut c_void)> {
|
||||||
|
let base = get_current_token_for_restriction()?;
|
||||||
|
let mut logon_sid_bytes = get_logon_sid_bytes(base)?;
|
||||||
|
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||||
|
let mut everyone = world_sid()?;
|
||||||
|
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||||
|
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||||
|
// Exact set and order: Capability, Logon, Everyone
|
||||||
|
entries[0].Sid = psid_capability;
|
||||||
|
entries[0].Attributes = 0;
|
||||||
|
entries[1].Sid = psid_logon;
|
||||||
|
entries[1].Attributes = 0;
|
||||||
|
entries[2].Sid = psid_everyone;
|
||||||
|
entries[2].Attributes = 0;
|
||||||
|
let mut new_token: HANDLE = 0;
|
||||||
|
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||||
|
let ok = CreateRestrictedToken(
|
||||||
|
base,
|
||||||
|
flags,
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
3,
|
||||||
|
entries.as_mut_ptr(),
|
||||||
|
&mut new_token,
|
||||||
|
);
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||||
|
Ok((new_token, psid_capability))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn create_readonly_token_with_cap(
|
||||||
|
psid_capability: *mut c_void,
|
||||||
|
) -> Result<(HANDLE, *mut c_void)> {
|
||||||
|
let base = get_current_token_for_restriction()?;
|
||||||
|
let mut logon_sid_bytes = get_logon_sid_bytes(base)?;
|
||||||
|
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||||
|
let mut everyone = world_sid()?;
|
||||||
|
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||||
|
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||||
|
// Exact set and order: Capability, Logon, Everyone
|
||||||
|
entries[0].Sid = psid_capability;
|
||||||
|
entries[0].Attributes = 0;
|
||||||
|
entries[1].Sid = psid_logon;
|
||||||
|
entries[1].Attributes = 0;
|
||||||
|
entries[2].Sid = psid_everyone;
|
||||||
|
entries[2].Attributes = 0;
|
||||||
|
let mut new_token: HANDLE = 0;
|
||||||
|
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||||
|
let ok = CreateRestrictedToken(
|
||||||
|
base,
|
||||||
|
flags,
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
3,
|
||||||
|
entries.as_mut_ptr(),
|
||||||
|
&mut new_token,
|
||||||
|
);
|
||||||
|
if ok == 0 {
|
||||||
|
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||||
|
}
|
||||||
|
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||||
|
Ok((new_token, psid_capability))
|
||||||
|
}
|
||||||
43
codex-rs/windows-sandbox-rs/src/winutil.rs
Normal file
43
codex-rs/windows-sandbox-rs/src/winutil.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
use windows_sys::Win32::Foundation::LocalFree;
|
||||||
|
use windows_sys::Win32::Foundation::HLOCAL;
|
||||||
|
use windows_sys::Win32::System::Diagnostics::Debug::FormatMessageW;
|
||||||
|
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER;
|
||||||
|
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_FROM_SYSTEM;
|
||||||
|
use windows_sys::Win32::System::Diagnostics::Debug::FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||||
|
|
||||||
|
pub fn to_wide<S: AsRef<OsStr>>(s: S) -> Vec<u16> {
|
||||||
|
let mut v: Vec<u16> = s.as_ref().encode_wide().collect();
|
||||||
|
v.push(0);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Produce a readable description for a Win32 error code.
|
||||||
|
pub fn format_last_error(err: i32) -> String {
|
||||||
|
unsafe {
|
||||||
|
let mut buf_ptr: *mut u16 = std::ptr::null_mut();
|
||||||
|
let flags = FORMAT_MESSAGE_ALLOCATE_BUFFER
|
||||||
|
| FORMAT_MESSAGE_FROM_SYSTEM
|
||||||
|
| FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||||
|
let len = FormatMessageW(
|
||||||
|
flags,
|
||||||
|
std::ptr::null(),
|
||||||
|
err as u32,
|
||||||
|
0,
|
||||||
|
// FORMAT_MESSAGE_ALLOCATE_BUFFER expects a pointer to receive the allocated buffer.
|
||||||
|
// Cast &mut *mut u16 to *mut u16 as required by windows-sys.
|
||||||
|
(&mut buf_ptr as *mut *mut u16) as *mut u16,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
if len == 0 || buf_ptr.is_null() {
|
||||||
|
return format!("Win32 error {}", err);
|
||||||
|
}
|
||||||
|
let slice = std::slice::from_raw_parts(buf_ptr, len as usize);
|
||||||
|
let mut s = String::from_utf16_lossy(slice);
|
||||||
|
s = s.trim().to_string();
|
||||||
|
let _ = LocalFree(buf_ptr as HLOCAL);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,12 @@ The mechanism Codex uses to enforce the sandbox policy depends on your OS:
|
|||||||
|
|
||||||
- **macOS 12+** uses **Apple Seatbelt**. Codex invokes `sandbox-exec` with a profile that corresponds to the selected `--sandbox` mode, constraining filesystem and network access at the OS level.
|
- **macOS 12+** uses **Apple Seatbelt**. Codex invokes `sandbox-exec` with a profile that corresponds to the selected `--sandbox` mode, constraining filesystem and network access at the OS level.
|
||||||
- **Linux** combines **Landlock** and **seccomp** APIs to approximate the same guarantees. Kernel support is required; older kernels may not expose the necessary features.
|
- **Linux** combines **Landlock** and **seccomp** APIs to approximate the same guarantees. Kernel support is required; older kernels may not expose the necessary features.
|
||||||
|
- **Windows (experimental)**:
|
||||||
|
- Launches commands inside a restricted token derived from an AppContainer profile.
|
||||||
|
- Grants only specifically requested filesystem capabilities by attaching capability SIDs to that profile.
|
||||||
|
- Disables outbound network access by overriding proxy-related environment variables and inserting stub executables for common network tools.
|
||||||
|
|
||||||
|
Windows sandbox support remains highly experimental. It cannot prevent file writes, deletions, or creations in any directory where the Everyone SID already has write permissions (for example, world-writable folders).
|
||||||
|
|
||||||
In containerized Linux environments (for example Docker), sandboxing may not work when the host or container configuration does not expose Landlock/seccomp. In those cases, configure the container to provide the isolation you need and run Codex with `--sandbox danger-full-access` (or the shorthand `--dangerously-bypass-approvals-and-sandbox`) inside that container.
|
In containerized Linux environments (for example Docker), sandboxing may not work when the host or container configuration does not expose Landlock/seccomp. In those cases, configure the container to provide the isolation you need and run Codex with `--sandbox danger-full-access` (or the shorthand `--dangerously-bypass-approvals-and-sandbox`) inside that container.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user