Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions workspace/a2a_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
37 changes: 34 additions & 3 deletions workspace/tests/test_a2a_tools_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,44 @@ async def test_peer_not_found_returns_error(self):
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."""
Expand Down
Loading