Merge 4516726e36 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2026-02-27 17:09:09 -08:00
committed by GitHub
4 changed files with 200 additions and 113 deletions

View File

@@ -21,6 +21,7 @@ pub mod responses;
pub mod streaming_sse;
pub mod test_codex;
pub mod test_codex_exec;
pub mod zsh_fork;
#[ctor]
fn enable_deterministic_unified_exec_process_ids_for_tests() {

View File

@@ -0,0 +1,118 @@
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use codex_core::config::Config;
use codex_core::config::Constrained;
use codex_core::features::Feature;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use crate::test_codex::TestCodex;
use crate::test_codex::test_codex;
#[derive(Clone)]
pub struct ZshForkRuntime {
zsh_path: PathBuf,
main_execve_wrapper_exe: PathBuf,
}
impl ZshForkRuntime {
fn apply_to_config(
&self,
config: &mut Config,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
) {
config.features.enable(Feature::ShellTool);
config.features.enable(Feature::ShellZshFork);
config.zsh_path = Some(self.zsh_path.clone());
config.main_execve_wrapper_exe = Some(self.main_execve_wrapper_exe.clone());
config.permissions.allow_login_shell = false;
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy);
}
}
pub fn restrictive_workspace_write_policy() -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
pub fn zsh_fork_runtime(test_name: &str) -> Result<Option<ZshForkRuntime>> {
let Some(zsh_path) = find_test_zsh_path()? else {
return Ok(None);
};
if !supports_exec_wrapper_intercept(&zsh_path) {
eprintln!(
"skipping {test_name}: zsh does not support EXEC_WRAPPER intercepts ({})",
zsh_path.display()
);
return Ok(None);
}
let Ok(main_execve_wrapper_exe) = codex_utils_cargo_bin::cargo_bin("codex-execve-wrapper")
else {
eprintln!("skipping {test_name}: unable to resolve `codex-execve-wrapper` binary");
return Ok(None);
};
Ok(Some(ZshForkRuntime {
zsh_path,
main_execve_wrapper_exe,
}))
}
pub async fn build_zsh_fork_test<F>(
server: &wiremock::MockServer,
runtime: ZshForkRuntime,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
pre_build_hook: F,
) -> Result<TestCodex>
where
F: FnOnce(&Path) + Send + 'static,
{
let mut builder = test_codex()
.with_pre_build_hook(pre_build_hook)
.with_config(move |config| {
runtime.apply_to_config(config, approval_policy, sandbox_policy);
});
builder.build(server).await
}
fn find_test_zsh_path() -> Result<Option<PathBuf>> {
let repo_root = codex_utils_cargo_bin::repo_root()?;
let dotslash_zsh = repo_root.join("codex-rs/app-server/tests/suite/zsh");
if !dotslash_zsh.is_file() {
eprintln!(
"skipping zsh-fork test: shared zsh DotSlash file not found at {}",
dotslash_zsh.display()
);
return Ok(None);
}
match crate::fetch_dotslash_file(&dotslash_zsh, None) {
Ok(path) => Ok(Some(path)),
Err(error) => {
eprintln!("skipping zsh-fork test: failed to fetch zsh via dotslash: {error:#}");
Ok(None)
}
}
}
fn supports_exec_wrapper_intercept(zsh_path: &Path) -> bool {
let status = std::process::Command::new(zsh_path)
.arg("-fc")
.arg("/usr/bin/true")
.env("EXEC_WRAPPER", "/usr/bin/false")
.status();
match status {
Ok(status) => !status.success(),
Err(_) => false,
}
}

View File

@@ -35,6 +35,9 @@ use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_with_timeout;
use core_test_support::zsh_fork::build_zsh_fork_test;
use core_test_support::zsh_fork::restrictive_workspace_write_policy;
use core_test_support::zsh_fork::zsh_fork_runtime;
use pretty_assertions::assert_eq;
use regex_lite::Regex;
use serde_json::Value;
@@ -1978,6 +1981,81 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg(unix)]
async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
skip_if_no_network!(Ok(()));
let Some(runtime) = zsh_fork_runtime("zsh-fork prefix rule unsandboxed test")? else {
return Ok(());
};
let approval_policy = AskForApproval::Never;
let sandbox_policy = restrictive_workspace_write_policy();
let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let outside_path = outside_dir
.path()
.join("zsh-fork-prefix-rule-unsandboxed.txt");
let command = format!("touch {outside_path:?}");
let rules = r#"prefix_rule(pattern=["touch"], decision="allow")"#.to_string();
let server = start_mock_server().await;
let outside_path_for_hook = outside_path.clone();
let test = build_zsh_fork_test(
&server,
runtime,
approval_policy,
sandbox_policy.clone(),
move |home| {
let _ = fs::remove_file(&outside_path_for_hook);
let rules_dir = home.join("rules");
fs::create_dir_all(&rules_dir).unwrap();
fs::write(rules_dir.join("default.rules"), &rules).unwrap();
},
)
.await?;
let call_id = "zsh-fork-prefix-rule-unsandboxed";
let event = shell_event(call_id, &command, 1_000, SandboxPermissions::UseDefault)?;
let _ = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-zsh-fork-prefix-1"),
event,
ev_completed("resp-zsh-fork-prefix-1"),
]),
)
.await;
let results = mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-zsh-fork-prefix-1", "done"),
ev_completed("resp-zsh-fork-prefix-2"),
]),
)
.await;
submit_turn(
&test,
"run allowed touch under zsh fork",
approval_policy,
sandbox_policy,
)
.await?;
wait_for_completion_without_approval(&test).await;
let result = parse_result(&results.single_request().function_call_output(call_id));
assert_eq!(result.exit_code.unwrap_or(0), 0);
assert!(
outside_path.exists(),
"expected matched prefix_rule to rerun touch unsandboxed; output: {}",
result.stdout
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
#[cfg(unix)]
async fn invalid_requested_prefix_rule_falls_back_for_compound_command() -> Result<()> {

View File

@@ -2,8 +2,6 @@
#![cfg(unix)]
use anyhow::Result;
use codex_core::config::Config;
use codex_core::features::Feature;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -18,9 +16,11 @@ use core_test_support::responses::mount_function_call_agent_response;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_match;
use core_test_support::zsh_fork::build_zsh_fork_test;
use core_test_support::zsh_fork::restrictive_workspace_write_policy;
use core_test_support::zsh_fork::zsh_fork_runtime;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::fs;
@@ -117,116 +117,6 @@ description: {name} skill
Ok(script_path)
}
fn find_test_zsh_path() -> Result<Option<PathBuf>> {
use core_test_support::fetch_dotslash_file;
let repo_root = codex_utils_cargo_bin::repo_root()?;
let dotslash_zsh = repo_root.join("codex-rs/app-server/tests/suite/zsh");
if !dotslash_zsh.is_file() {
eprintln!(
"skipping zsh-fork skill test: shared zsh DotSlash file not found at {}",
dotslash_zsh.display()
);
return Ok(None);
}
match fetch_dotslash_file(&dotslash_zsh, None) {
Ok(path) => Ok(Some(path)),
Err(error) => {
eprintln!("skipping zsh-fork skill test: failed to fetch zsh via dotslash: {error:#}");
Ok(None)
}
}
}
fn supports_exec_wrapper_intercept(zsh_path: &Path) -> bool {
let status = std::process::Command::new(zsh_path)
.arg("-fc")
.arg("/usr/bin/true")
.env("EXEC_WRAPPER", "/usr/bin/false")
.status();
match status {
Ok(status) => !status.success(),
Err(_) => false,
}
}
#[derive(Clone)]
struct ZshForkRuntime {
zsh_path: PathBuf,
main_execve_wrapper_exe: PathBuf,
}
impl ZshForkRuntime {
fn apply_to_config(
&self,
config: &mut Config,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
) {
use codex_config::Constrained;
config.features.enable(Feature::ShellTool);
config.features.enable(Feature::ShellZshFork);
config.zsh_path = Some(self.zsh_path.clone());
config.main_execve_wrapper_exe = Some(self.main_execve_wrapper_exe.clone());
config.permissions.allow_login_shell = false;
config.permissions.approval_policy = Constrained::allow_any(approval_policy);
config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy);
}
}
fn restrictive_workspace_write_policy() -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
read_only_access: Default::default(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}
}
fn zsh_fork_runtime(test_name: &str) -> Result<Option<ZshForkRuntime>> {
let Some(zsh_path) = find_test_zsh_path()? else {
return Ok(None);
};
if !supports_exec_wrapper_intercept(&zsh_path) {
eprintln!(
"skipping {test_name}: zsh does not support EXEC_WRAPPER intercepts ({})",
zsh_path.display()
);
return Ok(None);
}
let Ok(main_execve_wrapper_exe) = codex_utils_cargo_bin::cargo_bin("codex-execve-wrapper")
else {
eprintln!("skipping {test_name}: unable to resolve `codex-execve-wrapper` binary");
return Ok(None);
};
Ok(Some(ZshForkRuntime {
zsh_path,
main_execve_wrapper_exe,
}))
}
async fn build_zsh_fork_test<F>(
server: &wiremock::MockServer,
runtime: ZshForkRuntime,
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
pre_build_hook: F,
) -> Result<TestCodex>
where
F: FnOnce(&Path) + Send + 'static,
{
let mut builder = test_codex()
.with_pre_build_hook(pre_build_hook)
.with_config(move |config| {
runtime.apply_to_config(config, approval_policy, sandbox_policy);
});
builder.build(server).await
}
fn skill_script_command(test: &TestCodex, script_name: &str) -> Result<(String, String)> {
let script_path = fs::canonicalize(
test.codex_home_path()