From 81c7e5de48dcccf358b7ca01d405f2339ef485a7 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 2 Aug 2026 14:49:06 -0700 Subject: [PATCH] docs(a2a): website docs page + canonical agent-card.json path in prose - New website/docs/user-guide/messaging/a2a.md: when/where to use A2A (cross-machine, specialist peers, being callable) vs delegation/kanban for same-machine multi-agent; enable, outbound tools, inbound surface, security model, env reference, quick test, troubleshooting. Registered in sidebars.ts and the messaging index. - README/DESIGN/plugin.yaml/protocol.py prose updated to name the A2A v1.0 canonical discovery path /.well-known/agent-card.json (the code already served both; only the docs lagged). --- plugins/platforms/a2a/DESIGN.md | 2 +- plugins/platforms/a2a/README.md | 4 +- plugins/platforms/a2a/plugin.yaml | 3 +- plugins/platforms/a2a/protocol.py | 2 +- website/docs/user-guide/messaging/a2a.md | 122 +++++++++++++++++++++ website/docs/user-guide/messaging/index.md | 1 + website/sidebars.ts | 1 + 7 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 website/docs/user-guide/messaging/a2a.md diff --git a/plugins/platforms/a2a/DESIGN.md b/plugins/platforms/a2a/DESIGN.md index 66656d3735..035e41f0fc 100644 --- a/plugins/platforms/a2a/DESIGN.md +++ b/plugins/platforms/a2a/DESIGN.md @@ -40,7 +40,7 @@ Peers resolved from `config.yaml` → `a2a_agents`, or a direct URL. class that killed inbound serving in forks). The request handler is a module-level class (`A2ARequestHandler`) reached through `server.adapter`, so RPC handlers are unit-testable without HTTP. -- Agent Card at `GET /.well-known/agent.json` (v1.0: `supportedInterfaces[]`, +- Agent Card at `GET /.well-known/agent-card.json` (canonical v1.0 path; legacy `agent.json` also answers) (v1.0: `supportedInterfaces[]`, `provider`, `capabilities.extendedAgentCard`). **Dynamic**: skills are built from the live tool registry at serve time (`A2A_ADVERTISED_TOOLSETS` / `extra.advertised_toolsets` restricts them). diff --git a/plugins/platforms/a2a/README.md b/plugins/platforms/a2a/README.md index b95ad82828..9f6e3d7b26 100644 --- a/plugins/platforms/a2a/README.md +++ b/plugins/platforms/a2a/README.md @@ -43,7 +43,9 @@ The agent gets five tools: ## Inbound — be callable When the `a2a` platform is enabled, Hermes serves a v1.0 Agent Card at -`http://:/.well-known/agent.json` and accepts JSON-RPC +`http://:/.well-known/agent-card.json` (the legacy +`/.well-known/agent.json` path is also answered for pre-1.0 clients) and +accepts JSON-RPC `message/send`, `message/stream` (SSE), `tasks/get|list|cancel|subscribe`, and push notification configs (inline or via `tasks/pushNotificationConfig/create`). Incoming tasks are injected into your diff --git a/plugins/platforms/a2a/plugin.yaml b/plugins/platforms/a2a/plugin.yaml index 049809e99c..9f08b7f9e6 100644 --- a/plugins/platforms/a2a/plugin.yaml +++ b/plugins/platforms/a2a/plugin.yaml @@ -12,7 +12,8 @@ description: > CrewAI, Google ADK, OpenClaw, ...). INBOUND (platform adapter): exposes Hermes as an A2A-discoverable agent. An - Agent Card is served at /.well-known/agent.json and incoming tasks are routed + Agent Card is served at /.well-known/agent-card.json (v1.0 canonical path; + legacy agent.json also answers) and incoming tasks are routed into the agent's live gateway session like any other platform — so the agent that replies is the same one talking to its user, with full memory and context, not a throwaway clone. diff --git a/plugins/platforms/a2a/protocol.py b/plugins/platforms/a2a/protocol.py index 99fedbb241..f1522fccb1 100644 --- a/plugins/platforms/a2a/protocol.py +++ b/plugins/platforms/a2a/protocol.py @@ -3,7 +3,7 @@ A2A protocol helpers — Agent Card construction, JSON-RPC framing, task store, and disk-backed conversation persistence. Wire shape follows A2A Protocol v1.0 (JSON-RPC 2.0 binding over HTTP): - - Agent Card served at GET /.well-known/agent.json (and agent-card.json) + - Agent Card served at GET /.well-known/agent-card.json (canonical v1.0; legacy agent.json also answers) - Tasks via POST {jsonrpc:"2.0", method:"message/send", params:{...}} - Streaming via ``message/stream`` → SSE; events are StreamResponse objects discriminated by member presence (``statusUpdate`` / ``artifactUpdate``), diff --git a/website/docs/user-guide/messaging/a2a.md b/website/docs/user-guide/messaging/a2a.md new file mode 100644 index 0000000000..71aecfaa0a --- /dev/null +++ b/website/docs/user-guide/messaging/a2a.md @@ -0,0 +1,122 @@ +# A2A (Agent-to-Agent) + +[A2A](https://a2a-protocol.org) is the open Agent2Agent protocol (v1.0, stewarded by the Linux Foundation) for communication between independent AI agents. The Hermes A2A plugin works in **both directions**: your agent can call other A2A agents as tools, and other agents can send tasks to your Hermes over HTTP. + +It interoperates with any A2A-compliant peer — another Hermes, LangChain, CrewAI, Google ADK agents, or anything built on the official `a2a-sdk`. + +## When to use A2A + +- **Hermes ↔ Hermes across machines** — let your desktop agent hand tasks to a Hermes on a server, or vice versa, each with its own memory, tools, and credentials. +- **Delegating to specialist agents** — a peer that advertises `web_search`/`research`/`coding` skills on its Agent Card can be discovered and called mid-conversation. +- **Being a callable service** — expose your Hermes so other frameworks' agents can send it tasks. + +When you want multiple agents on the **same machine**, prefer [delegation](../features/delegation.md) (in-process subagents) or the [kanban board](../features/kanban.md) (durable multi-profile work queue) — A2A is for crossing process/machine/framework boundaries. + +## Enable + +```bash +hermes gateway setup # pick A2A +``` + +Or in `~/.hermes/config.yaml`: + +```yaml +gateway: + platforms: + a2a: + enabled: true + extra: + port: 9900 +``` + +The outbound client tools ship as the `a2a` toolset, **off by default** — enable it with `hermes tools`. + +## Outbound: calling other agents + +With the `a2a` toolset enabled, the agent gets: + +| Tool | What it does | +|---|---| +| `a2a_discover(url)` | Fetch and summarize a peer's Agent Card | +| `a2a_call(agent, message, context_id?)` | Send a task, get the reply; multi-turn via `context_id` | +| `a2a_list()` | Configured peers, saved conversations, metrics | +| `a2a_history(context_id)` | Recall a persisted A2A conversation | +| `a2a_orchestrate(capability, message, mode?)` | Fan a task out to every peer advertising a capability (`all` / `first` / `best`) | + +Configure known peers in `config.yaml`: + +```yaml +a2a_agents: + researcher: + url: "http://research-box.local:9900" + auth: { type: bearer, token: "..." } + timeout: 120 + capabilities: [web_search, research] +``` + +Then just ask: *"Ask the researcher agent to summarize today's arXiv postings."* Direct URLs work too — `a2a_call` accepts any A2A endpoint. + +## Inbound: being callable + +With the platform enabled, Hermes serves: + +- **Agent Card** at `GET /.well-known/agent-card.json` (canonical v1.0 path; the legacy `agent.json` also answers) — advertises your agent's name, skills (derived from enabled toolsets), and auth requirements. +- **JSON-RPC 2.0** at `POST /` — canonical v1.0 methods (`SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, push-notification config CRUD) plus the pre-1.0 path-style aliases (`message/send`, …). +- **SSE streaming** for `SendStreamingMessage`, with spec-correct JSON-RPC-enveloped frames. +- **Push notifications** (webhooks) for long-running tasks, HMAC-SHA256 signed. + +Inbound tasks are injected into a **live gateway session** — the same agent, memory, and tools that serve your other channels — and the final reply is returned to the caller as the task result. Conversations are keyed by the A2A `contextId`, so a peer can hold a multi-turn exchange. + +Interoperability is verified against the official Python `a2a-sdk` (card resolution, `SendMessage`, streaming). + +## Security model + +Secure by default; every widening step is explicit: + +- **No token ⇒ localhost only.** The server binds `127.0.0.1`. Remote exposure requires a bearer token **and** an explicit `A2A_HOST`. +- **Per-peer tokens** — `A2A_PEER_TOKENS="alice:tok1,bob:tok2"` gives each peer its own credential; the authenticated name drives rate limiting, trust, and audit. +- **Prompt-injection filtering** — inbound text is filtered and framed as untrusted peer input. Remote peers cannot invoke operator slash commands. +- **Outbound redaction** — credential-shaped strings (API keys, JWTs, tokens) are scrubbed from replies. +- **Audit log** — every exchange appends to `~/.hermes/a2a_audit.jsonl`. +- **Anti-loop** — per-context turn caps stop two agents ping-ponging forever. + +## Configuration reference + +| Env var | Default | Meaning | +|---|---|---| +| `A2A_PEER_TOKENS` | _(unset)_ | Per-peer credentials `name:token,…` (preferred) | +| `A2A_BEARER_TOKEN` | _(unset)_ | Shared token; identity falls back to caller IP | +| `A2A_HOST` | `127.0.0.1` | Bind host — only widens when a token is set | +| `A2A_PORT` | `9900` | Inbound port | +| `A2A_AGENT_NAME` | hostname-derived | Name on the Agent Card | +| `A2A_PUBLIC_URL` | _(unset)_ | Routable URL advertised on the card (reverse proxies / k8s) | +| `A2A_TRUSTED_PEERS` | _(unset)_ | Allow-list of authenticated identities | +| `A2A_ALLOW_ALL_USERS` | `false` | Allow any authenticated peer (dev only) | +| `A2A_RATE_LIMIT` | `60` | Requests/minute per identity | +| `A2A_MAX_PINGPONG_TURNS` | `5` | Anti-loop turn cap per context (max 20) | +| `A2A_REPLY_TIMEOUT` | `300` | Seconds to wait for the agent's reply | +| `A2A_PUSH_SECRET` | bearer token | HMAC secret for push-notification signing | +| `A2A_ADVERTISED_TOOLSETS` | all registered | Restrict which skills appear on the Agent Card | + +Behind a reverse proxy or Kubernetes Service, set `A2A_PUBLIC_URL` (or rely on `X-Forwarded-Host`/`X-Forwarded-Proto`) so the Agent Card advertises a URL peers can actually call back. + +## Quick test + +```bash +# From another machine / agent: +curl http://your-host:9900/.well-known/agent-card.json + +curl -X POST http://your-host:9900/ \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage", + "params":{"message":{"messageId":"m1","role":"ROLE_USER", + "parts":[{"text":"What tools do you have?"}]}}}' +``` + +## Troubleshooting + +- **Peers can't reach the card URL** — the card was advertising your bind address; set `A2A_PUBLIC_URL` to the externally routable URL. +- **`401 Unauthorized`** — token mismatch; check `A2A_PEER_TOKENS`/`A2A_BEARER_TOKEN` on the server and the peer's `auth:` block. +- **Server won't bind non-localhost** — by design: set a bearer token first, then `A2A_HOST=0.0.0.0`. +- **Replies time out on long tasks** — raise `A2A_REPLY_TIMEOUT`, or have the caller register a push-notification config and poll `GetTask`. diff --git a/website/docs/user-guide/messaging/index.md b/website/docs/user-guide/messaging/index.md index b8a24508ae..8cf1491ad3 100644 --- a/website/docs/user-guide/messaging/index.md +++ b/website/docs/user-guide/messaging/index.md @@ -803,4 +803,5 @@ Defaults to `false`. Only platforms whose adapter implements `delete_message` ho - [Raft Setup](raft.md) - [IRC Setup](irc.md) - [Buzz Setup](buzz.md) +- [A2A (Agent-to-Agent) Setup](a2a.md) - [Webhooks](webhooks.md) diff --git a/website/sidebars.ts b/website/sidebars.ts index 1af0efb48e..7b1e47098d 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -657,6 +657,7 @@ const sidebars: SidebarsConfig = { type: 'category', label: 'Other', items: [ + 'user-guide/messaging/a2a', 'user-guide/messaging/homeassistant', 'user-guide/messaging/mattermost', 'user-guide/messaging/matrix',