molecule-core/scripts/test-a2a-cross-runtime.sh
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

243 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# E2E test: Claude Code agent ↔ OpenClaw agent A2A communication
# Tests cross-runtime peer messaging between two different agent infras.
set -euo pipefail
PLATFORM="${1:-http://localhost:8080}"
OPENAI_KEY="${OPENAI_API_KEY:?Set OPENAI_API_KEY env var before running this test}"
PASS=0
FAIL=0
check() {
local label="$1" expected="$2" actual="$3"
if echo "$actual" | grep -qi "$expected"; then
echo "PASS: $label"
PASS=$((PASS + 1))
else
echo "FAIL: $label"
echo " expected to contain: $expected"
echo " got: $actual"
FAIL=$((FAIL + 1))
fi
}
wait_online() {
local id="$1" name="$2" max="${3:-30}"
for i in $(seq 1 "$max"); do
local s
s=$(curl -s "$PLATFORM/workspaces/$id" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null)
[ "$s" = "online" ] && return 0
[ "$s" = "failed" ] && echo " $name FAILED" && return 1
[ $((i % 5)) -eq 0 ] && echo " [$name] ${i}/${max}... ($s)"
sleep 5
done
echo " $name did not come online within $((max*5))s"
return 1
}
# Send A2A message with retry (free OpenRouter has rate limits — 1 min cooldown)
a2a_send() {
local id="$1" message="$2" max_retries="${3:-3}"
for attempt in $(seq 1 "$max_retries"); do
local resp
resp=$(curl -s -X POST "$PLATFORM/workspaces/$id/a2a" \
-H 'Content-Type: application/json' \
-d "{\"method\":\"message/send\",\"params\":{\"message\":{\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"$message\"}]}}}")
local text
text=$(echo "$resp" | python3 -c "import sys,json; r=json.load(sys.stdin); print(r.get('result',{}).get('parts',[{}])[0].get('text',''))" 2>/dev/null)
# Check for rate limit / billing errors
if echo "$text" | grep -qi "rate\|billing\|credits\|limit\|429\|throttl"; then
if [ "$attempt" -lt "$max_retries" ]; then
echo " Rate limited, waiting 60s before retry ($attempt/$max_retries)..."
sleep 60
continue
fi
fi
echo "$text"
return 0
done
echo "ERROR: all retries exhausted"
return 1
}
echo "============================================"
echo " Cross-Runtime A2A: Claude Code ↔ OpenClaw"
echo "============================================"
echo ""
# -------------------------------------------------------
# Step 1: Create Claude Code agent
# -------------------------------------------------------
echo "--- Step 1: Create Claude Code agent ---"
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Alice","role":"Claude Code assistant","tier":2,"template":"claude-code-default"}')
ALICE_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check "Create Alice (claude-code)" "provisioning" "$R"
echo " Alice: $ALICE_ID"
# -------------------------------------------------------
# Step 2: Create OpenClaw agent
# -------------------------------------------------------
echo ""
echo "--- Step 2: Create OpenClaw agent ---"
R=$(curl -s -X POST "$PLATFORM/workspaces" -H 'Content-Type: application/json' \
-d '{"name":"Bob","role":"OpenClaw research assistant","tier":2,"template":"openclaw"}')
BOB_ID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
check "Create Bob (openclaw)" "provisioning" "$R"
echo " Bob: $BOB_ID"
# -------------------------------------------------------
# Step 3: Set Bob as Alice's peer (same parent = siblings)
# -------------------------------------------------------
echo ""
echo "--- Step 3: Make them peers (root-level siblings) ---"
# Root-level workspaces with no parent are siblings — they can communicate
echo " Both are root-level → siblings → A2A allowed"
# -------------------------------------------------------
# Step 4: Set API key for Bob (OpenClaw needs OpenRouter key)
# -------------------------------------------------------
echo ""
echo "--- Step 4: Set Bob's API key ---"
R=$(curl -s -X POST "$PLATFORM/workspaces/$BOB_ID/secrets" \
-H 'Content-Type: application/json' \
-d "{\"key\":\"OPENAI_API_KEY\",\"value\":\"$OPENAI_KEY\"}")
check "Set Bob's OPENAI_API_KEY" "saved" "$R"
# -------------------------------------------------------
# Step 5: Wait for both to come online
# -------------------------------------------------------
echo ""
echo "--- Step 5: Wait for agents to come online ---"
echo " (OpenClaw takes ~2 min for npm install + gateway start)"
if wait_online "$ALICE_ID" "Alice" 20; then
check "Alice online" "ok" "ok"
else
check "Alice online" "online" "timeout"
fi
if wait_online "$BOB_ID" "Bob" 360; then
check "Bob online" "ok" "ok"
else
check "Bob online" "online" "timeout"
fi
# -------------------------------------------------------
# Step 6: Customize prompts
# -------------------------------------------------------
echo ""
echo "--- Step 6: Set agent prompts ---"
# Alice's system prompt (written to container via Files API)
R=$(curl -s -X PUT "$PLATFORM/workspaces/$ALICE_ID/files/system-prompt.md" \
-H 'Content-Type: application/json' \
-d '{"content":"You are Alice, a helpful assistant. When asked to introduce yourself, say exactly: I am Alice, running on Claude Code. Keep responses under 20 words."}')
check "Set Alice prompt" "saved" "$R"
# Bob's SOUL.md (OpenClaw convention)
R=$(curl -s -X PUT "$PLATFORM/workspaces/$BOB_ID/files/SOUL.md" \
-H 'Content-Type: application/json' \
-d '{"content":"You are Bob, a research assistant. When asked to introduce yourself, say exactly: I am Bob, running on OpenClaw. Keep responses under 20 words."}')
check "Set Bob prompt (SOUL.md)" "saved" "$R"
# Restart both to pick up new prompts
echo " Restarting agents..."
curl -s -X POST "$PLATFORM/workspaces/$ALICE_ID/restart" > /dev/null
curl -s -X POST "$PLATFORM/workspaces/$BOB_ID/restart" > /dev/null
sleep 5
echo " Waiting for restart..."
wait_online "$ALICE_ID" "Alice" 20 || true
wait_online "$BOB_ID" "Bob" 360 || true
# -------------------------------------------------------
# Step 7: Test direct A2A messages
# -------------------------------------------------------
echo ""
echo "--- Step 7: Direct A2A messages ---"
echo " Talking to Alice..."
ALICE_RESP=$(a2a_send "$ALICE_ID" "introduce yourself in one sentence")
echo " Alice says: $ALICE_RESP"
check "Alice responds" "Alice" "$ALICE_RESP"
echo ""
echo " Talking to Bob..."
BOB_RESP=$(a2a_send "$BOB_ID" "introduce yourself in one sentence")
echo " Bob says: $BOB_RESP"
check "Bob responds" "Bob" "$BOB_RESP"
# -------------------------------------------------------
# Step 8: Verify peer discovery
# -------------------------------------------------------
echo ""
echo "--- Step 8: Peer discovery ---"
R=$(curl -s "$PLATFORM/registry/$ALICE_ID/peers" | python3 -c "
import sys,json
peers = json.load(sys.stdin)
names = [p.get('name','') for p in peers]
print(' '.join(names))
" 2>/dev/null)
echo " Alice's peers: $R"
check "Alice sees Bob" "Bob" "$R"
R=$(curl -s "$PLATFORM/registry/$BOB_ID/peers" | python3 -c "
import sys,json
peers = json.load(sys.stdin)
names = [p.get('name','') for p in peers]
print(' '.join(names))
" 2>/dev/null)
echo " Bob's peers: $R"
check "Bob sees Alice" "Alice" "$R"
# -------------------------------------------------------
# Step 9: Verify cross-runtime access control
# -------------------------------------------------------
echo ""
echo "--- Step 9: Access control ---"
R=$(curl -s -X POST "$PLATFORM/registry/check-access" -H 'Content-Type: application/json' \
-d "{\"caller_id\":\"$ALICE_ID\",\"target_id\":\"$BOB_ID\"}")
check "Alice -> Bob allowed" "true" "$R"
R=$(curl -s -X POST "$PLATFORM/registry/check-access" -H 'Content-Type: application/json' \
-d "{\"caller_id\":\"$BOB_ID\",\"target_id\":\"$ALICE_ID\"}")
check "Bob -> Alice allowed" "true" "$R"
# -------------------------------------------------------
# Step 10: Verify no ws-* dirs on host
# -------------------------------------------------------
echo ""
echo "--- Step 10: Verify isolation ---"
HOST_WS=$(find /Users/hongming/Documents/GitHub/molecule-monorepo/workspace-configs-templates -maxdepth 1 -name 'ws-*' -type d 2>/dev/null | wc -l | tr -d ' ')
check "No ws-* dirs on host" "0" "$HOST_WS"
echo ""
echo " Alice container: $(docker ps --format '{{.Names}}' | grep "${ALICE_ID:0:12}" || echo 'not found')"
echo " Bob container: $(docker ps --format '{{.Names}}' | grep "${BOB_ID:0:12}" || echo 'not found')"
# -------------------------------------------------------
# Step 11: Cleanup
# -------------------------------------------------------
echo ""
echo "--- Step 11: Cleanup ---"
curl -s -X DELETE "$PLATFORM/workspaces/$ALICE_ID" > /dev/null
curl -s -X DELETE "$PLATFORM/workspaces/$BOB_ID" > /dev/null
check "Cleanup" "ok" "ok"
# -------------------------------------------------------
# Results
# -------------------------------------------------------
echo ""
echo "============================================"
echo " Results: $PASS passed, $FAIL failed"
echo "============================================"
exit $FAIL