@perkos/perkos-a2a-agent
v0.8.34
Published
Standalone PerkOS A2A bridge CLI for Hermes/custom runtimes
Maintainers
Readme
@perkos/perkos-a2a
Agent-to-Agent (A2A) protocol plugin for OpenClaw. Enables secure multi-agent communication using Google's A2A protocol specification with enterprise-grade relay infrastructure for NAT traversal.
🔒 Security First
A2A communication MUST be secured. Without authentication, anyone on your network can send tasks to your agent, potentially executing arbitrary commands.
Enable Authentication (REQUIRED for production)
{
"plugins": {
"entries": {
"perkos-a2a": {
"config": {
"agentName": "my-agent",
"port": 5050,
"auth": {
"requireApiKey": true,
"apiKeys": ["YOUR_SECRET_API_KEY"]
},
"peerAuth": {
"other-agent": "THEIR_API_KEY"
},
"peers": {
"other-agent": "http://10.0.0.2:5050"
}
}
}
}
}
}Generate a secure API key:
python3 -c "import secrets; print(secrets.token_hex(32))"Security checklist:
- ✅
auth.requireApiKey: true— reject unauthenticated inbound requests - ✅
auth.apiKeys— list of accepted API keys for inbound requests - ✅
peerAuth— API keys to send when making outbound requests to each peer - ✅ All peers share the same API key (or use per-peer keys)
- ✅ API keys are never committed to public repos
- ✅ On VPS: bind A2A ports to
127.0.0.1in Docker/firewall (see below)
Without auth enabled:
- ❌ Anyone on your network can send tasks to your agent
- ❌ Tasks can instruct the agent to execute commands, send messages, access files
- ❌ This is equivalent to giving someone shell access
VPS Security: Bind Ports to Localhost
On VPS deployments (Docker Compose), bind A2A ports to 127.0.0.1 so they're not exposed externally:
# docker-compose.yml
services:
my-agent:
ports:
- "127.0.0.1:5050:5050" # A2A only accessible from localhostFor external agent communication, use the relay hub instead of exposing ports.
How Message Delivery Works
Understanding the delivery model is critical:
- When Agent A sends a task to Agent B, the task is received by Agent B's A2A server
- The plugin enqueues a system event in the agent's session and triggers a wake to process it immediately
- The task is also injected via the
before_agent_starthook as prepended context on the next agent turn - A
completedstatus onperkos_a2a_sendmeans "delivered to the server and queued" — the agent may need a moment to wake and process
v0.8.1 delivery pipeline:
Task received → enqueueSystemEvent() → requestHeartbeatNow() → Agent wakes → Processes task
↘ before_agent_start hook (backup) ↗Inspect pending tasks:
curl -s -X POST http://localhost:5050/a2a/jsonrpc \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"jsonrpc":"2.0","method":"tasks/list","id":1,"params":{}}' | python3 -m json.toolQuick Start
# 1. Install the plugin
openclaw plugins install @perkos/perkos-a2a
# 2. Configure (see config section below)
# 3. Restart gateway to load the plugin
openclaw gateway restart
# 4. Run the setup wizard to detect your environment
openclaw perkos-a2a setup
# 5. Check status
openclaw perkos-a2a statusConfiguration Reference
{
"plugins": {
"entries": {
"perkos-a2a": {
"enabled": true,
"config": {
"agentName": "my-agent",
"port": 5050,
"bindHost": "0.0.0.0",
"publicUrl": "https://my-agent.example.com",
"mode": "auto",
"skills": [
{
"id": "research",
"name": "Research",
"description": "Web research and analysis",
"tags": ["research", "analysis"]
}
],
"peers": {
"other-agent": "http://10.0.0.2:5050"
},
"peerAuth": {
"other-agent": "shared-secret-key"
},
"auth": {
"requireApiKey": true,
"apiKeys": ["shared-secret-key"]
},
"relay": {
"url": "wss://relay.example.com:8787",
"apiKey": "relay-api-key",
"enabled": true
},
"runtime": {
"kind": "openclaw",
"sessionKey": "agent:main"
}
}
}
}
}
}| Option | Type | Default | Description |
|---|---|---|---|
| agentName | string | "agent" | This agent's name in the network |
| port | number | 5050 | HTTP server port. Use one unique port per agent/container on shared VPS hosts |
| bindHost | string | 0.0.0.0 | Interface to bind. Use 0.0.0.0 in Docker, 127.0.0.1 behind local reverse proxies/tunnels |
| publicUrl | string | — | Externally reachable base URL advertised in Agent Card; use DNS/tunnel/LAN URL when localhost is not reachable |
| mode | string | "auto" | Operating mode: auto, full, client-only, relay |
| skills | array | [] | Skills exposed via the agent card |
| peers | object | {} | Map of peer names → A2A base URLs |
| peerAuth | object | {} | Map of peer names → API keys for outbound requests |
| auth.requireApiKey | boolean | false | Set to true for production |
| auth.apiKeys | string[] | [] | Accepted API keys for inbound requests |
| relay.url | string | — | Relay hub WebSocket URL |
| relay.apiKey | string | — | API key for relay hub authentication |
| relay.enabled | boolean | false | Enable relay connectivity |
| runtime.kind | string | "openclaw" | Inbound execution target: openclaw, hermes-api/hermes, or none |
| runtime.sessionKey | string | runtime-specific | Target session (agent:main for OpenClaw, a2a for Hermes API) |
| runtime.hermesUrl | string | http://127.0.0.1:8642 | Hermes API Server URL when runtime.kind = "hermes-api" or "hermes" |
| runtime.hermesToken | string | — | Optional Hermes API Server bearer token/API key. Env fallback: API_SERVER_KEY or HERMES_API_KEY |
| runtime.hermesEndpoint | string | /v1/responses | Hermes API Server endpoint; also supports /v1/runs and /v1/chat/completions |
OpenClaw ↔ Hermes Delivery
PerkOS A2A separates the transport from the local runtime. Direct HTTP and relay routing stay the same, but inbound tasks can be delivered into either OpenClaw or the official Hermes API Server.
OpenClaw receiver
"runtime": {
"kind": "openclaw",
"sessionKey": "agent:main"
}OpenClaw uses enqueueSystemEvent + requestHeartbeatNow when available.
Hermes receiver
"runtime": {
"kind": "hermes-api",
"sessionKey": "a2a-apollo",
"hermesUrl": "http://127.0.0.1:8642",
"hermesEndpoint": "/v1/responses",
"hermesToken": "OPTIONAL_API_SERVER_KEY"
}Hermes delivery uses the supported Hermes API Server HTTP surface. By default the bridge posts to /v1/responses; /v1/runs is available when you want observable run state/events, and /v1/chat/completions is available for OpenAI-compatible chat payloads. Validate the local Hermes API Server with GET /health and GET /v1/capabilities. Do not use UI/workspace endpoints such as /api/session-send or /api/sessions/send; those are not the stable Hermes runtime delivery interface.
Hermes does not load OpenClaw plugins, so run the standalone bridge next to Hermes with API Server enabled:
A2A_AGENT_NAME=hermes-agent \
A2A_RUNTIME=hermes-api \
A2A_MODE=client-only \
A2A_RELAY_ENABLED=true \
A2A_RELAY_URL=wss://relay.example.com \
A2A_RELAY_API_KEY=*** \
HERMES_API_URL=http://127.0.0.1:8642 \
HERMES_API_ENDPOINT=/v1/responses \
API_SERVER_KEY=*** \
perkos-a2a-agentThe bridge keeps an outbound relay WebSocket open, so agents behind NAT or dynamic IPs receive tasks without cron polling or inbound port forwarding.
Deployment Reality: Docker, VPS, NAT, and Dynamic IPs
PerkOS A2A must not assume one public machine equals one agent. Real deployments often run many agents as Docker containers on one VPS, or several local machines behind one office/home NAT.
Multiple Docker agents on one VPS
Each agent needs a unique internal/listening port and, if exposed through the host, a unique host port or reverse-proxy route.
services:
morpheus:
image: openclaw-agent
ports:
- "127.0.0.1:5050:5050"
environment:
A2A_AGENT_NAME: morpheus
neo:
image: openclaw-agent
ports:
- "127.0.0.1:5051:5050"
environment:
A2A_AGENT_NAME: neoRecommended pattern: keep container ports private, put Caddy/Nginx/Traefik in front, and set each agent's publicUrl to its stable route, e.g. https://morpheus-a2a.example.com.
Same-host / same-IP agents (OpenClaw + Hermes on one macOS)
PerkOS A2A must treat IP address as transport only, never as agent identity. Multiple agents can share the same macOS host, LAN IP, and public IP. For example, Morpheus/OpenClaw and Apollo/Hermes may both run on one Mac.
Rules for this edge case:
- Give every local agent a unique
agentName. - Give every local A2A server a unique
port. - Prefer loopback peer URLs for direct same-host routing:
http://127.0.0.1:<peer-port>. - Set each agent's
publicUrlto its own reachable URL, e.g.http://127.0.0.1:5050for local-only tests. - Use relay as fallback/discovery by
agentName, not by IP. - Do not assume a shared LAN/public IP means “self”; compare
agentNameand the full peer URL/port.
Example: Apollo/Hermes on the same Mac connecting to Morpheus/OpenClaw:
{
"agentName": "Apollo-Hermes-OSX",
"port": 5060,
"bindHost": "127.0.0.1",
"publicUrl": "http://127.0.0.1:5060",
"mode": "full",
"peers": {
"Perkos-Claw-Tester": "http://127.0.0.1:5050"
},
"relay": {
"enabled": true,
"url": "wss://transport.perkos.xyz/a2a",
"apiKey": "..."
},
"runtime": {
"kind": "hermes-api",
"sessionKey": "a2a-apollo",
"hermesUrl": "http://127.0.0.1:8642",
"hermesEndpoint": "/v1/responses"
}
}Run perkos-a2a-agent --setup after installing from npm to print same-host guidance, current port availability, peer URL hints, and relay fallback status.
NAT / changing public IP
For office/home/local agents behind NAT, direct P2P is brittle because all machines share one external IP and that IP can change. Use one of these patterns:
- Relay hub — preferred default. Every agent opens an outbound WebSocket to the relay; no inbound ports or static IP required.
- Tunnel/DNS — Cloudflare Tunnel, Tailscale Funnel, ngrok, or similar. Set
publicUrlto the stable tunnel hostname. - Direct LAN/VPN only — okay for trusted LAN/Tailscale networks, but still require API keys.
Do not expose unauthenticated A2A ports publicly. If direct HTTP is used, pair auth.requireApiKey with per-peer peerAuth.
Modes
| Mode | HTTP Server | Relay Client | Best For |
|---|---|---|---|
| auto | Conditional | If configured | Most setups — auto-detects NAT |
| full | Yes | If configured | VPS with public IP, or LAN agents |
| client-only | No | If configured | Behind NAT, send only |
| relay | No (hub mode) | No | Running as relay hub |
Authentication
Inbound Auth (protecting your agent)
When auth.requireApiKey: true, all inbound HTTP requests must include an API key via one of:
X-API-Key: <key>header (recommended)Authorization: Bearer <key>header?apiKey=<key>query parameter
Requests without a valid key receive 401 Unauthorized.
The agent card endpoint (/.well-known/agent-card.json) and health endpoint (/health) are always public — they don't contain sensitive information.
Outbound Auth (authenticating to peers)
Use peerAuth to send API keys when making requests to specific peers:
"peerAuth": {
"agent-b": "agent-b-accepts-this-key",
"agent-c": "agent-c-accepts-this-key"
}Shared Key Setup (simplest)
For a small team of trusted agents, use the same API key everywhere:
# Generate one shared key
python3 -c "import secrets; print(secrets.token_hex(32))"
# Example: a1b2c3d4e5f6...Each agent configures:
"auth": { "requireApiKey": true, "apiKeys": ["a1b2c3d4e5f6..."] },
"peerAuth": { "peer-name": "a1b2c3d4e5f6..." }Per-Peer Keys (more secure)
For larger deployments, each agent pair can use unique keys. Agent A's outbound key to B must match B's inbound apiKeys, and vice versa.
Relay Auth
Agents authenticate with the relay hub using the relay.apiKey. The hub rejects connections with invalid keys.
Agent Tools
When the plugin is active, three tools are available to the agent:
| Tool | Description |
|---|---|
| perkos_a2a_discover | Discover all configured peer agents and their capabilities |
| perkos_a2a_send | Send a task to a named peer (direct HTTP → relay fallback) |
| perkos_a2a_status | Check the status of a previously sent task by ID |
CLI Commands
openclaw perkos-a2a setup # Detect environment and show recommendations
openclaw perkos-a2a status # Show agent status, peers, and config
openclaw perkos-a2a discover # Discover peer agents (direct + relay)
openclaw perkos-a2a send <target> <message> # Send a task to a peerArchitecture
Direct Peer-to-Peer
Agents on the same network or with public IPs communicate directly via HTTP JSON-RPC 2.0.
Agent A Agent B
┌─────────────┐ ┌─────────────┐
│ OpenClaw GW │ │ OpenClaw GW │
│ └─ A2A │──── HTTP ────────│ └─ A2A │
│ plugin │ JSON-RPC 2.0 │ plugin │
│ :5050 │◄────────────────│ :5051 │
└─────────────┘ └─────────────┘Registrar / Rendezvous Security Model
The relay hub should be treated as a registrar/rendezvous server, not as an unrestricted public chat server. Its jobs are:
- Keep presence: which approved agents are currently connected.
- Route frames when direct HTTP is impossible because of NAT, Docker, or dynamic IPs.
- Reject unapproved agents before they can discover or message anyone.
In production, prefer an explicit approved-agent registry instead of one shared relay key:
a2a-relay \
--port 6060 \
--agents morpheus:KEY_FOR_MORPHEUS,neo:KEY_FOR_NEO,hermes-agent:KEY_FOR_HERMESOr via environment:
RELAY_AGENTS="morpheus:KEY_FOR_MORPHEUS,neo:KEY_FOR_NEO,hermes-agent:KEY_FOR_HERMES" a2a-relayWith registeredAgents enabled:
- An agent can only register under its approved name with its own key.
- A stolen/shared key cannot impersonate another agent name.
- Messages to unapproved target names are rejected.
- Discovery only returns currently connected approved agents.
The pairing flow now generates these entries automatically: a PerkOS system creates an invite, the external agent claims it with a local Ed25519 identity, a human/system approves the request, and the registry issues a scoped relay credential for that agentName.
Agent-side pairing:
perkos-a2a-agent pair \
--invite https://transport.perkos.xyz/pairing/invites/inv_... \
--agent-name Apollo \
--runtime hermes \
--capabilities chat,code,research,tasks:receive,messages:sendSee docs/pairing-registration.md for the full Invite → Pairing → Approval → Registry → Relay Credential workflow.
For Nexus-style product backends, see docs/nexus-communications-server.md for the communications-server pattern: backend orchestrator → A2A relay → OpenClaw/Hermes runtime worker → authenticated backend callbacks.
Relay Hub (NAT Traversal)
Agents behind NAT connect outbound to the relay hub via WebSocket. No port forwarding needed.
Agent A (NAT) Relay Hub (VPS) Agent B (NAT)
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ A2A │──WSS──▶│ WS Broker │◀─WSS─│ A2A │
│ plugin │◀──WSS──│ Msg Queue │──WSS─▶│ plugin │
└──────────┘ │ Agent Registry│ └──────────┘
│ Rate Limiter │
└──────────────┘Multi-Agent LAN Setup (Same WiFi)
Step 1: Assign unique ports per agent
| Agent | Machine IP | Port | |-------|-----------|------| | alice | 192.168.10.89 | 5055 | | morpheus | 192.168.10.88 | 5051 |
Step 2: Generate a shared API key
python3 -c "import secrets; print(secrets.token_hex(32))"Step 3: Configure each agent
Alice (192.168.10.89:5055):
{
"agentName": "alice",
"port": 5055,
"mode": "full",
"peers": {
"morpheus": "http://192.168.10.88:5051"
},
"peerAuth": {
"morpheus": "SHARED_API_KEY"
},
"auth": {
"requireApiKey": true,
"apiKeys": ["SHARED_API_KEY"]
}
}Morpheus (192.168.10.88:5051):
{
"agentName": "morpheus",
"port": 5051,
"mode": "full",
"peers": {
"alice": "http://192.168.10.89:5055"
},
"peerAuth": {
"alice": "SHARED_API_KEY"
},
"auth": {
"requireApiKey": true,
"apiKeys": ["SHARED_API_KEY"]
}
}Step 4: Restart gateways and test
# On each machine:
openclaw gateway restart
# Verify peer is reachable:
curl -s http://192.168.10.88:5051/.well-known/agent-card.json
# Send authenticated test:
curl -s -X POST http://192.168.10.88:5051/a2a/jsonrpc \
-H "Content-Type: application/json" \
-H "x-api-key: SHARED_API_KEY" \
-d '{"jsonrpc":"2.0","method":"tasks/list","id":1,"params":{}}'Running the Relay Hub
Deploy the relay hub on a VPS with a public IP for NAT traversal.
# Via npx
npx tsx bin/relay.ts --port 8787 --api-keys key1,key2
# Via environment variables
RELAY_PORT=8787 RELAY_API_KEYS=key1,key2 npx tsx bin/relay.ts| Option | Env Var | Default | Description |
|---|---|---|---|
| --port | RELAY_PORT | 6060 | WebSocket listen port |
| --api-keys | RELAY_API_KEYS | — | Comma-separated accepted API keys |
| --max-queue | RELAY_MAX_QUEUE | 200 | Max queued messages per offline agent |
| --rate-limit | RELAY_RATE_LIMIT | 60 | Max messages per agent per minute |
Troubleshooting
| Problem | Solution |
|---|---|
| 401 Unauthorized | Ensure your x-api-key header matches the target's auth.apiKeys |
| Port in use | Change port in config. Run lsof -i :5050 to find conflicts. After gateway restart, old ports may linger — do a full stop + start |
| Peers offline | Verify peer URL and port. Check firewall. Use curl to test reachability |
| Tasks received but not processed | Check logs for enqueueSystemEvent and Wake triggered. If missing, update to v0.8.1+ |
| Relay connection failing | Verify relay URL. Check API key matches hub config. Look for [perkos-a2a] log messages |
| Port 5000 conflict on macOS | AirPlay Receiver uses port 5000. Use 5050+ instead |
View plugin logs:
# Find log file
openclaw gateway status 2>&1 | grep "File logs"
# Filter A2A logs
grep "perkos-a2a" /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log | tail -20Changelog
v0.8.1
- Wake mechanism: Uses
requestHeartbeatNowfrom the gateway runtime for reliable immediate wake (no WebSocket auth needed) - System event injection: Tasks are enqueued as system events via
enqueueSystemEventfor the main session - Dual delivery: Both system event +
before_agent_starthook for belt-and-suspenders reliability
v0.8.0
- Added
enqueueSystemEventintegration for task delivery - WebSocket-based wake (replaced in v0.8.1 due to gateway auth complexity)
v0.6.1
- Fixed install command in README
- Added
peerAuthto config schema and types
v0.6.0
- Initial public release
- Direct HTTP + relay hub communication
- Agent tools: discover, send, status
- CLI commands: setup, status, discover, send
License
MIT — PerkOS
