AshA2A
A Spark.Dsl.Extension that exposes Ash.Resource/Ash.Domain actions as
A2A protocol agent skills, built on the
:a2a Elixir SDK (a published
Hex package, {:a2a, "~> 0.2"}, implementing A2A protocol v0.3 over
JSON-RPC 2.0/HTTP with SSE streaming).
Naming: the library and module prefix is AshA2A, the Hex package is
ash_a2a, and SA2A is the internal project codename used by the RFCs (docs/rfc/) and the conformance courts (docs/explanation/).
Every public Ash action on an extended resource or domain is projected
into a verified capability index — no declaration required. An optional
a2a do skill ... end block adds A2A-only metadata overrides, and the
compiled index:
- builds a real
A2A.AgentCard(AshA2A.Info.agent_card/2) advertising each exposed skill, - fails closed at compile time (
AshA2A.Verify) if a skill override names a nonexistent action (:REFUSED_ACTION_NOT_FOUND) or duplicates a skill name (:REFUSED_DUPLICATE_SKILL_NAME), - and dispatches an inbound
A2A.Messageto the right Ash action (AshA2A.Dispatcher.dispatch/5), either as a bare function call or through a real supervisedA2A.Agentprocess (AshA2A.Agent) — routing consequence-bearing skills (:change/:external_do) through the receiptedAshA2A.CommandBuswith authority admission and replay-safe receipts.
Requirements
- Elixir
~> 1.19with OTP 27+ (CI runs OTP 28.0 / Elixir 1.19.0;.tool-versionsand the swarm Docker image pin OTP 27.2.4). - Ash
~> 3.0. - No Rust toolchain is needed to use the published package. Rust is only
needed to build the native HDDL/FOND planner when developing this repo or
when you use the deterministic planning path — the binary is not shipped
on Hex, so point
config :ash_a2a, :hddl_cli_pathat your own build (see Configuration).
Installation
def deps do
[
{:ash_a2a, "~> 26.9"}
]
end
:a2a arrives transitively; pin {:a2a, "~> 0.2"} explicitly only if your
own code calls A2A.* modules directly.
mix ash_a2a.install (an Igniter task) wires the extension into an
existing project; see Mix.Tasks.AshA2a.Install.
Usage
Declare the DSL on a resource (or domain):
defmodule MyApp.Echo do
use Ash.Resource,
domain: MyApp.Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshA2A]
attributes do
uuid_primary_key(:id)
attribute(:message, :string, public?: true)
end
actions do
defaults([:read])
end
end
defmodule MyApp.Domain do
use Ash.Domain, extensions: [AshA2A]
resources do
resource(MyApp.Echo)
end
end
Public actions are exposed with no a2a block at all; a2a do skill(:echo, :read) end is an optional override (rename, describe, tag,
exclude, or classify). AshA2A.Transformers.BuildCapabilityIndex compiles
the residual overrides and AshA2A.Verify checks the projection fail-closed
after compilation.
Dispatch a message directly (no process):
message = A2A.Message.new_user([A2A.Part.Data.new(%{})])
{:reply, [%A2A.Part.Data{data: %{results: []}}]} =
AshA2A.Dispatcher.dispatch(:echo, message, MyApp.Echo)
dispatch/5 takes further history and auth_identity arguments
(both default to nil). auth_identity is the trust boundary: with the
default nil, context.actor/context.tenant resolve to nil and
dispatch fails closed for anything your Ash policies gate on identity —
identity only ever arrives from transport-verified auth
(A2A.Plug.Auth), never from message metadata. See
Authenticate inbound A2A requests.
To boot supervised agent processes, serve them over HTTP (agent card +
JSON-RPC + SSE), and drive them with A2A.Client, continue with the
Getting Started tutorial; for the exact
wire contract, see the
A2A endpoint reference.
Local development setup
Running this repo's own mix test requires one native Rust CLI binary to be
built first. CI (.github/workflows/ci.yml) builds it automatically (pinned
Rust 1.97.1); a local clone does not, so this is a real, one-time manual
step:
cd native/hddl_cli && cargo build --release --locked && cd -
native/hddl_cliis the sole integration surface for the real FOND/HTN planner (ferroplan) — dozens of tests (deterministic HDDL synthesis, the FreedomGym facilitator fixture, request-router phase dispatch, Chicago qualification courts) invoke this binary as a subprocess and raise a clear, actionable error (hddl_cli_not_built) if it is missing — never a silent skip. Without it,mix testreports real test failures (not skips), which is easy to mistake for a code regression.native/graphlaw_host(optional, NOT built by CI) backs the GraphLaw WASM runtime B; if left unbuilt, the one test that needs it reports a named, correctly-handled skip rather than a failure.- Postgres 16 on
localhost:55432(user/passwordpostgres, dbash_a2a_test, perconfig/test.exs) is needed by the real Oban delivery qualification tests. - The canonical full-suite invocation is
mix test --max-cases 6(the suite spawns:peernodes and subprocesses; higher parallelism trips port-bind races). 1–2 known-flaky tests are documented in the CHANGELOG under[26.9.17].
Full detail: Testing ash_a2a (your app and this
repo). Both target/ directories are
gitignored build artifacts and are never committed.
Documentation
This project follows the Diataxis documentation
framework: tutorials for learning, how-to guides for specific tasks,
reference for lookup, and explanation for understanding. It is published on
HexDocs and buildable locally with mix docs.
- Tutorials — Getting Started: a complete, end-to-end walkthrough from resource declaration through direct dispatch, a supervised agent process, and serving the agent over HTTP.
- How-to guides:
- Authenticate inbound A2A requests
— wire
A2A.Plug.Authso a verified credential becomescontext.actor/context.tenant, and grants — not authentication — decide consequential authority. - Verify authority on async (Oban) paths
— re-verify grants in your own Oban workers with
ObanAuthority.verify_live!/3so a revoked grant cannot actuate from a stale queue payload. - Observe dispatch with OCEL — forward every dispatch as an OCEL v2 event to a process-mining ingest endpoint.
- Use role-based LLM resolution — resolve LLM-backed actions through abstract roles instead of hardcoded provider strings.
- Enable semantic requests —
opt a resource and caller into the semantic-compilation pipeline
(
AshA2A.Semantic.Compiler). - Test your ash_a2a app — run commands, native prerequisites, test taxonomy, known flakiness.
- Authenticate inbound A2A requests
— wire
- Reference:
- Module index
- DSL reference — the
a2asection,skillentity,hddl_operator, andsemantic_requestsgate. - Configuration — every application config key and environment variable the library reads.
- Telemetry events — the event catalog with payloads.
- Mix tasks — the 13 shipped tasks.
- A2A endpoint contract — served HTTP surface: agent card, JSON-RPC methods, error codes, streaming, auth.
- Explanation:
- Architecture — the capability projection, admission/receipt layers, adapters, and consequence semantics.
- Message lifecycle — one request end to end, from wire to receipt.
- Canonical graph identity and GraphLaw WASM integration.
Internal evidence and reports (not user documentation)
These artifacts are deliberately published with the repository but are point-in-time engineering records, not guides:
docs/explanation/chicago-benchmark-report.md,v26.9.17-stress-report.md,v26.9.17-hardening-audit.md,sa2a-v26-9-17-capability-coverage-sweep.md,sa2a-v26-9-17-hddl-reachability-analysis.md— measured evidence from the v26.9.17 hardening/benchmark/stress pass (referenced from the CHANGELOG).docs/explanation/chicago-conformance-court.md— what the RFC-SA2A-002 conformance court is.docs/AIRGAP_READINESS_REPORT.md,docs/ENTERPRISE_READINESS_REPORT.md,docs/SSP_CONTROL_APPENDIX.md— security-posture evidence gathered against thek8s/swarm manifests (kind-cluster scope, 2026-09-15); explicitly not an ATO.docs/rfc/— RFC-SA2A-001/002 (Proposed Standard status).MANUFACTURING_RECEIPT.md,docs/jira/,litho.docs/,research/— internal session history and manufacturing records.
Security
Identity is a trust boundary: actor/tenant only ever come from
transport-verified A2A.Plug.Auth output, and consequential
(:change/:external_do) skills additionally require a standing grant from
AshA2A.Authority.Grant — authentication alone never confers authority
(RFC-SA2A-001 S29). Neither shipped broker (InMemory, Ekv) is
Sybil-resistant; bring your own identity system for production. See
the authentication how-to and
SECURITY.md.
Status
Versioning is calendar-based (26.9.x); ~> 26.9 pins within the 26.9
series. Semantic Versioning is intended after 1.0. See the
CHANGELOG — note that main routinely runs ahead of the
latest published Hex release.
License
MIT — see LICENSE.