//! Unified Exec: interactive process execution orchestrated with approvals + sandboxing. //! //! Responsibilities //! - Manages interactive processes (create, reuse, buffer output with caps). //! - Supports completion-only calls that terminate on timeout or cancellation. //! - 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_output_truncation::TruncationPolicy; use codex_utils_path_uri::PathUri; use rand::Rng; use rand::rng; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use crate::sandboxing::SandboxPermissions; use crate::session::session::Session; use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnEnvironment; use crate::shell::ShellType; use crate::tools::network_approval::DeferredNetworkApproval; use codex_core_plugins::PluginMetricsSidecar; mod async_watcher; mod errors; mod head_tail_buffer; mod oneshot; mod process; mod process_manager; mod process_state; mod shell_snapshot; mod stdin_approval; 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) use stdin_approval::TerminalPermissions; pub(crate) use stdin_approval::TerminalSandboxSource; pub(crate) const MIN_YIELD_TIME_MS: u64 = 250; pub(crate) const WINDOWS_INITIAL_EXEC_YIELD_TIME_FLOOR_MS: u64 = 10_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 MiB pub(crate) const UNIFIED_EXEC_OUTPUT_MAX_TOKENS: usize = UNIFIED_EXEC_OUTPUT_MAX_BYTES / 4; pub(crate) const MAX_UNIFIED_EXEC_PROCESSES: usize = 64; const MAX_TRACE_ID_BYTES: usize = 256; fn trace_id(id: &str) -> Option<&str> { (!id.is_empty() && id.len() <= MAX_TRACE_ID_BYTES).then_some(id) } pub(crate) struct UnifiedExecContext { pub session: Arc, pub step_context: Arc, pub cancellation_token: CancellationToken, pub call_id: String, } impl UnifiedExecContext { pub fn new( session: Arc, step_context: Arc, cancellation_token: CancellationToken, call_id: String, ) -> Self { Self { session, step_context, cancellation_token, call_id, } } } #[derive(Debug)] pub(crate) struct ExecCommandRequest { pub command: Vec, pub shell_type: ShellType, pub hook_command: String, pub process_id: i32, pub yield_time_ms: u64, pub max_output_tokens: Option, pub cwd: PathUri, pub sandbox_cwd: PathUri, pub turn_environment: TurnEnvironment, pub shell_mode: UnifiedExecShellMode, pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, pub additional_permissions: Option, pub additional_permissions_preapproved: bool, pub justification: Option, pub prefix_rule: Option>, } #[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, pub truncation_policy: TruncationPolicy, pub interaction_event: Option>, } pub(crate) struct WriteStdinInteractionEvent<'a> { pub session: &'a Arc, pub turn: &'a Arc, } impl std::fmt::Debug for WriteStdinInteractionEvent<'_> { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("WriteStdinInteractionEvent") } } #[derive(Default)] pub(crate) struct ProcessStore { processes: HashMap, reserved_process_ids: HashSet, } impl ProcessStore { fn remove(&mut self, process_id: i32) -> Option { self.reserved_process_ids.remove(&process_id); self.processes.remove(&process_id) } } pub(crate) struct UnifiedExecProcessManager { process_store: Mutex, 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, plugin_metrics_sidecar: Option, call_id: String, process_id: i32, cwd: PathUri, initial_exec_command_active: Arc, hook_command: String, tty: bool, environment_id: String, permissions: TerminalPermissions, network_approval: Option, session: Weak, last_used: tokio::time::Instant, } type SharedPluginMetricsSidecar = Arc>>; fn take_plugin_metrics_sidecar( sidecar: &SharedPluginMetricsSidecar, ) -> Option { sidecar .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take() } 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 { max_tokens.unwrap_or(DEFAULT_MAX_OUTPUT_TOKENS) } pub(crate) fn format_output_omission_marker(omitted_bytes: usize) -> String { format!("... {omitted_bytes} bytes omitted ...") } 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;