From 0c4db2686d29ac7d0804514770beab1e0550b3d5 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 25 Jul 2026 06:58:06 -0400 Subject: [PATCH] fix(sandbox): preserve Daytona streaming completion Run the streaming Bash wrapper as a child of Daytona's session shell so the provider can resume its bookkeeping and persist the command exit code. Add a regression test that exercises the sourced-command contract and preserves a nonzero exit status. --- .../fabro-sandbox/src/daytona/mod.rs | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/components/fabro-sandbox/src/daytona/mod.rs b/lib/components/fabro-sandbox/src/daytona/mod.rs index 12a253e82..c87ddb440 100644 --- a/lib/components/fabro-sandbox/src/daytona/mod.rs +++ b/lib/components/fabro-sandbox/src/daytona/mod.rs @@ -2365,14 +2365,16 @@ fn wrap_bash_command(command: &str) -> String { format!("{REMOTE_BASH} -c \"echo '{encoded}' | base64 -d | {REMOTE_BASH}\"") } -/// Enter the canonical Bash interpreter from a Daytona streaming session. +/// Invoke the canonical Bash interpreter from a Daytona streaming session. /// /// Session commands already pass through the provider's shell parser, so an /// audited shell-quoted argument avoids the direct-exec path's base64 process /// and second Bash while keeping caller source inert until `/bin/bash -c` -/// evaluates it. +/// evaluates it. Bash must remain a child process: Daytona sources this command +/// inside a wrapper that resumes afterward to drain logs and persist the exit +/// code. fn wrap_bash_session_script(script: &str) -> String { - format!("exec {REMOTE_BASH} -c {}", shell_quote(script)) + format!("{REMOTE_BASH} -c {}", shell_quote(script)) } #[cfg(test)] @@ -2990,12 +2992,53 @@ mod tests { assert_eq!( wrapped, - format!("exec /bin/bash -c {}", shell_quote(&script)), + format!("/bin/bash -c {}", shell_quote(&script)), "the streaming path must enter the canonical Bash exactly once" ); assert!(!wrapped.contains("base64")); } + #[cfg(unix)] + #[test] + #[expect( + clippy::disallowed_methods, + reason = "test executes the generated shell transport to verify provider bookkeeping resumes" + )] + fn streaming_session_script_returns_to_provider_bookkeeping() { + let dir = tempfile::tempdir().expect("create session transport temp dir"); + let command_file = dir.path().join("cmd.sh"); + let exit_code_file = dir.path().join("exit_code"); + let script = build_bash_session_script("printf 'command-finished\\n'; exit 7", "/", None); + std::fs::write(&command_file, wrap_bash_session_script(&script)) + .expect("write generated session command"); + + // Daytona sources the command file, then records the exit code. The + // generated command must return control so that bookkeeping can run. + let provider_wrapper = format!( + "{{ . {}; }}\n\ + command_exit_code=$?\n\ + printf '%s\\n' \"$command_exit_code\" > {}", + shell_quote(&command_file.to_string_lossy()), + shell_quote(&exit_code_file.to_string_lossy()), + ); + let output = std::process::Command::new(REMOTE_BASH) + .args(["-c", &provider_wrapper]) + .env_remove(BASH_ENV_VAR) + .output() + .expect("execute generated session command"); + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "command-finished\n" + ); + assert_eq!( + std::fs::read_to_string(exit_code_file) + .expect("provider bookkeeping should record the exit code"), + "7\n" + ); + assert!(output.status.success(), "{output:?}"); + } + fn bash_probe_result(exit_code: i32, stdout: impl Into) -> ExecResult { ExecResult { stdout: stdout.into(),