From d939865090b9c36679564c642ab14a19f6079091 Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-Runtime-BE Date: Sun, 10 May 2026 11:42:06 +0000 Subject: [PATCH 1/2] fix(mcp): apply review fixes to HTTP/SSE transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick PR #6 review fixes from closed molecule-ai-workspace-runtime PR: - serverInfo.name: "a2a-delegation" → "molecule" (matches registration name) - conn_id: full UUID instead of [:8] slice to avoid collision across connections - heartbeat: emit "data: null" instead of "data: {}" (correct SSE null value) - Remove dead _sse_broadcaster (unused, superseded by queue.put in _run_http_server) Co-Authored-By: Claude Opus 4.7 --- molecule_runtime/a2a_mcp_server.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/molecule_runtime/a2a_mcp_server.py b/molecule_runtime/a2a_mcp_server.py index 22a5497..f0b6668 100644 --- a/molecule_runtime/a2a_mcp_server.py +++ b/molecule_runtime/a2a_mcp_server.py @@ -314,7 +314,7 @@ async def _handle_stdio(): "result": { "protocolVersion": "2024-11-05", "capabilities": {"tools": {"listChanged": False}}, - "serverInfo": {"name": "a2a-delegation", "version": "1.0.0"}, + "serverInfo": {"name": "molecule", "version": "1.0.0"}, }, }) @@ -361,15 +361,7 @@ _connection_queues: dict[str, asyncio.Queue] = {} _connection_lock = asyncio.Lock() -async def _sse_broadcaster(request_id: str, response: dict, conn_id: str): - """Send a JSON-RPC response to a specific SSE connection.""" - async with _connection_lock: - queue = _connection_queues.get(conn_id) - if queue is not None: - await queue.put(response) - - -async def _handle_http_mcp(request) -> dict: +async def _handle_http_mcp(request) -> dict | None: """Handle an incoming JSON-RPC request over HTTP. Returns the JSON-RPC response.""" try: body = await request.json() @@ -386,7 +378,7 @@ async def _handle_http_mcp(request) -> dict: "result": { "protocolVersion": "2024-11-05", "capabilities": {"tools": {"listChanged": False}}, - "serverInfo": {"name": "a2a-delegation", "version": "1.0.0"}, + "serverInfo": {"name": "molecule", "version": "1.0.0"}, }, } @@ -440,7 +432,7 @@ async def _run_http_server(port: int): async def sse_handler(request): """GET endpoint — SSE stream for push-based responses.""" - conn_id = str(uuid.uuid4())[:8] + conn_id = str(uuid.uuid4()) # full UUID to avoid collision across connections queue: asyncio.Queue = asyncio.Queue(maxsize=100) async with _connection_lock: @@ -452,8 +444,9 @@ async def _run_http_server(port: int): while True: response = await asyncio.wait_for(queue.get(), timeout=300) yield f"event: message\ndata: {json.dumps(response)}\n\n" + # Emit a heartbeat when the queue is drained (connection alive but idle) if queue.empty(): - yield "event: heartbeat\ndata: {}\n\n" + yield "event: heartbeat\ndata: null\n\n" except asyncio.TimeoutError: pass finally: -- 2.45.2 From d21f8c20647fbb02155cacc6c0436f08e90ae971 Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-Runtime-BE Date: Mon, 11 May 2026 03:12:37 +0000 Subject: [PATCH 2/2] fix(runtime): align PLATFORM_URL default to host.docker.internal across all modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unified the fallback default for PLATFORM_URL from `http://platform:8080` (Docker Compose service name) to `http://host.docker.internal:8080` across all 13 modules that declare it. This matches: - The provisioner's default (buildContainerEnv injects PLATFORM_URL from cfg.PlatformURL, which defaults to host.docker.internal on the platform side — main.go:platformURL) - The molecule-git-token-helper.sh script (already uses host.docker.internal) - The MCP client (MOLECULE_URL injected by provisioner) The provisioner always sets PLATFORM_URL in production containers, so this is a development/Docker-only improvement: without this change, a workspace started outside the Docker Compose network (e.g. via `docker run` with `--network host`) would fail platform API calls with "Connection refused" because `platform:8080` resolves nowhere. 13 modules updated: a2a_cli, a2a_client, a2a_mcp_server, adapters/base, builtin_tools/a2a_tools, builtin_tools/approval, builtin_tools/delegation, builtin_tools/hitl, builtin_tools/memory, consolidation, coordinator, main, molecule_ai_status. All docstrings updated to match. Co-Authored-By: Claude Opus 4.7 --- molecule_runtime/a2a_cli.py | 2 +- molecule_runtime/a2a_client.py | 2 +- molecule_runtime/a2a_mcp_server.py | 2 +- molecule_runtime/adapters/base.py | 2 +- molecule_runtime/builtin_tools/a2a_tools.py | 2 +- molecule_runtime/builtin_tools/approval.py | 4 ++-- molecule_runtime/builtin_tools/delegation.py | 2 +- molecule_runtime/builtin_tools/hitl.py | 2 +- molecule_runtime/builtin_tools/memory.py | 2 +- molecule_runtime/consolidation.py | 2 +- molecule_runtime/coordinator.py | 2 +- molecule_runtime/main.py | 2 +- molecule_runtime/molecule_ai_status.py | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/molecule_runtime/a2a_cli.py b/molecule_runtime/a2a_cli.py index 9aae8f4..5fe97f6 100644 --- a/molecule_runtime/a2a_cli.py +++ b/molecule_runtime/a2a_cli.py @@ -24,7 +24,7 @@ import httpx from builtin_tools.validation import WorkspaceIdValidationError, get_validated_workspace_id WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") # used for discover() headers only; URL uses validated version -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") async def discover(target_id: str) -> dict | None: diff --git a/molecule_runtime/a2a_client.py b/molecule_runtime/a2a_client.py index 76fb205..e9b2956 100644 --- a/molecule_runtime/a2a_client.py +++ b/molecule_runtime/a2a_client.py @@ -16,7 +16,7 @@ from .platform_auth import auth_headers logger = logging.getLogger(__name__) WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") # Cache workspace ID → name mappings (populated by list_peers calls) _peer_names: dict[str, str] = {} diff --git a/molecule_runtime/a2a_mcp_server.py b/molecule_runtime/a2a_mcp_server.py index f0b6668..33d93a7 100644 --- a/molecule_runtime/a2a_mcp_server.py +++ b/molecule_runtime/a2a_mcp_server.py @@ -14,7 +14,7 @@ the claude --print invocation. Environment variables (set by the workspace container): WORKSPACE_ID — this workspace's ID - PLATFORM_URL — platform API base URL (e.g. http://platform:8080) + PLATFORM_URL — platform API base URL (default: http://host.docker.internal:8080) """ import argparse diff --git a/molecule_runtime/adapters/base.py b/molecule_runtime/adapters/base.py index a1820e7..2c22b5a 100644 --- a/molecule_runtime/adapters/base.py +++ b/molecule_runtime/adapters/base.py @@ -229,7 +229,7 @@ class BaseAdapter(ABC): from builtin_tools.memory import commit_memory, search_memory from builtin_tools.sandbox import run_code - platform_url = os.environ.get("PLATFORM_URL", "http://platform:8080") + platform_url = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") # Load plugins from per-workspace dir first, then shared fallback workspace_plugins_dir = os.path.join(config.config_path, "plugins") diff --git a/molecule_runtime/builtin_tools/a2a_tools.py b/molecule_runtime/builtin_tools/a2a_tools.py index 61c21c1..ef15b53 100644 --- a/molecule_runtime/builtin_tools/a2a_tools.py +++ b/molecule_runtime/builtin_tools/a2a_tools.py @@ -11,7 +11,7 @@ import httpx from builtin_tools.validation import WorkspaceIdValidationError, get_validated_workspace_id -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") # used only for tracing headers; URLs use validated version diff --git a/molecule_runtime/builtin_tools/approval.py b/molecule_runtime/builtin_tools/approval.py index 4a87b0c..790e844 100644 --- a/molecule_runtime/builtin_tools/approval.py +++ b/molecule_runtime/builtin_tools/approval.py @@ -32,7 +32,7 @@ RBAC denials emit an ``rbac / rbac.deny / denied`` event instead. Environment variables --------------------- -PLATFORM_URL Platform base URL (default: http://platform:8080) +PLATFORM_URL Platform base URL (default: http://host.docker.internal:8080) WORKSPACE_ID This workspace's ID (validated at startup by platform_auth) APPROVAL_TIMEOUT Max wait in seconds (default: 300) APPROVAL_POLL_INTERVAL Polling interval in seconds (default: 5, polling path only) @@ -55,7 +55,7 @@ from builtin_tools.validation import WorkspaceIdValidationError, get_validated_w logger = logging.getLogger(__name__) -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") from molecule_runtime.platform_auth import WORKSPACE_ID APPROVAL_POLL_INTERVAL = float(os.environ.get("APPROVAL_POLL_INTERVAL", "5")) APPROVAL_TIMEOUT = float(os.environ.get("APPROVAL_TIMEOUT", "300")) diff --git a/molecule_runtime/builtin_tools/delegation.py b/molecule_runtime/builtin_tools/delegation.py index 4e42438..e21c0e1 100644 --- a/molecule_runtime/builtin_tools/delegation.py +++ b/molecule_runtime/builtin_tools/delegation.py @@ -30,7 +30,7 @@ from builtin_tools.telemetry import ( inject_trace_headers, ) -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") from molecule_runtime.platform_auth import WORKSPACE_ID DELEGATION_RETRY_ATTEMPTS = int(os.environ.get("DELEGATION_RETRY_ATTEMPTS", "3")) DELEGATION_RETRY_DELAY = float(os.environ.get("DELEGATION_RETRY_DELAY", "5.0")) diff --git a/molecule_runtime/builtin_tools/hitl.py b/molecule_runtime/builtin_tools/hitl.py index d7bccc2..5335838 100644 --- a/molecule_runtime/builtin_tools/hitl.py +++ b/molecule_runtime/builtin_tools/hitl.py @@ -177,7 +177,7 @@ async def _notify_channels( Errors in individual channels are logged but never re-raised so that a misconfigured Slack webhook cannot block the approval flow. """ - platform_url = os.environ.get("PLATFORM_URL", "http://platform:8080") + platform_url = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") workspace_id = os.environ.get("WORKSPACE_ID", "") for channel in cfg.channels: diff --git a/molecule_runtime/builtin_tools/memory.py b/molecule_runtime/builtin_tools/memory.py index 268494b..1908684 100644 --- a/molecule_runtime/builtin_tools/memory.py +++ b/molecule_runtime/builtin_tools/memory.py @@ -42,7 +42,7 @@ try: # pragma: no cover - optional runtime dependency in lightweight test envs except ImportError: # pragma: no cover httpx = SimpleNamespace(AsyncClient=None) -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") diff --git a/molecule_runtime/consolidation.py b/molecule_runtime/consolidation.py index 6d6ef61..58c8d15 100644 --- a/molecule_runtime/consolidation.py +++ b/molecule_runtime/consolidation.py @@ -19,7 +19,7 @@ from .platform_auth import auth_headers logger = logging.getLogger(__name__) -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") CONSOLIDATION_INTERVAL = float(os.environ.get("CONSOLIDATION_INTERVAL", "300")) # 5 min CONSOLIDATION_THRESHOLD = int(os.environ.get("CONSOLIDATION_THRESHOLD", "10")) # min memories before consolidating diff --git a/molecule_runtime/coordinator.py b/molecule_runtime/coordinator.py index ce9d0cb..c1f28ee 100644 --- a/molecule_runtime/coordinator.py +++ b/molecule_runtime/coordinator.py @@ -23,7 +23,7 @@ from policies.routing import build_team_routing_payload logger = logging.getLogger(__name__) -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") diff --git a/molecule_runtime/main.py b/molecule_runtime/main.py index 77d6247..ce28eba 100644 --- a/molecule_runtime/main.py +++ b/molecule_runtime/main.py @@ -63,7 +63,7 @@ from transcript_auth import transcript_authorized as _transcript_authorized async def main(): # pragma: no cover workspace_id = os.environ.get("WORKSPACE_ID", "workspace-default") config_path = os.environ.get("WORKSPACE_CONFIG_PATH", "/configs") - platform_url = os.environ.get("PLATFORM_URL", "http://platform:8080") + platform_url = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") awareness_config = get_awareness_config() # 0. Normalise LLM auth env vars based on token type. diff --git a/molecule_runtime/molecule_ai_status.py b/molecule_runtime/molecule_ai_status.py index f01b48f..03d4d96 100644 --- a/molecule_runtime/molecule_ai_status.py +++ b/molecule_runtime/molecule_ai_status.py @@ -25,7 +25,7 @@ import httpx from builtin_tools.validation import WorkspaceIdValidationError, get_validated_workspace_id WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") -PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") +PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") def set_status(task: str): -- 2.45.2