# A2A_EX
Elixir client and server library for the [Agent2Agent (A2A) protocol](https://a2a-protocol.org/latest/). Provides typed structs, REST and JSON-RPC transports, SSE streaming utilities, Plug integration, and a task store abstraction with an ETS adapter.
## Features
- Agent card discovery, validation, and optional signature verification.
- Client APIs for message, task, streaming, and push notification flows.
- Server plugs for REST and JSON-RPC transports with SSE streaming support.
- Task store behavior with ETS adapter.
- Extension negotiation helpers and header utilities.
- gRPC transport placeholder returning unsupported operation errors.
## Protocol compatibility
- [v0.3.0](https://a2a-protocol.org/v0.3.0/specification/) and [latest Release Candidate v1.0](https://a2a-protocol.org/latest/specification/)-compatible mode.
- REST + SSE and JSON-RPC + SSE.
### REST wire format compatibility
By default, REST uses spec-compliant JSON wire format (`wire_format: :spec_json`), e.g.
`role: "user" | "agent"` and `parts`.
For interoperability with SDKs that use protobuf-style REST JSON (`ROLE_USER` / `content`),
you can opt in to compatibility mode with `wire_format: :proto_json`.
Client example:
```elixir
config =
A2A.Client.Config.new("https://example.com",
transport: A2A.Transport.REST,
version: :v0_3,
wire_format: :proto_json
)
{:ok, result} =
A2A.Client.send_message(config,
message: %A2A.Types.Message{
role: :user,
parts: [%A2A.Types.TextPart{text: "Hello"}]
}
)
```
Server example:
```elixir
plug A2A.Server.REST.Plug,
executor: MyApp.A2AExecutor,
version: :v0_3,
wire_format: :proto_json
```
Use `:proto_json` only as an interoperability workaround; keep `:spec_json` for standards-compliant v0.3 REST deployments.
## Installation
Add `a2a_ex` to your dependencies:
```elixir
def deps do
[
{:a2a_ex, "~> 0.1.0"}
]
end
```
## Usage
### Client
```elixir
{:ok, card} = A2A.Client.discover("https://example.com")
{:ok, task_or_message} =
A2A.Client.send_message(card,
message: %A2A.Types.Message{
role: :user,
parts: [%A2A.Types.TextPart{text: "Draft an outline"}]
}
)
```
### Streaming
```elixir
{:ok, stream} =
A2A.Client.stream_message(card,
message: %A2A.Types.Message{
role: :user,
parts: [%A2A.Types.TextPart{text: "Stream updates"}]
}
)
for event <- stream do
case event do
%A2A.Types.StreamResponse{} ->
IO.inspect(event)
%A2A.Types.StreamError{error: error} ->
IO.warn("Stream error: #{error.type} #{error.message}")
end
end
```
Stream cancellation: halting enumeration cancels the underlying Req stream.
You can also call `A2A.Client.Stream.cancel(stream)` to cancel explicitly.
Streaming order: when the handler returns a `Task`, the stream begins with a Task event
followed by update events. To emit the initial Task immediately, call `emit.(task)` early
in your executor before streaming status or artifact updates.
### Server (Plug)
```elixir
plug A2A.Server.REST.Plug,
executor: MyApp.A2AExecutor,
task_store: {A2A.TaskStore.ETS, name: MyApp.A2ATaskStore}
plug A2A.Server.AgentCardPlug,
card: MyApp.AgentCard.build(),
legacy_path: "/.well-known/agent.json"
```
### Router helpers (Plug/Phoenix)
```elixir
defmodule MyApp.Router do
use Plug.Router
import A2A.Server.Router
rest "/v1", executor: MyApp.A2AExecutor
jsonrpc "/rpc", executor: MyApp.A2AExecutor
end
```
### Push notifications
```elixir
A2A.Server.Push.deliver(config, task,
security: [
strict: true,
allowed_hosts: ["example.com"],
replay_protection: true,
signing_key: System.fetch_env!("PUSH_SIGNING_KEY")
]
)
```
## Examples
- `examples/helloworld_server.exs`
- `examples/helloworld_client.exs`
- `examples/rest_server.exs`
- `examples/rest_client.exs`
- `examples/jsonrpc_server.exs`
- `examples/jsonrpc_client.exs`
- `examples/e2e_protocol_suite.exs` (real transport E2E protocol smoke suite)
- `examples/e2e_elixir_client_js_server.exs` (Elixir client -> JS SDK server)
- `examples/e2e_js_client_elixir_server.mjs` (JS SDK client -> Elixir server)
### E2E protocol smoke suite
Run:
```bash
elixir examples/e2e_protocol_suite.exs
```
This script starts real local servers and validates, end-to-end:
- REST + JSON-RPC discovery and core task/message flows.
- Streaming (`message:stream`) event delivery on both transports.
- Task lifecycle operations (`get`, `list`, `cancel`).
- Push notification lifecycle (`set/get/list/delete`) with real webhook delivery and token checks.
- JSON-RPC required extension enforcement and positive/negative extension-header paths.
### Cross-language E2E scripts (official JS SDK)
These scripts use a pinned JS harness at `examples/js_sdk_e2e/package.json` with
`@a2a-js/sdk` version `0.3.10`.
Run Elixir client against JS SDK server:
```bash
elixir examples/e2e_elixir_client_js_server.exs
```
Covers:
- discovery and interface advertisement checks
- auth challenge/retry (401 + `WWW-Authenticate`)
- REST + JSON-RPC `send_message`
- JSON-RPC streaming assertion
- transport path checks against official JS SDK server mounts
Run JS SDK client against Elixir server:
```bash
node examples/e2e_js_client_elixir_server.mjs
```
Covers:
- discovery and transport negotiation via `ClientFactory`
- auth challenge/retry using `createAuthenticatingFetchWithRetry`
- JSON-RPC `sendMessage` + `sendMessageStream` + push config lifecycle via JS SDK
- REST `sendMessage` via JS SDK against Elixir `wire_format: :proto_json` compatibility mode
- REST `message:stream` + push config lifecycle using authenticated raw transport calls
Note: some JS SDK REST stream/push paths still have shape/decoding inconsistencies in this interop setup,
so the suite validates those REST endpoints with raw authenticated HTTP calls while keeping SDK coverage
for JSON-RPC and REST `sendMessage`.
## Security notes
- Push notifications should use HTTPS webhooks and SSRF protections such as allowlists
or URL validation before delivery.
- Reject redirects to non-HTTPS URLs and enforce request timeouts/size limits.
- Validate resolved IPs (block localhost, private ranges, and metadata endpoints).
- Resolve DNS per request to reduce DNS rebinding risk.
- Use replay protection for webhook payloads (timestamps, nonces/jti, exp windows).
- Verify webhook auth headers or `X-A2A-Notification-Token` when configured.
- If agent cards include signatures, enable verification with `verify_signatures: true`
and provide a `signature_verifier` callback.
- Signature verification is application-provided; A2A_EX does not implement JWS/JCS itself.
## Documentation
HexDocs: <https://hexdocs.pm/a2a_ex/0.1.0>
Generate docs locally:
```bash
mix docs
```
## Development
```bash
mix format --check-formatted
mix test
mix dialyzer
```
## License
Apache 2.0. See `LICENSE`.