A2A.Plug (A2A v0.3.0)

Copy Markdown View Source

Plug for serving A2A agents over HTTP.

Handles agent card discovery (GET), JSON-RPC dispatch (POST), and SSE streaming. Works standalone with Bandit or mounted inside Phoenix via forward.

Usage

# In a Phoenix router:
forward "/a2a", A2A.Plug, agent: MyAgent, base_url: "http://localhost:4000/a2a"

# Standalone with Bandit:
Bandit.start_link(plug: {A2A.Plug, agent: MyAgent, base_url: "http://localhost:4000"})

Options

  • :agent — GenServer name or pid of the agent (required)
  • :base_url — the public URL of the agent endpoint. Required unless always provided at runtime via put_base_url/2. When nil, agent card requests raise ArgumentError.
  • :agent_card_path — path segments for the agent card endpoint (default: [".well-known", "agent-card.json"]). Set to false to disable built-in agent card serving — useful when you want to serve the card from a custom route using A2A.get_agent_card/2.
  • :json_rpc_path — path segments for the JSON-RPC endpoint (default: [])
  • :agent_card_opts — keyword options forwarded to A2A.JSON.encode_agent_card/2
  • :last_modified — DateTime served as the agent card's Last-Modified header (default: DateTime.utc_now() at init/1). Under Phoenix's plug macro init/1 runs at compile time, so the default is the build time — which is accurate, since the card is itself a compile-time literal. Called directly, it is boot time. The card also carries a sha256 ETag and Cache-Control: public, max-age=300.
  • :metadata — static metadata merged into every JSON-RPC call (default: %{}). Useful for deployment-level metadata like %{"env" => "prod"}. Overridden per-request by put_metadata/2.
  • :resubscribe_timeout — how long a tasks/resubscribe stream waits without an event before closing, in ms (default: 60_000). A task that never reaches a terminal state would otherwise hold its connection process open indefinitely.
  • :authorize_task — optional authorization callback for task-scoped operations. Called as (operation, task, context) before returning, canceling, or listing tasks, and before any push notification config operation. operation is one of :get, :cancel, :list, :resubscribe, :push_set, :push_get, :push_list, or :push_delete — the push operations are distinct so an authorizer can grant read access to a task without also granting the ability to rewrite its webhooks. Denied requests return TaskNotFoundError so task IDs are not leaked.
  • :extensions — list of A2A.Extension modules (or {module, opts} tuples) declaring protocol extensions this server supports. Required extensions are validated against the client's A2A-Extensions request header; missing required extensions return ExtensionSupportRequiredError (-32008). The server sets the A2A-Extensions response header to the URIs that were activated. Declarations are merged into capabilities.extensions on the served agent card.
  • :versions — list of supported A2A protocol versions as Major.Minor strings (default: A2A.Version.supported_default/0, currently ["0.3", "1.0"]). The client's A2A-Version header is normalized to Major.Minor and validated against this list; unsupported versions return VersionNotSupportedError (-32009). Missing/empty headers are treated as "0.3" (spec §3.6.2). The negotiated version is echoed in the A2A-Version response header.

Per-Request Overrides

Use put_base_url/2 and put_metadata/2 in an upstream plug or Phoenix pipeline to set per-request values. These are stored in conn.private[:a2a] following the Ash/Absinthe convention.

plug :set_tenant_a2a

defp set_tenant_a2a(conn, _opts) do
  conn
  |> A2A.Plug.put_base_url("https://#{conn.host}/a2a")
  |> A2A.Plug.put_metadata(%{"tenant_id" => conn.assigns.tenant_id})
end

Metadata Merge Order

Metadata is merged in three layers (later wins):

  1. :metadata from init/1 (static defaults)
  2. put_metadata/2 on conn (per-request)
  3. "metadata" from JSON-RPC params (per-call from client)

Summary

Functions

Returns the per-request base URL, or nil if not set.

Returns the per-request metadata, or nil if not set.

Stores a per-request base URL in conn.private[:a2a].

Stores per-request metadata in conn.private[:a2a].

Functions

get_base_url(conn)

@spec get_base_url(Plug.Conn.t()) :: String.t() | nil

Returns the per-request base URL, or nil if not set.

get_metadata(conn)

@spec get_metadata(Plug.Conn.t()) :: map() | nil

Returns the per-request metadata, or nil if not set.

put_base_url(conn, url)

@spec put_base_url(Plug.Conn.t(), String.t()) :: Plug.Conn.t()

Stores a per-request base URL in conn.private[:a2a].

Use this in an upstream plug or Phoenix pipeline to override the base_url configured at init time.

put_metadata(conn, metadata)

@spec put_metadata(Plug.Conn.t(), map()) :: Plug.Conn.t()

Stores per-request metadata in conn.private[:a2a].

This metadata is merged between the init-time :metadata and the per-call JSON-RPC "metadata" field.