diff --git a/app/assets/bundled/bootstrap/bash_body.sh b/app/assets/bundled/bootstrap/bash_body.sh index 2e082387827..dc3ea059d53 100644 --- a/app/assets/bundled/bootstrap/bash_body.sh +++ b/app/assets/bundled/bootstrap/bash_body.sh @@ -1517,6 +1517,22 @@ esac export PATH="$PATH:$WARP_PATH_APPEND" unset WARP_PATH_APPEND fi + # Direct non-root cloud containers use a private command directory when system installation fails. + # Restore the entrypoint-provided directory's precedence after profiles reset PATH. + if [[ "${WARP_CLOUD_AGENT_FALLBACK_BIN:-}" == /* && "$WARP_CLOUD_AGENT_FALLBACK_BIN" != *:* \ + && -d "$WARP_CLOUD_AGENT_FALLBACK_BIN" && "$(command -p uname -s)" == Linux ]]; then + _warp_runtime_remaining="$PATH:" + _warp_runtime_path="$WARP_CLOUD_AGENT_FALLBACK_BIN" + while [[ -n "$_warp_runtime_remaining" ]]; do + _warp_runtime_entry="${_warp_runtime_remaining%%:*}" + _warp_runtime_remaining="${_warp_runtime_remaining#*:}" + if [[ "$_warp_runtime_entry" != "$WARP_CLOUD_AGENT_FALLBACK_BIN" ]]; then + _warp_runtime_path="$_warp_runtime_path:$_warp_runtime_entry" + fi + done + export PATH="$_warp_runtime_path" + unset _warp_runtime_remaining _warp_runtime_path _warp_runtime_entry + fi # Read through shell options to determine if the user has enabled vi mode. vi_mode_enabled=0 diff --git a/app/assets/bundled/bootstrap/fish.sh b/app/assets/bundled/bootstrap/fish.sh index 4f323a98a82..ecce8127555 100644 --- a/app/assets/bundled/bootstrap/fish.sh +++ b/app/assets/bundled/bootstrap/fish.sh @@ -44,6 +44,20 @@ if test -n "$WARP_PATH_APPEND" set -gx --path PATH "$PATH:$WARP_PATH_APPEND" set -e WARP_PATH_APPEND end +# Direct non-root cloud containers use a private command directory when system installation fails. +# Restore the entrypoint-provided directory's precedence after profiles reset PATH. +if string match -q '/*' -- "$WARP_CLOUD_AGENT_FALLBACK_BIN" + and not string match -q '*:*' -- "$WARP_CLOUD_AGENT_FALLBACK_BIN" + and test -d "$WARP_CLOUD_AGENT_FALLBACK_BIN" + and test (command uname -s) = Linux + set -l runtime_path "$WARP_CLOUD_AGENT_FALLBACK_BIN" + for runtime_entry in $PATH + if test "$runtime_entry" != "$WARP_CLOUD_AGENT_FALLBACK_BIN" + set -a runtime_path "$runtime_entry" + end + end + set -gx PATH $runtime_path +end function warp_send_json_message # Sends a message to the controlling terminal as a DSC control sequence. diff --git a/app/assets/bundled/bootstrap/zsh_body.sh b/app/assets/bundled/bootstrap/zsh_body.sh index d0786051e0b..a8fa4902f85 100644 --- a/app/assets/bundled/bootstrap/zsh_body.sh +++ b/app/assets/bundled/bootstrap/zsh_body.sh @@ -1356,6 +1356,19 @@ esac export PATH="$PATH:$WARP_PATH_APPEND" unset WARP_PATH_APPEND fi + # Direct non-root cloud containers use a private command directory when system installation fails. + # Restore the entrypoint-provided directory's precedence after profiles reset PATH. + if [[ "${WARP_CLOUD_AGENT_FALLBACK_BIN:-}" == /* && "$WARP_CLOUD_AGENT_FALLBACK_BIN" != *:* \ + && -d "$WARP_CLOUD_AGENT_FALLBACK_BIN" && "$(command -p uname -s)" == Linux ]]; then + local -a runtime_path=("$WARP_CLOUD_AGENT_FALLBACK_BIN") + local runtime_entry="" + for runtime_entry in "${path[@]}"; do + if [[ "$runtime_entry" != "$WARP_CLOUD_AGENT_FALLBACK_BIN" ]]; then + runtime_path+=("$runtime_entry") + fi + done + export PATH="${(j.:.)runtime_path}" + fi local -a shell_plugins diff --git a/crates/integration/src/bin/integration.rs b/crates/integration/src/bin/integration.rs index 8f879b1c4df..4b51a919801 100644 --- a/crates/integration/src/bin/integration.rs +++ b/crates/integration/src/bin/integration.rs @@ -141,6 +141,7 @@ fn register_tests() -> HashMap<&'static str, BoxedBuilderFn> { register_test!(test_bootstrap_with_no_script_execution_block); register_test!(test_instant_prompt_bootstrap); register_test!(test_rc_files_only_sourced_once_during_bootstrapping); + register_test!(test_cloud_agent_fallback_commands_survive_profile_path_reset); register_test!(test_unescaped_prompt_bootstraps); register_test!(test_detect_powerlevel10k); register_test!(test_open_and_close_resource_center); diff --git a/crates/integration/src/test/bootstrapping.rs b/crates/integration/src/test/bootstrapping.rs index ac043d9a051..607a2a06654 100644 --- a/crates/integration/src/test/bootstrapping.rs +++ b/crates/integration/src/test/bootstrapping.rs @@ -1,5 +1,9 @@ //! Integration tests for bootstrapping logic. +use std::fs; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt as _; + use settings::Setting as _; use version_compare::Cmp; use warp::cmd_or_ctrl_shift; @@ -62,6 +66,61 @@ pub fn test_rc_files_only_sourced_once_during_bootstrapping() -> Builder { )) } +pub fn test_cloud_agent_fallback_commands_survive_profile_path_reset() -> Builder { + new_builder() + .set_should_run_test(|| { + let (starter, _version) = current_shell_starter_and_version(); + cfg!(target_os = "linux") + && matches!( + starter.shell_type(), + ShellType::Bash | ShellType::Zsh | ShellType::Fish + ) + }) + .with_setup(|utils| { + let dir = utils.test_dir(); + let fallback = dir.join("fallback bin"); + let profile = dir.join("profile-bin"); + for (bin, output) in [ + (&fallback, "fallback-command-selected"), + (&profile, "profile-command-selected"), + ] { + fs::create_dir_all(bin).expect("should create command directory"); + let tool = bin.join("warp-fallback-probe"); + fs::write(&tool, format!("#!/bin/sh\nprintf '{output}\\n'\n")) + .expect("should write fixture command"); + #[cfg(unix)] + fs::set_permissions(&tool, fs::Permissions::from_mode(0o755)) + .expect("fixture command should be executable"); + } + utils.set_env("WARP_CLOUD_AGENT_FALLBACK_BIN", Some(&fallback)); + utils.set_env( + "PATH", + Some(format!( + "{}:{}", + fallback.display(), + std::env::var("PATH").unwrap() + )), + ); + write_rc_files_for_test( + &dir, + r#"export PATH="$HOME/profile-bin:/usr/bin:/bin""#, + [ShellRcType::Bash, ShellRcType::Zsh], + ); + write_rc_files_for_test( + &dir, + r#"set -gx PATH "$HOME/profile-bin" /usr/bin /bin"#, + [ShellRcType::Fish], + ); + }) + .with_step(wait_until_bootstrapped_single_pane_for_tab(0)) + .with_step(execute_command_for_single_terminal_in_tab( + 0, + "warp-fallback-probe".to_string(), + ExpectedExitStatus::Success, + "fallback-command-selected", + )) +} + pub fn test_unescaped_prompt_bootstraps() -> Builder { new_builder() .with_setup(|utils| { diff --git a/crates/integration/tests/integration/shell_integration_tests.rs b/crates/integration/tests/integration/shell_integration_tests.rs index c345c62570d..6675d625ecd 100644 --- a/crates/integration/tests/integration/shell_integration_tests.rs +++ b/crates/integration/tests/integration/shell_integration_tests.rs @@ -19,6 +19,8 @@ integration_tests! { // Test properties of bootstrap. test_bootstrap_with_no_script_execution_block, test_rc_files_only_sourced_once_during_bootstrapping, + // Tests Linux cloud-agent command precedence after Bash, Zsh, and Fish profiles reset PATH. + test_cloud_agent_fallback_commands_survive_profile_path_reset, // Test ctrl-c terminates long-running commands. test_ctrl_c, // Test copying a block's command gives us the expected command string.