From 99ba6b9f464d8c6159bef769b461003a39ed878c Mon Sep 17 00:00:00 2001 From: vkodithala Date: Mon, 26 Oct 2026 15:12:01 -0400 Subject: [PATCH 1/2] Restore Linux runtime commands after shell profile initialization Co-Authored-By: Warp --- .github/workflows/ci.yml | 5 +- app/assets/bundled/bootstrap/bash_body.sh | 15 ++++ app/assets/bundled/bootstrap/fish.sh | 13 ++++ app/assets/bundled/bootstrap/zsh_body.sh | 12 +++ script/test_runtime_bin_handoff.py | 95 +++++++++++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 script/test_runtime_bin_handoff.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37e9aa668d8..a9eb17d9950 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -722,11 +722,14 @@ jobs: - name: Install ShellCheck uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30 # v1 with: - apt: shellcheck + apt: shellcheck zsh fish - name: Run ShellCheck run: ./script/lint_shellcheck --ci + - name: Test runtime command PATH handoff + run: python3 script/test_runtime_bin_handoff.py + - name: Run PSScriptAnalyzer (PowerShell Lint) run: ./script/lint_powershell -ci shell: pwsh diff --git a/app/assets/bundled/bootstrap/bash_body.sh b/app/assets/bundled/bootstrap/bash_body.sh index 2e082387827..167cbf826b6 100644 --- a/app/assets/bundled/bootstrap/bash_body.sh +++ b/app/assets/bundled/bootstrap/bash_body.sh @@ -1517,6 +1517,21 @@ esac export PATH="$PATH:$WARP_PATH_APPEND" unset WARP_PATH_APPEND fi + # System profiles can discard the runtime commands inherited from the container entrypoint. + if [[ "${WARP_RUNTIME_BIN:-}" == /* && "$WARP_RUNTIME_BIN" != *:* \ + && -d "$WARP_RUNTIME_BIN" && "$(command -p uname -s)" == Linux ]]; then + _warp_runtime_remaining="$PATH:" + _warp_runtime_path="$WARP_RUNTIME_BIN" + while [[ -n "$_warp_runtime_remaining" ]]; do + _warp_runtime_entry="${_warp_runtime_remaining%%:*}" + _warp_runtime_remaining="${_warp_runtime_remaining#*:}" + if [[ "$_warp_runtime_entry" != "$WARP_RUNTIME_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..3dd162ad5f5 100644 --- a/app/assets/bundled/bootstrap/fish.sh +++ b/app/assets/bundled/bootstrap/fish.sh @@ -44,6 +44,19 @@ if test -n "$WARP_PATH_APPEND" set -gx --path PATH "$PATH:$WARP_PATH_APPEND" set -e WARP_PATH_APPEND end +# System profiles can discard the runtime commands inherited from the container entrypoint. +if string match -q '/*' -- "$WARP_RUNTIME_BIN" + and not string match -q '*:*' -- "$WARP_RUNTIME_BIN" + and test -d "$WARP_RUNTIME_BIN" + and test (command uname -s) = Linux + set -l runtime_path "$WARP_RUNTIME_BIN" + for runtime_entry in $PATH + if test "$runtime_entry" != "$WARP_RUNTIME_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..269c3b71bf4 100644 --- a/app/assets/bundled/bootstrap/zsh_body.sh +++ b/app/assets/bundled/bootstrap/zsh_body.sh @@ -1356,6 +1356,18 @@ esac export PATH="$PATH:$WARP_PATH_APPEND" unset WARP_PATH_APPEND fi + # System profiles can discard the runtime commands inherited from the container entrypoint. + if [[ "${WARP_RUNTIME_BIN:-}" == /* && "$WARP_RUNTIME_BIN" != *:* \ + && -d "$WARP_RUNTIME_BIN" && "$(command -p uname -s)" == Linux ]]; then + local -a runtime_path=("$WARP_RUNTIME_BIN") + local runtime_entry="" + for runtime_entry in "${path[@]}"; do + if [[ "$runtime_entry" != "$WARP_RUNTIME_BIN" ]]; then + runtime_path+=("$runtime_entry") + fi + done + export PATH="${(j.:.)runtime_path}" + fi local -a shell_plugins diff --git a/script/test_runtime_bin_handoff.py b/script/test_runtime_bin_handoff.py new file mode 100644 index 00000000000..699333e6be3 --- /dev/null +++ b/script/test_runtime_bin_handoff.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Run the runtime-bin bootstrap blocks without the terminal protocol/UI.""" + +import os +import shlex +import shutil +import subprocess +import tempfile +from pathlib import Path + + +BOOTSTRAP = Path(__file__).resolve().parents[1] / "app/assets/bundled/bootstrap" +MARKER = "# System profiles can discard the runtime commands inherited from the container entrypoint." + + +def check_shell(shell, root): + executable = shutil.which(shell) + if executable is None: + raise RuntimeError(f"{shell} is required") + filename = {"bash": "bash_body.sh", "zsh": "zsh_body.sh", "fish": "fish.sh"}[shell] + block = (BOOTSTRAP / filename).read_text().split(MARKER, 1)[1].split("\n\n", 1)[0] + runtime = str(root / "runtime [bin]") + profile_path = f"{root}/customer:{root}/system:/usr/bin:/bin" + command_shim = """ +command() { + if [[ "$*" == "-p uname -s" ]]; then + printf '%s\\n' "$TEST_KERNEL" + else + builtin command "$@" + fi +} +""" + scenarios = [ + ("profile reset", runtime, profile_path, "Linux", f"{runtime}:{profile_path}"), + ( + "duplicate entries", + runtime, + f"{runtime}:{profile_path}:{runtime}", + "Linux", + f"{runtime}:{profile_path}", + ), + ("unset", None, profile_path, "Linux", profile_path), + ("missing directory", str(root / "missing"), profile_path, "Linux", profile_path), + ("relative directory", "runtime [bin]", profile_path, "Linux", profile_path), + ("non-Linux", runtime, profile_path, "Darwin", profile_path), + ] + for label, directory, initial_path, kernel, expected in scenarios: + environment = os.environ.copy() + environment.pop("WARP_RUNTIME_BIN", None) + environment["TEST_KERNEL"] = kernel + if directory is not None: + environment["WARP_RUNTIME_BIN"] = directory + profile = root / f"profile.{shell}" + if shell == "fish": + profile.write_text(f"set -gx PATH {shlex.quote(initial_path)}\n") + setup = "" + report = 'printf "%s\\n" (string join : $PATH); ffmpeg' + else: + profile.write_text(f"export PATH={shlex.quote(initial_path)}\n") + setup = command_shim + report = 'printf "%s\\n" "$PATH"; ffmpeg' + script = f"{setup}\nsource {shlex.quote(str(profile))}\n{block}\n{block}\n{report}" + result = subprocess.run( + [executable, "-c", script], + env=environment, + cwd=root, + text=True, + capture_output=True, + check=True, + ) + selected_tool = "runtime" if expected.startswith(runtime + ":") else "customer" + assert result.stdout.splitlines() == [expected, selected_tool], ( + f"{shell}: {label}: {result.stdout!r}\n{result.stderr}" + ) + print(f"PASS {shell}: {label}") + + +def main(): + with tempfile.TemporaryDirectory(prefix="warp-runtime-bin-") as directory: + root = Path(directory) + for name in ("runtime [bin]", "customer", "system"): + (root / name).mkdir() + for name, result in (("runtime [bin]", "runtime"), ("customer", "customer")): + tool = root / name / "ffmpeg" + tool.write_text(f"#!/bin/sh\nprintf '{result}\\n'\n") + tool.chmod(0o755) + uname = root / "system" / "uname" + uname.write_text('#!/bin/sh\nprintf "%s\\n" "$TEST_KERNEL"\n') + uname.chmod(0o755) + for shell in ("bash", "zsh", "fish"): + check_shell(shell, root) + + +if __name__ == "__main__": + main() From 41b11317a2c85da760ca8e24f1bf9c161c00b791 Mon Sep 17 00:00:00 2001 From: vkodithala Date: Mon, 26 Oct 2026 18:21:43 -0400 Subject: [PATCH 2/2] Clarify cloud fallback handoff and reuse shell integration coverage Co-Authored-By: Warp --- .github/workflows/ci.yml | 5 +- app/assets/bundled/bootstrap/bash_body.sh | 11 ++- app/assets/bundled/bootstrap/fish.sh | 13 +-- app/assets/bundled/bootstrap/zsh_body.sh | 11 ++- crates/integration/src/bin/integration.rs | 1 + crates/integration/src/test/bootstrapping.rs | 59 ++++++++++++ .../integration/shell_integration_tests.rs | 2 + script/test_runtime_bin_handoff.py | 95 ------------------- 8 files changed, 82 insertions(+), 115 deletions(-) delete mode 100644 script/test_runtime_bin_handoff.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9eb17d9950..37e9aa668d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -722,14 +722,11 @@ jobs: - name: Install ShellCheck uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30 # v1 with: - apt: shellcheck zsh fish + apt: shellcheck - name: Run ShellCheck run: ./script/lint_shellcheck --ci - - name: Test runtime command PATH handoff - run: python3 script/test_runtime_bin_handoff.py - - name: Run PSScriptAnalyzer (PowerShell Lint) run: ./script/lint_powershell -ci shell: pwsh diff --git a/app/assets/bundled/bootstrap/bash_body.sh b/app/assets/bundled/bootstrap/bash_body.sh index 167cbf826b6..dc3ea059d53 100644 --- a/app/assets/bundled/bootstrap/bash_body.sh +++ b/app/assets/bundled/bootstrap/bash_body.sh @@ -1517,15 +1517,16 @@ esac export PATH="$PATH:$WARP_PATH_APPEND" unset WARP_PATH_APPEND fi - # System profiles can discard the runtime commands inherited from the container entrypoint. - if [[ "${WARP_RUNTIME_BIN:-}" == /* && "$WARP_RUNTIME_BIN" != *:* \ - && -d "$WARP_RUNTIME_BIN" && "$(command -p uname -s)" == Linux ]]; then + # 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_RUNTIME_BIN" + _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_RUNTIME_BIN" ]]; then + if [[ "$_warp_runtime_entry" != "$WARP_CLOUD_AGENT_FALLBACK_BIN" ]]; then _warp_runtime_path="$_warp_runtime_path:$_warp_runtime_entry" fi done diff --git a/app/assets/bundled/bootstrap/fish.sh b/app/assets/bundled/bootstrap/fish.sh index 3dd162ad5f5..ecce8127555 100644 --- a/app/assets/bundled/bootstrap/fish.sh +++ b/app/assets/bundled/bootstrap/fish.sh @@ -44,14 +44,15 @@ if test -n "$WARP_PATH_APPEND" set -gx --path PATH "$PATH:$WARP_PATH_APPEND" set -e WARP_PATH_APPEND end -# System profiles can discard the runtime commands inherited from the container entrypoint. -if string match -q '/*' -- "$WARP_RUNTIME_BIN" - and not string match -q '*:*' -- "$WARP_RUNTIME_BIN" - and test -d "$WARP_RUNTIME_BIN" +# 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_RUNTIME_BIN" + set -l runtime_path "$WARP_CLOUD_AGENT_FALLBACK_BIN" for runtime_entry in $PATH - if test "$runtime_entry" != "$WARP_RUNTIME_BIN" + if test "$runtime_entry" != "$WARP_CLOUD_AGENT_FALLBACK_BIN" set -a runtime_path "$runtime_entry" end end diff --git a/app/assets/bundled/bootstrap/zsh_body.sh b/app/assets/bundled/bootstrap/zsh_body.sh index 269c3b71bf4..a8fa4902f85 100644 --- a/app/assets/bundled/bootstrap/zsh_body.sh +++ b/app/assets/bundled/bootstrap/zsh_body.sh @@ -1356,13 +1356,14 @@ esac export PATH="$PATH:$WARP_PATH_APPEND" unset WARP_PATH_APPEND fi - # System profiles can discard the runtime commands inherited from the container entrypoint. - if [[ "${WARP_RUNTIME_BIN:-}" == /* && "$WARP_RUNTIME_BIN" != *:* \ - && -d "$WARP_RUNTIME_BIN" && "$(command -p uname -s)" == Linux ]]; then - local -a runtime_path=("$WARP_RUNTIME_BIN") + # 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_RUNTIME_BIN" ]]; then + if [[ "$runtime_entry" != "$WARP_CLOUD_AGENT_FALLBACK_BIN" ]]; then runtime_path+=("$runtime_entry") fi done 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. diff --git a/script/test_runtime_bin_handoff.py b/script/test_runtime_bin_handoff.py deleted file mode 100644 index 699333e6be3..00000000000 --- a/script/test_runtime_bin_handoff.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 -"""Run the runtime-bin bootstrap blocks without the terminal protocol/UI.""" - -import os -import shlex -import shutil -import subprocess -import tempfile -from pathlib import Path - - -BOOTSTRAP = Path(__file__).resolve().parents[1] / "app/assets/bundled/bootstrap" -MARKER = "# System profiles can discard the runtime commands inherited from the container entrypoint." - - -def check_shell(shell, root): - executable = shutil.which(shell) - if executable is None: - raise RuntimeError(f"{shell} is required") - filename = {"bash": "bash_body.sh", "zsh": "zsh_body.sh", "fish": "fish.sh"}[shell] - block = (BOOTSTRAP / filename).read_text().split(MARKER, 1)[1].split("\n\n", 1)[0] - runtime = str(root / "runtime [bin]") - profile_path = f"{root}/customer:{root}/system:/usr/bin:/bin" - command_shim = """ -command() { - if [[ "$*" == "-p uname -s" ]]; then - printf '%s\\n' "$TEST_KERNEL" - else - builtin command "$@" - fi -} -""" - scenarios = [ - ("profile reset", runtime, profile_path, "Linux", f"{runtime}:{profile_path}"), - ( - "duplicate entries", - runtime, - f"{runtime}:{profile_path}:{runtime}", - "Linux", - f"{runtime}:{profile_path}", - ), - ("unset", None, profile_path, "Linux", profile_path), - ("missing directory", str(root / "missing"), profile_path, "Linux", profile_path), - ("relative directory", "runtime [bin]", profile_path, "Linux", profile_path), - ("non-Linux", runtime, profile_path, "Darwin", profile_path), - ] - for label, directory, initial_path, kernel, expected in scenarios: - environment = os.environ.copy() - environment.pop("WARP_RUNTIME_BIN", None) - environment["TEST_KERNEL"] = kernel - if directory is not None: - environment["WARP_RUNTIME_BIN"] = directory - profile = root / f"profile.{shell}" - if shell == "fish": - profile.write_text(f"set -gx PATH {shlex.quote(initial_path)}\n") - setup = "" - report = 'printf "%s\\n" (string join : $PATH); ffmpeg' - else: - profile.write_text(f"export PATH={shlex.quote(initial_path)}\n") - setup = command_shim - report = 'printf "%s\\n" "$PATH"; ffmpeg' - script = f"{setup}\nsource {shlex.quote(str(profile))}\n{block}\n{block}\n{report}" - result = subprocess.run( - [executable, "-c", script], - env=environment, - cwd=root, - text=True, - capture_output=True, - check=True, - ) - selected_tool = "runtime" if expected.startswith(runtime + ":") else "customer" - assert result.stdout.splitlines() == [expected, selected_tool], ( - f"{shell}: {label}: {result.stdout!r}\n{result.stderr}" - ) - print(f"PASS {shell}: {label}") - - -def main(): - with tempfile.TemporaryDirectory(prefix="warp-runtime-bin-") as directory: - root = Path(directory) - for name in ("runtime [bin]", "customer", "system"): - (root / name).mkdir() - for name, result in (("runtime [bin]", "runtime"), ("customer", "customer")): - tool = root / name / "ffmpeg" - tool.write_text(f"#!/bin/sh\nprintf '{result}\\n'\n") - tool.chmod(0o755) - uname = root / "system" / "uname" - uname.write_text('#!/bin/sh\nprintf "%s\\n" "$TEST_KERNEL"\n') - uname.chmod(0o755) - for shell in ("bash", "zsh", "fish"): - check_shell(shell, root) - - -if __name__ == "__main__": - main()