mirror of
https://github.com/openai/codex.git
synced 2026-11-05 08:28:29 +00:00
## Why Managed configuration can disable resumable unified execution while leaving shell tools enabled. This should not remove command execution entirely or grant access to retained processes. ## What changed - Keep `exec_command` available in completion-only mode when `unified_exec` is disabled. - Remove `tty`, `yield_time_ms`, `session_id`, and `write_stdin` from that mode, and add a `timeout_ms` argument. - Terminate one-shot commands on timeout or turn cancellation, reporting timed-out commands with exit code 124. ## Testing Added integration coverage for tool availability, timeout cleanup, and interruption cleanup under managed configuration. GitOrigin-RevId: a382d34f922fdc4ba52889272c6db985abd42e47
242 lines
7.7 KiB
Rust
242 lines
7.7 KiB
Rust
//! 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;
|
||
|
||
pub(crate) struct UnifiedExecContext {
|
||
pub session: Arc<Session>,
|
||
pub step_context: Arc<StepContext>,
|
||
pub cancellation_token: CancellationToken,
|
||
pub call_id: String,
|
||
}
|
||
|
||
impl UnifiedExecContext {
|
||
pub fn new(
|
||
session: Arc<Session>,
|
||
step_context: Arc<StepContext>,
|
||
cancellation_token: CancellationToken,
|
||
call_id: String,
|
||
) -> Self {
|
||
Self {
|
||
session,
|
||
step_context,
|
||
cancellation_token,
|
||
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: PathUri,
|
||
pub sandbox_cwd: PathUri,
|
||
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,
|
||
pub interaction_event: Option<WriteStdinInteractionEvent<'a>>,
|
||
}
|
||
|
||
pub(crate) struct WriteStdinInteractionEvent<'a> {
|
||
pub session: &'a Arc<Session>,
|
||
pub turn: &'a Arc<TurnContext>,
|
||
}
|
||
|
||
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<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>,
|
||
plugin_metrics_sidecar: Option<SharedPluginMetricsSidecar>,
|
||
call_id: String,
|
||
process_id: i32,
|
||
cwd: PathUri,
|
||
initial_exec_command_active: Arc<std::sync::atomic::AtomicBool>,
|
||
hook_command: String,
|
||
tty: bool,
|
||
environment_id: String,
|
||
permissions: TerminalPermissions,
|
||
network_approval: Option<DeferredNetworkApproval>,
|
||
session: Weak<Session>,
|
||
last_used: tokio::time::Instant,
|
||
}
|
||
|
||
type SharedPluginMetricsSidecar = Arc<std::sync::Mutex<Option<PluginMetricsSidecar>>>;
|
||
|
||
fn take_plugin_metrics_sidecar(
|
||
sidecar: &SharedPluginMetricsSidecar,
|
||
) -> Option<PluginMetricsSidecar> {
|
||
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>) -> 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;
|