fix: sandbox zsh fork unified exec trampoline

This commit is contained in:
Michael Bolin
2026-06-07 09:25:44 -07:00
parent 2ee3358c00
commit cc8196ca02
6 changed files with 821 additions and 12 deletions

View File

@@ -630,9 +630,11 @@ impl EscalationPolicy for CoreShellActionProvider {
let decision_driven_by_policy =
Self::decision_driven_by_policy(&evaluation.matched_rules, evaluation.decision);
let unsandboxed_allowed = unsandboxed_execution_allowed(&self.file_system_sandbox_policy);
let needs_escalation = unsandboxed_allowed
&& (self.sandbox_permissions.requires_escalated_permissions()
|| decision_driven_by_policy);
let needs_escalation = match self.sandbox_permissions {
SandboxPermissions::UseDefault => unsandboxed_allowed && decision_driven_by_policy,
SandboxPermissions::RequireEscalated => unsandboxed_allowed,
SandboxPermissions::WithAdditionalPermissions => true,
};
let decision_source = if decision_driven_by_policy {
DecisionSource::PrefixRule
@@ -858,7 +860,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
EscalationExecution::Unsandboxed => PreparedExec {
command,
cwd: workdir.to_path_buf(),
env,
env: exec_env_for_sandbox_permissions(&env, SandboxPermissions::RequireEscalated),
arg0: Some(first_arg.clone()),
},
EscalationExecution::TurnDefault => {

View File

@@ -1,4 +1,5 @@
use super::CoreShellActionProvider;
use super::CoreShellCommandExecutor;
use super::InterceptedExecPolicyContext;
use super::ParsedShellCommand;
use super::commands_for_intercepted_exec_policy;
@@ -16,6 +17,8 @@ use codex_execpolicy::PolicyParser;
use codex_execpolicy::RuleMatch;
use codex_hooks::Hooks;
use codex_hooks::HooksConfig;
use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
use codex_network_proxy::PROXY_ENV_KEYS;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions;
@@ -30,13 +33,16 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::GuardianCommandSource;
use codex_sandboxing::SandboxType;
use codex_sandboxing::policy_transforms::effective_permission_profile;
use codex_shell_escalation::EscalationExecution;
use codex_shell_escalation::EscalationPermissions;
use codex_shell_escalation::ExecResult;
use codex_shell_escalation::ResolvedPermissionProfile;
use codex_shell_escalation::ShellCommandExecutor;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -349,6 +355,109 @@ fn shell_request_escalation_execution_is_explicit() {
);
}
#[tokio::test]
async fn unsandboxed_intercepted_exec_strips_managed_network_env() -> anyhow::Result<()> {
let workdir = test_sandbox_cwd();
let executor = CoreShellCommandExecutor {
command: Vec::new(),
cwd: workdir.clone(),
permission_profile: PermissionProfile::workspace_write(),
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox: SandboxType::None,
env: HashMap::new(),
network: None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
arg0: None,
sandbox_policy_cwd: workdir.clone(),
windows_sandbox_workspace_roots: vec![workdir.clone()],
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
};
let mut env = HashMap::new();
env.insert(PROXY_ACTIVE_ENV_KEY.to_string(), "1".to_string());
for key in PROXY_ENV_KEYS {
env.insert((*key).to_string(), format!("proxy-{key}"));
}
let prepared = executor
.prepare_escalated_exec(
&AbsolutePathBuf::from_absolute_path("/usr/bin/curl")?,
&["curl".to_string(), "example.com".to_string()],
&workdir,
env,
EscalationExecution::Unsandboxed,
)
.await?;
assert!(
!prepared.env.contains_key(PROXY_ACTIVE_ENV_KEY),
"unsandboxed intercepted exec should strip the managed-network active marker"
);
for key in PROXY_ENV_KEYS {
assert!(
!prepared.env.contains_key(*key),
"unsandboxed intercepted exec should strip managed-network proxy env var {key}"
);
}
Ok(())
}
#[tokio::test]
async fn preapproved_additional_permissions_escalate_intercepted_exec() -> anyhow::Result<()> {
let (session, turn_context) = make_session_and_context().await;
let requested_permissions = AdditionalPermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![
AbsolutePathBuf::from_absolute_path("/tmp/output").unwrap(),
]),
)),
..Default::default()
};
let workdir = test_sandbox_cwd();
let permission_profile = effective_permission_profile(
&PermissionProfile::workspace_write(),
Some(&requested_permissions),
);
let provider = CoreShellActionProvider {
policy: Arc::new(RwLock::new(codex_execpolicy::Policy::empty())),
session: Arc::new(session),
turn: Arc::new(turn_context),
call_id: "preapproved-additional-permissions".to_string(),
tool_name: GuardianCommandSource::Shell,
approval_policy: AskForApproval::OnRequest,
permission_profile: permission_profile.clone(),
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
sandbox_policy_cwd: workdir.clone(),
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
approval_sandbox_permissions: SandboxPermissions::UseDefault,
prompt_permissions: Some(requested_permissions),
stopwatch: codex_shell_escalation::Stopwatch::new(Duration::from_secs(1)),
};
let action = codex_shell_escalation::EscalationPolicy::determine_action(
&provider,
&AbsolutePathBuf::from_absolute_path("/usr/bin/printf")?,
&["printf".to_string(), "hello".to_string()],
&workdir,
)
.await?;
let expected = codex_shell_escalation::EscalationDecision::Escalate(
EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile(
ResolvedPermissionProfile { permission_profile },
)),
);
assert_eq!(
action, expected,
"preapproved with_additional_permissions should escalate through the resolved permission profile"
);
Ok(())
}
#[tokio::test(flavor = "current_thread")]
async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Result<()> {
let (session, mut turn_context) = make_session_and_context().await;

View File

@@ -265,17 +265,15 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
let base_command = &req.command;
let session_shell = ctx.session.user_shell();
let (file_system_sandbox_policy, _) = attempt.permissions.to_runtime_permissions();
let sandbox_permissions = sandbox_permissions_preserving_denied_reads(
let launch_sandbox_permissions = sandbox_permissions_preserving_denied_reads(
req.sandbox_permissions,
&file_system_sandbox_policy,
);
let req = &UnifiedExecRequest {
sandbox_permissions,
..req.clone()
};
let managed_network =
managed_network_for_sandbox_permissions(req.network.as_ref(), req.sandbox_permissions);
let mut env = exec_env_for_sandbox_permissions(&req.env, req.sandbox_permissions);
let managed_network = managed_network_for_sandbox_permissions(
req.network.as_ref(),
launch_sandbox_permissions,
);
let mut env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions);
if let Some(network) = managed_network {
network.apply_to_env(&mut env);
}
@@ -412,6 +410,7 @@ mod tests {
use crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS;
use crate::tools::sandboxing::ToolRuntime;
use codex_exec_server::Environment;
use codex_tools::ZshForkConfig;
use std::time::Duration;
use tempfile::tempdir;
@@ -473,4 +472,107 @@ mod tests {
assert_eq!(runtime.sandbox_cwd(&request), Some(&sandbox_cwd));
}
#[tokio::test]
async fn zsh_fork_first_attempt_preserves_parent_sandbox_override() {
let manager = UnifiedExecProcessManager::default();
let request = test_request(
SandboxPermissions::RequireEscalated,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
},
);
let direct_runtime = UnifiedExecRuntime::new(&manager, UnifiedExecShellMode::Direct);
let zsh_fork_runtime = UnifiedExecRuntime::new(&manager, zsh_fork_mode());
assert_eq!(
direct_runtime.sandbox_permissions(&request),
SandboxPermissions::RequireEscalated,
"direct unified exec should preserve a parent require_escalated request"
);
assert_eq!(
zsh_fork_runtime.sandbox_permissions(&request),
SandboxPermissions::RequireEscalated,
"zsh-fork unified exec should preserve the same parent require_escalated request"
);
}
#[tokio::test]
async fn zsh_fork_first_attempt_preserves_additional_permissions_request() {
let manager = UnifiedExecProcessManager::default();
let request = test_request(
SandboxPermissions::WithAdditionalPermissions,
ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
},
);
let zsh_fork_runtime = UnifiedExecRuntime::new(&manager, zsh_fork_mode());
assert_eq!(
zsh_fork_runtime.sandbox_permissions(&request),
SandboxPermissions::WithAdditionalPermissions,
"zsh-fork unified exec should keep bounded additional-permissions requests sandboxed"
);
}
#[tokio::test]
async fn zsh_fork_execpolicy_allow_preserves_parent_sandbox_override() {
let manager = UnifiedExecProcessManager::default();
let request = test_request(
SandboxPermissions::UseDefault,
ExecApprovalRequirement::Skip {
bypass_sandbox: true,
proposed_execpolicy_amendment: None,
},
);
let runtime = UnifiedExecRuntime::new(&manager, zsh_fork_mode());
assert_eq!(
runtime.exec_approval_requirement(&request),
Some(ExecApprovalRequirement::Skip {
bypass_sandbox: true,
proposed_execpolicy_amendment: None,
}),
"zsh-fork unified exec should preserve exec-policy allow decisions that bypass the sandbox"
);
}
fn test_request(
sandbox_permissions: SandboxPermissions,
exec_approval_requirement: ExecApprovalRequirement,
) -> UnifiedExecRequest {
let cwd = AbsolutePathBuf::try_from(std::env::current_dir().unwrap())
.expect("current dir is absolute");
UnifiedExecRequest {
command: vec!["zsh".to_string(), "-c".to_string(), "echo hi".to_string()],
shell_type: ShellType::Zsh,
hook_command: "echo hi".to_string(),
process_id: 1000,
cwd: cwd.clone(),
sandbox_cwd: cwd,
environment: Arc::new(Environment::default_for_tests()),
env: HashMap::new(),
exec_server_env_config: None,
explicit_env_overrides: HashMap::new(),
network: None,
tty: false,
sandbox_permissions,
additional_permissions: None,
#[cfg(unix)]
additional_permissions_preapproved: false,
justification: None,
exec_approval_requirement,
}
}
fn zsh_fork_mode() -> UnifiedExecShellMode {
let cwd = std::env::current_dir().expect("read current dir");
UnifiedExecShellMode::ZshFork(ZshForkConfig {
shell_zsh_path: AbsolutePathBuf::try_from(cwd.join("zsh")).expect("absolute zsh path"),
main_execve_wrapper_exe: AbsolutePathBuf::try_from(cwd.join("execve-wrapper"))
.expect("absolute wrapper path"),
})
}
}

View File

@@ -94,6 +94,33 @@ where
builder.build(server).await
}
pub async fn build_unified_exec_zsh_fork_test<F>(
server: &wiremock::MockServer,
runtime: ZshForkRuntime,
approval_policy: AskForApproval,
permission_profile: PermissionProfile,
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, permission_profile);
config.use_experimental_unified_exec_tool = true;
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
config
.features
.enable(Feature::UnifiedExecZshFork)
.expect("test config should allow feature update");
});
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");

View File

@@ -115,6 +115,8 @@ mod tools;
mod truncation;
mod turn_state;
mod unified_exec;
#[cfg(unix)]
mod unified_exec_zsh_fork_approvals;
mod unstable_features_warning;
mod user_notification;
mod user_shell_cmd;

View File

@@ -0,0 +1,567 @@
use anyhow::Context;
use anyhow::Result;
use codex_config::permissions_toml::FilesystemPermissionToml;
use codex_config::permissions_toml::PermissionProfileToml;
use codex_config::types::ApprovalsReviewer;
use codex_core::sandboxing::SandboxPermissions;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::user_input::UserInput;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_function_call;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
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::turn_permission_fields;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_with_timeout;
use core_test_support::zsh_fork::build_unified_exec_zsh_fork_test;
use core_test_support::zsh_fork::restrictive_workspace_write_profile;
use core_test_support::zsh_fork::zsh_fork_runtime;
use pretty_assertions::assert_eq;
use regex_lite::Regex;
use serde_json::Value;
use serde_json::json;
use std::fs;
use std::path::Path;
use std::time::Duration;
use toml_edit::Key as TomlKey;
use wiremock::MockServer;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_zsh_fork_parent_approval_preserves_denied_reads() -> Result<()> {
skip_if_no_network!(Ok(()));
let denied_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let denied_path = denied_dir.path().join("secret.env");
let secret = "unified-exec-zsh-fork-denied-read-secret";
fs::write(&denied_path, format!("{secret}\n"))?;
let permission_profile = denied_read_permission_profile(&denied_path)?;
assert!(
permission_profile
.file_system_sandbox_policy()
.has_denied_read_restrictions(),
"test must exercise a permission profile with denied reads"
);
let approval_policy = AskForApproval::OnRequest;
let command = format!("cat {denied_path:?}");
let Some((server, test)) = build_unified_exec_zsh_fork_test_or_skip(
"unified-exec zsh-fork denied-read approval test",
approval_policy,
permission_profile,
move |_home| {},
)
.await?
else {
return Ok(());
};
let call_id = "uexec-zsh-fork-parent-approval-denied-read";
let results = mount_unified_exec_command(
&server,
"uexec-zsh-fork-denied-read",
call_id,
&command,
"attempt a denied read for the test",
)
.await?;
submit_turn_with_session_permissions(
&test,
"run approved unified exec denied read through zsh fork",
approval_policy,
)
.await?;
approve_expected_exec(&test, &command).await?;
wait_for_completion_without_approval(&test).await;
let result = command_result(&results, call_id);
assert_ne!(
result.exit_code.unwrap_or(0),
0,
"denied-read command should stay sandboxed after parent approval"
);
assert!(
!result.stdout.contains(secret),
"denied-read command unexpectedly printed the secret: {}",
result.stdout
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_zsh_fork_parent_approval_escalates_intercepted_exec() -> Result<()> {
skip_if_no_network!(Ok(()));
let approval_policy = AskForApproval::OnRequest;
let permission_profile = restrictive_workspace_write_profile();
let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let outside_path = outside_dir
.path()
.join("unified-exec-zsh-fork-parent-approval.txt");
let command = format!("printf hi > {outside_path:?}");
let outside_path_for_hook = outside_path.clone();
let Some((server, test)) = build_unified_exec_zsh_fork_test_or_skip(
"unified-exec zsh-fork parent approval test",
approval_policy,
permission_profile,
move |_home| {
let _ = fs::remove_file(&outside_path_for_hook);
},
)
.await?
else {
return Ok(());
};
let call_id = "uexec-zsh-fork-parent-approval";
let results = mount_unified_exec_command(
&server,
"uexec-zsh-fork-parent-approval",
call_id,
&command,
"write outside the workspace for the test",
)
.await?;
submit_turn_with_session_permissions(
&test,
"run approved unified exec through zsh fork",
approval_policy,
)
.await?;
approve_expected_exec(&test, &command).await?;
wait_for_completion_without_approval(&test).await;
let result = command_result(&results, call_id);
assert_eq!(
result.exit_code.unwrap_or(0),
0,
"approved unified exec zsh-fork command should complete: {}",
result.stdout
);
let contents = fs::read_to_string(&outside_path)
.with_context(|| format!("read {}", outside_path.display()))?;
assert_eq!(
contents, "hi",
"approved parent sandbox override should allow zsh-fork shell redirection to write outside the workspace"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_zsh_fork_parent_approval_keeps_explicit_prompt_rule() -> Result<()> {
skip_if_no_network!(Ok(()));
let approval_policy = AskForApproval::OnRequest;
let permission_profile = restrictive_workspace_write_profile();
let outside_dir = tempfile::tempdir_in(std::env::current_dir()?)?;
let outside_path = outside_dir
.path()
.join("unified-exec-zsh-fork-explicit-prompt-rule.txt");
let command = format!("touch {outside_path:?}");
let rules = r#"prefix_rule(pattern=["touch"], decision="prompt")"#.to_string();
let outside_path_for_hook = outside_path.clone();
let Some((server, test)) = build_unified_exec_zsh_fork_test_or_skip(
"unified-exec zsh-fork prompt rule approval test",
approval_policy,
permission_profile,
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?
else {
return Ok(());
};
let call_id = "uexec-zsh-fork-parent-approval-explicit-prompt-rule";
let results = mount_unified_exec_command(
&server,
"uexec-zsh-fork-prompt-rule",
call_id,
&command,
"write outside the workspace for the test",
)
.await?;
submit_turn_with_session_permissions(
&test,
"run approved unified exec prompt rule through zsh fork",
approval_policy,
)
.await?;
approve_expected_exec(&test, &command).await?;
let approval_event = wait_for_event_with_timeout(
&test.codex,
|event| {
matches!(
event,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
)
},
Duration::from_secs(10),
)
.await;
let EventMsg::ExecApprovalRequest(inner_approval) = approval_event else {
panic!("expected explicit prompt rule approval before completion");
};
assert!(
inner_approval
.command
.iter()
.any(|arg| arg.ends_with("/touch"))
&& inner_approval
.command
.iter()
.any(|arg| arg == outside_path.to_string_lossy().as_ref()),
"expected explicit prompt rule approval for intercepted touch, got: {:?}",
inner_approval.command
);
approve_exec(&test, inner_approval.effective_approval_id()).await?;
wait_for_completion(&test).await;
let result = command_result(&results, call_id);
assert_eq!(
result.exit_code.unwrap_or(0),
0,
"approved unified exec zsh-fork prompt-rule command should complete: {}",
result.stdout
);
assert!(
outside_path.exists(),
"approved intercepted touch should create the out-of-workspace file"
);
Ok(())
}
struct CommandResult {
exit_code: Option<i64>,
stdout: String,
}
async fn build_unified_exec_zsh_fork_test_or_skip<F>(
test_name: &str,
approval_policy: AskForApproval,
permission_profile: PermissionProfile,
pre_build_hook: F,
) -> Result<Option<(MockServer, TestCodex)>>
where
F: FnOnce(&Path) + Send + 'static,
{
let Some(runtime) = zsh_fork_runtime(test_name)? else {
return Ok(None);
};
let server = start_mock_server().await;
let test = build_unified_exec_zsh_fork_test(
&server,
runtime,
approval_policy,
permission_profile,
pre_build_hook,
)
.await?;
Ok(Some((server, test)))
}
fn denied_read_permission_profile(denied_path: &Path) -> Result<PermissionProfile> {
let denied_path_key = TomlKey::new(denied_path.to_string_lossy().into_owned());
permission_profile_from_toml(&format!(
r#"
[filesystem]
"/" = "read"
":project_roots" = "write"
{denied_path_key} = "deny"
[network]
enabled = false
"#
))
}
fn permission_profile_from_toml(profile: &str) -> Result<PermissionProfile> {
let profile = toml::from_str::<PermissionProfileToml>(profile)
.context("test permission profile should deserialize")?;
let filesystem = profile
.filesystem
.as_ref()
.context("test permission profile should include filesystem entries")?;
let entries = filesystem
.entries
.iter()
.map(|(path, permission)| {
let FilesystemPermissionToml::Access(access) = permission else {
anyhow::bail!("unexpected scoped filesystem permission in test profile: {path}");
};
let path = match path.as_str() {
"/" => FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
":project_roots" => FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
_ if *access == FileSystemAccessMode::Deny => FileSystemPath::GlobPattern {
pattern: path.clone(),
},
_ => anyhow::bail!("unexpected filesystem entry in test profile: {path}"),
};
Ok(FileSystemSandboxEntry {
path,
access: *access,
})
})
.collect::<Result<Vec<_>>>()?;
let mut file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(entries);
file_system_sandbox_policy.glob_scan_max_depth = filesystem.glob_scan_max_depth;
let network_sandbox_policy = match profile.network.as_ref().and_then(|network| network.enabled)
{
Some(true) => NetworkSandboxPolicy::Enabled,
Some(false) | None => NetworkSandboxPolicy::Restricted,
};
Ok(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
network_sandbox_policy,
))
}
async fn mount_unified_exec_command(
server: &MockServer,
response_prefix: &str,
call_id: &str,
command: &str,
justification: &str,
) -> Result<ResponseMock> {
let first_response_id = format!("resp-{response_prefix}-1");
let second_response_id = format!("resp-{response_prefix}-2");
let message_id = format!("msg-{response_prefix}-1");
let event = exec_command_event(
call_id,
command,
Some(30_000),
SandboxPermissions::RequireEscalated,
justification,
)?;
let _ = mount_sse_once(
server,
sse(vec![
ev_response_created(&first_response_id),
event,
ev_completed(&first_response_id),
]),
)
.await;
let results = mount_sse_once(
server,
sse(vec![
ev_assistant_message(&message_id, "done"),
ev_completed(&second_response_id),
]),
)
.await;
Ok(results)
}
async fn submit_turn_with_session_permissions(
test: &TestCodex,
prompt: &str,
approval_policy: AskForApproval,
) -> Result<()> {
let session_model = test.session_configured.model.clone();
let (sandbox_policy, permission_profile) = turn_permission_fields(
test.session_configured.permission_profile.clone(),
test.cwd.path(),
);
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: prompt.into(),
text_elements: Vec::new(),
}],
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
cwd: Some(test.cwd.path().to_path_buf()),
approval_policy: Some(approval_policy),
approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy: Some(sandbox_policy),
permission_profile,
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: session_model,
reasoning_effort: None,
developer_instructions: None,
},
}),
..Default::default()
},
})
.await?;
Ok(())
}
async fn approve_expected_exec(test: &TestCodex, expected_command: &str) -> Result<()> {
let approval = expect_exec_approval(test, expected_command).await;
approve_exec(test, approval.effective_approval_id()).await
}
async fn approve_exec(test: &TestCodex, approval_id: String) -> Result<()> {
test.codex
.submit(Op::ExecApproval {
id: approval_id,
turn_id: None,
decision: ReviewDecision::Approved,
})
.await?;
Ok(())
}
fn command_result(results: &ResponseMock, call_id: &str) -> CommandResult {
parse_result(&results.single_request().function_call_output(call_id))
}
fn exec_command_event(
call_id: &str,
cmd: &str,
yield_time_ms: Option<u64>,
sandbox_permissions: SandboxPermissions,
justification: &str,
) -> Result<Value> {
let mut args = json!({
"cmd": cmd.to_string(),
});
if let Some(yield_time_ms) = yield_time_ms {
args["yield_time_ms"] = json!(yield_time_ms);
}
if sandbox_permissions.requests_sandbox_override() {
args["sandbox_permissions"] = json!(sandbox_permissions);
args["justification"] = json!(justification);
}
let args_str = serde_json::to_string(&args)?;
Ok(ev_function_call(call_id, "exec_command", &args_str))
}
fn parse_result(item: &Value) -> CommandResult {
let Some(output_str) = item.get("output").and_then(Value::as_str) else {
return CommandResult {
exit_code: None,
stdout: String::new(),
};
};
match serde_json::from_str::<Value>(output_str) {
Ok(parsed) => {
let exit_code = parsed["metadata"]["exit_code"].as_i64();
let stdout = parsed["output"].as_str().unwrap_or_default().to_string();
CommandResult { exit_code, stdout }
}
Err(_) => parsed_regex_result(r"(?s)^Exit code:\s*(-?\d+).*?Output:\n(.*)$", output_str)
.or_else(|| {
parsed_regex_result(
r"(?s)^.*?Process exited with code (\d+)\n.*?Output:\n(.*)$",
output_str,
)
})
.unwrap_or_else(|| CommandResult {
exit_code: None,
stdout: output_str.to_string(),
}),
}
}
fn parsed_regex_result(pattern: &str, output_str: &str) -> Option<CommandResult> {
let regex = Regex::new(pattern).ok()?;
let captures = regex.captures(output_str)?;
let exit_code = captures.get(1)?.as_str().parse::<i64>().ok()?;
let output = captures.get(2)?.as_str();
Some(CommandResult {
exit_code: Some(exit_code),
stdout: output.to_string(),
})
}
async fn expect_exec_approval(
test: &TestCodex,
expected_command: &str,
) -> ExecApprovalRequestEvent {
let event = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
)
})
.await;
match event {
EventMsg::ExecApprovalRequest(approval) => {
let last_arg = approval
.command
.last()
.map(std::string::String::as_str)
.unwrap_or_default();
assert_eq!(
last_arg, expected_command,
"approval request should be for the parent unified-exec command"
);
approval
}
EventMsg::TurnComplete(_) => panic!("expected approval request before completion"),
other => panic!("unexpected event: {other:?}"),
}
}
async fn wait_for_completion_without_approval(test: &TestCodex) {
let event = wait_for_event(&test.codex, |event| {
matches!(
event,
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
)
})
.await;
match event {
EventMsg::TurnComplete(_) => {}
EventMsg::ExecApprovalRequest(event) => {
panic!("unexpected approval request: {:?}", event.command)
}
other => panic!("unexpected event: {other:?}"),
}
}
async fn wait_for_completion(test: &TestCodex) {
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
}