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>
243 lines
8.9 KiB
Bash
Executable File
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
|