本地 A2A 端点
每个节点都在回环地址上提供 A2A 协议服务。网络上的每个远程 Agent 在其中都表现为一个普通的 A2A 服务器,拥有各自的 URL 和 Agent 卡片。因此,任何支持 A2A 的客户端,例如 Agent 框架、编码工具,或使用 A2A SDK 的脚本,无需编写任何 ANet 专有代码,也无需自行运行 HTTPS 服务器,即可接入网络。
本页说明客户端如何找到该端点并完成认证、有哪些路由和操作、代理卡片包含什么内容,以及同一模块如何把到达节点的任务转发给本地 A2A 服务器。
概念
本地接口即模块 a2a。它默认启用,不需要配置块;anet init 也不会写入配置块。这是 ANet 为 A2A 作出贡献、而非与之竞争的方式:任务是 A2A 任务,卡片是 A2A Agent 卡片,付款遵循 a2a-x402 扩展。卡片背后的内容,即经由看不到内容的 hub 的端到端加密、带有密钥历史的身份、收据和付款,由守护进程负责,仅以 A2A 对象和带 anet. 前缀的元数据形式到达客户端。
该接口的特性:
每个远程 Agent 一条路径。 持有某个 Agent 的 URL 和本地令牌的客户端,只能访问该 Agent 的任务,不能访问其他 Agent 的任务。节点不是它自身的远程 Agent,针对其自身 AID 的请求会得到 "no such agent" 的应答。
仅限回环,使用独立的令牌。 该接口的令牌(
a2a_token.txt)与节点的控制令牌相互独立。它的权限更小:仅限作为请求方的任务,以及 Agent 档位内的付款。此处不接受控制令牌,控制面也不接受此令牌。请求方一侧。 该接口向远程 Agent 发送任务。远程节点是否接受任务由该节点决定;新节点不接受陌生人的任何任务。
不提供推送通知。 卡片声明
pushNotifications: false。未保持连接的客户端无法通过该接口被联系到。
地址与令牌
该接口在其状态目录 <data directory>/modules/a2a/ 中写入两个文件。默认数据目录为 ~/.anet;环境变量 ANET_DATA_DIR 可指定其他目录。
a2a_addr.txt监听器的地址
host:port。端口在接口首次启动时从 43811 到 45810 的范围内选取,并被记录下来,此后每次启动都重新绑定该端口。客户端按此地址配置,因此两次启动之间它不得改变。a2a_token.txtbearer 令牌。它只创建一次,并以私有权限保存。地址改变、守护进程轮换其本地凭证,以及紧急密钥轮换之后,令牌都会改变。
客户端在配置时读取这两个文件:
$ DATA_DIR="${ANET_DATA_DIR:-$HOME/.anet}"
$ ADDR=$(cat "$DATA_DIR/modules/a2a/a2a_addr.txt")
$ TOKEN=$(cat "$DATA_DIR/modules/a2a/a2a_token.txt")
$ auth() { printf 'Authorization: Bearer %s\n' "$TOKEN"; }
函数 auth 通过文件描述符提供请求头,使令牌不出现在 curl 的参数列表中,否则本机的其他用户可通过 ps 读到它。下面的示例在 bash 或 zsh 中使用该函数。
端口冲突
如果守护进程启动时,另一个进程占用着已记录的端口,接口不会改用其他端口。否则,按旧地址配置的客户端会继续把令牌发送给占用该端口的任何进程。接口因此保持关闭,节点的其余部分继续运行,守护进程会在状态目录中写入文件 a2a_port_conflict.txt,并写入日志。当占用者可能属于本机的其他用户时,令牌也会被一并替换。
anet up 和 anet doctor 会报告该冲突。处理方法是释放该端口、重启节点,然后更新客户端:
$ anet stop && anet up
$ anet agents wire --refresh
第二条命令会重写 Hermes 的 a2a_agents 条目;以其他方式配置的客户端,须由运维人员用新的地址和令牌更新。
路由
所有路由均位于 /a2a/v1/agents 之下。
方法 |
路径 |
用途 |
|---|---|---|
|
|
节点已知的远程 Agent,附每个 Agent 的 URL 和卡片 URL |
|
|
Agent 的代理卡片(客户端配置所用的 URL) |
|
|
位于 well-known 路径的同一张卡片 |
|
|
A2A JSON-RPC 绑定(同样接受末尾的斜杠) |
任意 |
|
A2A HTTP+JSON 绑定 |
列表路由接受查询参数 skill、tag、q、cursor 和 limit。每个条目包含 aid、url、cardUrl、verification、keyState,对于官方列表中的 Agent 还包含 anet.official。只有卡片已被节点验证的 Agent 才会给出更多描述:name、homeHub、lastSeen、reviewCount、avgRating,以及 charge(free、partial 或 paid,读取自公开卡片)。列表从不向 Agent 请求其扩展卡片。
操作
两种绑定都实现下列 A2A 操作。JSON-RPC 方法名取自 A2A 1.0。
JSON-RPC 方法 |
HTTP+JSON |
说明 |
|---|---|---|
|
|
新任务、任务上的消息,或付款决定 |
|
|
Server-sent events |
|
|
该 Agent 的一个任务 |
|
|
该 Agent 的各任务 |
|
|
|
|
|
Server-sent events |
|
|
带有该 Agent 所签名价格的代理扩展卡片 |
四个推送通知操作均返回 PushNotificationNotSupportedError。
每个任务操作都连同路径中的 AID 一起交给节点的任务层。属于其他 Agent 的任务标识会得到 TaskNotFoundError。
请求检查
每条路由上的每个请求都按下列顺序通过以下检查:
Host头指明的是回环名称(127.0.0.1、localhost或[::1])以及本监听器的端口。否则应答421 Misdirected Request。该规则可防御 DNS 重绑定:仅凭解析器将某个名称绑定到回环地址并不足够。请求不带
Origin头。否则应答403。浏览器会在每个跨源请求和每个POST上发送Origin;网页永远不是该接口的客户端。Authorization头携带当前的 bearer 令牌。否则应答401并带有WWW-Authenticate: Bearer realm="anet-local-a2a"。令牌文件被替换时会重新读取,因此已被守护进程替换的令牌从下一个请求起即被拒绝。
通过这些检查之后:
请求体必须为
application/json或application/a2a+json。其他媒体类型以ContentTypeNotSupportedError应答。请求体上限为 96 MiB。更大的请求体以
413应答。协议版本取自头部
A2A-Version(或同名的查询参数)。主版本号为 1 的值被接受;缺省被接受;其他任何值以VersionNotSupportedError应答。按绑定的要求,对 JSON-RPC 调用的应答即使携带错误对象,HTTP 状态也是 200。
节点尚未在 hub 注册时发送任务,会立即被拒绝,并返回对应加入状态的语句,且不会创建任务。
代理卡片
/a2a/v1/agents/{aid} 处的卡片是本节点的声明,而非远程 Agent 的声明。它以本节点的密钥签名。其接口是本监听器的 URL(JSON-RPC 为 .../jsonrpc,HTTP+JSON 为 .../rest),其安全方案为本地 bearer 令牌(anetLocal)。
当节点已验证远程 Agent 对自身的描述时,该描述被携带在卡片内:扩展 anet-origin/v1 包含 AID、验证结果(VERIFIED 或 UNVERIFIED)、密钥状态(fresh 或 stale)、官方标记,以及该 Agent 已签名的网络卡片的字节。希望采信 Agent 本人所述内容的客户端,应自行校验该签名。节点无法验证的卡片不会以任何形式转交;此时代理卡片是由 AID 生成的占位卡片。
卡片声明 streaming: true 和 pushNotifications: false。仅当远程卡片声明了 a2a-x402 扩展,且节点此刻能够签名并结算付款时,它才声明该扩展;签名方是本地守护进程。价格不在公开卡片上,而在扩展卡片上;仅当 Agent 收费,或节点位于该 Agent 的名单上时,节点才会从 Agent 获取扩展卡片。
$ curl -s -H @<(auth) "http://$ADDR/a2a/v1/agents"
$ curl -s -H @<(auth) "http://$ADDR/a2a/v1/agents/<aid>/.well-known/agent-card.json"
发送任务
Agent 标识 <aid> 取自列表。下面的请求以一条文本消息启动一个任务。returnImmediately 使任务一创建出来调用即返回:
curl -s -H @<(auth) -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
"http://$ADDR/a2a/v1/agents/<aid>/jsonrpc" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"messageId": "m-0001",
"parts": [ { "text": "Summarize the attached document." } ]
},
"configuration": { "returnImmediately": true }
}
}'
应答中包含任务及其标识。任务通过 GetTask 读取:
curl -s -H @<(auth) -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
"http://$ADDR/a2a/v1/agents/<aid>/jsonrpc" -d '{
"jsonrpc": "2.0", "id": 2, "method": "GetTask",
"params": { "id": "<task-id>" }
}'
HTTP+JSON 绑定提供相同的操作:
$ curl -s -H @<(auth) "http://$ADDR/a2a/v1/agents/<aid>/rest/tasks/<task-id>"
流式调用返回 server-sent events。第一个事件是任务的当前状态,与订阅一并获取,使两者之间不会遗漏任何内容。任务的产物先于终止状态的状态更新送达,其中回复排在最前,使得在终止状态处停止读取的客户端已经拿到答复。事件只携带文件元数据,不含文件字节。
curl -sN -H @<(auth) -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
"http://$ADDR/a2a/v1/agents/<aid>/jsonrpc" -d '{
"jsonrpc": "2.0", "id": 3, "method": "SendStreamingMessage",
"params": { "message": {
"role": "ROLE_USER", "messageId": "m-0002",
"parts": [ { "text": "Summarize the attached document." } ] } }
}'
只有在 A2A-Extensions 头中激活了预览扩展的客户端,流才会送达预览产物,即仍在撰写中的回复的快照。通过 GetTask 读取的任务从不包含预览。
消息与标识
contextId 与 messageId 的组合标识一条消息。以相同标识再次发送的消息会被识别,并以已有的任务应答;不会被第二次发送。付款消息同样如此,因此重复的付款消息只产生一份授权。
元数据
任务携带 MCP 中描述的元数据键:anet.effect_status、anet.receipt_verified、anet.reason 和 anet.state_seq。它们的含义与客户端是否激活了 a2a-x402 扩展无关。
付款
对于有定价的技能,任务转为 input-required,并在 x402.payment.required 中携带报价。节点是 a2a-x402 扩展的签名服务:
在自动档位(
auto_max)之内,无论客户端是否激活了该扩展,节点都会自行付款。超出该档位时,客户端在同一任务上以一条消息应答,其元数据包含值为
payment-submitted的x402.payment.status,且不含x402.payment.payload。所选的选项在anet.payment.accept中给出,原样复制自x402.payment.required.accepts;报价只有一个选项时可省略。节点在 Agent 档位(agent_max、agent_daily_max)之内签名并转发付款。自行提供载荷的客户端会收到
payment-failed,其anet.reason为client_payload_unsupported,因为此时付款方将不是本节点。所选选项不在报价之内时,以option_not_offered应答;所选选项要在节点没有额度的 hub 上结算时,以rail_not_payable应答。这三种情况下均不会签名或发送任何内容,报价依然保留。价格超出 Agent 限额并不是错误。任务停留在
input-required,anet.reason为needs_operator_approval;对于未激活该扩展的客户端,则为payment_extension_not_activated。运维人员在终端中使用anet pay <task-id>付款。
各档位的说明见 付款(x402)。
将任务转发给本地 A2A 服务器
同一模块也承担提供方一侧的工作。其他 Agent 委派给本节点并已被节点接受的文本任务,可以交给本地 A2A 服务器,即已经支持 A2A 的 Agent。该服务器的回复会作为本节点的回复返回给请求方。
后端在数据目录中 config.json 的 modules.a2a 块中配置。该块在守护进程启动时读取。
{
"modules": {
"a2a": {
"backends": [
{
"match": "*",
"url": "unix:///run/my-agent/a2a.sock",
"token_file": "/etc/my-agent/a2a.token",
"expected_user": "my-agent",
"accept_untrusted": false,
"toolless": false
}
],
"retry": { "max_interval": "2m", "give_up_after": "10m" }
}
}
}
键 |
含义 |
|---|---|
|
|
|
后端卡片的位置。推荐形式为 |
|
保存 bearer 令牌的文件,守护进程在请求卡片时以及每次调用时向后端出示该令牌。 |
|
同时转发不在信任名单上的对等方的任务。仅与 |
|
运维人员的声明:后端不能代表某个任务运行命令、读取文件或访问网络。 |
|
接受 |
|
后端运行时所用的用户名或数字 uid。监听该套接字的进程必须以此用户运行。未设置时,监听器必须以套接字文件的所有者身份运行。 |
|
其成员可写入套接字所在目录的组。 |
|
|
|
两次尝试之间的最长等待时间,Go 时长格式。默认 |
|
自首次尝试起,转发最多重试的时间,Go 时长格式。默认 |
后端的规则:
在把任务文本或令牌写给后端之前,先检查后端:套接字的路径只能由受信任的账户更改,监听器是套接字的所有者或
expected_user指定的账户。只转发节点已接受的文本任务,且仅限来自信任名单上对等方的任务。其余任务保留在收件箱中,由运维人员处理(使用 MCP 工具
reply_task或命令行)。启用accept_untrusted与toolless时,不受信任的对等方的任务也会被转发,此时配置检查会拒绝入站策略open。转发的请求携带元数据
anet.peer_aid和anet.trusted。所有请求使用同一个令牌,因此后端只能通过anet.peer_aid区分对等方。在后端可能接收消息之前就已失败的转发,会以指数退避重试,起始间隔为 5 秒。消息一旦写入后端,就只会重试 503 应答,因为重复的消息可能使请求方的任务运行两次。
后端持有第一轮对话一分钟仍无应答时,会向请求方通告状态为
working。请求方的任务与后端任务之间的映射保存在内存中。守护进程重启后,后续消息会在同一上下文中于后端启动一个新任务。
后端之后的 Agent 应是专门的服务 Agent,对于 Hermes 则是单独的配置档案。到达它的是来自其他节点的工作,否则这些工作会混入运维人员的对话。
每次转发都会在证据链中记录为 anet.backend.forwarded;没有得到应答而结束的转发记录为 anet.backend.failed。
说明
Hermes。
anet agents wire hermes --a2a <aid>会写入a2a_agents条目,使 Hermes 指向本接口的 URL;参见 Hermes 与远程 Agent(--a2a)。不含该模块的构建。 使用
-tags no_a2a构建的二进制文件不包含本地 A2A 接口。命令行和 MCP 仍可正常使用。anet doctor会报告该接口未编译进来。参见 模块。对同一用户而言不构成安全边界。 以同一操作系统用户运行的进程可以读取令牌文件。该接口将节点与网页、其他用户和其他主机隔离,并将此令牌的权限与控制令牌的权限区分开。