# `A2A.Plug`
[🔗](https://github.com/actioncard/a2a-elixir/blob/main/lib/a2a/plug.ex#L2)

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)

# `get_base_url`

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

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

# `get_metadata`

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

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

# `put_base_url`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
