Files
codex/codex-rs/exec/src/event_processor.rs
T
viyatb-oai 636cdcb7ec Bind final-message output before running sandboxed tools (#58974)
## Why

`codex exec --output-last-message` wrote through a pathname after tools finished, allowing sandbox activity to redirect the final write by replacing that path with a symlink. The related risk is described in https://github.com/openai/codex/pull/25956.

## What changed

- Open the destination before starting the agent and retain its handle until the final write.
- Reject symlinks, Windows reparse points, and multiply linked files; verify that the pathname still identifies the bound file before truncating and after writing.
- Propagate output errors so the command fails when the destination cannot be opened or written safely.
- Preserve inherited Unix output streams, FIFOs, devices, and Windows `NUL` support, while avoiding duplicate final answers when human output targets stdout.

## Testing

Add regression coverage for sandbox-created symlinks across consecutive invocations, redirected stdout, delayed truncation, hard links, replaced paths, and Windows stream binding and filesystem identity checks.

GitOrigin-RevId: 4a31bd28a5eb2d0a9e200cb658eb7d19dc93e933
2026-11-02 20:36:39 +00:00

48 lines
1.4 KiB
Rust

use anyhow::Context;
use std::path::Path;
use codex_app_server_protocol::ServerNotification;
use codex_core::config::Config;
use codex_exec_server::BoundOutput;
use codex_protocol::protocol::SessionConfiguredEvent;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodexStatus {
Running,
InitiateShutdown,
}
pub(crate) trait EventProcessor {
/// Print summary of effective configuration and user prompt.
fn print_config_summary(
&mut self,
config: &Config,
prompt: &str,
session_configured: &SessionConfiguredEvent,
);
/// Handle a single typed app-server notification emitted by the agent.
fn process_server_notification(&mut self, notification: ServerNotification) -> CodexStatus;
/// Handle a local exec warning that is not represented as an app-server notification.
fn process_warning(&mut self, message: String) -> CodexStatus;
fn print_final_output(&mut self, output: Option<BoundOutput>) -> anyhow::Result<()>;
}
pub(crate) fn handle_last_message(
last_agent_message: Option<&str>,
path: &Path,
file: BoundOutput,
) -> anyhow::Result<()> {
file.write(last_agent_message.unwrap_or_default().as_bytes())
.with_context(|| format!("Failed to write last message file {path:?}"))?;
if last_agent_message.is_none() {
eprintln!(
"Warning: no last agent message; wrote empty content to {}",
path.display()
);
}
Ok(())
}