From 9ffdb4bef3947ee6c7f7b5ebb50861a7b8973598 Mon Sep 17 00:00:00 2001 From: Dimitri Krattiger Date: Thu, 9 Jul 2026 21:01:42 +0000 Subject: [PATCH] fix(terminal): don't ssh-wrap the local runner's own tmux attach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local runner registers its own hostname as runner_host (ADR 0013 §6), which switch_to forwarded unchecked — every `t` press ssh-wrapped a same-machine tmux attach (`ssh -t tmux ...`). run_console_local's attach swallows failures, so a slow/flaky loopback ssh silently no-op'd, requiring repeated `t` presses. switch_to now only treats host as remote when it differs from the console's own hostname. Co-Authored-By: Claude Sonnet 5 --- src/panopticon/terminal/console.py | 12 ++++++++++-- tests/test_console.py | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/panopticon/terminal/console.py b/src/panopticon/terminal/console.py index dc6d2e31..3c65c13c 100644 --- a/src/panopticon/terminal/console.py +++ b/src/panopticon/terminal/console.py @@ -21,6 +21,7 @@ from __future__ import annotations +import socket as socket_module import subprocess import sys import tempfile @@ -69,14 +70,21 @@ def switch_to( host: str | None = None, switch_file: Path, detach: Callable[[], None] = _tmux_detach, + local_host: Callable[[], str] = socket_module.gethostname, ) -> None: """The dashboard's `t` hook, run inside its tmux session: record the picked ``session`` for the supervisor, then detach this client so the supervisor attaches the task. The dashboard process keeps running (detached), so returning to it shows the same live view. - When ``host`` is set the switch-file carries ``\\t`` so the - supervisor can ssh-wrap the attach; a plain ```` (no tab) means local. + ``host`` is the claiming runner's registered hostname (ADR 0013 §6) — populated even for the + ordinary local runner. It only means "remote" when it differs from this machine's own + hostname (``local_host``, injectable for tests); otherwise it's treated as local so `t` never + ssh-wraps a same-machine attach. When genuinely remote, the switch-file carries + ``\\t`` so the supervisor can ssh-wrap the attach; a plain ```` (no + tab) means local. """ + if host == local_host(): + host = None switch_file.write_text(f"{host}\t{session}" if host else session) detach() diff --git a/tests/test_console.py b/tests/test_console.py index 445522cd..7f4da19f 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -79,11 +79,28 @@ def test_switch_to_with_remote_host_encodes_host_and_session(tmp_path: Path) -> # parse it and pass host= to attach_command for the ssh-wrapped attach. switch = tmp_path / "switch" - switch_to("panopticon-t1", host="box.example.com", switch_file=switch, detach=lambda: None) + switch_to( + "panopticon-t1", host="box.example.com", switch_file=switch, detach=lambda: None, + local_host=lambda: "this-machine", + ) assert switch.read_text() == "box.example.com\tpanopticon-t1" +def test_switch_to_with_own_hostname_is_treated_as_local(tmp_path: Path) -> None: + # The ordinary local runner registers its own hostname too (ADR 0013 §6), but that must not + # ssh-wrap a same-machine attach: when host == local_host() it's dropped, so the switch-file + # carries a plain session (no tab) just like the no-host case. + switch = tmp_path / "switch" + + switch_to( + "panopticon-t1", host="this-machine", switch_file=switch, detach=lambda: None, + local_host=lambda: "this-machine", + ) + + assert switch.read_text() == "panopticon-t1" + + def test_supervisor_parses_remote_host_from_switch_file(tmp_path: Path) -> None: # run_console_local's attach() closure parses "\t" from the switch-file and # passes host= to attach_command; a plain session (no tab) means local (host=None).