Packages

A Spark.Dsl.Extension that exposes Ash.Resource/Ash.Domain actions as A2A protocol agent skills, compiling a verified AgentCard and dispatching inbound A2A messages to Ash actions.

Current section

Files

Jump to
ash_a2a README.md
Raw

README.md

# AshA2A

[![Hex Version](https://img.shields.io/hexpm/v/ash_a2a.svg)](https://hex.pm/packages/ash_a2a)
[![HexDocs](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/ash_a2a/)
[![CI](https://github.com/seanchatmangpt/ash_a2a/actions/workflows/ci.yml/badge.svg)](https://github.com/seanchatmangpt/ash_a2a/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

A `Spark.Dsl.Extension` that exposes `Ash.Resource`/`Ash.Domain` actions as
[A2A protocol](https://github.com/a2aproject/A2A) agent skills, built on the
[`:a2a` Elixir SDK](https://github.com/actioncard/a2a-elixir) (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.Message` to the right Ash action
  (`AshA2A.Dispatcher.dispatch/6`; arities `/3`–`/5` remain valid via
  defaults), either as a bare function call or
  through a real supervised `A2A.Agent` process (`AshA2A.Agent`) — routing
  consequence-bearing skills (`:change`/`:external_do`) through the
  receipted `AshA2A.CommandBus` with authority admission and replay-safe
  receipts,
- and carries the HILT work-order binding plane (`AshA2A.Hilt.WorkOrder`):
  `bind_command/2` places the work order's content-addressed identity into
  command metadata for `CommandBus` to verify before admission, and the
  carried `graph_digest` is pinned against the executing command's semantic
  subject (`checkpoint_graph_digest/2`) — drift refuses `:stale_graph_identity`.

## Requirements

- Elixir `~> 1.19` (developed and tested on Elixir 1.20.4 / OTP 29.1.1; CI
  installs exactly `.tool-versions` via `erlef/setup-beam` with
  `version-type: strict`, and the swarm Docker image pins the same pair).
- 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_path` at your own build (see
  [Configuration](docs/reference/configuration.md)).

## Installation

```elixir
def deps do
  [
    {:ash_a2a, "~> 26.9.31"}
  ]
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):

```elixir
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):

```elixir
message = A2A.Message.new_user([A2A.Part.Data.new(%{})])

{:reply, [%A2A.Part.Data{data: %{results: []}}]} =
  AshA2A.Dispatcher.dispatch(:echo, message, MyApp.Echo)
```

`dispatch/6` takes further `history`, `auth_identity`, and `opts` arguments
(`history`/`auth_identity` default to `nil`-able values, `opts` to `[]` —
e.g. `:resolved_skill` carries the exact resolved skill through a
`CommandBus` re-dispatch, fixing multi-resource namesake
`:capability_mismatch` refusals). `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](docs/how-to/authenticate-agent-requests.md).

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](docs/tutorials/getting-started.md); for the exact
wire contract, see the
[A2A endpoint reference](docs/reference/a2a-endpoint-contract.md). Beyond the
vendored SDK plug, the library ships its own
`AshA2A.A2ATransport.Plug` — a drop-in wrapper implementing the methods the
vendored plug refuses: supervised `message/stream` fan-out with
`tasks/resubscribe` replay, push-notification config RPCs with signed
webhook delivery, and the authenticated extended card (same 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:

```sh
cd native/hddl_cli && cargo build --release --locked && cd -
```

- `native/hddl_cli` is 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 test` reports 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/password `postgres`, db
  `ash_a2a_test`, per `config/test.exs`) is needed by the real Oban
  delivery qualification tests.
- `mix test` (no args) is the fast-iteration default — it excludes the
  `:serial`-tagged tail (see the how-to guide below). The canonical
  full-suite invocation, and what CI runs, is `mix test.all --max-cases 6`
  (the suite spawns `:peer` nodes and subprocesses; higher parallelism
  trips port-bind races). Known-flaky tests are documented in the
  [CHANGELOG](CHANGELOG.md).

Full detail: [Testing ash_a2a (your app and this
repo)](docs/how-to/test-your-ash_a2a-app.md). Both `target/` directories are
gitignored build artifacts and are never committed.

## Documentation

This project follows the [Diataxis](https://diataxis.fr/) documentation
framework: tutorials for learning, how-to guides for specific tasks,
reference for lookup, and explanation for understanding. It is published on
[HexDocs](https://hexdocs.pm/ash_a2a/) and buildable locally with `mix docs`.

- **Tutorials** — [Getting Started](docs/tutorials/getting-started.md): 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](docs/how-to/authenticate-agent-requests.md)
    — wire `A2A.Plug.Auth` so a verified credential becomes
    `context.actor`/`context.tenant`, and grants — not authentication —
    decide consequential authority.
  - [Verify authority on async (Oban) paths](docs/how-to/verify-authority-on-async-paths.md)
    — re-verify grants in your own Oban workers with
    `ObanAuthority.verify_live!/3` so a revoked grant cannot actuate from a
    stale queue payload.
  - [Observe dispatch with OCEL](docs/how-to/observe-dispatch-with-ocel.md)
    — forward every dispatch as an OCEL v2 event to a process-mining
    ingest endpoint.
  - [Use role-based LLM resolution](docs/how-to/use-role-based-llm-resolution.md)
    — resolve LLM-backed actions through abstract roles instead of
    hardcoded provider strings.
  - [Enable semantic requests](docs/how-to/enable-semantic-requests.md) —
    opt a resource and caller into the semantic-compilation pipeline
    (`AshA2A.Semantic.Compiler`).
  - [Test your ash_a2a app](docs/how-to/test-your-ash_a2a-app.md) — run
    commands, native prerequisites, test taxonomy, known flakiness.
  - [Test governed actions](docs/how-to/test-governed-actions.md) — dispatch
    through the real CommandBus with an in-process broker and store, no mocks.
  - [Migrate legacy to strict](docs/how-to/migrate-legacy-to-strict.md) — the
    seven behavior changes, what breaks, and the fix for each.
- **Reference**:
  - [Module index](docs/reference/index.md)
  - [DSL reference](docs/reference/dsl.md) — the `a2a` section, `skill`
    entity, `hddl_operator`, and `semantic_requests` gate.
  - [Configuration](docs/reference/configuration.md) — every application
    config key and environment variable the library reads.
  - [Telemetry events](docs/reference/telemetry.md) — the event catalog
    with payloads.
  - [Mix tasks](docs/reference/mix-tasks.md) — the 15 shipped tasks.
  - [A2A endpoint contract](docs/reference/a2a-endpoint-contract.md) —
    served HTTP surface: agent card, JSON-RPC methods, error codes,
    streaming, auth.
  - [A2A spec version mapping](docs/reference/a2a-spec-version-mapping.md)
    — which A2A protocol spec version this library targets and how its
    JSON-RPC methods map onto it.
- **Explanation**:
  - [Architecture](docs/explanation/architecture.md) — the capability
    projection, admission/receipt layers, adapters, and consequence
    semantics.
  - [Message lifecycle](docs/explanation/message-lifecycle.md) — one
    request end to end, from wire to receipt.
  - [Canonical graph identity](docs/explanation/canonical-graph-identity.md)
    and [GraphLaw WASM integration](docs/explanation/graphlaw-wasm-integration.md).
- **Control plane & conformance** (shipped with the repo): [C2 certificate
  model](docs/reference/c2-certificate.md),
  [C2 wire interop](docs/reference/c2-wire-interop.md) (external
  authority/actuator projects, AuthZEN PDP policy evidence),
  [conformance claim](docs/reference/conformance-claim.md) and
  [profiles](docs/reference/conformance-profiles.md),
  [C2 compromise court](docs/reference/c2-compromise-court.md) (repo-only),
  [GALL closure ownership](docs/explanation/closure-implementations.md) (repo-only),
  the [approver app](docs/how-to/approver-apps.md) (repo-only),
  and the [release path](docs/how-to/release.md) (repo-only).

### Internal evidence and reports (not user documentation)

These artifacts are deliberately published with the repository but are
point-in-time engineering records, not guides. As of v26.9.31 the audit-era
records live under `docs/archive/`, grouped by kind:

- `docs/archive/reports/` — measured evidence: the v26.9.17 hardening/
  benchmark/stress pass (`chicago-benchmark-report.md`,
  `v26.9.17-{stress-report,hardening-audit,commandbus-scale}.md`,
  `sa2a-v26-9-17-{capability-coverage-sweep,hddl-reachability-analysis}.md`,
  referenced from the CHANGELOG), the `partisan-integration-investigation.md`
  spike note, and the security-posture reports gathered against the `k8s/`
  swarm manifests (`AIRGAP_READINESS_REPORT.md`, `ENTERPRISE_READINESS_REPORT.md`,
  `SSP_CONTROL_APPENDIX.md`; kind-cluster scope, 2026-09-15; explicitly not
  an ATO).
- `docs/archive/jira/` — RFC-style tickets, checkpoints, and ARD/PRD pairs
  from v26.9.11 through v26.9.18's development.
- `docs/archive/session-history/` — `MANUFACTURING_RECEIPT.md` and
  `litho.docs/`, internal session history and manufacturing records.
- `DOCS_AUDIT_v26.9.21.md` (repo root) — the accounting of this
  documentation audit itself: every file kept/updated/archived, and why.
- `docs/explanation/chicago-conformance-court.md` — what the RFC-SA2A-002
  conformance court is (kept in place: a durable explanation, not a
  point-in-time record).
- `docs/rfc/` — RFC-SA2A-001–007 (Proposed Standard status; kept in place —
  001/002 date from v26.9.16, 003–007 from v26.9.28, plus the
  `adoption/SPIFFE_ABSORPTION.md` record and the v26.9.26 ABB/SBB
  implementation notes).
- `research/` — kept in place, outside `docs/archive/`.

## 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. Optional
absorbed boundaries extend that identity system: SPIFFE workload-attested
identities (`AshA2A.SPIFFE.*`) and OpenID AuthZEN PDP policy evidence
(`AshA2A.AuthZEN.*`) bind external decisions in as evidence — never as
authority. See [the authentication how-to](docs/how-to/authenticate-agent-requests.md)
and [SECURITY.md](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](CHANGELOG.md) — note that `main` routinely runs ahead of the
latest published Hex release.

## License

MIT — see [LICENSE](LICENSE).