fix(proxy+exec): harden proxy TLS forwarding (set servername, reset Host, normalize hop-by-hop headers) and make exec exit non-zero on agent Error events so workflows fail fast when streaming fails

This commit is contained in:
James Peter
2025-09-12 18:48:18 +10:00
parent 6ad9cb283c
commit fec6aa0f0c
5 changed files with 41 additions and 2 deletions
+15
View File
@@ -384,7 +384,16 @@ jobs:
if (!incoming['openai-beta']) incoming['openai-beta'] = 'responses=experimental';
}
if (!incoming['originator']) incoming['originator'] = 'codex_cli_rs';
// Normalize hop-by-hop and target-specific headers for upstream
// Ensure TLS SNI/Host align with the upstream authority
delete incoming['host'];
incoming['host'] = up.host; // includes port when present
incoming['connection'] = 'keep-alive';
// Normalize content-length to actual buffered body size when present
if (incoming['content-length']) incoming['content-length'] = String(body.length);
// Remove proxy-specific headers that should never reach upstream
delete incoming['proxy-connection'];
delete incoming['proxy-authorization'];
log({ level: 'info', rid, phase: 'request', method: req.method, url: up.toString(), headers: redactHeaders(incoming), body_bytes: body.length });
@@ -395,6 +404,12 @@ jobs:
path: up.pathname + up.search,
method: req.method,
headers: incoming,
// Ensure correct SNI for TLS handshake
servername: up.hostname,
// Reuse connections for streaming stability
agent: (up.protocol === 'https:'
? new https.Agent({ keepAlive: true, maxSockets: 64, servername: up.hostname })
: new http.Agent({ keepAlive: true, maxSockets: 64 }))
};
const upstream = (up.protocol === 'https:' ? https : http).request(opts, (upr) => {
+7
View File
@@ -15,6 +15,13 @@ pub(crate) trait EventProcessor {
/// Handle a single event emitted by the agent.
fn process_event(&mut self, event: Event) -> CodexStatus;
/// Exit code to use upon completion. Implementations should set a non-zero
/// value when a fatal error was encountered (e.g., upstream streaming
/// failure).
fn exit_code(&self) -> i32 {
0
}
}
pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path) {
@@ -63,6 +63,7 @@ pub(crate) struct EventProcessorWithHumanOutput {
reasoning_started: bool,
raw_reasoning_started: bool,
last_message_path: Option<PathBuf>,
had_error: bool,
}
impl EventProcessorWithHumanOutput {
@@ -91,6 +92,7 @@ impl EventProcessorWithHumanOutput {
reasoning_started: false,
raw_reasoning_started: false,
last_message_path,
had_error: false,
}
} else {
Self {
@@ -109,6 +111,7 @@ impl EventProcessorWithHumanOutput {
reasoning_started: false,
raw_reasoning_started: false,
last_message_path,
had_error: false,
}
}
}
@@ -171,6 +174,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
EventMsg::Error(ErrorEvent { message }) => {
let prefix = "ERROR:".style(self.red);
ts_println!(self, "{prefix} {message}");
self.had_error = true;
}
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
ts_println!(self, "{}", message.style(self.dimmed));
@@ -611,8 +615,15 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
CodexStatus::Running
}
fn exit_code(&self) -> i32 {
if self.had_error { 1 } else { 0 }
}
}
// Extend trait with exit_code override
// Add exit_code method to the existing EventProcessor impl
fn escape_command(command: &[String]) -> String {
try_join(command.iter().map(|s| s.as_str())).unwrap_or_else(|_| command.join(" "))
}
@@ -14,11 +14,12 @@ use codex_common::create_config_summary_entries;
pub(crate) struct EventProcessorWithJsonOutput {
last_message_path: Option<PathBuf>,
had_error: bool,
}
impl EventProcessorWithJsonOutput {
pub fn new(last_message_path: Option<PathBuf>) -> Self {
Self { last_message_path }
Self { last_message_path, had_error: false }
}
}
@@ -41,6 +42,7 @@ impl EventProcessor for EventProcessorWithJsonOutput {
fn process_event(&mut self, event: Event) -> CodexStatus {
match event.msg {
EventMsg::Error(_) => { self.had_error = true; CodexStatus::Running }
EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) => {
// Suppress streaming events in JSON mode.
CodexStatus::Running
@@ -60,4 +62,6 @@ impl EventProcessor for EventProcessorWithJsonOutput {
}
}
}
fn exit_code(&self) -> i32 { if self.had_error { 1 } else { 0 } }
}
+3 -1
View File
@@ -280,6 +280,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
}
}
}
// If any fatal Error events occurred, exit non‑zero so CI fails fast.
let exit_code = event_processor.exit_code();
if exit_code != 0 { std::process::exit(exit_code); }
Ok(())
}