From fe57896ac156f166a32632559c748ae271db20f4 Mon Sep 17 00:00:00 2001 From: Noppanat Wadlom Date: Sat, 13 Jun 2026 04:11:08 +0800 Subject: [PATCH 1/2] fix(sdk): preserve reverse-proxy base path in SSH proxy URL ssh_proxy_url rebuilt the websocket URL from scheme and netloc only, hardcoding an absolute /api/v1 path and discarding any base path. Behind a reverse proxy mounted at e.g. /flowmesh, the prefix was dropped from both the printed ProxyCommand instructions and the live `ssh proxy` backend. Join the base path with the API prefix, consistent with _make_url. Signed-off-by: Noppanat Wadlom --- sdk/src/flowmesh/ssh.py | 6 +++--- tests/sdk/test_resource_helpers.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/sdk/src/flowmesh/ssh.py b/sdk/src/flowmesh/ssh.py index d67f30fa..95f62581 100644 --- a/sdk/src/flowmesh/ssh.py +++ b/sdk/src/flowmesh/ssh.py @@ -8,6 +8,7 @@ import yaml +from ._constants import API_VERSION_PREFIX from .exceptions import FlowMeshError from .models.common import TaskStatus from .models.tasks import TaskInfo @@ -159,9 +160,8 @@ def ssh_proxy_url(base_url: str, task_id: str) -> str: """Build the websocket proxy URL for an SSH task.""" base = urlsplit(base_url) ws_scheme = "wss" if base.scheme == "https" else "ws" - return urlunsplit( - (ws_scheme, base.netloc, f"/api/v1/ssh/tasks/{task_id}/proxy", "", "") - ) + path = f"{base.path.rstrip('/')}{API_VERSION_PREFIX}/ssh/tasks/{task_id}/proxy" + return urlunsplit((ws_scheme, base.netloc, path, "", "")) def ssh_connection_commands( diff --git a/tests/sdk/test_resource_helpers.py b/tests/sdk/test_resource_helpers.py index 131f3885..52f9d27c 100644 --- a/tests/sdk/test_resource_helpers.py +++ b/tests/sdk/test_resource_helpers.py @@ -317,6 +317,36 @@ def test_proxy_url_and_connection_commands(self) -> None: cmds = ssh_connection_commands("task-1", ssh_info, base_url=base_url) assert isinstance(cmds, list) + def test_proxy_url_root_mounted(self) -> None: + assert ( + ssh_proxy_url("https://example.com", "task-1") + == "wss://example.com/api/v1/ssh/tasks/task-1/proxy" + ) + + def test_proxy_url_preserves_base_path(self) -> None: + assert ( + ssh_proxy_url("https://kv.run:8000/flowmesh", "task-1") + == "wss://kv.run:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" + ) + + def test_proxy_url_scheme_conversion(self) -> None: + assert ssh_proxy_url("http://localhost:8000", "t").startswith("ws://") + assert ssh_proxy_url("https://localhost:8000", "t").startswith("wss://") + + def test_proxy_url_trailing_slash(self) -> None: + assert ( + ssh_proxy_url("https://kv.run:8000/flowmesh/", "task-1") + == "wss://kv.run:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" + ) + + def test_connection_commands_proxy_mode_embeds_base_path(self) -> None: + ssh_info = {"mode": "proxy", "username": "flowmesh", "host": "h", "port": 22} + cmds = ssh_connection_commands( + "task-1", ssh_info, base_url="https://kv.run:8000/flowmesh" + ) + proxy_cmd = next(cmd for label, cmd in cmds if label == "ssh (proxy)") + assert "wss://kv.run:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" in proxy_cmd + def test_detect_public_key_prefers_standard_keys(self, tmp_path: Path) -> None: ssh_dir = tmp_path / ".ssh" ssh_dir.mkdir() From ee8a26b67bf6afe51a005f2527bf1997ec957435 Mon Sep 17 00:00:00 2001 From: Noppanat Wadlom Date: Sat, 13 Jun 2026 20:04:12 +0800 Subject: [PATCH 2/2] test(sdk): use example.com host in SSH proxy URL tests Signed-off-by: Noppanat Wadlom --- tests/sdk/test_resource_helpers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/sdk/test_resource_helpers.py b/tests/sdk/test_resource_helpers.py index 52f9d27c..c3c2f224 100644 --- a/tests/sdk/test_resource_helpers.py +++ b/tests/sdk/test_resource_helpers.py @@ -325,8 +325,8 @@ def test_proxy_url_root_mounted(self) -> None: def test_proxy_url_preserves_base_path(self) -> None: assert ( - ssh_proxy_url("https://kv.run:8000/flowmesh", "task-1") - == "wss://kv.run:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" + ssh_proxy_url("https://example.com:8000/flowmesh", "task-1") + == "wss://example.com:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" ) def test_proxy_url_scheme_conversion(self) -> None: @@ -335,17 +335,19 @@ def test_proxy_url_scheme_conversion(self) -> None: def test_proxy_url_trailing_slash(self) -> None: assert ( - ssh_proxy_url("https://kv.run:8000/flowmesh/", "task-1") - == "wss://kv.run:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" + ssh_proxy_url("https://example.com:8000/flowmesh/", "task-1") + == "wss://example.com:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" ) def test_connection_commands_proxy_mode_embeds_base_path(self) -> None: ssh_info = {"mode": "proxy", "username": "flowmesh", "host": "h", "port": 22} cmds = ssh_connection_commands( - "task-1", ssh_info, base_url="https://kv.run:8000/flowmesh" + "task-1", ssh_info, base_url="https://example.com:8000/flowmesh" ) proxy_cmd = next(cmd for label, cmd in cmds if label == "ssh (proxy)") - assert "wss://kv.run:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" in proxy_cmd + assert ( + "wss://example.com:8000/flowmesh/api/v1/ssh/tasks/task-1/proxy" in proxy_cmd + ) def test_detect_public_key_prefers_standard_keys(self, tmp_path: Path) -> None: ssh_dir = tmp_path / ".ssh"