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
This commit is contained in:
Teknium 2026-04-15 14:50:07 -07:00 committed by GitHub
parent 1b12f9b1d6
commit 18396af31e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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"