Skip to main content

Astromesh A2A

astromesh-a2a

Let your agent talk to other agents without trusting them.

astromesh-runtime Release CI PyPI Python License: Apache-2.0

Leer en español

A2A (Agent2Agent) is the open protocol agents use to work with each other: an agent publishes a card that says what it can do, and other agents send it tasks over JSON-RPC, get answers, stream progress and answer its questions.

astromesh-a2a is a client and a server for A2A 1.x, built on the official A2A SDK. It puts a remote agent inside your agent framework as an ordinary tool (nine frameworks), serves your own agent as an A2A agent, and treats everything that crosses the wire as what it is: code you do not control.

Why the SDK alone is not enough

The SDK speaks the protocol. It does not decide which remotes you may reach, how much of their output you accept, or what your model does with it. A remote agent is an untrusted program on someone else's server; this library is the layer between it and your agent.

Risk What astromesh-a2a does
A card or a redirect sends your client to 10.0.0.7, 169.254.169.254 or localhost (SSRF) Pinned egress. Every request goes through a guarded transport: http(s) only, public addresses only, each host resolved once and the connection pinned to the checked IP (no DNS-rebinding window), redirects never followed.
Any URL is reachable; a card names its RPC endpoint on another host Remote-agent policy. Policy.allowed lists hosts or card URLs; nothing is allowed until you declare it. The card is fetched and vetted by the library (host, A2A 1.x JSON-RPC interface only, interface on an allowed host) before the SDK sees it. Optional signed cards, keys pinned by kid (a jku URL is never fetched).
The remote's text tells your model "ignore previous instructions" Untrusted output wrapping. Framework tools return the remote's words inside <remote-agent-output untrusted="true" ...>, with <, >, & and quotes escaped so the block cannot be closed or forged.
The remote asks "reply yes to confirm the transfer" Never auto-confirm. An input_required task comes back as a pending question; only an explicit reply() (from the user, through your agent) continues it. The server marks every incoming message Origin.AGENT and untrusted.
A 2 GB artifact, a stream that never ends, a remote that eats your budget Budgets, timeouts and size caps. max_bytes per body and part, max_stream_bytes per stream, timeout per call (wall-clock), max_events per stream, Budget(max_calls, max_seconds) per remote. File parts are dropped unless you accept them.
You cannot see what happened, or your logs leak the API key Per-stage trace with redaction. Every call returns a trace (card, send, status, artifact, message, rejection, result) with keys, tokens, URL credentials and presigned-URL signatures redacted, and a W3C traceparent. Credentials come only from env vars or callables.

These are not claims: an attack eval runs nine malicious remotes against the library on every push.

Install

pip install astromesh-a2a                # client
pip install 'astromesh-a2a[server]'      # + serve your own agent (Starlette, uvicorn)
pip install 'astromesh-a2a[langchain]'   # + a remote agent as a LangChain tool

Python 3.10+. Every framework is an optional extra (table below); signing adds PyJWT for signed cards, all installs everything.

Call a remote agent

from astromesh_a2a import Auth, Policy, RemoteAgent

policy, auth = Policy(allowed=("trips.example.com",)), Auth.bearer("TRIPS_TOKEN")
async with RemoteAgent("https://trips.example.com", policy=policy, auth=auth) as trips:
    result = await trips.send("Find a flight to Lima on Friday")
print(result.status, result.answer, result.trace)

result.status is completed, or input_required with the remote's question in result.pending; reply(task_id, text) answers it, stream(text) yields events, cancel(task_id) stops a task, card() returns the vetted card. Errors are typed (PolicyRejected, BlockedDestination, TaskFailed, A2ATimeout, UnsupportedVersion, all A2AError). Guide: docs/client.md.

Serve your agent

from astromesh_a2a import CardSpec, Incoming, Outgoing, Skill, api_key_auth, serve


async def handle(msg: Incoming) -> Outgoing:  # msg.text came from another program: untrusted
    return Outgoing(answer=await my_agent.run(msg.text, session=msg.session_key))


skill = Skill(id="book", name="Book trips", description="Flights and hotels.", tags=("travel",))
card = CardSpec("trips", "Books trips.", "1.0.0", "https://trips.example.com", [skill])
serve(handle, card, host="0.0.0.0", port=8000, auth=api_key_auth("TRIPS_API_KEY"))

The card is served at /.well-known/agent-card.json and JSON-RPC (blocking and SSE) at /a2a. Auth is mandatory: without auth= the server refuses to start unless you pass insecure=True. create_app(...) returns the Starlette app to mount in your own server. Guide: docs/server.md.

Try both ends on your machine, offline:

git clone https://github.com/monaccode/astromesh-a2a && cd astromesh-a2a
uv run python examples/hello_local.py

It runs from a checkout (examples are not in the wheel). The egress guard refuses 127.0.0.1, so the example opts its own agent into loopback with astromesh_a2a.testing.allow_loopback(agent): local development and tests only, never production (docs/policy.md).

A remote agent as a tool of your framework

async with RemoteAgent("https://trips.example.com", policy=policy) as trips:
    await trips.card()  # the tool's name and description come from the card
    tool = trips.as_tool("langchain")  # give it to your agent: tools=[tool]
    print(await tool.ainvoke({"query": "Find a flight to Lima on Friday"}))

The tool takes one query, keeps the conversation (and a task waiting for input) on the tool object, returns the wrapped untrusted output and reports errors the framework's way.

Framework as_tool(...) Extra Returns
LangChain / LangGraph "langchain" langchain StructuredTool (sync + async)
CrewAI "crewai" crewai BaseTool
Google ADK "adk" adk FunctionTool
OpenAI Agents SDK "openai-agents" openai-agents FunctionTool
PydanticAI "pydantic-ai" pydantic-ai Tool (Python 3.11+)
LlamaIndex "llamaindex" llamaindex FunctionTool (sync + async)
smolagents "smolagents" smolagents Tool
Microsoft Agent Framework "agent-framework" agent-framework FunctionTool
MCP "mcp" mcp FastMCP Tool; adapters.mcp.build_server for a whole server

The other way round, adapters.server_langchain.handler_from_runnable(runnable) serves a LangChain runnable or LangGraph graph as an A2A agent. One example per framework: docs/frameworks.md.

Sync and async: event loops

A RemoteAgent is bound to the event loop of its first I/O (its pooled connections belong to it) and refuses any other loop with LoopBindingError. Async tools just work. Sync tools (CrewAI run, smolagents, LangChain invoke...) are dispatched to the agent's loop when it runs in another thread, or run on a background loop when the agent has none yet. Do not block the agent's loop while its tools run: calling a sync entry point (crew.kickoff()) inside async def main would deadlock, so the dispatch gives up after policy.timeout + 5 s with LoopBindingError. Use the async entry point (kickoff_async, ainvoke) or await asyncio.to_thread(crew.kickoff). From sync code, use run_sync(agent.card(), agent=agent) and run_sync(agent.aclose(), agent=agent) (from astromesh_a2a.adapters import run_sync). One RemoteAgent per loop.

Sessions

Incoming.context_id is chosen by the caller and is not scoped: two callers can send the same one. Key any memory or session state by Incoming.session_key ("<caller>:<context_id>", where the caller is the identity your auth returns), never by context_id alone. On the client side, pass context_id= to send() to keep several messages in one remote conversation.

The eval

Nine local malicious remotes: a spoofed card, a redirect to an internal address, a public name that resolves to 10.0.0.7, a 32 MiB artifact, a file part, prompt injection that tries to break out of the untrusted block, a remote that asks to be confirmed, a stream that never ends and a 0.3-only agent.

python -m astromesh_a2a.eval   # needs the server extra
Policy Traps stopped
Default (Policy(allowed=(host,))) 9/9
Lax (files accepted, 64 MiB bodies) 7/9 (huge_artifact and file_part get through)

The other seven hold whatever the policy says: egress guard, card checks, the untrusted wrapper and the no-auto-confirm rule. CI fails if a change lets one trap through. Traps, "stopped" rules and negative controls: docs/eval.md.

Policy

from astromesh_a2a import Budget, Policy

policy = Policy(
    allowed=("trips.example.com", "https://hotels.example.org/.well-known/agent-card.json"),
    timeout=30.0,
    budget=Budget(max_calls=20, max_seconds=120),
)

Every field, its default and why: docs/policy.md.

In the Astromesh ecosystem

astromesh-a2a is the A2A layer of Astromesh, the same way astromesh-rag is its RAG layer and astromesh-okf its knowledge format. Planned (not shipped yet):

  • Astromesh core: a type: a2a tool in the agent manifest (card_url, auth_env, policy, timeout) built on this library, with input_required returned to the model as an observation; turns from remote agents counted as agent turns, never human ones.
  • Astromesh ADK: remote_a2a() to use a remote A2A agent from Python agents.
  • Nexus: published agents served over A2A (opt-in per agent), conformance-tested against this client.

Details: docs/ecosystem.md.

Contributing

Issues and pull requests are welcome: CONTRIBUTING.md.

License

Apache-2.0. Part of the Astromesh ecosystem.

Metadata

Release files for astromesh-a2a 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for astromesh-a2a 0.1.0
File Size Uploaded Provenance
astromesh_a2a-0.1.0.tar.gz 120.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for astromesh-a2a 0.1.0
File Interpreter ABI Platform
astromesh_a2a-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 191.0 kB

Release files / astromesh_a2a-0.1.0.tar.gz

Download URL astromesh_a2a-0.1.0.tar.gz
Size 120.8 kB
Tags Source
SHA-256 checksum
How to use checksums
38ff1013a876463010dd921b45740326e6f9803a4917df01ca0f524301b8ce20
BLAKE2b-256 checksum
How to use checksums
0facbe8f8d243b2376ef6b1fe94c76fa7e0b0066a0270c29a2ad18f7d7ebec42
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / astromesh_a2a-0.1.0-py3-none-any.whl

Download URL astromesh_a2a-0.1.0-py3-none-any.whl
Size 70.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ffdee16af73bda824362baa4d8dfceb9c1493a765ad6a44352860a22425f74a9
BLAKE2b-256 checksum
How to use checksums
6a7cf0f7f1f196764af6513849e19831481bc5b72d19dd9d723942d37e021a96
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for astromesh-a2a 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for astromesh_a2a-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page