commit eab3b42d5b593b62d435de5e72512485fe2f6e79 equwal <13551856+equwal@users.noreply.github.com> 2026-10-03 21:44:10 -0700 Write the sshp hosts file with LF line endings On Windows the temp hosts file got CRLF, and sshp rejected each host name with a trailing CR. The new test checks the bytes sshp receives.
sshp_mcp.py | 3 ++- test_sshp_mcp.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/sshp_mcp.py b/sshp_mcp.py index 1d9e2a9..084124d 100644 --- a/sshp_mcp.py +++ b/sshp_mcp.py @@ -122,7 +122,8 @@ def sshp_run(args, cfg): for opt in cfg["ssh_options"]: argv += ["-o", opt] - with tempfile.NamedTemporaryFile("w", suffix=".hosts", delete=False) as f: + # newline="\n": sshp keeps a CR from a Windows CRLF line as part of the host name. + with tempfile.NamedTemporaryFile("w", suffix=".hosts", delete=False, newline="\n") as f: f.write("\n".join(hosts) + "\n") hosts_path = f.name try: diff --git a/test_sshp_mcp.py b/test_sshp_mcp.py index 5bfe9b1..8aaf793 100644 --- a/test_sshp_mcp.py +++ b/test_sshp_mcp.py @@ -66,6 +66,27 @@ class PolicyTest(unittest.TestCase): finally: os.unlink(f.name) + def test_hosts_file_has_no_cr(self): + # Regression: on Windows a CRLF temp file made sshp reject "host\r". + seen = {} + + def fake_run(argv, **kw): + with open(argv[argv.index("-f") + 1], "rb") as f: + seen["data"] = f.read() + return subprocess.CompletedProcess(argv, 0, "[a.example.com] exited: 0 (1 ms)\n", "") + + with tempfile.NamedTemporaryFile("w", delete=False) as f: + f.write("a.example.com\n") + old = sshp_mcp.subprocess.run + sshp_mcp.subprocess.run = fake_run + try: + out = sshp_mcp.sshp_run({"command": "uptime"}, cfg(f.name)) + finally: + sshp_mcp.subprocess.run = old + os.unlink(f.name) + self.assertEqual(seen["data"], b"a.example.com\n") + self.assertEqual(out["hosts"]["a.example.com"]["exit_code"], 0) + def test_bad_user(self): with tempfile.NamedTemporaryFile("w", delete=False) as f: f.write("a.example.com\n")