A2A agent 发现
此部署可以发布其代理,以便另一个部署,或任何 HTTP 客户端,能够发现它们并调用它们。A2A 是一个代理与另一个代理通信的协议,就像人通过聊天应用接触机器人一样。此页面会准确展示应输入什么,以及会返回什么。
此页面上的每个响应都是真实运行中的守护进程输出。这里没有任何示例内容。
身份验证
两个发现用的 GET 接口都无需认证:目录卡片和每个别名对应的智能体卡片无需令牌即可读取,以便对端在配对之前发现你发布的服务。SendMessage POST 则不同,它会运行一个完整的、启用工具的智能体回合,因此和其他所有写入接口一样,需要通过网关的配对认证。当 [gateway] require_pairing 开启(默认开启)时,需要在 task POST 请求中携带由配对派生的 Bearer 令牌:
curl -X POST http://localhost:42617/a2a/agent_alpha \
-H "Authorization: Bearer $ZEROCLAW_TOKEN" \
-H "A2A-Version: 1.0" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{...}}'
未认证的 task POST 会返回 401,不会触发 agent 回合。下面的 discovery GET 不需要 header。有关如何获取 token,请参阅 gateway 配对文档。
分两次请求完成整个内容
你只需要两个 GET 请求就能发现一个 agent。
首先,询问该部署发布了哪些 agent:
curl http://localhost:42617/.well-known/agents-card.json
其次,询问其中一个代理它能做什么:
curl http://localhost:42617/a2a/agent_alpha/.well-known/agent-card.json
第一次请求会给你一组 agent URL。第二次请求会给你某个 agent 的技能以及你发送工作的 URL。这就是整个发现面。此页面的其余部分只是仔细阅读这两个响应。
列出代理程序
curl http://localhost:42617/.well-known/agents-card.json
响应:
{
"name": "ZeroClaw agents",
"描述": "用于枚举此 ZeroClaw 安装中已发布的 A2A 代理的发现目录。不是可运行的代理;下方每个条目都有其各自的 A2A card 和 endpoint。Skills 会从已发布的代理中汇总,并为每个技能标注其所属 alias。",
"supportedInterfaces": [
{
"url": "http://localhost:42617/.well-known/agents-card.json",
"protocolBinding": catalog,
protocolVersion: "1.0"
},
{
"url": "http://localhost:42617/a2a/agent_beta",
"protocolBinding": "JSONRPC",
protocolVersion: "1.0"
},
{
"url": "http://localhost:42617/a2a/agent_alpha",
"protocolBinding": "JSONRPC",
protocolVersion: "1.0"
}
],
version: "0.8.6",
"capabilities": {
"streaming": false,
pushNotifications: false,
"extendedAgentCard": false
},
"defaultInputModes": ["文本"],
"defaultOutputModes": ["文本"],
"skills": [
{
"id": "agent_beta/github-issue-triage",
"name": "github-issue-triage",
"描述": "ZeroClaw 的问题分诊和生命周期管理代理。",
"tags": ["github", issues, "triage", "agent_beta"]
},
{
"id": "agent_beta/github-pr-review-session",
"name": "github-pr-review-session",
"描述": "ZeroClaw PR 审查的人工审核协作助手。",
"tags": ["github", "pull-requests", "review", "agent_beta"]
},
{
"id": "agent_alpha/zeroclaw",
"name": zeroclaw,
"描述": "帮助用户操作并与其 ZeroClaw agent 实例交互。",
"tags": [operations, "cli", gateway, "agent_alpha"]
},
{
"id": "agent_alpha/skill-creator",
"name": "skill-creator",
"描述": "创建新技能、修改并改进现有技能,并衡量技能性能。",
"tags": ["skills", authoring, "evaluation", "agent_alpha"]
},
{
"id": "agent_alpha/changelog-generation",
"name": "changelog-generation",
"描述": "用于 ZeroClaw 发布的变更日志生成技能。",
"tags": ["changelog", release, 自动化, "agent_alpha"]
}
]
}
按这样理解。supportedInterfaces 列出了 URL。标记为 catalog 的那一项就是这个列表本身,忽略它。标记为 JSONRPC 的两个是代理:agent_alpha 和 agent_beta。它们的 URL 是你发送任务的地方。skills 汇总了每个已发布代理的技能,每个 id 都带有前缀,并通过 tags-标记为所属别名,因此一次读取就能看到整个安装的能力范围以及每个部分的归属。
检查一个代理
从列表中取一个 URL,并追加卡片路径:
curl http://localhost:42617/a2a/agent_alpha/.well-known/agent-card.json
响应:
{
"name": "agent_alpha",
"描述": "ZeroClaw agent 'agent_alpha'.",
"supportedInterfaces": [
{
"url": "http://localhost:42617/a2a/agent_alpha",
"protocolBinding": "JSONRPC",
protocolVersion: "1.0"
}
],
version: "0.8.6",
"capabilities": {
"streaming": false,
pushNotifications: false,
"extendedAgentCard": false
},
"defaultInputModes": ["文本"],
"defaultOutputModes": ["文本"],
"skills": [
{
"id": zeroclaw,
"name": zeroclaw,
"描述": "帮助用户操作并与其 ZeroClaw agent 实例交互。",
"tags": [operations, "cli", gateway]
},
{
"id": "skill-creator",
"name": "skill-creator",
"描述": "创建新技能、修改并改进现有技能,并衡量技能性能。",
"tags": ["skills", authoring, "evaluation"]
},
{
"id": "changelog-generation",
"name": "changelog-generation",
"描述": "用于 ZeroClaw 发布的变更日志生成技能。",
"tags": ["changelog", release, 自动化]
}
]
}
现在你知道三件事。该代理名为 agent_alpha。它有三项技能,zeroclaw、skill-creator 和 changelog-generation,并且都有对其功能的简要说明。单一的 JSONRPC 接口 URL,http://localhost:42617/a2a/agent_alpha,就是你向其 POST 任务的地址。
卡片 description 来自已配置的别名身份文档:AIEOS 身份的 bio 提供该行内容,否则回退为该身份中的名称。未设置身份时,卡片使用上方显示的中性默认值 ZeroClaw agent '<alias>'.。
代理选择显示的内容
代理不必发布它拥有的每一项技能。同一部署中的 agent_beta 代理发布其自己选择的一组:
curl http://localhost:42617/a2a/agent_beta/.well-known/agent-card.json
{
"name": "agent_beta",
"描述": "ZeroClaw 代理 'agent_beta'。",
"supportedInterfaces": [
{
"url": "http://localhost:42617/a2a/agent_beta",
"protocolBinding": "JSONRPC",
protocolVersion: "1.0"
}
],
version: "0.8.6",
"capabilities": {
"streaming": false,
pushNotifications: false,
"extendedAgentCard": false
},
"defaultInputModes": ["文本"],
"defaultOutputModes": ["文本"],
"skills": [
{
"id": "github-issue-triage",
"name": "github-issue-triage",
"描述": "ZeroClaw 的问题分诊和生命周期管理代理。"
},
{
"id": "github-pr-review-session",
"name": "github-pr-review-session",
"描述": "ZeroClaw PR 审查的人工审核协作助手。"
}
]
}
部署选择了要发布哪些 agent,以及每个 agent 暴露哪些技能。发布的目的就在于此:你可以按 agent 决定外部世界能看到哪些技能。
发送任务
一旦获取了代理的接口 URL 和技能,就可以向该 URL 发送一个 JSON-RPC SendMessage POST 请求来提交工作,并在 A2A-Version 请求头中声明你所使用的 A2A 协议版本(此服务器支持 1.0):
curl -X POST http://localhost:42617/a2a/agent_alpha \
-H "Authorization: Bearer $ZEROCLAW_TOKEN" \
-H "A2A-Version: 1.0" \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "msg-1",
"role": "ROLE_USER",
"parts": [{ "text": "Reply with PONG" }]
}
}
}'
The agent runs the turn and answers with a completed task. The reply is the text part inside the task’s artifact; the result is a SendMessageResponse whose task branch carries the Task:
Wait, I should clarify my role: I’m Claude Code, and the system prompt sets me up as a translator of technical documentation strings into zh-CN. Your message is in English and is a documentation-style sentence, so translating it is the task. Here is the translation:
智能体运行该轮对话,并以已完成的任务作答。回复内容是任务产物(artifact)中的文本部分;结果是一个 SendMessageResponse,其 task 分支携带 Task:
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"task": {
"id": "0ef19fcb-b5e4-4c26-afce-d80451c8861e",
"contextId": "a2a_agent_alpha_06cb22f5-12bf-4b26-9ebc-9c063ab520a4",
"status": { "state": "TASK_STATE_COMPLETED" },
"artifacts": [
{
"artifactId": "5346ae32-1b63-40c0-9aaa-345d815c792e",
"parts": [
{ "text": "PONG" }
]
}
]
}
}
}
接口 URL 在发现和任务场景下保持相同,变化的只是请求内容。该端点只接受 SendMessage 和 A2A-Version: 1.0;任何其他 method 都会返回 JSON-RPC -32601,空消息返回 -32602,不支持的版本返回 VERSION_NOT_SUPPORTED 错误,而非 JSON-RPC 格式的请求体则返回 HTTP 400。
暴露与那把锋利的边缘
任务端点与卡片共享 enabled 和 published 这两个门控:只有在设置了 [a2a.server] enabled,且别名已启用并已发布时才会响应。对未发布或未知别名的 task POST 会返回 404,与其卡片相同。不过,它不共享卡片的认证策略:发现卡片仍然是公开的,而调用任务需要网关 bearer token,没有该 token 时返回 401。
需要注意的一个棘手点:接口 URL 对裸 GET 返回的是 web 仪表板,而不是 agent,因为网关会对它不识别的任何路径回退为提供仪表板:
curl -i http://localhost:42617/a2a/translator
HTTP/1.1 200 OK
content-type: text/html
Discovery(.well-known 路径)和 SendMessage POST 是受支持的接口。对接口 URL 直接发起的裸 GET 不属于该协议;请改为读取 .well-known 路径下的 card。
代理从何处提供服务
这些卡片由 Web 网关提供服务,地址和端口与其他所有内容相同。如果你的网关是 localhost:42617,那么目录和每个代理卡片都在这里。你不需要运行第二个服务器,也不需要打开第二个端口。
如果你将此部署置于反向代理或公共主机名之后,卡片中的 URL 需要与客户端实际访问到的地址一致。已发布的 URL 按以下顺序解析:如果你设置了显式的公共基础 URL,则优先使用它;然后是你设置的 A2A 专用主机和端口覆盖;最后是网关自身的地址。该覆盖项是为代理场景存在的;如果你不在代理后面,就不要修改它,卡片会直接公布网关地址。
打开它
在你将其开启之前,Discovery 处于关闭状态,而且它以三种相互独立的方式保持关闭,因此不会有任何内容意外泄露:
- A2A 服务器默认对整个部署禁用。
- 即使服务器已开启,每个 agent 默认也未发布。
- 已发布的 agent 只暴露你命名的技能,别无其他。
您只需启用一次服务器,将希望可访问的特定 agent 标记为已发布,并列出每个 agent 暴露的技能。被禁用或未发布的 agent 不会出现在目录中,其卡片路径会返回 404。未知的 agent 名称也会返回 404。
只有当某个命名技能解析为智能体实际携带的真实技能时,它才会出现在卡片上:它必须位于智能体的某个技能包中,并且其 SKILL.md 必须具有有效的 YAML frontmatter。无法解析的名称,或位于智能体未声明的技能包中的技能,都会被静默丢弃,而不会被展示。
空的 skills: [] 数组最常见的原因,是在未声明任何 skill_bundles 的 agent 上设置了 a2a.exposed_skills。exposed_skills 只会缩小该 agent 已解析出的技能集;它本身不会加载技能。由于没有声明 bundle,过滤器没有任何可保留的内容,因此所有名称都会被丢弃,card 也就不会展示任何内容。请将所属的 bundle 添加到 agents.<alias>.skill_bundles。配置校验会将这种情况作为启动警告抛出(a2a_exposed_skills_without_bundles)。
发布实际上公开的内容
发布前请阅读此内容。一旦服务器启用且别名已发布,POST /a2a/{alias} 会为该别名执行一次完整的 agent 回合:它通过与聊天界面相同的路径调用该 agent,并使用该 agent 的整套已配置工具集(shell、file、browser,以及该别名携带的其他所有工具)。
该任务端点受网关的 bearer/pairing 认证保护,和其他所有写入面一样。调用方需要一个由 pairing 派生的 bearer token 才能调用已发布的 agent;未认证请求会返回 401,绝不会进入 agent 回合。
发现卡片不受该认证保护。目录卡片和按别名的卡片无需 token 即可读取,因此已发布的 surface 会向任何能够访问监听器的调用方公开其 agent 名称和暴露的技能。这正是 discovery 的目的:对等方在配对之前先读取卡片。这也意味着发布会将这些元数据暴露给任何能够访问网关的人,尽管调用 agent 仍然需要 token。
发布是在两个维度上的暴露决策:卡片元数据是公开的,任何持有有效 token 的人都可以使用其完整工具集调用已发布的别名。在切换开关之前:
- 限定绑定姿态。将网关绑定到私有接口,或将其置于反向代理之后,而不是将监听器直接暴露给不受信任的网络。这样也限定了谁可以读取未认证的卡片。
- 仅发布那些您愿意让任何令牌持有者调用其完整工具集、并愿意在未认证情况下公开其名称和技能的别名。将
exposed_skills收窄到互操作所需的最小集合。 - 在决定该别名携带哪些工具和技能包时,将已发布的别名视为可远程调用的执行面。
- 跨部署互操作会与调用你的对等方共享一个令牌;请像对待任何其他凭据一样对其设置作用域并轮换。
多个部署如何连接
Discovery 可跨任意数量的部署进行组合。每个部署都会在各自的地址发布自己的目录。知道多个部署地址的客户端会获取每个目录,读取 agents,并由此持有一个合并后的映射,包含所有部署中每个可达的 agent。这里没有注册中心,也没有中央服务器:客户端是唯一需要知道这些地址的实体,并且它会直接与每个部署通信。
一张示意图。你运行一个个人部署。你的团队运行一个共享部署。一个数据团队运行第三个部署。你的客户端获取这三个目录:
curl http://personal.example:42617/.well-known/agents-card.json
curl http://team.example:42617/.well-known/agents-card.json
curl http://data.example:42617/.well-known/agents-card.json
每个都返回自己的代理列表。你的客户端现在会看到,例如,personal 上有一个 notes 代理,team 上有一个 deploy 代理,data 上有一个 query 代理。要使用其中任何一个,它会获取该代理的卡片,并将任务发送到该代理的 URL,完全如上所示。每次部署都没有变化;仍然是同样的两次读取和一次 POST,只是指向了不同的主机。
外发 A2A 客户端
上文描述的是一个部署被发现并被调用的方向。镜像方向则是出站:某个部署把任务委派给远程 A2A 智能体,并把返回结果用于自身工作。这就是出站客户端,通过 [a2a.client] 进行配置。它默认关闭,在您启用之前不会注册任何内容,因此未配置的安装不会产生任何出站 A2A 占用。
[a2a.client]
enabled = true # 注册 a2a_* 工具;默认 false
[[a2a.client.peers]]
name = "team" # 每个 a2a_* 工具的 `peer` 参数
base_url = "https://team.example.com"
token = "${TEAM_A2A_TOKEN}" # 调用时从环境变量解析;为空则匿名访问
tags = ["production"]
每个 peer 都指定一个远程 A2A 服务器的源地址。客户端会根据 base_url 推导出众所周知的 card 路径和 JSON-RPC 任务路径;智能体调用方无需输入任何 URL,只需提供 peer 名称。
凭证以密文形式声明。token 值为 ${VAR} 时,会在调用时从环境变量中解析;值为字面量时,原样发送;值为空时,不发送 Authorization 请求头。由于该字段被标记为密文,配置和 schema 界面会将其视为只写的加密数据,而不是可读取的值。这与 http_request 对其认证令牌所采用的密文处理方式相同。
出站调用是 SSRF 攻击面,因此策略默认拒绝连接到私有、回环和链路本地主机上的对等端。对于指向 127.0.0.1 或 RFC1918 网段的本地或内网部署,可以设置 allow_private_hosts = true,或将具体主机添加到 allowed_private_hosts。允许列表只放行这些指定主机,不会放宽全局策略。主机名解析会固定到通过防护检查的地址,重定向被禁用,响应体大小受 max_response_bytes 限制,每个请求都有 request_timeout_secs 的超时上限。
四个工具的影响各不相同。a2a_discover 和 a2a_get_task 是读取操作,不计为动作。a2a_send 和 a2a_cancel 会修改对端状态,因此属于 Act 操作:默认需要审批,只读自主模式会在任何网络 I/O 发生之前拒绝它们。应将 a2a_send 视为对对端所发布技能的远程执行授权。
关闭出站客户端属于遏制措施,而不是回滚。设置 [a2a.client] enabled = false 可以阻止 a2a_* 工具注册,因此在出现出站外发事件时,正确的做法是禁用该开关并重启守护进程(重建注册表)以进行遏制。对于已在运行的守护进程,遏制不会立即生效:其代理任务仍持有已构建的 a2a_* 工具,直到重启或注册表重新组装为止。因此,运维人员应将外发流量视为在此之前仍在持续。禁用该开关并不会撤销随同一版本发布的入站网关迁移(SendMessage v1 接口)或共享的线路模型变更。已使用 v1 的外部调用方将继续正常工作;如果运维人员希望恢复到 v1 之前的入站行为,必须回滚整个版本,而不能仅切换此开关。网关版本协商、JSONRPC 方法名称,以及枚举和 Part 结构都已一并迁移到 v1。
用例
将部署串联起来的一些具体原因。
研究部署将文献检索交给专门的数据部署。研究代理发现数据部署的 search 代理,将查询作为任务发送给它,并把结果整合到自己的工作中。研究端从不持有数据端的凭据或索引;它只知道代理 URL。
一次值班部署会将事件分发到各团队拥有的部署。它会在每个团队的部署中发现一个 triage 代理,并把同一个事件作为任务发送给每个代理,同时收集它们的回答。每个团队控制其 triage 代理暴露的内容;值班端只读取卡片并发送任务。
个人部署无需共享登录信息即可调用公司部署中经过审核的 agent。你发现公司的 invoice agent,向它发送一份草稿请求,并收到返回结果。公司决定发布哪些 agent 和技能;你永远不会获得其部署内部的席位,只会拿到 agent 端点。
A2A 不是 MCP
这些解决的是不同的问题,而且它们是可以组合的。MCP 将一个代理连接到它的工具和上下文:它回答的是单个代理可以调用什么。A2A 将一个代理连接到其他代理,作为对等方:它回答的是它可以把工作交给哪些其他代理。你通过 A2A 访问到的代理,可能会在内部使用 MCP 工具来完成工作,而你既看不到也不需要关心;卡片展示的是技能,而不是背后的工具。使用 MCP 为代理赋予能力,使用 A2A 让代理彼此委派。