mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(cli): disable prompt_toolkit CPR queries to stop escape-sequence leak (#13870)
prompt_toolkit's renderer sends ESC[6n cursor-position queries before painting in non-fullscreen mode; the terminal replies ESC[<row>;<col>R. Over SSH/cloudflared tunnels and slow PTYs these replies race past the input parser and land in the display as raw '20;1R21;1R' text, and the pending-CPR future can stall the renderer so the prompt freezes after the agent's final answer. Build the prompt_toolkit output with enable_cpr=False so CPR is marked NOT_SUPPORTED up front and ESC[6n is never sent. This is the root-cause counterpart to the existing input-side _strip_leaked_terminal_responses scrubbing. Vt100_Output.from_pty() does not expose enable_cpr in prompt_toolkit 3.x, so _build_cpr_disabled_output() reproduces its get_size setup and calls the constructor directly; it returns None on any failure so startup falls back to the default output. Verified in a real PTY: baseline emits 1 ESC[6n query, the fix emits 0, banner/UI render identically. Layout is unaffected — with CPR off the renderer sizes the prompt to its preferred height (the same fallback prompt_toolkit uses on any terminal that doesn't answer CPR). Co-authored-by: Hermes Agent <noreply@nousresearch.com>
This commit is contained in:
parent
e7c013494d
commit
b34771fc06
2 changed files with 157 additions and 1 deletions
|
|
@ -247,6 +247,73 @@ class TestPromptToolkitTerminalCompatibility:
|
|||
|
||||
assert renderer.cpr_not_supported_callback is None
|
||||
|
||||
def test_cpr_disabled_output_marks_renderer_not_supported(self):
|
||||
"""CPR-disabled output must make prompt_toolkit skip ESC[6n entirely.
|
||||
|
||||
The root cause of #13870 is that prompt_toolkit sends ESC[6n cursor
|
||||
queries whose CPR replies leak into the display over tunnels/slow PTYs.
|
||||
Building the output with enable_cpr=False is what stops the queries:
|
||||
the renderer marks CPR NOT_SUPPORTED and never calls ask_for_cpr().
|
||||
"""
|
||||
import sys as _sys
|
||||
from cli import _build_cpr_disabled_output
|
||||
from prompt_toolkit.application import Application
|
||||
from prompt_toolkit.layout import Layout, Window, FormattedTextControl
|
||||
from prompt_toolkit.renderer import CPR_Support
|
||||
|
||||
out = _build_cpr_disabled_output(_sys.stdout)
|
||||
assert out is not None
|
||||
# The contract: this output does not respond to CPR.
|
||||
assert out.enable_cpr is False
|
||||
assert out.responds_to_cpr is False
|
||||
|
||||
# And wired into an Application, the renderer treats CPR as unsupported,
|
||||
# so request_absolute_cursor_position() never sends ESC[6n.
|
||||
app = Application(
|
||||
layout=Layout(Window(FormattedTextControl("x"))),
|
||||
output=out,
|
||||
full_screen=False,
|
||||
)
|
||||
assert app.renderer.cpr_support == CPR_Support.NOT_SUPPORTED
|
||||
|
||||
def test_cpr_disabled_output_returns_none_on_failure(self):
|
||||
"""A non-fileno stdout must degrade to None (default output fallback)."""
|
||||
from cli import _build_cpr_disabled_output
|
||||
|
||||
class _NoFileno:
|
||||
def fileno(self):
|
||||
raise OSError("not a real fd")
|
||||
|
||||
# Build must not raise; worst case it returns a usable output or None.
|
||||
# The hard guarantee is no exception escapes (startup must never break).
|
||||
result = _build_cpr_disabled_output(_NoFileno())
|
||||
assert result is None or result.enable_cpr is False
|
||||
|
||||
def test_cpr_gating_local_vs_tunnel(self, monkeypatch):
|
||||
"""CPR is only suppressed on tunneled links / explicit opt-out.
|
||||
|
||||
CPR works fine on local terminals and is only a layout hint, so the fix
|
||||
for #13870 must not change default behavior locally — it gates on
|
||||
_terminal_may_leak_cpr(). Local (no SSH env) -> CPR left enabled;
|
||||
SSH session or PROMPT_TOOLKIT_NO_CPR=1 -> CPR suppressed.
|
||||
"""
|
||||
from cli import _terminal_may_leak_cpr
|
||||
|
||||
for var in ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY", "PROMPT_TOOLKIT_NO_CPR"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
|
||||
# Local terminal: leave prompt_toolkit's default (CPR on) untouched.
|
||||
assert _terminal_may_leak_cpr() is False
|
||||
|
||||
# SSH session: the tunnel where the leak reproduces.
|
||||
monkeypatch.setenv("SSH_CONNECTION", "10.0.0.1 22 10.0.0.2 51234")
|
||||
assert _terminal_may_leak_cpr() is True
|
||||
monkeypatch.delenv("SSH_CONNECTION", raising=False)
|
||||
|
||||
# prompt_toolkit's own explicit opt-out is honored.
|
||||
monkeypatch.setenv("PROMPT_TOOLKIT_NO_CPR", "1")
|
||||
assert _terminal_may_leak_cpr() is True
|
||||
|
||||
|
||||
class TestSingleQueryState:
|
||||
def test_voice_and_interrupt_state_initialized_before_run(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue