Documentation — A2A API reference
Reference · Protocols

A2A API reference

This page is the lookup reference for agentd's A2A listener: its HTTP routes, the JSON-RPC methods, headers and envelope, the order requests are checked in, every error, the wire rules, the Agent Card, principals, the command ops, the sign-in endpoints, the listener settings and the limits. It is for integrators building a client or a peer, and for operators checking exact behaviour. The A2A channel explains the model behind it.

agentd speaks A2A 1.0 over the JSON-RPC binding. The listener needs the a2a build feature, which the release binaries and image include.

Routes

RouteMethodServedWhat it is
/POSTalwaysthe JSON-RPC endpoint
/OPTIONSalwaysthe CORS preflight
/.well-known/agent-card.jsonGET, HEAD, OPTIONSalwaysthe public Agent Card
/oauth2/device_authorizationPOSTdevice grant onRFC 8628 §3.1: a client asks for a code
/oauth2/tokenPOSTdevice grant on, or a launcher attachedRFC 6749 §3.2, RFC 8628 §3.4: the client polls for its session
/oauth2/revokePOSTdevice grant on, or a launcher attachedRFC 7009: end a session by its token
/.well-known/oauth-authorization-serverGETdevice grant onRFC 8414 metadata; the issuer is the listener origin
/oauth2/deviceGETdevice grant onthe default verification page: plain text telling the person how an operator approves
/oauth2/launch_authorizationPOSTagentd ui attacheda launched web UI tab asks the launcher's terminal to sign it in

Every other path, and a route whose condition does not hold, is 404.

Methods

agentd answers the eleven methods A2A 1.0 defines for the JSON-RPC binding, plus the one method its events extension declares, matched by exact name. Every other name is -32601 for every caller.

MethodWhat it doesWho may call
SendMessagea conversation turn, or a command op; answers a Task, or a Message for a read opany named caller
SendStreamingMessagethe same, answered as a server-sent event streamany named caller
GetTaskone task the caller may see, with its historyany named caller, own tasks
ListTasksthe caller's tasks, filtered and pagedany named caller, own tasks
CancelTaskcancel one task in flightany named caller, own tasks
SubscribeToTaskfollow one task's updates as a streamany named caller, own tasks
CreateTaskPushNotificationConfigregister a webhook for one taskany named caller, own tasks
GetTaskPushNotificationConfigread one webhook registrationany named caller, own tasks
ListTaskPushNotificationConfigspage through one task's webhooksany named caller, own tasks
DeleteTaskPushNotificationConfigremove one webhook registrationany named caller, own tasks
GetExtendedAgentCardthe card for this callera caller named by a declared scheme
agentd.events/SubscribeToEventsthe instance-wide observation feedany named caller; declared only while a2a.events.enabled is on

The operator may call every method on every task. An anonymous principal may call none. The public card is not a method; it is the document at /.well-known/agent-card.json.

Headers

HeaderDirectionRule
Content-Typerequestapplication/json, parameters allowed; anything else is 415 with an empty body
A2A-Versionrequestrequired on every POST /; 1.0 with any patch component (1.0.3); read from the header only, never the query string
A2A-Extensionsrequestcomma-separated extension URIs; may repeat; the first 32 tokens are considered, and a token over 512 bytes is ignored
A2A-Extensionsresponsethe extensions this request activated; absent when none was
AuthorizationrequestBearer <token>, scheme case-insensitive
Originrequesta browser request; must name an origin in a2a.cors.origins
WWW-Authenticateresponseon 401 and on a bearer caller's 403
Retry-Afterresponseon 429, and on the card's 503

The card, the OPTIONS preflights and the /oauth2/* endpoints take no A2A-Version: they are not JSON-RPC.

CORS. A request carrying Origin, to POST / or any /oauth2/* endpoint, is served only when that origin is listed in a2a.cors.origins, compared as an origin (scheme, host, port with its default). Anything else, Origin: null included, is 403 with no CORS headers, before the body is read. A listed origin has its own origin echoed in Access-Control-Allow-Origin, and the response exposes A2A-Extensions, Retry-After and WWW-Authenticate. The preflight allows Content-Type, Authorization, Last-Event-ID, A2A-Extensions and A2A-Version. The one origin admitted unlisted is the web UI page agentd ui launches, for that process only.

The public card has its own policy: readable from any origin with Access-Control-Allow-Origin: *, never with credentials.

The request envelope

The body is one JSON-RPC 2.0 request object.

  • jsonrpc is exactly "2.0" and method a non-empty string.
  • id is required, a string or an integer. A request without one would be a notification, which is executed and never answered, so a missing or null id is -32600 "A2A requests must carry an id".
  • params, when present, is an object; otherwise -32602.
  • A batch (an array) is -32600.
  • An integer beyond ±2^53 is carried as the double the protocol holds it as.
curl -s https://agent.example.com:8443/ \
  -H 'content-type: application/json' \
  -H 'A2A-Version: 1.0' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"ListTasks","params":{"pageSize":10}}'

The request pipeline

Every POST / runs these checks in this order. Each refusal is final and is plain application/json, never an SSE frame, and no refused request reaches the runtime.

StepCheckRefusal
1Origin, when present, is listed in a2a.cors.origins403, empty body
2a source over its failed-credential limit presents a bearer that is not a live session429, RATE_LIMITED
3Content-Type is application/json415, empty body
4who is calling, from the headers and connection (evidence)401 nobody or a failed credential; 403 a certificate no rule gives a role
5the body is JSON-32700
6the JSON-RPC envelope-32600, -32602
7A2A-Version is 1.0-32009
8the caller's rate quota429, RATE_LIMITED
9the method is in the route table-32601
10the extensions A2A-Extensions activates; an extension method needs its extension declared and activated-32601
11the caller may call the method; the extended card needs a declared scheme's credential403; 401
12the params: a send's message shape, the task it names, its command envelope and the caller's reach to the op-32602, -32001, -32004, -32005, 403
13the answer

Errors

A peer branches on the code, never on the message, so every code means one thing. A refusal that has a machine-readable reason carries a google.rpc.ErrorInfo in error.data: its reason names the case, and its domain is a2a-protocol.org for a reason the specification defines and agentd.dev for one agentd coined. The plain protocol errors carry the code and a message only.

CodeHTTPReasonMeaning
-32700200the body is not JSON
-32600200not a JSON-RPC request: a batch, a missing id, a bad jsonrpc or method
-32601200no such method
-32601200EXTENSION_NOT_DECLARED, EXTENSION_NOT_ACTIVATEDan extension method whose extension is not declared or not activated
-32602200bad params: a malformed message, a filter the server cannot honour, a bad page size or token, a message that names a task and a different contextId
-32602200see A2A extensionsa malformed or unknown command
-32603200DRAININGthe agent is draining and takes no new work
-32603429RATE_LIMITEDa rate quota or the failed-credential limit; Retry-After and the retryAfterSeconds metadata say how long
-32001200no such task, or one the caller may not see: the same answer, so ids cannot be probed
-32002200the task is already terminal and cannot be canceled
-32003200push notifications are not enabled on this agent
-32004200UNSUPPORTED_OPERATIONthe extended card where no scheme is declared
-32004200INTROSPECTION_DISABLEDan introspection op while introspection is off
-32004200a message to a terminal task; subscribing to a task that has ended
-32005200CONTENT_TYPE_NOT_SUPPORTEDa raw or url part, or acceptedOutputModes excludes what a command answers with
-32009200VERSION_NOT_SUPPORTEDA2A-Version missing or not 1.0; metadata supportedVersions: "1.0"
-31401401UNAUTHENTICATEDwho you are: no credential, or one that is invalid, expired or revoked
-31403403PERMISSION_DENIEDwhat you may do: a known principal whose role and grants do not reach the call

-32007 (no extended card configured) and -32008 (a required extension not activated) are never sent. A listener either declares a scheme and serves the extended card or declares none and answers -32004, and none of agentd's extensions is required.

The identity codes. -31401 and -31403 sit outside JSON-RPC's reserved range so a client that branches on a specification code is never told the wrong thing. They travel with their HTTP status so a proxy, a browser and a plain HTTP client all see them.

ResponseWWW-Authenticateid
401, nothing presentedBearer realm="agentd"null
401, a credential presented and failedadds error="invalid_token" and a descriptionnull
403, a certificate no rule gives a rolenonenull
403, a method or op the caller may not callerror="insufficient_scope" when the caller used a bearerthe request's id

A browser refused only for being a browser, on a listener where the same request without Origin would have been the implicit operator, is told "browser requests must authenticate".

HTTP statuses outside JSON-RPC: 403 with an empty body (an origin not listed), 415 (not application/json), and on the card 304 and 503.

Wire format

A2A is defined in protocol buffers, and its JSON binding is proto3 JSON. agentd's tasks, messages and cards are written by the official SDK's canonical encoder.

  • Enums are the proto value names: TASK_STATE_COMPLETED, ROLE_USER.
  • Timestamps are RFC 3339 strings: "2026-08-17T13:41:27.824Z".
  • Field names are lowerCamelCase: historyLength, nextPageToken, contextId. A snake_case key is an unknown field, and an unknown field is ignored. GetTask names its task with id.
  • Every task is a Task. ListTasks returns the same object as GetTask, cut to what the listing asked for, so the state is always at status.state.
  • Required fields are always present. A listing's last page carries nextPageToken: "", an empty listing carries tasks: [] and totalSize: 0, and a deleted push config answers {}.
  • Ids are the SDK's. Task and context ids are UUIDv7s. A command result artifact is <taskId>.result.

Sending messages

SendMessage and SendStreamingMessage take the specification's send request; fields it does not define are ignored.

  • message.role must be ROLE_USER.
  • No taskId starts a new task with an id the server mints.
  • A taskId continues that task, which is how a caller answers a question the task is waiting on. A task the caller cannot see is -32001; a terminal one is -32004. A message that names a task and a different contextId is -32602.
  • contextId is the caller's own name for a conversation. For a non-operator it is bound per principal to a runtime key, so two callers' identical names are two conversations. An operator addresses conversations by the runtime's keys, which the status op lists.
  • Parts: text parts are accepted, and a DataPart without the agentd key is read as JSON. A raw or url part refuses the whole message with -32005. A DataPart under agentd is a command (see Command ops).
  • Blocking. A unary send waits for the task to reach a terminal state or input-required, for at most 25 seconds, unless configuration.returnImmediately is set. It then answers with the task as it stands.
  • Inline push config. A push config sent with the message is checked like a registration and attached only once the message is accepted.
  • While draining, a send is -32603 with reason DRAINING.

A read op answers with a Message and creates no task. Over SendStreamingMessage it answers with exactly one frame carrying that Message, then closes.

Listing tasks

ListTasks returns the caller's own tasks; an operator sees every task. totalSize counts only what the caller may know exists.

ParamRule
contextIdthe caller's own name for a conversation; an operator may also use a runtime key
statusone task state
statusTimestampAfterRFC 3339, inclusive
pageSizeunset or 0 is 50; otherwise 1 to 100; larger is -32602
pageTokenopaque; one this server did not issue is -32602
historyLengthunset omits the history; n keeps the newest n messages
includeArtifactsartifacts are included only when true

The order is newest status first. A filter the server cannot honour is refused with -32602, never ignored. The result always carries tasks, nextPageToken, pageSize and totalSize, and nextPageToken is "" on the last page.

ListTaskPushNotificationConfigs pages the same way: pageSize 1 to 100, default 50, ordered by config id, nextPageToken always present and "" on the last page.

Finished tasks are kept until store.retention.tasks drops them; unset keeps every task. A working or input-required task is never dropped, and a settled task nobody has read back yet is kept for at least 30 seconds.

Streams

  • SendStreamingMessage answers text/event-stream: a Task frame, then status and artifact updates, closing when the task reaches a terminal state. A task waiting at a gate keeps its stream open.
  • SubscribeToTask opens with the task as it stands, then each transition, closing at a terminal state. An unknown or unseen task is -32001 and a task that has ended is -32004, both as plain JSON with no stream opened; read an ended task with GetTask.
  • Reconnecting means subscribing again. Past transitions are not replayed, and Last-Event-ID is ignored.
  • A refusal before the first frame is plain JSON with its status, never an error inside a 200 stream.

Push notifications

A caller registers a webhook for one of its tasks with CreateTaskPushNotificationConfig, and agentd posts each of that task's updates to it.

SettingDefaultEffect
a2a.push.enabledfalseaccept registrations and deliver; off, registration is -32003 and the card says pushNotifications: false
a2a.push.allow_privatefalsepermit webhook targets on private and loopback addresses

Registration. The config takes url (required), token and authentication {scheme, credentials}. A config registered without an id gets push-<ULID>. The URL must be https, or http to a loopback host, and it must pass the SSRF guard, which refuses private and loopback addresses unless allow_private is on. It is checked at registration, where a refusal is -32602 naming the reason, and again at every delivery, dialling only the address the guard vetted. Reads return the registration with the authentication scheme only, never its credentials.

Delivery. Each delivery is a POST with Content-Type: application/a2a+json, whose body is a stream response carrying the Task: its status and artifacts, without history and without task annotations. The receiver is told that the task moved and reads the conversation with GetTask.

HeaderFrom
Authorization: <scheme> <credentials>the registration's authentication; reads echo the scheme only
X-A2A-Notification-Tokenthe registration's token; an interop header the official a2a-python receiver reads

Delivery is best effort: a delivery has 10 seconds, a webhook that is down does not fail the task, and a failed delivery is logged as a2a.push.failed and dropped.

The Agent Card

GET /.well-known/agent-card.json serves the public card with no credential and no A2A-Version. It carries an ETag over the served bytes and answers a matching If-None-Match with 304, and Cache-Control: public, max-age=60. When the runtime cannot produce it within 5 seconds, the answer is 503 with Retry-After: 5, never a stale or partial card.

FieldValue
nameagent.name, else agentd
descriptionagent.description, else a generic sentence
versionthe agentd build
supportedInterfacesone entry: the advertised URL, protocolBinding JSONRPC (or the unix binding URI on a unix listener), protocolVersion 1.0
capabilities.streamingtrue
capabilities.pushNotificationsa2a.push.enabled
capabilities.extendedAgentCardtrue exactly when the listener declares an authentication scheme
capabilities.extensionsthe declarations, with their public params (A2A extensions)
defaultInputModes, defaultOutputModestext/plain, application/json
skillsone, conversation
securitySchemes, securityRequirementswhat the listener enforces; absent when it declares nothing

The advertised URL is a2a.url; without it, a concrete bind advertises scheme://host:port/, and a unix listener advertises unix://<path>.

Security declarations. The card's schemes are derived from the posture the listener enforces:

ListenersecuritySchemessecurityRequirements
loopback, nothing configurednonenone
unix socketnonenone
a2a.bearer or a bearer_ref rulebearer{bearer}
a2a.device_grant.enabledbearer, device_code (OAuth 2.0 device flow){bearer}, {device_code}
a2a.tls.client_camtls, plus bearer when one is configured{mtls}, plus {mtls, bearer}

An any rule adds the empty alternative {} on a listener without a client CA. The device_code scheme's flow names <origin>/oauth2/device_authorization and <origin>/oauth2/token, and oauth2MetadataUrl when the origin is https.

The extended card. GetExtendedAgentCard adds a workflow:<name> skill for each workflow the caller may run and narrows each extension's params to this caller. A2A extensions lists what each extension adds.

Principals

Every request resolves to a principal from its headers and connection, before its body is read.

OrderEvidencePrincipal
1a unix-socket peer with the daemon's uid, or rootoperator
2a bearer starting agentd_at_the session's principal; unknown, expired or revoked is 401
2any other bearer, on a listener with a bearer mechanisma2a.bearer is operator; a bearer_ref rule is its principal; neither is 401
3a verified client certificatethe first san, sub or any rule it matches; operator while no rule exists; else 403
4none of the above, and an any rule existsthat rule's principal
5nothing presented, a loopback peer, a loopback-bound listener with no mechanism, no Originoperator (the implicit operator)
6anything else401

A bearer sent to a listener with no bearer mechanism is ignored. A presented bearer is resolved before any certificate rule.

Rules (a2a.principals[]):

KeyRule
matchexactly one of san, sub, bearer_ref, any
match.sana glob over the certificate's SANs and subject CN; a SPIFFE X.509-SVID's spiffe://… arrives as a URI SAN; needs a2a.tls.client_ca
match.subthe subject CN, exactly; needs a2a.tls.client_ca
match.bearer_refa secret reference, compared in constant time; may not equal a2a.bearer or another rule's secret, or start with agentd_at_
match.anytrue; never with role: operator; needs no client CA
roleoperator, user, agent or anonymous (names nobody)
idthe principal's name: 1 to 128 of A-Z a-z 0-9 . _ @ : / + -, unique across rules; required on bearer_ref and any rules
grantsop names or patterns beyond the role's defaults
labelsattributes carried into the run, the MCP request metadata and the audit line
quotas.rate<burst>/<window>, for example 20/1s or 30/1m; operators are exempt
quotas.budgeta token budget scoped to the principal (Control spend with budgets)

Certificate rules are first-match-wins, in the order written, and are not ranked by specificity: a broad rule placed early shadows every rule after it.

Principal ids. The operator role is always operator. A user or agent is <role>:<id> when the rule declares id; otherwise, for a certificate, <role>:cn=<CN> or <role>:san=<first SAN>. A device session is user:<name>. The = keeps certificate-derived ids out of the declared namespace, so a certificate whose CN equals a declared id never inherits that principal's work.

Grants. A grant names an op exactly, every op (*), or the ops that start with a prefix (workflow.*). A grant naming an operator-only op exactly on a non-operator rule is refused at load; a pattern may cover a family and grants only its members that are not operator-only. A workflow.run:<pattern> grant scopes workflow.run to the matching workflows and replaces the role's default reach for that op.

Identity registry. Device names and user-role ids share one namespace across time, since both spell user:<name>. The durable store records every claim: an approval may not use a name any rule declares or a user rule ever declared, and a user-role id that was ever approved as a device name is refused at startup (exit 2) and by a reload. Names compare with ASCII case folded.

Command ops

A command is a SendMessage or SendStreamingMessage whose message carries one DataPart {"agentd": {"op": "<op>", …arguments}}, with the command extension URI in both the A2A-Extensions header and message.extensions. A command message names no taskId.

curl -s https://agent.example.com:8443/ \
  -H 'content-type: application/json' \
  -H 'A2A-Version: 1.0' \
  -H 'A2A-Extensions: https://agentd.dev/a2a/ext/command' \
  -H "Authorization: Bearer $OPERATOR_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"role":"ROLE_USER","messageId":"m-1","extensions":["https://agentd.dev/a2a/ext/command"],"parts":[{"data":{"agentd":{"op":"admin.drain","reason":"rolling update"}}}]}}}'

A message op answers with a Message whose DataPart is the result. A task op answers with a Task whose <taskId>.result artifact carries the result.

OpReplyDefault callersServedArguments
statusmessageany named calleralwaysnone
configmessageoperatoralwaysnone
workflow.runtaskuser, agentalwaysworkflow; inputs
workflow.statusmessageuser, agentalwaysrun
workflow.canceltaskuseralwaysrun
workflow.signaltaska grantalwaysname; run, payload
subagent.sendtaskuseralwayshandle, message
subagent.killtaska grantalwayshandle; reason
subagent.statusmessageuseralwayshandle
plan.getmessageuseralwaysid
conversation.getmessageuserintrospection onid; limit
run.getmessageuserintrospection onrun
subagent.getmessageuserintrospection onhandle
debug.eventsmessageoperatorintrospection onafter, level, limit, prefix
admin.draintaskoperatoralwaysreason
admin.pausetaskoperatoralwaysrun, reason
admin.resumetaskoperatoralwaysrun
admin.canceltaskoperatoralwaysrun; reason
admin.settaskoperatoralwayspath, value
auth.device.pendingmessageoperatordevice grant onnone
auth.device.approvetaskoperatordevice grant onuser_code, as; scope
auth.device.denytaskoperatordevice grant onuser_code, or all: true
auth.sessionsmessageoperatora TCP listenernone
auth.sessions.revoketaskoperatora TCP listenersid, name, or all: true

Arguments before a semicolon are required. "A grant" means no role reaches the op by default and a principal needs a grant naming it. The operator reaches every op. An operator-only op is the operator's whatever a grant says. An op behind a closed switch is answered as an unknown op, except the introspection ops, which answer INTROSPECTION_DISABLED.

  • admin.drain begins a graceful drain, as SIGTERM does, and returns at once; the process exits 0 when the drain completes, within lifecycle.drain_timeout.
  • admin.pause with no run holds the whole instance at a safe boundary: the listener still answers and intake still queues. With run, it pauses that run alone. admin.resume clears either.
  • admin.set sets agent.approval or a2a.introspection.enabled until the next reload re-reads the file.
  • auth.device.approve's as is the person's name: lowercase, ^[a-z0-9][a-z0-9._-]{0,63}$, not operator, anonymous, unknown, launcher, runtime or system, and not a configured principal id. The answer's existing: true says the name was approved before.

Reserved names. _instance.result and _instance.emit are how a child instance reports to its parent; they are the operator's and are not listed in params.ops. ask_human is the model's tool and is not served over A2A. No workflow may declare a built-in op or a reserved name as its command.

Workflow-declared commands. A workflow whose start step is kind: a2a with command: adds that op. A caller may send it when its grants name the op (an operator always may) and the start's roles: admit its role, and the arguments are held to the start's schema. The extended card lists them in the command extension's commands param.

The normative op table, argument and result schemas are at https://agentd.dev/a2a/ext/command. Operate a running daemon shows the operator ops in use.

Device authorization grant

With a2a.device_grant.enabled, the listener origin is an OAuth 2.0 authorization server. Its endpoints take application/x-www-form-urlencoded bodies of at most 4 KiB (unknown parameters ignored, a repeated one refused), answer OAuth JSON with Cache-Control: no-store and sit behind the same origin gate as POST /.

EndpointParametersAnswer
POST /oauth2/device_authorizationclient_id (1 to 64 of A-Z a-z 0-9 . _ -); scope (user or operator, default user)device_code, user_code, verification_uri, expires_in, interval
POST /oauth2/tokengrant_type=urn:ietf:params:oauth:grant-type:device_code, device_code, client_idaccess_token, token_type: Bearer, expires_in, scope
POST /oauth2/revoketokenends the session the token names

While a code waits, a poll answers authorization_pending, and polling faster than interval answers slow_down. Other OAuth errors: access_denied (denied), expired_token (the code lapsed), invalid_grant, invalid_request, invalid_scope (a scope a2a.device_grant.scopes does not offer) and unsupported_grant_type.

{"device_code":"3f9d…6c14","expires_in":600,"interval":5,"user_code":"WJLG-DRVQ","verification_uri":"https://agent.example.com:8443/oauth2/device"}
{"access_token":"agentd_at_25f1…aa90","expires_in":28800,"scope":"user","token_type":"Bearer"}

Scopes. A session is a user unless the client asked for operator, a2a.device_grant.scopes lists it, and the approver grants it. An approver may narrow a request, never widen it. The session principal is user:<name>, or operator for an operator session.

Sessions. A session token is agentd_at_ followed by 256 random bits, held only as its SHA-256 in memory: a restart ends every session. There are no refresh tokens. No configured secret may start with agentd_at_. A session id is ds_ (device) or ls_ (launch) followed by 16 hex digits, and every audit line of the caller carries it.

Revocation. auth.sessions.revoke ends one session {sid}, every session of a name {name}, or all {all: true}; POST /oauth2/revoke ends the session whose token it is given. Revocation closes the session's open streams and refuses its waiting requests within 100 ms, and the feed ends with goodbye reason revoked, by session id, so a sibling session of the same name keeps working. Revoking never deletes what the principal owns.

Codes, tokens and user codes are never logged.

Launch grant

agentd tui and agentd ui sign their client in with the extension grant type https://agentd.dev/oauth/grant-type/launch (RFC 6749 §4.5) at /oauth2/token, while a launch is installed in the daemon's process:

  • with a code the launcher minted and handed its client: single use, valid for 60 seconds, bound to the client and, for the web UI, to the launched origin; or
  • with a request_code from POST /oauth2/launch_authorization, which a launched web UI tab makes and the person at the launcher's terminal approves by typing the code the tab shows. A request lives 2 minutes, and at most 16 wait at once.

Both are redeemed only from a loopback peer, and either buys an operator session: eight hours for a browser tab, the launcher's lifetime for the TUI. The grant is not declared on the card; the RFC 8414 metadata lists it while a launch is installed. Use the terminal and web UI describes the launcher.

Listener settings

Every a2a.* key is operator-only except a2a.peers: an instruction document's :::!config fragment may write only the outbound peers. Every path is also a flag (--a2a-<path>) and an environment variable (AGENTD_A2A_<PATH>); --listen is short for a2a.listen. Configuration reference lists every key.

KeyDefaultReloadMeaning
a2a.listenno listenerrestarthttps://host:port, http://host:port on loopback, or unix:///path
a2a.urlthe bind's own URLrestartthe public origin: scheme://host[:port], lowercase, no default port, https unless loopback
a2a.tls.cert, a2a.tls.keynonerestart; files re-read on changethe server identity
a2a.tls.client_canonerestart; file re-read on changerequire a client certificate of every caller
a2a.bearernonerestarta secret whose presenter is the operator
a2a.principals[]reloadthe principal rules
a2a.cors.origins[]reloadbrowser origins admitted, exactly; * and paths refused
a2a.device_grant.enabledfalserestartserve the device grant and declare it on the card
a2a.device_grant.scopes[user]restartwhat a client may request: user, operator
a2a.device_grant.token_ttl8hrestartsession lifetime, 5m to 30d
a2a.device_grant.code_ttl10mrestarthow long a code waits, 1m to 30m
a2a.device_grant.verification_urithe listener's own pagerestartwhere the approving person is sent: https://, or http:// on loopback
a2a.device_grant.rateunlimitedrestart<burst>/<window> per signed-in name
a2a.events.enabledfalserestartdeclare the events extension and serve the feed
a2a.introspection.enabledfalsereload; admin.setserve the introspection ops and audit events on the feed
a2a.push.enabledfalsereloadaccept webhook registrations and deliver
a2a.push.allow_privatefalsereloadpermit private and loopback webhook targets
a2a.peers[]reloadagents this instance dials

A rotated certificate is served on the next connection without a restart; a bad intermediate write keeps the last good identity. Whether client certificates are required is fixed at startup.

Peers (a2a.peers[]):

KeyRule
namerequired, unique; A-Z a-z 0-9 _ -
endpointrequired: https://host[:port], http:// to a loopback host, or unix:///path
autha credential provider: kind: static, oauth2 or spiffe (Outbound credentials); aws is refused
headersextra request headers; a credential-shaped header must be a secret reference
client_cert, client_keyan mTLS client identity; set together
servicea services: entry of kind: peer that supplies endpoint, auth and tags

security.tls_ca adds a trust anchor for every outbound TLS connection, peers included.

Load-time refusals

The load refuses these with exit code 2, before anything starts.

RefusedWhy
https:// listener without a2a.tls.cert and a2a.tls.keynothing to serve TLS with
non-loopback bind without a2a.bearer or a2a.tls.client_cano open control plane
http:// on a non-loopback bindplaintext is loopback only
wildcard bind (0.0.0.0, ::) without a2a.urlno address a caller can dial
a2a.url not a canonical originthe card and the OAuth issuer live at the origin root
a2a.tls on a unix:// listenerthe kernel authenticates the peer
a2a.url, cors, device_grant, events or introspection without a2a.listenthey are served on the listener
a2a.listen, webhooks.listen or a2a.peers in a build without the a2a featurenothing could serve or dial them
store.kind: none on a long-lived instancea daemon must keep its tasks
* or a path in a2a.cors.originsorigins are listed exactly
device grant without an operator credentialnobody could approve a code
device grant beside a2a.tls.client_caa device session has no certificate
device grant on a unix:// listenerthe kernel already vouches for every peer
a principal rule matching a certificate (san, sub) without a2a.tls.client_cathe listener never asks for a certificate
a rule with no matcher, or more than onea rule matches one way
any with role: operatoranyone cannot be the operator
a bearer_ref or any rule without idthe evidence names nobody
an invalid or duplicate idan id names one principal
a grant naming an operator-only op on a non-operator rulethe floor answers to the role
a malformed quotas.rate
a duplicate peer name, a bad peer endpoint, client_cert without client_key, auth.kind: aws on a peer
a workflow command that names a built-in opit would shadow the operator's control

Limits and constants

WhatValue
Blocking SendMessage wait25 s, then the task as it stands
A unary request's wait on the runtime120 s
Card build5 s, then 503 with Retry-After: 5
Card cachingCache-Control: public, max-age=60, with an ETag
Observation feed streamheld 10 minutes, then goodbye with reason deadline
Observation feed replay ring1024 events
Task historythe newest 64 messages, at most 256 KiB together
History on a feed task eventthe newest 4 messages, at most 32 KiB
List page sizedefault 50, maximum 100
A2A-Extensions32 tokens considered, 512 bytes each
Failed credentials per source20, one forgiven every 3 s; 4096 sources tracked; a source is an IPv4 address or an IPv6 /64
Refusal log linesone per source and reason per 60 s; 20 at once across sources, then one per second
Revocation reaching open requests100 ms
Device codes per source5, then one per 12 s
Token polls per source30, then one per second
Waiting device codes4 per source, 16 per network (an IPv6 /48), 64 in all
Device poll interval5 s; slow_down adds 5 s
User code8 letters from BCDFGHJKLMNPQRSTVWXZ, shown as XXXX-XXXX
OAuth form body4 KiB
Launch codesingle use, 60 s
Launch request2 min; at most 16 waiting
Push delivery10 s
Settled task kept unreadat least 30 s
Task retention sweepevery second
Peer card cachethe card's max-age, at most 5 min; 1 min when it names none; 5 s for a peer on this host
Peer card fetch5 s
Peer request30 s per round trip; GetTask polls 100 ms apart, bounded by the step's timeout

A card read under a client certificate or a per-request signature is not cached, and a delegation that fails drops the card it used.

Log events

EventWhen
a2a.listenthe listener bound: authority, advertised URL, TLS, mTLS, device grant, events, introspection, implicit operator
a2a.denieda request was refused: principal, rule, method, op, reason, status, session id; at most one line per source and reason per minute, with a suppressed count
a2a.unix.denieda unix-socket connection from another uid was dropped
a2a.push.registereda webhook was registered
a2a.push.faileda webhook delivery failed and was dropped
a2a.delegate, a2a.senda workflow step dialled a peer

The a2a.denied reasons are auth_failures_limited, unauthenticated, invalid_credential, no_role, rate_limited and not_permitted. Events and metrics reference lists every event.

Probes

These read a configuration without starting anything:

CommandPrints
agentd --capabilities --config <file>the manifest; its a2a block has methods, command_ops, extensions, events, introspection, url, cors_origins and auth (bearer, device, mtls, implicit_operator, required)
agentd --extensionsevery extension and binding URI agentd publishes
agentd --extension-schema <name>one extension's schema bundle
agentd --validate-config --config <file>the load verdict; exit 0 or 2

Next steps