fix: release Paramiko import traceback

Paramiko caches its optional invoke import error with the original traceback, retaining the code-execution tool-loading stack and first agent. Clear only the stale traceback while preserving the exception, and add focused regression coverage.

Alessandro committed Jul 21, 2026 at 00:56 UTC 0ddb5fb31d2abd1b0aeac80138c1dc58d64241fb
2 files changed +18
plugins/_code_execution/helpers/shell_ssh.py
+4
@@ -7,6 +7,10 @@ from helpers.log import Log
7 from helpers.print_style import PrintStyle
8 # from helpers.strings import calculate_valid_match_lengths
9
10 +# Paramiko caches this optional import error; its traceback retains our tool-loading stack.
11 +if paramiko.config.invoke_import_error:
12 + paramiko.config.invoke_import_error.__traceback__ = None
13 +
14
15 # Injected into every new SSH shell to keep it safe for non-interactive use.
16 # Pagers (more/less) would otherwise block forever waiting for input that
tests/test_code_execution_pager.py
+14
@@ -5,6 +5,7 @@ code execution tool: without user input they block forever and spin at 100% CPU.
5 """
6
7 import asyncio
8 +import importlib
9 from types import SimpleNamespace
10
11 from plugins._code_execution.helpers import shell_local, shell_ssh
@@ -43,6 +44,19 @@ def test_ssh_command_disables_pagers():
44 assert "PAGER=cat" in shell_ssh.PAGER_DISABLE_COMMAND
45
46
47 +def test_paramiko_import_error_does_not_retain_tool_loading_stack(monkeypatch):
48 + try:
49 + raise ImportError("invoke")
50 + except ImportError as error:
51 + saved_error = error
52 + monkeypatch.setattr(shell_ssh.paramiko.config, "invoke_import_error", error)
53 +
54 + importlib.reload(shell_ssh)
55 +
56 + assert shell_ssh.paramiko.config.invoke_import_error is saved_error
57 + assert saved_error.__traceback__ is None
58 +
59 +
60 def test_multiline_terminal_commands_are_one_current_shell_compound():
61 assert _group_multiline_command("pwd") == "pwd"
62 assert _group_multiline_command("cd /tmp\npwd") == "{\ncd /tmp\npwd\n}"