//! Unified Exec: interactive process execution orchestrated with approvals + sandboxing.//!//! Responsibilities//! - Manages interactive processes (create, reuse, buffer output with caps).//! - Uses the shared ToolOrchestrator to handle approval, sandbox selection, and//! retry semantics in a single, descriptive flow.//! - Spawns the PTY from a sandbox-transformed `ExecRequest`; on sandbox denial,//! retries without sandbox when policy allows (no re‑prompt thanks to caching).//! - Uses the shared `is_likely_sandbox_denied` heuristic to keep denial messages//! consistent with other exec paths.//!//! Flow at a glance (open process)//! 1) Build a small request `{ command, cwd }`.//! 2) Orchestrator: approval (bypass/cache/prompt) → select sandbox → run.//! 3) Runtime: transform `SandboxTransformRequest` -> `ExecRequest` -> spawn PTY.//! 4) If denial, orchestrator retries with `SandboxType::None`.//! 5) Process handle is returned with streaming output + metadata.//!//! This keeps policy logic and user interaction centralized while the PTY/process//! concerns remain isolated here. The implementation is split between://! - `process.rs`: PTY process lifecycle + output buffering.//! - `process_state.rs`: shared exit/failure state for local and remote processes.//! - `process_manager.rs`: orchestration (approvals, sandboxing, reuse) and request handling.use std::collections::HashMap;use std::collections::HashSet;use std::sync::Arc;use std::sync::Weak;use codex_network_proxy::NetworkProxy;use codex_protocol::models::AdditionalPermissionProfile;use codex_tools::UnifiedExecShellMode;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_output_truncation::TruncationPolicy;use rand::Rng;use rand::rng;use tokio::sync::Mutex;use crate::sandboxing::SandboxPermissions;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::session::turn_context::TurnEnvironment;use crate::shell::ShellType;use crate::tools::network_approval::DeferredNetworkApproval;mod async_watcher;mod errors;mod head_tail_buffer;mod process;mod process_manager;mod process_state;pub(crate) fn set_deterministic_process_ids_for_tests(enabled: bool) { process_manager::set_deterministic_process_ids_for_tests(enabled);}pub(crate) use errors::UnifiedExecError;pub(crate) use process::NoopSpawnLifecycle;#[cfg(unix)]pub(crate) use process::SpawnLifecycle;pub(crate) use process::SpawnLifecycleHandle;pub(crate) use process::UnifiedExecProcess;pub(crate) const MIN_YIELD_TIME_MS: u64 = 250;pub(crate) const WINDOWS_INITIAL_EXEC_YIELD_TIME_FLOOR_MS: u64 = 2_000;// Minimum yield time for an empty `write_stdin`.pub(crate) const MIN_EMPTY_YIELD_TIME_MS: u64 = 5_000;pub(crate) const MAX_YIELD_TIME_MS: u64 = 30_000;pub(crate) const DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS: u64 = 300_000;pub(crate) const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;pub(crate) const UNIFIED_EXEC_OUTPUT_MAX_BYTES: usize = 1024 * 1024; // 1 MiBpub(crate) const UNIFIED_EXEC_OUTPUT_MAX_TOKENS: usize = UNIFIED_EXEC_OUTPUT_MAX_BYTES / 4;pub(crate) const MAX_UNIFIED_EXEC_PROCESSES: usize = 64;pub(crate) struct UnifiedExecContext { pub session: Arc<Session>, pub turn: Arc<TurnContext>, pub call_id: String,}impl UnifiedExecContext { pub fn new(session: Arc<Session>, turn: Arc<TurnContext>, call_id: String) -> Self { Self { session, turn, call_id, } }}#[derive(Debug)]pub(crate) struct ExecCommandRequest { pub command: Vec<String>, pub shell_type: ShellType, pub hook_command: String, pub process_id: i32, pub yield_time_ms: u64, pub max_output_tokens: Option<usize>, pub cwd: AbsolutePathBuf, pub sandbox_cwd: AbsolutePathBuf, pub turn_environment: TurnEnvironment, pub shell_mode: UnifiedExecShellMode, pub network: Option<NetworkProxy>, pub tty: bool, pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option<AdditionalPermissionProfile>, pub additional_permissions_preapproved: bool, pub justification: Option<String>, pub prefix_rule: Option<Vec<String>>,}#[derive(Debug)]pub(crate) struct WriteStdinRequest<'a> { pub process_id: i32, pub input: &'a str, pub yield_time_ms: u64, pub max_output_tokens: Option<usize>, pub truncation_policy: TruncationPolicy,}#[derive(Default)]pub(crate) struct ProcessStore { processes: HashMap<i32, ProcessEntry>, reserved_process_ids: HashSet<i32>,}impl ProcessStore { fn remove(&mut self, process_id: i32) -> Option<ProcessEntry> { self.reserved_process_ids.remove(&process_id); self.processes.remove(&process_id) }}pub(crate) struct UnifiedExecProcessManager { process_store: Mutex<ProcessStore>, max_write_stdin_yield_time_ms: u64,}impl UnifiedExecProcessManager { pub(crate) fn new(max_write_stdin_yield_time_ms: u64) -> Self { Self { process_store: Mutex::new(ProcessStore::default()), max_write_stdin_yield_time_ms: max_write_stdin_yield_time_ms .max(MIN_EMPTY_YIELD_TIME_MS), } }}impl Default for UnifiedExecProcessManager { fn default() -> Self { Self::new(DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS) }}struct ProcessEntry { process: Arc<UnifiedExecProcess>, call_id: String, process_id: i32, cwd: AbsolutePathBuf, initial_exec_command_active: Arc<std::sync::atomic::AtomicBool>, hook_command: String, tty: bool, network_approval: Option<DeferredNetworkApproval>, session: Weak<Session>, last_used: tokio::time::Instant,}pub(crate) fn clamp_yield_time(yield_time_ms: u64) -> u64 { let yield_time_ms = if cfg!(windows) { yield_time_ms.max(WINDOWS_INITIAL_EXEC_YIELD_TIME_FLOOR_MS) } else { yield_time_ms }; yield_time_ms.clamp(MIN_YIELD_TIME_MS, MAX_YIELD_TIME_MS)}pub(crate) fn resolve_max_tokens(max_tokens: Option<usize>) -> usize { max_tokens.unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS)}pub(crate) fn generate_chunk_id() -> String { let mut rng = rng(); (0..6) .map(|_| format!("{:x}", rng.random_range(0..16))) .collect()}#[cfg(test)]#[cfg(unix)]#[path = "process_tests.rs"]mod process_tests;#[cfg(test)]#[cfg(unix)]#[path = "mod_tests.rs"]mod tests;