From 18396af31ede5ce9127c966281eb748d89192156 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:50:07 -0700 Subject: [PATCH] fix: handle cross-device shutil.move failure in tirith auto-install (#10127) (#10524) _install_tirith() uses shutil.move() to place the binary from tmpdir to ~/.hermes/bin/. When these are on different filesystems (common in Docker, NFS), shutil.move() falls back to copy2 + unlink, but copy2's metadata step can raise PermissionError. This exception propagated past the fail_open guard, crashing the terminal tool entirely. Additionally, a failed install could leave a non-executable tirith binary at the destination, causing a retry loop on every subsequent terminal command. Fix: - Catch OSError from shutil.move() and fall back to shutil.copy() (skips metadata/xattr copying that causes PermissionError) - If even copy fails, clean up the partial dest file to prevent the non-executable retry loop - Return (None, 'cross_device_copy_failed') so the failure routes through the existing install-failure caching and fail_open logic Closes #10127 --- tools/tirith_security.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/tirith_security.py b/tools/tirith_security.py index b3055944e..44710ee60 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -360,7 +360,21 @@ def _install_tirith(*, log_failures: bool = True) -> tuple[str | None, str]: src = os.path.join(tmpdir, "tirith") dest = os.path.join(_hermes_bin_dir(), "tirith") - shutil.move(src, dest) + try: + shutil.move(src, dest) + except OSError: + # Cross-device move (common in Docker, NFS): shutil.move() falls + # back to copy2 + unlink, but copy2's metadata step can raise + # PermissionError. Use plain copy + manual chmod instead. + try: + shutil.copy(src, dest) + except OSError: + # Clean up partial dest to prevent a non-executable retry loop + try: + os.unlink(dest) + except OSError: + pass + return None, "cross_device_copy_failed" os.chmod(dest, os.stat(dest).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) verification = "cosign + SHA-256" if cosign_verified else "SHA-256 only"