a2a-cli 0.4.2

Standalone A2A CLI client
Documentation

a2acli

Standalone A2A CLI client built on top of a2a-client.

This crate is published as a2a-cli and installs the a2acli binary.

Install

From the workspace checkout:

cargo install --path a2acli

From crates.io after release:

cargo install a2a-cli

Verifying a download

The prebuilt archives attached to an a2a-cli-v* GitHub Release are signed with Sigstore and carry SLSA build provenance. Both are keyless: there is no long-lived key, and the certificate binds the artifact to the workflow that produced it.

Check the signature (<archive>.sigstore.json, a Sigstore bundle carrying both the signature and the signing certificate):

cosign verify-blob \
  --bundle a2acli-v0.1.11-x86_64-unknown-linux-gnu.tar.gz.sigstore.json \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/a2aproject/a2a-rs/\.github/workflows/' \
  a2acli-v0.1.11-x86_64-unknown-linux-gnu.tar.gz

Check the provenance — what built the archive, from which commit:

gh attestation verify a2acli-v0.1.11-x86_64-unknown-linux-gnu.tar.gz \
  --repo a2aproject/a2a-rs

The <archive>.sha256 file alongside them is a plain integrity checksum. It tells you the download is intact, not who produced it — only the signature and the provenance do that.

What It Provides

  • Fetch and print the public agent card for an A2A deployment, named by -a/--agent-card as a host, a full card URL, or a local file — or skipped entirely with -e/--endpoint
  • Send one-shot or streaming messages, with multi-part content (--text-part/--file-part/--data-part/--media-type)
  • Blocks by default until a task settles (terminal or interrupted state); --async returns immediately instead
  • Inspect (optionally waiting on it with --wait), list, cancel, and subscribe to tasks
  • Create, fetch, list, and delete task push notification configs
  • Request an extended agent card when the server exposes one
  • Bearer token, API key, and general service-parameter authentication on every request, with an ordered --transport preference and an --insecure escape hatch (development only, always warns) for self-signed endpoints
  • Human-readable text output by default, or the protocol's own JSON (-o/--output json); every failure is a machine-readable error object with a stable code and exit status
  • Full configuration precedence (flag > environment variable > local .env

    global .env > built-in default) with a read-only config show to inspect it

  • Shell completion scripts for bash, zsh, fish, PowerShell, and elvish via completion <shell>
  • card get --validate against the A2A JSON schema, reporting every violation with its JSON pointer path

Run

Commands are namespaced by the resource they act on (card get, task get, task push-config create, …), matching the command taxonomy in the a2a-cli specification:

cargo run --bin a2acli -- card get
cargo run --bin a2acli -- card get --extended
cargo run --bin a2acli -- send "hello from rust"
cargo run --bin a2acli -- send "hello from rust" --stream
cargo run --bin a2acli -- send "hello from rust" --async
cargo run --bin a2acli -- task get task-123
cargo run --bin a2acli -- task get task-123 --wait
cargo run --bin a2acli -- task list
cargo run --bin a2acli -- task cancel task-123
cargo run --bin a2acli -- task subscribe task-123
cargo run --bin a2acli -- task push-config list task-123
cargo run --bin a2acli -- task push-config create task-123 https://example.com/callback --auth-scheme Bearer --auth-credentials secret
cargo run --bin a2acli -- completion zsh

By default the CLI targets http://localhost:3000. Use --transport jsonrpc or --transport rest (repeatable and ordered, highest preference first) to pin the transport when the agent card exposes more than one compatible interface. The global --tenant, --bearer, --api-key, and repeated --svc-param Name:Value options also apply to task push-config commands.

Capability pre-flight

The Agent Card is the contract, so a capability-gated operation is checked against it before the call goes out (§13.3): streaming for send --stream and task subscribe, extendedAgentCard for card get --extended, and pushNotifications for every task push-config subcommand. An absent field counts as undeclared, the same way card get prints it.

send --stream against a card that does not declare streaming still returns a result — it sends without streaming and polls, with the reason on stderr — because the request was for a result, not specifically for a stream. The others have no equivalent path, so they fail with the reason instead, using the same protocol error code the agent itself would have returned.

--endpoint resolves no card at all, so there is nothing to verify and the pre-flight stands down: the agent answers for itself, as before.

Naming the agent

-a/--agent-card <ref> names the agent as an Agent Card reference, in any of three forms:

cargo run --bin a2acli -- -a agent.example.com card get      # host or origin
cargo run --bin a2acli -- -a https://agent.example.com/custom/card.json card get
cargo run --bin a2acli -- -a ./fixtures/card.json card get   # local file
cargo run --bin a2acli -- -a file:///etc/a2a/card.json card get

A bare host or origin gets the well-known path /.well-known/agent-card.json appended, and takes http:// when it is loopback (localhost, 127.0.0.1, [::1]) or https:// otherwise. A reference that already carries a path is a full card URL and is used as-is. A file:// URL or a plain filesystem path is read from disk, so the CLI can be driven with no agent running at all — a missing file reports A2ACLI_ERR_CARD_NOT_FOUND and a file that is not a card reports A2ACLI_ERR_CARD_INVALID, matching how the HTTP path classifies an unreachable card and an unparseable body.

-e/--endpoint <url> skips card resolution entirely and connects straight to an agent interface. Since no card is fetched, there is nothing to declare the protocol binding, so it requires exactly one --transport and cannot be combined with --agent-card:

cargo run --bin a2acli -- -e https://agent.example.com/jsonrpc --transport jsonrpc task get task-123

--base-url is a deprecated alias for the bare-origin form. It still works so existing invocations keep running, but it warns and is hidden from --help; prefer --agent-card, which accepts all three forms.

Authentication and transport

cargo run --bin a2acli -- --bearer "$TOKEN" card get
cargo run --bin a2acli -- --api-key "$KEY" card get
cargo run --bin a2acli -- --svc-param "X-Trace-Id:abc123" send "hello"
cargo run --bin a2acli -- --transport jsonrpc --transport rest card get
cargo run --bin a2acli -- --a2a-version 1.0 card get
cargo run --bin a2acli -- --insecure --bearer "$TOKEN" card get  # dev only; always warns
cargo run --bin a2acli -- --debug send "hello"                  # raw wire messages + diagnostics to stderr

--a2a-version pins the protocol version signaled on every request. Absent it, the version is negotiated down to the highest one both a2acli and the agent's selected card interface declare, bounded to 1.x and never below 1.0 — A2A reads an empty or pre-1.0 value as 0.3, so a version outside 1.x is refused as a usage error rather than downgraded, and any effective version other than the tool's own is named on stderr rather than applied silently.

--bearer/--api-key (env A2ACLI_BEARER/A2ACLI_API_KEY) supply credentials; --svc-param is a separate, general-purpose transport-level key-value pair, never itself a credential flag. --insecure disables TLS certificate verification and always prints a warning naming the risk when a credential is also configured — it never disables verification silently.

--debug writes developer diagnostics to stderr, including the raw protocol messages exchanged on the wire: the request and response body of every call and each streamed event. Credential values never appear there, regardless of verbosity — a credential header is shown as authorization: (redacted), so the log confirms it was attached without disclosing it. Redaction is applied where the log line is built rather than filtered afterwards, so no flag or environment variable can turn it off. Protocol metadata such as a2a-version stays visible, since version negotiation is one of the things --debug exists to explain. --tenant supplies a routing tenant when the selected Agent Card interface declares none; when the interface does declare one, A2A §8.3.2 requires that declared value to be sent exactly, so it is used and --tenant has no effect.

Blocking and polling

send blocks by default until the resulting task reaches a terminal (COMPLETED/FAILED/CANCELED/REJECTED) or interrupted (INPUT_REQUIRED/AUTH_REQUIRED) state, polling task get under the hood; pass --async to get the task identifiers back immediately instead. task get is one-shot by default — add --wait to poll it the same way. Tune the loop with --poll-interval (default 2s) and --timeout (default 30s, after which the command exits with a timeout error).

Stream resumption

task subscribe reconnects when its connection is cut before the task settles — an SSE stream held open for a long-running task will eventually be dropped by a proxy, a load balancer, or a laptop going to sleep, and a dropped connection looks identical to a finished task unless something distinguishes them. --poll-interval and --timeout apply here too: --poll-interval is the delay between reconnect attempts, and --timeout bounds the reconnection budget cumulatively across all of them — not any single attempt, and not a healthy stream that never disconnects at all. Each attempt is reported on stderr; stdout carries only the protocol events.

Reconnecting never issues a task get: the server's first event after resuming is the full current Task, which is enough to reconcile state through the protocol itself. If that reconciling event reports the same state the connection had already reached before the cut, it is suppressed rather than printed a second time — under -o json --stream the output is JSONL a consumer reads incrementally, and a duplicate Task line there would look like a second, spurious transition.

Message parts

A message can carry more than one part, built from repeatable, order-preserving flags:

cargo run --bin a2acli -- send \
  --text-part "Review this" \
  --file-part report.pdf --media-type application/pdf \
  --file-part https://example.com/spec.pdf \
  --data-part '{"priority":"high"}'

--text-part adds a text part, --file-part <path|url> a file part (a local path is inlined as base64 bytes, a URL is carried by reference and never fetched by the CLI), and --data-part <path|-> a structured JSON part read from a file, or from stdin when the value is -; anything else is parsed as an inline JSON string. --media-type sets the media type of the part flag immediately preceding it. The plain positional form (send "hello") is shorthand for a single --text-part and cannot be combined with the part flags above.

Output and errors

text (labeled Label: value fields, with a copy-pasteable resume command whenever a task pauses at INPUT_REQUIRED/AUTH_REQUIRED) is the default, human-readable format. Pass -o json (or --output json) for the protocol's own JSON types instead — --stream then switches its cardinality from one document to JSON Lines (one object per event); --compact only affects the single-document form.

cargo run --bin a2acli -- task get task-123            # text (default)
cargo run --bin a2acli -- task get task-123 -o json    # one JSON document
cargo run --bin a2acli -- send "hello" --stream -o json  # JSONL, one event per line

A failure — from the agent or from the tool itself, including a malformed flag or a missing subcommand — always prints one compact JSON error object to stderr, in every output mode:

{"error":{"code":"TASK_NOT_FOUND","message":"task not found: t-1","a2aCode":-32001}}

A task that the CLI conducted and reported exits 0 even when the agent did not succeed, so the exit status alone cannot say that. A FAILED or REJECTED outcome, and a task paused at INPUT_REQUIRED/AUTH_REQUIRED, is therefore also named in a one-line warning on stderr — in every output mode, leaving stdout exactly the payload. CANCELED is not warned about: after task cancel it is the outcome you asked for.

code is the A2A protocol's own error name for a protocol failure (with the numeric a2aCode alongside it), or an A2ACLI_ERR_* symbol for a failure the protocol never saw (a bad flag, an unreachable agent, a --timeout expiry). The exit status reports only whether the CLI did its job — 0 even when a task ends FAILED/REJECTED or pauses at INPUT_REQUIRED/AUTH_REQUIRED — while 1/2/3/4/5 distinguish a generic failure, a usage error, an unreachable agent, a rejected credential, and a timeout respectively.

Configuration

Every global option listed above can also be set from the environment or a .env file, so repeated invocations stay short. Precedence, highest wins:

  1. an explicit flag,
  2. a real environment variable,
  3. a local .env (found by walking up from the working directory, or the file named by --config <path>),
  4. the global .env at ~/.config/a2a-cli/.env ($XDG_CONFIG_HOME honored),
  5. the built-in default.

The environment variable name is A2ACLI_ followed by the long flag name, upper-snake-cased — --bearer → A2ACLI_BEARER, --transport → A2ACLI_TRANSPORT (comma-separated: A2ACLI_TRANSPORT=jsonrpc,rest), --context-id → A2ACLI_CONTEXT_ID. The same names work in a .env file, one KEY=value per line; blank lines and # comments are ignored, a leading export is tolerated, and one layer of surrounding quotes is stripped:

A2ACLI_AGENT_CARD=https://agent.example.com
A2ACLI_TRANSPORT=rest,jsonrpc
A2ACLI_TIMEOUT=60s
A2ACLI_A2A_VERSION=1.0
# credentials
A2ACLI_BEARER="Bearer <token>"

--stream, -h/--help, and -v/--version are never read from the environment or a file — they must always be passed explicitly.

cargo run --bin a2acli -- config show                  # inspect effective settings
cargo run --bin a2acli -- config show -o json
cargo run --bin a2acli -- --config ./prod.env config show

config show is read-only: it prints each effective setting and the source it resolved from (redacting credential values), so you can confirm precedence without guessing. Change settings by exporting the variable or editing a .env file directly — the command never mutates anything. a2acli never writes a .env file itself, but warns if one it reads is readable by users other than its owner (mode should be 0600).

Schema validation

card get --validate checks the fetched card against the A2A JSON schema (§10.1) — a stricter, complementary check to the type-checked deserialization card get always does. The two catch different things: the type check rejects a card that does not parse as an AgentCard at all, while the schema additionally rejects a card that parses fine but violates the schema in a way the Rust type is too permissive to notice — an unrecognised property, most commonly, since AgentCard's fields accept any JSON object as long as the ones the type cares about are present and well-typed. --validate therefore needs the bytes the agent actually served, not a round trip through the typed value, which would have already dropped whatever the schema is being asked to catch.

cargo run --bin a2acli -- card get --validate

A card that passes is printed exactly as card get prints it. A card that fails exits with A2ACLI_ERR_CARD_INVALID and every violation under the error envelope's details, each with a JSON pointer path into the card and a message — never only the first one found. The schema is vendored at a pinned A2A version rather than fetched, and that version is reported in the envelope's hint alongside A2ACLI_ERR_CARD_INVALID, since "invalid" means nothing without saying invalid against what.

--validate combined with --extended validates the extended card the same way, with one gap worth stating: that card arrives through the A2A client's typed protocol pipeline rather than as bytes this CLI reads itself, so it cannot see an unrecognised property the same pipeline already discarded. A wrong type or an unrecognised enum value are still caught on that path.

Shell completion

completion <shell> writes a completion script to stdout and nothing else, so it can be redirected to a file or eval'd directly. It needs no agent and makes no network call, so it works before anything is configured.

# bash — user-level; use /etc/bash_completion.d/a2acli to install system-wide
a2acli completion bash > ~/.local/share/bash-completion/completions/a2acli

# zsh — any directory on $fpath
a2acli completion zsh > "${fpath[1]}/_a2acli"

# fish
a2acli completion fish > ~/.config/fish/completions/a2acli.fish

# elvish
a2acli completion elvish > ~/.config/elvish/lib/a2acli.elv
# PowerShell — add to $PROFILE to persist
a2acli completion powershell | Out-String | Invoke-Expression

Naming an unsupported shell is a usage error (exit 2) listing the accepted values.

Conformance

This CLI is tracked against the a2a-cli specification Tier 1 ("Core") requirements; see a2aproject/a2a-rs#164 for the current gap list and in-progress work (blocking-by-default send/polling, human-readable text output, message parts, and more). Tier 2 ("Extended") requirements are tracked in a2aproject/a2a-rs#181; completion <shell> above is the first of them.