From 8b645ee518028a40e6388e0936ca52e958e5f179 Mon Sep 17 00:00:00 2001 From: rootkiller6788 Date: Sat, 22 Aug 2026 01:18:20 +0800 Subject: [PATCH] fix(cli): fail sandbox exec when the relay closes without an exit status `sandbox exec` seeded its exit code to 0 and only overwrote it on an `Exit` event, so a stream that ended early, was cancelled, or was truncated reported a successful run. The gateway already treats the same condition as a relay failure (`Status::unavailable`); mirror that on the CLI side so exit 0 always means an observed exit status of 0. Fixes #2732 Signed-off-by: rootkiller6788 --- crates/openshell-cli/src/run.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index fd0e585068..f3c63e1493 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -1394,7 +1394,8 @@ const MAX_STDIN_PAYLOAD: usize = 4 * 1024 * 1024; /// Execute a command in a running sandbox via gRPC, streaming output to the terminal. /// -/// Returns the remote command's exit code. +/// Returns the remote command's exit code, or an error if the event stream +/// closes before the command reports an exit status. #[allow(clippy::too_many_arguments, clippy::implicit_hasher)] pub async fn sandbox_exec_grpc( server: &str, @@ -1489,6 +1490,7 @@ pub async fn sandbox_exec_grpc( // Stream output to terminal in real-time. let mut exit_code = 0i32; + let mut exit_seen = false; let stdout = std::io::stdout(); let stderr = std::io::stderr(); @@ -1507,11 +1509,21 @@ pub async fn sandbox_exec_grpc( } Some(exec_sandbox_event::Payload::Exit(exit)) => { exit_code = exit.exit_code; + exit_seen = true; } None => {} } } + // A stream that closes without an Exit event means we never observed the + // command's outcome. The server treats the same condition as a relay + // failure; mirror that here so exit 0 stays meaningful. + if !exit_seen { + return Err(miette::miette!( + "sandbox exec relay closed before the command reported an exit status" + )); + } + Ok(exit_code) } @@ -1922,6 +1934,7 @@ async fn sandbox_exec_interactive_grpc( let _resize_guard = TaskGuard(resize_task); let mut exit_code = 0i32; + let mut exit_seen = false; let stdout = std::io::stdout(); let stderr = std::io::stderr(); @@ -1940,6 +1953,7 @@ async fn sandbox_exec_interactive_grpc( } Some(exec_sandbox_event::Payload::Exit(exit)) => { exit_code = exit.exit_code; + exit_seen = true; break; } None => {} @@ -1951,6 +1965,15 @@ async fn sandbox_exec_interactive_grpc( // Drop the raw mode guard to restore the terminal before returning. drop(raw_guard); + // A stream that closes without an Exit event means we never observed the + // command's outcome. Treat it as a relay failure rather than reporting a + // successful (0) exit. + if !exit_seen { + return Err(miette::miette!( + "sandbox exec relay closed before the command reported an exit status" + )); + } + Ok(exit_code) }