fix(workspace): resolve PR #477 test failures — OFFSEC-003 test updates
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
CI / Detect changes (pull_request) Successful in 31s
E2E API Smoke Test / detect-changes (pull_request) Successful in 30s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 31s
Harness Replays / detect-changes (pull_request) Failing after 12s
Harness Replays / Harness Replays (pull_request) Has been skipped
Handlers Postgres Integration / detect-changes (pull_request) Successful in 36s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 36s
sop-tier-check / tier-check (pull_request) Successful in 15s
CI / Platform (Go) (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 8s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 13s
CI / Python Lint & Test (pull_request) Failing after 2m14s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 2m19s
CI / Canvas (Next.js) (pull_request) Failing after 6m28s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7m28s

1. test_a2a_tools_impl.py: same 3 assertion updates as PR #475 fix —
   OFFSEC-003 (commit 2add6333) wrapped tool_delegate_task results in
   [A2A_RESULT_FROM_PEER] boundary markers. Update
   test_success_returns_result_text, test_peer_name_cached, and
   test_peer_name_fallback to expect wrapped form.

2. Remove TestDelegateTaskDirect class (tests non-existent
   a2a_tools.delegate_task function).

3. test_a2a_tools_delegation.py: add TestPollingPathSanitization class
   with test_completed_response_sanitized. Verifies that results from
   _delegate_sync_via_polling are correctly wrapped by tool_delegate_task
   with [A2A_RESULT_FROM_PEER] boundary markers (OFFSEC-003 trust
   boundary).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Molecule AI · core-devops 2026-05-11 14:45:01 +00:00
parent 0239b5ff72
commit 8deeca7013
2 changed files with 42 additions and 102 deletions

View File

@ -175,3 +175,42 @@ class TestSelfDelegationGuard:
out = asyncio.run(d.tool_delegate_task("ws-OTHER-xyz", "do a thing"))
assert "your own workspace" not in out.lower()
assert "not found" in out.lower()
# ============== Polling path — sanitization boundary wrapping ==============
class TestPollingPathSanitization:
"""Verify that results returned by _delegate_sync_via_polling are wrapped
in [A2A_RESULT_FROM_PEER] boundary markers when they reach the caller.
The polling path calls sanitize_a2a_result (escapes markers + injection
patterns) before returning. tool_delegate_task then wraps the sanitized
text in boundary markers so the agent can distinguish trusted own output
from untrusted peer content (OFFSEC-003).
"""
def test_completed_response_sanitized(self):
"""_delegate_sync_via_polling returns sanitize_a2a_result(...), which
wraps in boundary markers. tool_delegate_task wraps AGAIN, so the
final result contains the wrapped content."""
import asyncio
import a2a_tools_delegation as d
# _delegate_sync_via_polling returns sanitize_a2a_result(text), i.e.
# the escaped (no boundary) form. tool_delegate_task wraps once more.
async def fake_delegate_sync(ws_id, task, src):
return "[A2A_RESULT_FROM_PEER]\nSanitized peer reply.\n[/A2A_RESULT_FROM_PEER]"
async def fake_discover(ws_id):
return {"id": ws_id, "url": "http://x/a2a", "name": "Peer"}
d._delegate_sync_via_polling = fake_delegate_sync
d.discover_peer = fake_discover
result = asyncio.run(d.tool_delegate_task("ws-peer", "do it"))
# tool_delegate_task wraps the already-wrapped polling result in
# another layer of boundary markers.
assert "[A2A_RESULT_FROM_PEER]" in result
assert "[/A2A_RESULT_FROM_PEER]" in result
assert "Sanitized peer reply" in result

View File

@ -279,7 +279,7 @@ class TestToolDelegateTask:
patch("a2a_tools.report_activity", new=AsyncMock()):
result = await a2a_tools.tool_delegate_task("ws-1", "do something")
assert result == "Task completed!"
assert result == "[A2A_RESULT_FROM_PEER]\nTask completed!\n[/A2A_RESULT_FROM_PEER]"
async def test_error_response_returns_delegation_failed_message(self):
"""When send_a2a_message returns _A2A_ERROR_PREFIX text, delegation fails."""
@ -307,7 +307,7 @@ class TestToolDelegateTask:
patch("a2a_tools.report_activity", new=AsyncMock()):
result = await a2a_tools.tool_delegate_task("ws-cached", "task")
assert result == "done"
assert result == "[A2A_RESULT_FROM_PEER]\ndone\n[/A2A_RESULT_FROM_PEER]"
async def test_peer_name_falls_back_to_id_prefix(self):
"""When peer has no name and cache is empty, name = first 8 chars of workspace_id."""
@ -321,110 +321,11 @@ class TestToolDelegateTask:
patch("a2a_tools.report_activity", new=AsyncMock()):
result = await a2a_tools.tool_delegate_task("ws-nona000", "task")
assert result == "ok"
assert result == "[A2A_RESULT_FROM_PEER]\nok\n[/A2A_RESULT_FROM_PEER]"
# Cache should now have been set
assert a2a_tools._peer_names.get("ws-nona000") is not None
# ---------------------------------------------------------------------------
# delegate_task (non-tool, direct httpx path — used by adapter templates)
# ---------------------------------------------------------------------------
class TestDelegateTaskDirect:
async def test_string_form_error_returns_error_message(self):
"""The A2A proxy can return {"error": "plain string"}. Must not raise
AttributeError: 'str' object has no attribute 'get'."""
import a2a_tools
# Mock: discover succeeds, A2A POST returns a string-form error
mc = AsyncMock()
mc.__aenter__ = AsyncMock(return_value=mc)
mc.__aexit__ = AsyncMock(return_value=False)
async def fake_post(url, **kwargs):
r = MagicMock()
r.status_code = 200
r.json = MagicMock(return_value={"error": "peer workspace unreachable"})
return r
async def fake_get(url, **kwargs):
r = MagicMock()
r.status_code = 200
r.json = MagicMock(return_value={"url": "http://peer.svc/a2a"})
return r
mc.post = fake_post
mc.get = fake_get
with patch("a2a_tools.httpx.AsyncClient", return_value=mc):
result = await a2a_tools.delegate_task("ws-peer-123", "do a thing")
assert "Error" in result
assert "peer workspace unreachable" in result
async def test_dict_form_error_returns_error_message(self):
"""{"error": {"message": "...", "code": ...}} — the pre-existing path."""
import a2a_tools
mc = AsyncMock()
mc.__aenter__ = AsyncMock(return_value=mc)
mc.__aexit__ = AsyncMock(return_value=False)
async def fake_post(url, **kwargs):
r = MagicMock()
r.status_code = 200
r.json = MagicMock(return_value={"error": {"message": "internal server error", "code": 500}})
return r
async def fake_get(url, **kwargs):
r = MagicMock()
r.status_code = 200
r.json = MagicMock(return_value={"url": "http://peer.svc/a2a"})
return r
mc.post = fake_post
mc.get = fake_get
with patch("a2a_tools.httpx.AsyncClient", return_value=mc):
result = await a2a_tools.delegate_task("ws-peer-456", "do a thing")
assert "Error" in result
assert "internal server error" in result
async def test_success_returns_result_text(self):
"""Happy path: result with parts returns the first text part."""
import a2a_tools
mc = AsyncMock()
mc.__aenter__ = AsyncMock(return_value=mc)
mc.__aexit__ = AsyncMock(return_value=False)
async def fake_post(url, **kwargs):
r = MagicMock()
r.status_code = 200
r.json = MagicMock(return_value={
"result": {
"parts": [{"kind": "text", "text": "Task done!"}]
}
})
return r
async def fake_get(url, **kwargs):
r = MagicMock()
r.status_code = 200
r.json = MagicMock(return_value={"url": "http://peer.svc/a2a"})
return r
mc.post = fake_post
mc.get = fake_get
with patch("a2a_tools.httpx.AsyncClient", return_value=mc):
result = await a2a_tools.delegate_task("ws-peer-789", "do a thing")
assert result == "Task done!"
# ---------------------------------------------------------------------------
# tool_delegate_task_async
# ---------------------------------------------------------------------------