Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
# protoc is bundled via the `protoc-bin-vendored` crate (see the
# `grpc` build scripts in crates/a2a-client and crates/a2a-server),
# `grpc` build scripts in crates/a2a-protocol-client and crates/a2a-protocol-server),
# so no system protoc install is needed. Contributors building
# `--features grpc` on a clean machine get the same behaviour.
- name: Clippy (default features)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1

# protoc is bundled via the `protoc-bin-vendored` crate (see the
# grpc build scripts in crates/a2a-client and crates/a2a-server).
# grpc build scripts in crates/a2a-protocol-client and crates/a2a-protocol-server).

- name: Install cargo-llvm-cov
run: cargo install cargo-llvm-cov --locked
Expand Down
62 changes: 30 additions & 32 deletions .github/workflows/mutants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# minutes when run sequentially.
# - A lightweight aggregation job merges per-crate reports and produces the
# combined score.
# - Incremental mode (PRs) tests only changed files in a single job.
# - Incremental mode (PRs) mutates only PR-diff changes (--in-diff) in one job.
#
# CI output is optimised for readability:
# - cargo-mutants raw output is captured to a file (not streamed to the log)
Expand All @@ -25,8 +25,9 @@ name: Mutation Testing

on:
# PRs run the **incremental** job (`mutants-incremental` below): only the
# Rust source files changed in the PR are mutated, which fits inside the
# per-job timeout without needing a full sweep. The full sweep across
# source lines changed in the PR diff are mutated (cargo-mutants `--in-diff`),
# which fits inside the per-job timeout without needing a full sweep. The
# full sweep across
# every crate (`mutants-crate`) is gated on the matrix itself (`if:
# github.event_name != 'pull_request'`) so incremental runs on PRs and
# the full sweep runs on schedule / workflow_dispatch.
Expand Down Expand Up @@ -404,43 +405,40 @@ jobs:
- name: Install cargo-mutants
run: cargo install cargo-mutants

- name: Determine changed files
id: changed
- name: Build PR source diff (rename-aware)
id: diff
run: |
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'crates/*/src/**/*.rs')
FILE_COUNT=$(echo "$FILES" | grep -c . 2>/dev/null || echo 0)

if [ -z "$FILES" ] || [ "$FILE_COUNT" -eq 0 ]; then
BASE="origin/${{ github.base_ref }}"
# `-M` so a pure file/directory rename carries no content hunks.
# Without it, a large rename (e.g. crates/a2a-* -> crates/a2a-protocol-*)
# looks like thousands of brand-new lines and balloons the run to the
# entire mutant set, blowing the timeout. We scope to library source.
git diff -M "${BASE}...HEAD" -- 'crates/*/src/**/*.rs' > pr-src.diff
ADDED=$(grep -cE '^\+[^+]' pr-src.diff || true)

if [ "${ADDED:-0}" -eq 0 ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "No Rust source files changed in crates/ — mutation testing will be skipped."
echo "No added/changed lines in crates/*/srcnothing to mutate (renames carry no content)."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
# Convert to comma-separated for later parsing
echo "files=$(echo "$FILES" | tr '\n' ',')" >> "$GITHUB_OUTPUT"
echo ""
echo "Changed files to mutate (${FILE_COUNT}):"
echo "$FILES" | sed 's/^/ /'
echo "Changed source lines to mutate: ${ADDED}"
echo "::group::PR source diff"
cat pr-src.diff
echo "::endgroup::"
fi

- name: Run mutation tests on changed files
- name: Run incremental mutation testing
id: mutants
if: steps.changed.outputs.skip == 'false'
if: steps.diff.outputs.skip == 'false'
run: |
echo "::group::cargo-mutants execution log"

IFS=',' read -ra FILE_ARRAY <<< "${{ steps.changed.outputs.files }}"
GLOB_ARGS=""
for f in "${FILE_ARRAY[@]}"; do
if [ -n "$f" ]; then
GLOB_ARGS="$GLOB_ARGS --file $f"
fi
done

set +e
# --timeout matches `cap_timeout` in mutants.toml so trivial
# accessors / Debug impls aren't reported as TIMEOUT on slower
# GitHub runners; see the lint in mutants.toml for context.
cargo mutants $GLOB_ARGS \
# `--in-diff` restricts mutants to functions whose lines were added or
# changed in this PR's diff, so renames contribute nothing and only
# genuinely edited code is mutated. --timeout matches `cap_timeout`
# in mutants.toml (see the lint there for context).
cargo mutants --in-diff pr-src.diff \
--output mutants.out \
--timeout 300 \
--jobs 4 \
Expand All @@ -452,14 +450,14 @@ jobs:
echo "exit_code=${MUTANTS_EXIT}" >> "$GITHUB_OUTPUT"

- name: No source changes to mutate
if: steps.changed.outputs.skip == 'true'
if: steps.diff.outputs.skip == 'true'
run: |
echo "# Mutation Testing — Skipped" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No Rust source files changed in \`crates/*/src/\` — nothing to mutate." >> "$GITHUB_STEP_SUMMARY"

- name: Generate mutation report
if: always() && steps.changed.outputs.skip == 'false'
if: always() && steps.diff.outputs.skip == 'false'
run: |
count() { if [ -f "$1" ]; then grep -c . "$1" 2>/dev/null || echo 0; else echo 0; fi; }

Expand Down Expand Up @@ -569,7 +567,7 @@ jobs:
fi

- name: Upload mutation report
if: always() && steps.changed.outputs.skip == 'false'
if: always() && steps.diff.outputs.skip == 'false'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mutation-report-incremental
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
FAILED=0

echo "::group::Version consistency"
for toml in crates/a2a-types/Cargo.toml crates/a2a-client/Cargo.toml crates/a2a-server/Cargo.toml crates/a2a-sdk/Cargo.toml; do
for toml in crates/a2a-protocol-types/Cargo.toml crates/a2a-protocol-client/Cargo.toml crates/a2a-protocol-server/Cargo.toml crates/a2a-protocol-sdk/Cargo.toml; do
CARGO_VER=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)".*/\1/')
if [[ "$CARGO_VER" != "$TAG_VER" ]]; then
echo "::error file=$toml::$toml version ($CARGO_VER) != tag ($TAG_VER)."
Expand Down
7 changes: 4 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@
# Copyright 2026 Tom F. <tomf@tomtomtech.net> (https://github.com/tomtom215)

cff-version: 1.2.0
title: "a2a-rust: A2A Protocol SDK for Rust"
title: "a2a-rust: Agent2Agent (A2A) Protocol SDK for Rust"
message: "If you use this software, please cite it using the metadata from this file."
type: software
authors:
- name: "Tom F."
email: "tomf@tomtomtech.net"
alias: "tomtom215"
repository-code: "https://github.com/tomtom215/a2a-rust"
url: "https://github.com/tomtom215/a2a-rust"
url: "https://a2a-rust.com"
license: Apache-2.0
version: "0.5.1"
date-released: "2026-04-15"
keywords:
- a2a
- agent2agent
- agent-to-agent
- rust
- sdk
- protocol
- json-rpc
- grpc
abstract: >-
A complete Rust implementation of the Agent-to-Agent (A2A) protocol v1.0,
A complete Rust implementation of the Agent2Agent (A2A) protocol v1.0,
providing types, server framework, and client library for building
interoperable AI agent systems.
12 changes: 6 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ Before adding a dependency:
|---|---|---|
| Unit tests | `#[cfg(test)]` modules in source files | `cargo test --workspace` |
| Integration tests | `crates/*/tests/` | included in workspace test |
| TCK conformance | `crates/a2a-types/tests/tck_wire_format.rs` | `cargo test -p a2a-protocol-types --test tck_wire_format` |
| Property-based tests | `crates/a2a-types/tests/proptest_types.rs` | `cargo test -p a2a-protocol-types --test proptest_types` |
| Corpus-based JSON tests | `crates/a2a-types/tests/corpus_json.rs` | `cargo test -p a2a-protocol-types --test corpus_json` |
| TCK conformance | `crates/a2a-protocol-types/tests/tck_wire_format.rs` | `cargo test -p a2a-protocol-types --test tck_wire_format` |
| Property-based tests | `crates/a2a-protocol-types/tests/proptest_types.rs` | `cargo test -p a2a-protocol-types --test proptest_types` |
| Corpus-based JSON tests | `crates/a2a-protocol-types/tests/corpus_json.rs` | `cargo test -p a2a-protocol-types --test corpus_json` |
| Mutation tests | `mutants.toml` (workspace root) | `cargo mutants --workspace` |
| End-to-end examples | `examples/echo-agent`, `examples/agent-team`, `examples/multi-lang-team`, `examples/rig-agent`, `examples/genai-agent` | `cargo run -p echo-agent` |
| Benchmarks | `crates/*/benches/` | `cargo bench` |
Expand Down Expand Up @@ -136,7 +136,7 @@ cargo mutants --workspace
cargo mutants -p a2a-protocol-types

# Run on a specific file
cargo mutants --file crates/a2a-types/src/task.rs
cargo mutants --file crates/a2a-protocol-types/src/task.rs

# List mutants without running (dry-run)
cargo mutants --list --workspace
Expand Down Expand Up @@ -165,7 +165,7 @@ Examples:

### Property-Based Tests (`proptest`)

Located in `crates/a2a-types/tests/proptest_types.rs`. These verify invariants
Located in `crates/a2a-protocol-types/tests/proptest_types.rs`. These verify invariants
that must hold for all possible inputs:

- **TaskState** — round-trip, terminal classification, wire format prefix
Expand All @@ -174,7 +174,7 @@ that must hold for all possible inputs:

### Corpus-Based JSON Tests

Located in `crates/a2a-types/tests/corpus_json.rs`. Each test deserializes a
Located in `crates/a2a-protocol-types/tests/corpus_json.rs`. Each test deserializes a
representative JSON sample matching the A2A v1.0 wire format and verifies
`deserialize → serialize → deserialize` round-trip fidelity. Covers:

Expand Down
12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
[workspace]
resolver = "2"
members = [
"crates/a2a-types",
"crates/a2a-client",
"crates/a2a-server",
"crates/a2a-sdk",
"crates/a2a-protocol-types",
"crates/a2a-protocol-client",
"crates/a2a-protocol-server",
"crates/a2a-protocol-sdk",
"examples/echo-agent",
"examples/agent-team",
"examples/multi-lang-team",
Expand All @@ -23,9 +23,9 @@ rust-version = "1.93"
license = "Apache-2.0"
authors = ["Tom F."]
repository = "https://github.com/tomtom215/a2a-rust"
homepage = "https://github.com/tomtom215/a2a-rust"
homepage = "https://a2a-rust.com"
documentation = "https://docs.rs/a2a-protocol-sdk"
keywords = ["a2a", "agent", "protocol", "sdk", "ai"]
keywords = ["a2a", "agent2agent", "agent", "protocol", "ai"]
categories = ["network-programming", "web-programming", "api-bindings"]

[workspace.dependencies]
Expand Down
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
<!-- Copyright 2026 Tom F. <tomf@tomtomtech.net> (https://github.com/tomtom215) -->

# a2a-rust
<p align="center">
<a href="https://a2a-rust.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="book/static/brand/og-card-editorial-dark.png">
<img alt="a2a-rust — Agent2Agent (A2A) Protocol SDK for Rust" src="book/static/brand/og-card-editorial-light.png" width="840">
</picture>
</a>
</p>

# a2a-rust — Agent2Agent (A2A) Protocol SDK for Rust

[![CI](https://github.com/tomtom215/a2a-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/tomtom215/a2a-rust/actions/workflows/ci.yml)
[![TCK](https://github.com/tomtom215/a2a-rust/actions/workflows/tck.yml/badge.svg)](https://github.com/tomtom215/a2a-rust/actions/workflows/tck.yml)
[![codecov](https://codecov.io/gh/tomtom215/a2a-rust/graph/badge.svg)](https://codecov.io/gh/tomtom215/a2a-rust)
[![Crates.io](https://img.shields.io/crates/v/a2a-protocol-sdk.svg)](https://crates.io/crates/a2a-protocol-sdk)
[![docs.rs](https://img.shields.io/docsrs/a2a-protocol-sdk)](https://docs.rs/a2a-protocol-sdk)
[![Guide](https://img.shields.io/badge/guide-a2a--rust.com-blue)](https://a2a-rust.com)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![MSRV](https://img.shields.io/badge/rust-1.93%2B-orange.svg)](https://www.rust-lang.org)
[![A2A Conformance](https://img.shields.io/badge/A2A%20v1.0-TCK%20conformant-brightgreen)](tck/)

Pure Rust implementation of the [A2A (Agent-to-Agent) protocol](https://google.github.io/A2A/) v1.0.0.
Pure Rust implementation of the [**Agent2Agent (A2A) protocol**](https://a2a-protocol.org/), built to the final **v1.0.0** wire specification — the open, vendor-neutral standard for AI-agent interoperability.

Build, connect, and orchestrate AI agents using a type-safe, async-first SDK with both JSON-RPC 2.0 and REST transport bindings.
Build, connect, and orchestrate AI agents with a type-safe, async-first SDK spanning four transports — JSON-RPC 2.0, REST, WebSocket, and gRPC — for both client and server.

## Motivation

Expand Down Expand Up @@ -90,10 +100,10 @@ This project aims to be the first **v1.0.0-compliant** Rust SDK for A2A. We inte

| Crate | Purpose | When to Use |
|---|---|---|
| [`a2a-protocol-types`](crates/a2a-types) | All A2A wire types — `serde` only, no I/O | You need types without the HTTP stack |
| [`a2a-protocol-client`](crates/a2a-client) | HTTP client for A2A requests | Building an orchestrator, gateway, or test harness |
| [`a2a-protocol-server`](crates/a2a-server) | Server framework for A2A agents | Building an agent that handles A2A requests |
| [`a2a-protocol-sdk`](crates/a2a-sdk) | Umbrella re-export + prelude | Quick-start / full-stack usage |
| [`a2a-protocol-types`](crates/a2a-protocol-types) | All A2A wire types — `serde` only, no I/O | You need types without the HTTP stack |
| [`a2a-protocol-client`](crates/a2a-protocol-client) | HTTP client for A2A requests | Building an orchestrator, gateway, or test harness |
| [`a2a-protocol-server`](crates/a2a-protocol-server) | Server framework for A2A agents | Building an agent that handles A2A requests |
| [`a2a-protocol-sdk`](crates/a2a-protocol-sdk) | Umbrella re-export + prelude | Quick-start / full-stack usage |

`a2a-protocol-client` and `a2a-protocol-server` are **siblings** — neither depends on the other. Use only what you need.

Expand Down
8 changes: 4 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ Publishing must happen in this order (each crate depends on the ones above it):
git checkout -b release/vX.Y.Z main

# Update version in all 4 crate Cargo.toml files (must all match)
# crates/a2a-types/Cargo.toml
# crates/a2a-client/Cargo.toml
# crates/a2a-server/Cargo.toml
# crates/a2a-sdk/Cargo.toml
# crates/a2a-protocol-types/Cargo.toml
# crates/a2a-protocol-client/Cargo.toml
# crates/a2a-protocol-server/Cargo.toml
# crates/a2a-protocol-sdk/Cargo.toml

# Update CHANGELOG.md: move [Unreleased] content to [X.Y.Z] with date
# Add new empty [Unreleased] section
Expand Down
6 changes: 3 additions & 3 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ authors.workspace = true

[dependencies]
# SDK crates under test
a2a-protocol-types = { path = "../crates/a2a-types" }
a2a-protocol-client = { path = "../crates/a2a-client" }
a2a-protocol-server = { path = "../crates/a2a-server" }
a2a-protocol-types = { path = "../crates/a2a-protocol-types" }
a2a-protocol-client = { path = "../crates/a2a-protocol-client" }
a2a-protocol-server = { path = "../crates/a2a-protocol-server" }

# Benchmark harness
criterion = { workspace = true, features = ["html_reports", "async_tokio"] }
Expand Down
2 changes: 1 addition & 1 deletion book/book.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[book]
title = "a2a-rust"
description = "Official Rust SDK for the A2A (Agent-to-Agent) protocol — build interoperable AI agents with type-safe, async Rust"
description = "Rust SDK for the Agent2Agent (A2A) protocol — build interoperable AI agents with type-safe, async Rust"
authors = ["Tom F."]
language = "en"
src = "src"
Expand Down
2 changes: 1 addition & 1 deletion book/src/concepts/protocol-overview.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Protocol Overview

The A2A (Agent-to-Agent) protocol defines how AI agents discover each other, exchange messages, manage task lifecycles, and stream results. This page covers the conceptual model — the "what" before the "how."
The Agent2Agent (A2A) protocol defines how AI agents discover each other, exchange messages, manage task lifecycles, and stream results. This page covers the conceptual model — the "what" before the "how."

## The Big Picture

Expand Down
2 changes: 1 addition & 1 deletion book/src/deployment/dogfooding-bugs.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ Auth interceptor headers were applied to JSON-RPC and REST HTTP requests but not

**Fix (initial):** Added `validate_webhook_url_with_dns()` that resolves DNS before IP validation and checks every resolved IP against the private/loopback ranges.

**Fix (hardening, 2026-04-15):** The initial fix validated a set of IPs but still allowed the HTTP client to re-resolve the hostname on connect, leaving a narrow TOCTOU window. `validate_webhook_url_with_dns()` now returns the specific `SocketAddr` it validated, and `HttpPushSender::send()` rewrites the outgoing URI so the request connects directly to the literal pinned IP (`http://<validated-ip>:<port>/…`) with the original hostname preserved via an explicit `Host:` header. This closes the window: the HTTP client sees an IP literal and never re-enters DNS resolution, so a rebinding attacker cannot flip the record between validation and connect. See `crates/a2a-server/src/push/sender.rs:rewrite_uri_with_pinned_addr` and the `rewrite_uri_*` / `host_header_*` tests.
**Fix (hardening, 2026-04-15):** The initial fix validated a set of IPs but still allowed the HTTP client to re-resolve the hostname on connect, leaving a narrow TOCTOU window. `validate_webhook_url_with_dns()` now returns the specific `SocketAddr` it validated, and `HttpPushSender::send()` rewrites the outgoing URI so the request connects directly to the literal pinned IP (`http://<validated-ip>:<port>/…`) with the original hostname preserved via an explicit `Host:` header. This closes the window: the HTTP client sees an IP literal and never re-enters DNS resolution, so a rebinding attacker cannot flip the record between validation and connect. See `crates/a2a-protocol-server/src/push/sender.rs:rewrite_uri_with_pinned_addr` and the `rewrite_uri_*` / `host_header_*` tests.

### Bug H7: Retry Transport Deep-Clones serde_json::Value Per Attempt

Expand Down
6 changes: 3 additions & 3 deletions book/src/deployment/dogfooding-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,9 @@ In addition to the 81 agent-team E2E tests (94 with optional features), the SDK

| Suite | Location | Tests | What it covers |
|---|---|---|---|
| **TLS/mTLS** | `crates/a2a-client/tests/tls_integration_tests.rs` | 7 | Client cert validation, SNI hostname verification, unknown CA rejection, mutual TLS |
| **WebSocket server** | `crates/a2a-server/tests/websocket_tests.rs` | 7 | Send/stream, error handling, ping/pong, connection reuse, close frames |
| **Memory & load stress** | `crates/a2a-server/tests/stress_tests.rs` | 5 | 200 concurrent requests, sustained load (500 requests/10 waves), eviction under load, multi-tenant isolation (10×50), rapid connect/disconnect |
| **TLS/mTLS** | `crates/a2a-protocol-client/tests/tls_integration_tests.rs` | 7 | Client cert validation, SNI hostname verification, unknown CA rejection, mutual TLS |
| **WebSocket server** | `crates/a2a-protocol-server/tests/websocket_tests.rs` | 7 | Send/stream, error handling, ping/pong, connection reuse, close frames |
| **Memory & load stress** | `crates/a2a-protocol-server/tests/stress_tests.rs` | 5 | 200 concurrent requests, sustained load (500 requests/10 waves), eviction under load, multi-tenant isolation (10×50), rapid connect/disconnect |

## Features NOT Covered by E2E Tests

Expand Down
Loading
Loading