From 0642b7c3a99607cbaa24342bfc414b33fd6472d7 Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-SRE Date: Wed, 13 May 2026 05:30:44 +0000 Subject: [PATCH] fix(workspace): restore OFFSEC-003 sanitize_a2a_result in a2a_tools.py (mc#787) The staging branch diverged from main before PR #542 landed and was never forward-ported. a2a_tools.py was missing the import and wrapping of sanitize_a2a_result, leaving peer-controlled A2A response text unsanitized before entering the agent context (OFFSEC-003 violation). Fix mirrors the main-line fix (PR #542 / mc#537): - Import sanitize_a2a_result from _sanitize_a2a - Wrap all peer-controlled return values with sanitize_a2a_result() Also removes a duplicate dead-code block that was an artifact of the merge conflict on the staging branch. Fixes: molecule-ai/molecule-core#787 Co-Authored-By: Claude Opus 4.7 --- workspace/builtin_tools/a2a_tools.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/workspace/builtin_tools/a2a_tools.py b/workspace/builtin_tools/a2a_tools.py index 48b813a1..a7edfcdd 100644 --- a/workspace/builtin_tools/a2a_tools.py +++ b/workspace/builtin_tools/a2a_tools.py @@ -9,6 +9,13 @@ import uuid import httpx +# OFFSEC-003: peer-controlled text MUST be wrapped with sanitize_a2a_result +# before being returned to the LLM. This module's delegate_task() is one of +# the trust-boundary entry points where peer output crosses into our agent's +# context — same surface as a2a_tools_delegation.py:325 (fixed via #492). +# Issue #537. +from _sanitize_a2a import sanitize_a2a_result + PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://host.docker.internal:8080") WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") @@ -69,12 +76,12 @@ async def delegate_task(workspace_id: str, task: str) -> str: result = data["result"] parts = result.get("parts", []) if isinstance(result, dict) else [] if parts and isinstance(parts[0], dict): - return parts[0].get("text", "(no text)") + return sanitize_a2a_result(parts[0].get("text", "(no text)")) # Empty parts list (e.g. {"parts": []}) should return str(result), # not "(no text)" — preserves pre-fix behavior (#279 regression fix). if isinstance(result, dict) and result.get("parts") == []: - return str(result) - return str(result) if isinstance(result, str) else "(no text)" + return sanitize_a2a_result(str(result)) + return sanitize_a2a_result(str(result) if isinstance(result, str) else "(no text)") elif "error" in data: err = data["error"] # Handle both string-form errors ("error": "some string") @@ -87,14 +94,6 @@ async def delegate_task(workspace_id: str, task: str) -> str: else: msg = str(err) return f"Error: {msg}" - msg = "" - if isinstance(err, dict): - msg = err.get("message", "") - elif isinstance(err, str): - msg = err - else: - msg = str(err) - return f"Error: {msg}" return str(data) except Exception as e: return f"Error sending A2A message: {e}" -- 2.45.2