Merge pull request #2418 from Molecule-AI/fix/external-delegate-via-platform-proxy
fix(workspace-runtime): route delegate_task through platform A2A proxy
This commit is contained in:
commit
665582b612
@ -188,14 +188,34 @@ async def tool_delegate_task(workspace_id: str, task: str) -> str:
|
||||
if not workspace_id or not task:
|
||||
return "Error: workspace_id and task are required"
|
||||
|
||||
# Discover the target
|
||||
# Discover the target. We still call discover_peer because it
|
||||
# enforces access control + tells us whether the peer is online,
|
||||
# but we DO NOT use the peer's reported URL for routing — see below.
|
||||
peer = await discover_peer(workspace_id)
|
||||
if not peer:
|
||||
return f"Error: workspace {workspace_id} not found or not accessible (check access control)"
|
||||
|
||||
target_url = peer.get("url", "")
|
||||
if not target_url:
|
||||
return f"Error: workspace {workspace_id} has no URL (may be offline)"
|
||||
if (peer.get("status") or "").lower() == "offline":
|
||||
return f"Error: workspace {workspace_id} is offline"
|
||||
|
||||
# Route through the platform's A2A proxy instead of POSTing
|
||||
# directly to peer["url"]. The peer's URL is whatever it last
|
||||
# registered — for in-container peers that's a Docker-internal
|
||||
# hostname like ``http://ws-X-Y:8000`` which only resolves inside
|
||||
# the platform's container DNS. External callers (the standalone
|
||||
# molecule-mcp wrapper running on an operator's laptop) hit
|
||||
# `[Errno 8] nodename nor servname` every time they try to reach
|
||||
# an in-container peer that way. The platform's
|
||||
# ``/workspaces/:peer-id/a2a`` proxy works for BOTH paths: it
|
||||
# forwards over the Docker network for in-container peers and is
|
||||
# the only path external runtimes can use, so unifying on it
|
||||
# makes the universal-MCP path actually universal. In-container
|
||||
# callers pay one extra HTTP hop on the same bridge — microseconds
|
||||
# — in exchange for one consistent code path. Verified live on
|
||||
# 2026-04-30 against workspace 8dad3e29 → 97ac32e9 (Claude Code
|
||||
# Agent) which replied correctly through the proxy after failing
|
||||
# via direct connect.
|
||||
target_url = f"{PLATFORM_URL}/workspaces/{workspace_id}/a2a"
|
||||
|
||||
# Report delegation start — include the task text for traceability
|
||||
peer_name = peer.get("name") or _peer_names.get(workspace_id) or workspace_id[:8]
|
||||
|
||||
@ -230,13 +230,44 @@ class TestToolDelegateTask:
|
||||
result = await a2a_tools.tool_delegate_task("ws-missing", "task")
|
||||
assert "not found" in result or "Error" in result
|
||||
|
||||
async def test_peer_has_no_url_returns_error(self):
|
||||
async def test_offline_peer_returns_error(self):
|
||||
"""A peer with status=offline short-circuits before we hit the proxy."""
|
||||
import a2a_tools
|
||||
with patch("a2a_tools.discover_peer", return_value={"id": "ws-1", "url": ""}):
|
||||
with patch("a2a_tools.discover_peer", return_value={"id": "ws-1", "status": "offline"}):
|
||||
mc = _make_http_mock()
|
||||
with patch("a2a_tools.httpx.AsyncClient", return_value=mc):
|
||||
result = await a2a_tools.tool_delegate_task("ws-1", "task")
|
||||
assert "no URL" in result or "Error" in result
|
||||
assert "offline" in result.lower()
|
||||
|
||||
async def test_routes_through_platform_proxy_not_peer_url(self):
|
||||
"""tool_delegate_task must POST to ${PLATFORM_URL}/workspaces/:peer-id/a2a,
|
||||
NOT to peer["url"]. The peer's URL is a Docker-internal hostname for
|
||||
in-container peers; external molecule-mcp callers cannot resolve it.
|
||||
Routing through the platform proxy works for both."""
|
||||
import a2a_tools
|
||||
from a2a_client import PLATFORM_URL
|
||||
|
||||
peer = {
|
||||
"id": "ws-target",
|
||||
# Internal-only URL — must NOT be used.
|
||||
"url": "http://ws-target-internal:8000",
|
||||
"name": "Worker",
|
||||
"status": "online",
|
||||
}
|
||||
captured = {}
|
||||
async def fake_send(target_url, message):
|
||||
captured["target_url"] = target_url
|
||||
captured["message"] = message
|
||||
return "ok"
|
||||
|
||||
with patch("a2a_tools.discover_peer", return_value=peer), \
|
||||
patch("a2a_tools.send_a2a_message", side_effect=fake_send), \
|
||||
patch("a2a_tools.report_activity", new=AsyncMock()):
|
||||
await a2a_tools.tool_delegate_task("ws-target", "do thing")
|
||||
|
||||
assert captured["target_url"] == f"{PLATFORM_URL}/workspaces/ws-target/a2a"
|
||||
# Sanity: definitely NOT the peer's reported URL
|
||||
assert captured["target_url"] != peer["url"]
|
||||
|
||||
async def test_success_returns_result_text(self):
|
||||
"""Happy path: peer found with URL, A2A returns a result."""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user