app-server: Add streaming and tty/pty capabilities to command/exec (#13640)

* Add an ability to stream stdin, stdout, and stderr
* Streaming of stdout and stderr has a configurable cap for total amount
of transmitted bytes (with an ability to disable it)
* Add support for overriding environment variables
* Add an ability to terminate running applications (using
`command/exec/terminate`)
* Add TTY/PTY support, with an ability to resize the terminal (using
`command/exec/resize`)
This commit is contained in:
Ruslan Nigmatullin
2026-03-06 17:30:17 -08:00
committed by GitHub
parent 61098c7f51
commit e9bd8b20a1
43 changed files with 4205 additions and 70 deletions

View File

@@ -34,6 +34,7 @@ use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use crate::text_encoding::bytes_to_string_smart;
use codex_network_proxy::NetworkProxy;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use codex_utils_pty::process_group::kill_child_process_group;
pub const DEFAULT_EXEC_COMMAND_TIMEOUT_MS: u64 = 10_000;
@@ -53,12 +54,21 @@ const AGGREGATE_BUFFER_INITIAL_CAPACITY: usize = 8 * 1024; // 8 KiB
///
/// This mirrors unified exec's output cap so a single runaway command cannot
/// OOM the process by dumping huge amounts of data to stdout/stderr.
const EXEC_OUTPUT_MAX_BYTES: usize = 1024 * 1024; // 1 MiB
const EXEC_OUTPUT_MAX_BYTES: usize = DEFAULT_OUTPUT_BYTES_CAP;
/// Limit the number of ExecCommandOutputDelta events emitted per exec call.
/// Aggregation still collects full output; only the live event stream is capped.
pub(crate) const MAX_EXEC_OUTPUT_DELTAS_PER_CALL: usize = 10_000;
// Wait for the stdout/stderr collection tasks but guard against them
// hanging forever. In the normal case, both pipes are closed once the child
// terminates so the tasks exit quickly. However, if the child process
// spawned grandchildren that inherited its stdout/stderr file descriptors
// those pipes may stay open after we `kill` the direct child on timeout.
// That would cause the `read_capped` tasks to block on `read()`
// indefinitely, effectively hanging the whole agent.
pub const IO_DRAIN_TIMEOUT_MS: u64 = 2_000; // 2 s should be plenty for local pipes
#[derive(Debug)]
pub struct ExecParams {
pub command: Vec<String>,
@@ -157,6 +167,27 @@ pub async fn process_exec_tool_call(
use_linux_sandbox_bwrap: bool,
stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
let exec_req = build_exec_request(
params,
sandbox_policy,
sandbox_cwd,
codex_linux_sandbox_exe,
use_linux_sandbox_bwrap,
)?;
// Route through the sandboxing module for a single, unified execution path.
crate::sandboxing::execute_env(exec_req, stdout_stream).await
}
/// Transform a portable exec request into the concrete argv/env that should be
/// spawned under the requested sandbox policy.
pub fn build_exec_request(
params: ExecParams,
sandbox_policy: &SandboxPolicy,
sandbox_cwd: &Path,
codex_linux_sandbox_exe: &Option<PathBuf>,
use_linux_sandbox_bwrap: bool,
) -> Result<ExecRequest> {
let windows_sandbox_level = params.windows_sandbox_level;
let enforce_managed_network = params.network.is_some();
let sandbox_type = match &sandbox_policy {
@@ -226,9 +257,7 @@ pub async fn process_exec_tool_call(
windows_sandbox_level,
})
.map_err(CodexErr::from)?;
// Route through the sandboxing module for a single, unified execution path.
crate::sandboxing::execute_env(exec_req, stdout_stream).await
Ok(exec_req)
}
pub(crate) async fn execute_exec_request(
@@ -796,16 +825,6 @@ async fn consume_truncated_output(
}
};
// Wait for the stdout/stderr collection tasks but guard against them
// hanging forever. In the normal case, both pipes are closed once the child
// terminates so the tasks exit quickly. However, if the child process
// spawned grandchildren that inherited its stdout/stderr file descriptors
// those pipes may stay open after we `kill` the direct child on timeout.
// That would cause the `read_capped` tasks to block on `read()`
// indefinitely, effectively hanging the whole agent.
const IO_DRAIN_TIMEOUT_MS: u64 = 2_000; // 2 s should be plenty for local pipes
// We need mutable bindings so we can `abort()` them on timeout.
use tokio::task::JoinHandle;