refactor(terminal): compact terminal_tool_sudo docstrings/comments (WHY/invariants kept)

This commit is contained in:
Teknium
2026-09-02 20:26:02 -07:00
parent 5ef79fa8af
commit db87f06c80
+46 -74
View File
@@ -1,11 +1,8 @@
"""Sudo password plumbing and shell-command rewrites for the terminal tool: the per-scope
interactive password cache, the /dev/tty prompt, the quote-aware shell scanner behind the
real-sudo rewrite (``sudo -S -p ''``) and the compound-background brace-group rewrite, and
the NOPASSWD probe.
Split out of ``tools/terminal_tool.py``; every public/patched name is re-imported there,
so ``tools.terminal_tool.<name>`` keeps resolving (and monkeypatching) as before.
"""
"""Sudo password plumbing and shell-command rewrites for the terminal tool: per-scope
password cache, /dev/tty prompt, the quote-aware shell scanner behind the real-sudo rewrite
(``sudo -S -p ''``) and the compound-background brace-group rewrite, and the NOPASSWD probe.
Split out of ``tools/terminal_tool.py``; every public/patched name is re-imported there so
``tools.terminal_tool.<name>`` keeps resolving (and monkeypatching) as before."""
import logging
import os
@@ -69,14 +66,11 @@ def _reset_cached_sudo_passwords() -> None:
def _in_delegated_child_context() -> bool:
"""True while running inside a delegate_task child.
Subagents run on parent-process worker threads and inherit process-wide
``HERMES_INTERACTIVE=1``, which does NOT mean this context can reach the user: a raw
``/dev/tty`` sudo prompt from a child races the TUI for the tty and blocks the child for
the full timeout. Children must always be headless for sudo prompting. The ContextVar is
set by ``delegated_child_context()`` and propagates via ``copy_context``.
"""
"""True while running inside a delegate_task child. Subagents run on parent-process worker
threads and inherit process-wide ``HERMES_INTERACTIVE=1``, which does NOT mean they can reach
the user: a raw ``/dev/tty`` sudo prompt from a child races the TUI for the tty and blocks the
child for the full timeout, so children are always headless for sudo prompting. The ContextVar
is set by ``delegated_child_context()`` and propagates via ``copy_context``."""
try:
from agent.delegation_context import is_delegated_child_context
return is_delegated_child_context()
@@ -180,12 +174,9 @@ def _read_hidden_password(result: dict) -> None:
def _prompt_for_sudo_password(timeout_seconds: int = 45) -> str:
"""Prompt for a sudo password; "" on skip (empty Enter), timeout, or error.
Prefers the CLI-registered callback (prompt_toolkit-integrated); otherwise reads
/dev/tty (msvcrt on Windows) with echo disabled. Time spent waiting on the human is
excluded from tool deadlines via ``human_wait_window``.
"""
"""Prompt for a sudo password; "" on skip (empty Enter), timeout, or error. Prefers the
CLI-registered callback (prompt_toolkit-integrated); otherwise reads /dev/tty (msvcrt on
Windows) with echo disabled. Human wait time is excluded from tool deadlines (``human_wait_window``)."""
from tools.terminal_tool import _get_sudo_password_callback
_sudo_cb = _get_sudo_password_callback()
if _sudo_cb is not None:
@@ -250,9 +241,8 @@ def _looks_like_env_assignment(token: str) -> bool:
return bool(re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name))
# One shell word: single-quoted run (unterminated ok), double-quoted run with backslash
# escapes (unterminated ok), a backslash escape (a trailing lone `\` is a plain char), or
# any char other than whitespace and the metacharacters `;|&()`.
# One shell word: single-/double-quoted runs (unterminated ok; backslash escapes inside double
# quotes), a backslash escape (a trailing lone `\` is a plain char), or any non-`;|&()`/space char.
_SHELL_WORD_RE = re.compile(r"""(?:'[^']*'?|"(?:\\.|[^"])*"?|\\.|[^\s;|&()])*""", re.DOTALL)
@@ -312,12 +302,9 @@ def _scan_shell(command: str, background: bool = False) -> Iterator[tuple[str, i
def _rewrite_real_sudo_invocations(command: str) -> tuple[str, int]:
"""Rewrite only real unquoted sudo command words, not plain text mentions.
A word is a sudo invocation only at a command-start position (see ``_scan_shell``);
comments there are copied through verbatim. Returns the rewritten command and the
number of sudo invocations rewritten.
"""
"""Rewrite only real unquoted sudo command words (at a command-start position, see
``_scan_shell``), not plain text mentions; comments are copied through verbatim.
Returns the rewritten command and the number of sudo invocations rewritten."""
out: list[str] = []
sudo_count = 0
for kind, start, end, at_start in _scan_shell(command):
@@ -335,12 +322,9 @@ def _count_real_sudo_invocations(command: str) -> int:
def _sudo_nopasswd_works() -> bool:
"""True when local sudo currently works without prompting.
Local backend only — Docker/SSH/Modal must not inherit host sudo state. Re-probes every
call (no cache) so an expired sudo timestamp can't make a later command silently block
waiting for a password.
"""
"""True when local sudo currently works without prompting. Local backend only — Docker/SSH/
Modal must not inherit host sudo state. Re-probes every call (no cache) so an expired sudo
timestamp can't make a later command silently block waiting for a password."""
from tools.terminal_tool import _tenv
if (_tenv("TERMINAL_ENV", "local").strip().lower() or "local") != "local":
return False
@@ -355,23 +339,17 @@ def _sudo_nopasswd_works() -> bool:
def _rewrite_compound_background(command: str) -> str:
"""Wrap `A && B &` (or `A || B &`) to `A && { B & }` at depth 0.
Bash binds `&&` tighter than `&`, so `A && B &` backgrounds a subshell that runs B in the
foreground and waits for it; a long-running B leaves that subshell stuck in ``wait4``
forever, and its open stdout pipe can keep the terminal tool from returning. The brace
group keeps `&&`'s skip-B-on-failure semantics without a fork: bash backgrounds B as a
simple command and exits immediately, orphaning B normally.
Redirects (``&>``, ``2>&1``), quoted strings, comments and ``(...)``/``{ ... }`` group
bodies never count as the backgrounding ``&`` (see ``_scan_shell``); tracking brace depth
also makes the rewrite idempotent. `(...)` subshells have the same bug class but are not
the common agent pattern; left for a follow-up. Simple ``cmd &`` is left alone — it
doesn't have the subshell-wait bug.
"""
# Position just after the most recent `&&` / `||` at depth 0 in the current statement;
# -1 when no chain operator is active.
chain_end = -1
"""Wrap `A && B &` (or `A || B &`) to `A && { B & }` at depth 0. Bash binds `&&` tighter
than `&`, so `A && B &` backgrounds a subshell that runs B in the foreground and waits for
it; a long-running B leaves that subshell stuck in ``wait4`` forever, and its open stdout
pipe can keep the terminal tool from returning. The brace group keeps `&&`'s
skip-B-on-failure semantics without a fork: bash backgrounds B as a simple command and
exits immediately, orphaning B normally. Redirects (``&>``, ``2>&1``), quoted strings,
comments and ``(...)``/``{ ... }`` bodies never count as the backgrounding ``&`` (see
``_scan_shell``); tracking brace depth also makes the rewrite idempotent. `(...)` subshells
have the same bug class but are not the common agent pattern; left for a follow-up.
Simple ``cmd &`` is left alone — it doesn't have the subshell-wait bug."""
chain_end = -1 # just after the last depth-0 `&&`/`||` of this statement; -1 = none active
rewrites: list[tuple[int, int]] = [] # (chain_op_end, amp_pos)
for kind, start, end, _ in _scan_shell(command, background=True):
text = command[start:end]
@@ -406,18 +384,15 @@ def _rewrite_compound_background(command: str) -> str:
def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None]:
"""Rewrite bare ``sudo`` to ``sudo -S -p ''`` when a password is available.
Shared by every execution environment. Returns ``(command, sudo_stdin)``: ``sudo_stdin``
is one password line per sudo invocation that the caller must PREPEND to the process
stdin (sudo -S consumes exactly one line and passes the rest through, so it's safe
alongside the caller's own stdin_data). Backends that can't pipe stdin (modal, daytona,
vercel_sandbox) embed the password in the command string themselves. With no password
available the command is returned unchanged and ``sudo_stdin`` is None, so it fails
gracefully with "sudo: a password is required". Password sources, in order: configured
SUDO_PASSWORD, the session cache, then an interactive prompt (45s timeout, cached on
success) when a UI is reachable.
"""
"""Rewrite bare ``sudo`` to ``sudo -S -p ''`` when a password is available (shared by every
execution environment). Returns ``(command, sudo_stdin)``: ``sudo_stdin`` is one password
line per sudo invocation that the caller must PREPEND to the process stdin (sudo -S consumes
exactly one line and passes the rest through, so it's safe alongside the caller's own
stdin_data). Backends that can't pipe stdin (modal, daytona, vercel_sandbox) embed the
password in the command string themselves. With no password available the command is
returned unchanged and ``sudo_stdin`` is None, so it fails gracefully with "sudo: a password
is required". Password sources, in order: configured SUDO_PASSWORD, the session cache, then
an interactive prompt (45s timeout, cached on success) when a UI is reachable."""
from tools.terminal_tool import _get_sudo_password_callback, _prompt_for_sudo_password, _sudo_nopasswd_works
if command is None:
return None, None
@@ -435,15 +410,13 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
has_configured_password = _configured_password is not None
sudo_password = _configured_password if has_configured_password else _get_cached_sudo_password()
# sudoers NOPASSWD hosts must not be forced through the prompt or the -S password pipe
# (local backend only; re-probed every call).
# sudoers NOPASSWD hosts must not be forced through the prompt or the -S pipe (local only).
if not has_configured_password and not sudo_password and _sudo_nopasswd_works():
return command, None
# delegate_task children inherit HERMES_INTERACTIVE=1 (and possibly a stale
# thread-local callback on a recycled worker) but have no user on the other side —
# they always behave as headless; configured password, session cache and the
# NOPASSWD probe still apply.
# delegate_task children inherit HERMES_INTERACTIVE=1 (and possibly a stale thread-local
# callback on a recycled worker) but have no user on the other side — always headless;
# configured password, session cache and the NOPASSWD probe still apply.
should_prompt_for_sudo = (
env_var_enabled("HERMES_INTERACTIVE") or _get_sudo_password_callback() is not None
) and not _in_delegated_child_context()
@@ -453,7 +426,6 @@ def _transform_sudo_command(command: str | None) -> tuple[str | None, str | None
_set_cached_sudo_password(sudo_password)
if has_configured_password or sudo_password:
# Trailing newline is required: sudo -S reads one line per invocation. Compound
# commands (`sudo a && sudo b`) need one password line each.
# sudo -S reads one line per invocation: compound `sudo a && sudo b` needs one line each.
return transformed, (sudo_password + "\n") * sudo_count
return command, None