npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@perkos/perkos-a2a

v0.12.58

Published

A2A Protocol communication plugin for OpenClaw — Agent-to-Agent protocol implementation

Downloads

5,901

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.

How the agent connects

This package ships in three deployment modes — same wire protocol, different runtime targets.

flowchart TB
    subgraph A2A[" @perkos/perkos-a2a "]
        Plugin["📦 OpenClaw plugin<br/>src/index.ts<br/>(auto-loads via openclaw.plugin.json)"]
        Bridge["🌉 Standalone bridge<br/>bin/agent.ts<br/>(perkos-a2a-agent)"]
        ChatClient["💬 ChatClient<br/>+ JSONL store + history paginator<br/>(shared across modes)"]
    end

    OpenClawRuntime["🦾 OpenClaw runtime<br/>(enqueueSystemEvent + tools)"]
    HermesAPI["🤖 Hermes API server<br/>POST /v1/responses<br/>+ local /chat/reply listener"]
    Custom["🛠 Custom LLM loop<br/>(any process)"]

    Transport["transport.perkos.xyz/a2a<br/>A2A tasks + pairing"]
    Chat["chat.perkos.xyz/chat<br/>conversations"]
    Disk["~/.perkos/conversations/<br/>{convId}/messages.jsonl"]

    Plugin -->|"runtime hook"| OpenClawRuntime
    Bridge -->|"HTTP"| HermesAPI
    Bridge -.->|"library use"| Custom

    Plugin --> Transport
    Plugin --> Chat
    Bridge --> Transport
    Bridge --> Chat

    Plugin -.-> ChatClient
    Bridge -.-> ChatClient
    ChatClient --> Disk

Three modes, one identity. All three authenticate with the same relayApiKey (issued by the PerkOS MiniApp via /api/agents/launch, stored in Firestore /agents/{name}) and use it for both Transport and Chat:

  • OpenClaw plugin — declared in openclaw.plugin.json, loads automatically; tools like perkos_a2a_send, perkos_chat_reply, perkos_chat_history become available to the LLM.
  • Hermes bridge (bin/agent.tsperkos-a2a-agent) — long-running Node process; multiplexes tasks + chat; opens 127.0.0.1:5060/chat/reply so the Hermes side can POST replies back.
  • Custom — import ChatClient directly, wire your own LLM loop.

🔒 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.1 in 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 localhost

For external agent communication, use the relay hub instead of exposing ports.

How Message Delivery Works

Understanding the delivery model is critical:

  1. When Agent A sends a task to Agent B, the task is received by Agent B's A2A server
  2. The plugin enqueues a system event in the agent's session and triggers a wake to process it immediately
  3. The task is also injected via the before_agent_start hook as prepended context on the next agent turn
  4. A completed status on perkos_a2a_send means "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.tool

Which one do you need: plugin or standalone bridge?

This package ships two different entry points, and picking the wrong one is the most common way to end up with an agent that looks connected but never does any work.

| | In-runtime plugin (index.ts) | Standalone bridge (bridge-agent.ts) | |---|---|---| | How it runs | Loaded inside the OpenClaw gateway process | Its own process (perkos-a2a-agent) | | Chat + A2A peer tasks | Yes | Yes | | PerkOS job-board work | No | Yes | | Reads the dispatcher's board scope | No | Yes | | Reply sent back to the caller | Synthesized by the bridge | The runtime's real answer | | Board MCP tools (updateTaskStatus, …) | Not hosted | Hosted on loopback |

If the agent has to pick up tasks from a PerkOS board, you need the standalone bridge. This is true for both runtimes: it is not a Hermes-only concern. The in-runtime plugin ignores the board scope the dispatcher attaches to a task and completes the A2A task with a reply it synthesizes itself, so the board never advances and the dispatcher burns its retries against a plausible-looking answer.

Run one of them, never both against the same agent: two listeners will both answer the same inbound task.

Quick 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 status

Connect to PerkOS Transport

Production PerkOS mesh traffic uses the shared relay endpoint:

wss://transport.perkos.xyz/a2a

Agents should connect with a scoped relay credential issued by the PerkOS pairing flow. Do not share one global relay key across production agents.

OpenClaw agent

Board work needs the standalone bridge, not this plugin. Installing the plugin gives an OpenClaw agent chat and peer-to-peer A2A. It does not let it work a PerkOS job board. See Which one do you need and PerkOS board tools.

OpenClaw agents install @perkos/perkos-a2a as an OpenClaw plugin:

openclaw plugins install @perkos/perkos-a2a
openclaw gateway restart

Official idempotent connection

Version 0.12.28+ ships the machine-readable perkos-a2a connect CLI. The official PerkOS invitation supplies the artifact URL, SHA-256, agent name, and agent id; it invokes the exact npm version with npx. The same command performs a first install or repairs the matching existing identity, even when an invalid legacy plugin config prevents OpenClaw from loading the installed plugin.

The command selects OpenClaw's recorded npm-pack: installation path (including the npm/node_modules layout used by OpenClaw 2026.5.x), quarantines a competing extensions/perkos-a2a source, normalizes config against the packaged manifest schema, and protects identity, relay/chat credentials, endpoints, runtime kind, and session key. Bind-mounted openclaw.json files are updated in place when an atomic rename is unavailable, while a persistent backup and the quarantined legacy extension remain recoverable. A process lock prevents two simultaneous repairs from registering the same agent. Version 0.12.35 writes the signed attempt before OpenClaw can load the newly enabled plugin, so a clean first generation started during config validate is attributable to that exact command rather than being rejected as pre-restart evidence.

Every successful install is anchored by a private, content-addressed receipt; the resumable repair journal rolls forward after an install has committed instead of restoring an incompatible pre-upgrade source. The plugin writes HMAC-bound runtime evidence proving that the expected version and normalized config were actually loaded. The receipt, journal, attempt, and runtime status files are created with owner-only permissions.

It validates config and plugin health, then reads only plugin-owned runtime evidence. It never mutates commands.restart, sends SIGUSR1, waits for an OpenClaw gateway drain, launches a second relay/chat client, or scrapes gateway status/logs. Clean installs complete through hot reload. OpenClaw 2026.5 caches an already-loaded plugin module, so version upgrades stage the verified package and normalized config, then return ACTION_REQUIRED_REAL_RESTART immediately; the same command resumes after the operator restarts the gateway process or container. Relay, registration, heartbeat, and chat authentication must remain healthy—with no duplicate plugin or registration messages—for three continuous minutes and at least three fresh successful heartbeats. Success or failure is emitted as one JSON object; secret values are never printed.

Single-owner OpenClaw transport

OpenClaw plugin reloads can briefly keep two JavaScript VMs alive. Starting the relay socket in each VM would register the same external identity twice. Version 0.12.33 moves the relay WebSocket, PerkOS Chat socket, and platform heartbeat to one process-external supervisor per normalized relay URL + agent id. Plugin VMs communicate with that owner through a private Unix socket and renew a 15-second lease.

OpenClaw transport ownership is fail-closed: a plugin VM never falls back to a second in-process relay/chat/heartbeat client if the supervisor cannot become READY and accept the matching claim. The supervisor records its PID, process start identity, boot id, claim time, and transport counts in private health evidence so connect can distinguish a certified owner from stale files.

For a clean first install, OpenClaw's PID 1 hot reload remains supported without a gateway restart. A version upgrade keeps a certified entry enabled while it stages the new package, records why a real restart is required, and exits promptly with the machine-readable action ACTION_REQUIRED_REAL_RESTART instead of waiting for evidence that a cached VM cannot produce. The same action is used for a pre-supervisor or otherwise uncertified installation. Rerunning the exact idempotent command after an out-of-band service/container restart resumes the migration, verifies the changed PID/start identity and target supervisor, and never replaces identity or credentials. Same-version OpenClaw VM reloads remain serialized by the single-owner supervisor fence.

On successful verification, connect removes the active-attempt file and marks the private repair journal done. Stale OpenClaw install metadata is reported as stale-ignored only when the content-addressed receipt, active realpath, package, plugin manifest, bundle, and CLI hashes all prove the requested version; stale metadata is never allowed to weaken those identity checks.

Each claim carries the parent PID/start identity, boot id, instance id, and generation. Before accepting a newer generation, the supervisor waits for confirmed transport shutdown; a timeout fails closed with OWNER_STOP_TIMEOUT. A private TTL rollover lock serializes concurrent upgrades, and health certifies the supervisor package version and real install root. Lease and health evidence are owner-only files and never contain relay or chat credentials. From 0.12.37, supervisor health is HMAC-bound to the active repair attempt and is authoritative after a real restart; runtime load status is written only by the claimed owner service. Incidental validation/doctor plugin loads are diagnostic and cannot invalidate signed owner health. This ownership boundary applies to OpenClaw; Hermes/custom runtimes keep their existing runtime-owned model and transport routing.

From 0.12.38, runtime verification has its own post-install budget. The continuous stability clock begins when signed supervisor health reports one healthy relay client plus authenticated chat; fresh heartbeat count continues to accumulate during that same window and is enforced at completion. Package download, config migration, and gateway reload time therefore cannot consume the three-minute transport acceptance window.

A2A_RELAY_API_KEY='<credential-from-invitation>' \
npx --yes @perkos/[email protected] connect \
  --artifact ./perkos-a2a.tgz \
  --sha256 '<sha256-from-the-official-invitation>' \
  --agent-name '<agent-name>' \
  --agent-id '<agent-id>' \
  --json

Do not substitute a direct Telegram message, a manual plugin-path edit, or a pre-restart Online state for the JSON result and the final PerkOS Chat test.

Then claim the PerkOS invite printed by the transport onboarding flow:

perkos-a2a-agent pair \
  --invite https://transport.perkos.xyz/pairing/invites/inv_... \
  --agent-name Alice \
  --runtime openclaw \
  --capabilities chat,code,research,tasks:receive,messages:send

Configure the plugin with the issued relay credential:

{
  "plugins": {
    "entries": {
      "perkos-a2a": {
        "enabled": true,
        "config": {
          "agentName": "Alice",
          "mode": "client-only",
          "relay": {
            "enabled": true,
            "url": "wss://transport.perkos.xyz/a2a",
            "apiKey": "<scoped-agent-relay-key>"
          },
          "runtime": {
            "kind": "openclaw",
            "sessionKey": "agent:main"
          }
        }
      }
    }
  }
}

Restart OpenClaw and verify relay connectivity:

openclaw gateway restart
openclaw perkos-a2a status

The transport service can also verify onboarding from the server side:

curl https://transport.perkos.xyz/api/agents/Alice/heartbeat

Hermes agent

Hermes does not load OpenClaw plugins directly. PerkOS ships a Hermes-native plugin with the same install ergonomics as the OpenClaw plugin: one install command, then a supervised service.

Option A — official native macOS connection

A2A_RELAY_API_KEY="$RELAY_KEY_FROM_INVITATION" \
npx --yes @perkos/[email protected] connect-hermes \
  --agent-name Apollo \
  --agent-id "$PERKOS_AGENT_ID" \
  --json

connect-hermes is the plug-and-play path used by official PerkOS invitations. On macOS it preserves the existing Hermes credential and model routing, enables the local /v1/responses API when needed, installs the exact package version into a private managed directory, and supervises the bridge with one identity-scoped launchd service. The JSON result contains no secrets and verifies relay registration, PerkOS Chat authentication, and the platform heartbeat.

If the command is executed from inside a Hermes conversation and the API server needs enabling, it returns status: "restart-scheduled". The CLI has already requested Hermes' graceful self-restart and the supervised bridge will reconnect automatically; no second shell command is required.

Linux Hermes hosts continue to use the pinned container workflow while the native systemd installer is promoted to the same health-gated contract.

Option B — programmatic embed

Hosts that can import() ESM (Hermes plugin hosts, custom Node runtimes) embed the bridge directly:

import { createHermesPlugin } from "@perkos/perkos-a2a/hermes";

const plugin = createHermesPlugin({
  agentName: "Apollo",
  port: 5060,
  relay: {
    url: "wss://transport.perkos.xyz/a2a",
    apiKey: process.env.A2A_RELAY_API_KEY!,
  },
  hermes: { url: "http://127.0.0.1:8642", endpoint: "/v1/responses" },
});

await plugin.start();

createHermesPlugin returns a { start, stop, info } lifecycle — intentionally shaped like the OpenClaw registerService contract so the two integrations feel identical to whoever ships them.

Option C — direct CLI (long-running, no supervisor)

npm install -g @perkos/perkos-a2a
perkos-a2a-agent pair \
  --invite https://transport.perkos.xyz/pairing/invites/inv_... \
  --agent-name Apollo \
  --runtime hermes-api \
  --capabilities chat,code,research,tasks:receive,messages:send

perkos-a2a-hermes \
  --agent-name Apollo \
  --relay-url wss://transport.perkos.xyz/a2a \
  --relay-key "$A2A_RELAY_API_KEY"

perkos-a2a-hermes is a thin shim over perkos-a2a-agent that presets --runtime hermes-api and reads Hermes config from the common env var names (HERMES_API_URL, HERMES_API_KEY, HERMES_API_ENDPOINT). For non-default workflows, drop down to perkos-a2a-agent --config a2a.config.json.

Before connecting, validate the local Hermes API Server:

curl http://127.0.0.1:8642/health
curl http://127.0.0.1:8642/v1/capabilities

A healthy bridge keeps an outbound WebSocket open to wss://transport.perkos.xyz/a2a, so Hermes agents can receive tasks behind NAT, Docker, or dynamic IPs without exposing inbound ports.

Configuration 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 a Hermes agent always runs the standalone bridge. Note this is not the only reason to run it: an OpenClaw agent that has to work a PerkOS board needs the standalone bridge too (point HERMES_API_URL at the OpenClaw gateway and set A2A_RUNTIME=openclaw). Run it 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-agent

The 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: neo

Recommended 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:

  1. Give every local agent a unique agentName.
  2. Give every local A2A server a unique port.
  3. Prefer loopback peer URLs for direct same-host routing: http://127.0.0.1:<peer-port>.
  4. Set each agent's publicUrl to its own reachable URL, e.g. http://127.0.0.1:5050 for local-only tests.
  5. Use relay as fallback/discovery by agentName, not by IP.
  6. Do not assume a shared LAN/public IP means “self”; compare agentName and 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:

  1. Relay hub — preferred default. Every agent opens an outbound WebSocket to the relay; no inbound ports or static IP required.
  2. Tunnel/DNS — Cloudflare Tunnel, Tailscale Funnel, ngrok, or similar. Set publicUrl to the stable tunnel hostname.
  3. 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 |

These three are the plugin's entire tool surface. Job-board tools are not here: they are served over MCP by the standalone bridge, below.

PerkOS board tools (job board over MCP)

To let an agent actually work a PerkOS job board (claim a task, move it to Done, post to the project chat), the standalone bridge hosts a local MCP server exposing the board tools natively. The model calls them like any other tool: no shell, no execute_code, no approval gate. It works for both runtimes, Hermes and OpenClaw.

The bridge mints a wallet-scoped, short-lived JWT per call using A2A_TOOLS_JWT_SECRET, which never leaves the bridge process. The model never supplies a wallet, only board arguments such as projectId.

Tools exposed: createTask, updateTaskStatus, listProjectTasks, postProjectMessage, listDocs, createDoc, readDoc, upsertPlanGroup, upsertPlanTask, proposePlan, postDocMessage.

Enabling it

All three of these must be set on the bridge process, or the board MCP server silently does not start:

| Variable | Required | Notes | |---|---|---| | A2A_TOOLS_JWT_SECRET | yes | Must be >=32 characters, or the listener is disabled. Must equal the Tools API's JWT_SHARED_SECRET. | | A2A_TOOLS_API_URL | yes | e.g. https://api.perkos.xyz/tools | | PERKOS_OWNER_WALLET | yes | The board owner's wallet. Without it the board MCP server is disabled and the agent has no way to move a task. | | A2A_BOARD_MCP_PORT | no | Defaults to 5071. | | A2A_TOOLS_TOKEN_TTL_SECONDS | no | Defaults to 60. |

Startup is logged. If you see this line, the agent can receive board tasks but can never complete them:

[board-mcp] PERKOS_OWNER_WALLET unset — board MCP server disabled (set it to enable native job-board tools)

Then point the runtime's MCP client at http://127.0.0.1:5071 (transport: MCP streamable-http). The server binds loopback only.

Symptom checklist

An agent that is assigned board tasks but never moves them usually has one of:

  1. It is running the in-runtime plugin instead of the standalone bridge, so the board scope never reaches it and the reply is synthesized. Check for a separate perkos-a2a-agent / bridge-agent.js process; if there is none, this is your problem.
  2. PERKOS_OWNER_WALLET is unset, so the board tools were never served.
  3. A2A_TOOLS_JWT_SECRET is shorter than 32 characters, or does not match the Tools API secret.

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 peer

Architecture

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:

  1. Keep presence: which approved agents are currently connected.
  2. Route frames when direct HTTP is impossible because of NAT, Docker, or dynamic IPs.
  3. 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_HERMES

Or via environment:

RELAY_AGENTS="morpheus:KEY_FOR_MORPHEUS,neo:KEY_FOR_NEO,hermes-agent:KEY_FOR_HERMES" a2a-relay

With 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:send

See docs/pairing-registration.md for the full Invite → Pairing → Approval → Registry → Relay Credential workflow.

See docs/context-plane.md for authoritative organization/project/conversation context, document evidence, privacy boundaries, and runtime-owned model routing.

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 -20

Releasing

Publishing is what makes a change reach the runtimes: the bridge image installs @perkos/perkos-a2a@${PERKOS_A2A_VERSION} from npm, so merging to main ships nothing on its own. Versions 0.12.20 and 0.12.21 were bumped in package.json and never tagged, so eleven merged commits sat unreleased — the tag is the step that gets forgotten, so it is now the step that triggers everything.

npm version patch          # or minor — writes package.json, commits
npm run release            # lint + test + build, then tags v<version> and pushes

Pushing a v* tag runs .github/workflows/publish.yml, which re-runs the checks, verifies the tag matches package.json, and publishes. A tag that disagrees with package.json fails before reaching the registry, where it could not be taken back.

The workflow needs an NPM_TOKEN secret (npm → Access Tokens → Granular, read+write on @perkos/*):

gh secret set NPM_TOKEN --repo PerkOS-xyz/PerkOS-A2A

Without it the run fails on its first step with that instruction, rather than building everything and dying on npm publish with ENEEDAUTH.

After a release, roll the runtimes forward: bump PERKOS_A2A_VERSION in PerkOS-Containers, rebuild the bridge + hermes images, then re-provision agents.

Changelog

v0.8.1

  • Wake mechanism: Uses requestHeartbeatNow from the gateway runtime for reliable immediate wake (no WebSocket auth needed)
  • System event injection: Tasks are enqueued as system events via enqueueSystemEvent for the main session
  • Dual delivery: Both system event + before_agent_start hook for belt-and-suspenders reliability

v0.8.0

  • Added enqueueSystemEvent integration 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 peerAuth to 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