fix(terminal): skip sudo prompt when local NOPASSWD sudo works
When running on a host with sudoers NOPASSWD configured for the current user, interactive Hermes sessions were unnecessarily entering the password prompt path before executing sudo commands. Outside Hermes, `sudo -n true` exits 0 for that user. Add `_sudo_nopasswd_works()` that probes `sudo -n true` and, when it succeeds, lets `_transform_sudo_command()` return the command unchanged with no stdin password. The probe: - Is scoped to the `local` terminal backend only, so Docker/SSH/Modal and other remote backends do not inherit host sudo state. - Re-probes every call (no process-lifetime cache) so an expired sudo timestamp cannot silently make a later command block waiting for a password that Hermes never prompts for. - Is bypassed entirely when `SUDO_PASSWORD` is configured or a cached password already exists, preserving existing explicit-password flows. Co-authored-by: Junting Wu <juntingpublic@gmail.com>
This commit is contained in:
parent
ccfe6a47c3
commit
ab6c629ccc
@ -104,6 +104,57 @@ def test_cached_sudo_password_isolated_by_session_key(monkeypatch):
|
||||
assert terminal_tool._get_cached_sudo_password() == "alpha-pass"
|
||||
|
||||
|
||||
def test_passwordless_sudo_skips_interactive_prompt_and_rewrite(monkeypatch):
|
||||
monkeypatch.delenv("SUDO_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("TERMINAL_ENV", raising=False)
|
||||
monkeypatch.setenv("HERMES_INTERACTIVE", "1")
|
||||
|
||||
def _fail_prompt(*_args, **_kwargs):
|
||||
raise AssertionError(
|
||||
"interactive sudo prompt should not run when sudo -n already works"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(terminal_tool, "_prompt_for_sudo_password", _fail_prompt)
|
||||
monkeypatch.setattr(terminal_tool, "_sudo_nopasswd_works", lambda: True, raising=False)
|
||||
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command("sudo whoami")
|
||||
|
||||
assert transformed == "sudo whoami"
|
||||
assert sudo_stdin is None
|
||||
|
||||
|
||||
def test_passwordless_sudo_probe_rechecks_local_terminal(monkeypatch):
|
||||
monkeypatch.delenv("TERMINAL_ENV", raising=False)
|
||||
calls = []
|
||||
|
||||
class Result:
|
||||
def __init__(self, returncode):
|
||||
self.returncode = returncode
|
||||
|
||||
def fake_run(args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
return Result(0 if len(calls) == 1 else 1)
|
||||
|
||||
monkeypatch.setattr(terminal_tool.subprocess, "run", fake_run)
|
||||
|
||||
assert terminal_tool._sudo_nopasswd_works() is True
|
||||
assert terminal_tool._sudo_nopasswd_works() is False
|
||||
assert len(calls) == 2
|
||||
assert calls[0][0] == ["sudo", "-n", "true"]
|
||||
assert calls[1][0] == ["sudo", "-n", "true"]
|
||||
|
||||
|
||||
def test_passwordless_sudo_probe_is_disabled_for_nonlocal_terminal_env(monkeypatch):
|
||||
monkeypatch.setenv("TERMINAL_ENV", "docker")
|
||||
|
||||
def _fail_run(*_args, **_kwargs):
|
||||
raise AssertionError("host sudo probe must not run for non-local terminal envs")
|
||||
|
||||
monkeypatch.setattr(terminal_tool.subprocess, "run", _fail_run)
|
||||
|
||||
assert terminal_tool._sudo_nopasswd_works() is False
|
||||
|
||||
|
||||
def test_validate_workdir_allows_windows_drive_paths():
|
||||
assert terminal_tool._validate_workdir(r"C:\Users\Alice\project") is None
|
||||
assert terminal_tool._validate_workdir("C:/Users/Alice/project") is None
|
||||
|
||||
@ -620,6 +620,32 @@ def _rewrite_real_sudo_invocations(command: str) -> tuple[str, bool]:
|
||||
return "".join(out), found
|
||||
|
||||
|
||||
def _sudo_nopasswd_works() -> bool:
|
||||
"""Return True when local sudo currently works without prompting.
|
||||
|
||||
Only probes for the `local` terminal backend; Docker/SSH/Modal/etc. must
|
||||
not inherit the host's sudo state. Re-probes every call (no process-level
|
||||
cache) so an expired sudo timestamp cannot make a later command silently
|
||||
block waiting for a password.
|
||||
"""
|
||||
terminal_env = os.getenv("TERMINAL_ENV", "local").strip().lower() or "local"
|
||||
if terminal_env != "local":
|
||||
return False
|
||||
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
["sudo", "-n", "true"],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=3,
|
||||
check=False,
|
||||
)
|
||||
return probe.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _rewrite_compound_background(command: str) -> str:
|
||||
"""Wrap `A && B &` (or `A || B &`) to `A && { B & }` at depth 0.
|
||||
|
||||
@ -833,6 +859,15 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
|
||||
else _get_cached_sudo_password()
|
||||
)
|
||||
|
||||
# Local hosts with sudoers NOPASSWD should not be forced through the
|
||||
# interactive Hermes password prompt or the sudo -S password-pipe path.
|
||||
# Scoped to the local terminal backend so Docker/SSH/Modal/etc. can't
|
||||
# inherit host sudo state. Re-probes every call (no process-lifetime
|
||||
# cache) so an expired sudo timestamp doesn't make a later command block
|
||||
# silently without Hermes prompting.
|
||||
if not has_configured_password and not sudo_password and _sudo_nopasswd_works():
|
||||
return command, None
|
||||
|
||||
if not has_configured_password and not sudo_password and os.getenv("HERMES_INTERACTIVE"):
|
||||
sudo_password = _prompt_for_sudo_password(timeout_seconds=45)
|
||||
if sudo_password:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user