A2A Agent Card 实现指南:构建、校验与签名
这篇指南带您从一个空文件,走到一张可以放心发布的 Agent Card。它假定您已经知道 Agent Card 是什么;如果还不清楚,请先读 Agent Card 。逐字段的解释仍然留在概念页。这一页补上概念页没有的部分:填写卡片时要做的决策、可以直接运行的校验程序,以及从运行时错误反推卡片问题的对照表。
所有内容都对照 A2A v1.0.1 核对过:该标签下的 a2a.proto、规范本身,以及规范自带的示例卡。规范与 proto 不一致时,以 proto 为准(规范第 1.4 节)。
路线
- 声明接口:客户端在哪里、以什么方式访问智能体。
- 声明能力:您承诺了什么,以及没有承诺时要返回的错误。
- 描述技能。
- 声明安全方案。
- 校验:反复运行校验程序,直到输出
OK: no findings。 - 签名(可选),然后发布并设置缓存。
签名放在最后,因为签名覆盖的是最终内容:签名之后再改任何内容,签名都会失效。
1. 声明接口
supportedInterfaces 是一个列表,每一项包含 url、protocolBinding 和 protocolVersion,三个字段都必填。列表按顺序读取,所以顺序本身就是契约的一部分。
| 您的情况 | 声明方式 | 规则与出处 |
|---|---|---|
| 一个端点、一种传输 | 一项 | 这三个字段在 a2a.proto 中标记为 REQUIRED |
| 多种传输 | 每个「协议绑定 + URL」一项,最优先的放最前 | 列表应当按优先顺序声明所有支持的组合,第一项是首选接口(第 8.3.1 节) |
| 多种传输共用一个端点 | 每种绑定各写一项,url 重复使用 | URL 可以重复使用(第 8.3.1 节) |
| 一个端点后面有多个智能体或租户 | 在该项上设置 tenant | 客户端必须把它复制到每条请求消息中;协议不定义其格式,含义由服务器决定(AgentInterface.tenant,第 8.3.2 节)。见多租户 |
JSONRPC、GRPC、HTTP+JSON 之外的绑定 | 用 URI 作为 protocolBinding | 自定义绑定应当用 URI 标识,绑定发生破坏性变更时必须使用新的 URI(第 5.8 节)。见自定义协议绑定 |
| 本地开发 | 普通的 http:// URL | proto 要求生产环境使用绝对 HTTPS URL,所以切换之前校验程序会给出警告 |
2. 声明能力
能力声明是一份契约。能力为 false 或缺失,而客户端仍然使用它时,智能体必须返回特定的错误(第 3.3.4 节)。只对您已经实现的能力声明 true。
| 能力 | 声明 true 的前提 | 为 false 或缺失时,智能体必须返回 |
|---|---|---|
streaming | SendStreamingMessage 和 SubscribeToTask 可用 | 两者都返回 UnsupportedOperationError |
pushNotifications | 推送通知配置的创建、获取、列出、删除可用 | PushNotificationNotSupportedError |
extendedAgentCard | 已认证的 GetExtendedAgentCard 能返回已配置的卡片 | UnsupportedOperationError;如果声明了 true 却没有配置扩展卡片,则返回 ExtendedAgentCardNotConfiguredError |
带 required: true 的 extensions[] | 没有这个扩展,您的智能体就无法工作 | 服务器要求使用该扩展而客户端没有声明支持时,返回 ExtensionSupportRequiredError |
错误名称及其在 JSON-RPC、gRPC、HTTP 中的映射见 A2A 错误码
。客户端在每个请求中通过 A2A-Extensions 服务参数(以逗号分隔的扩展 URI 列表)指明想使用的扩展;见 A2A 扩展
。
streaming、pushNotifications 和 extendedAgentCard 都是 optional 布尔值。第 3.3.4 节把 false 和缺失同等对待,但签名不会:显式写出的可选字段,即使是 false,也属于被签名的内容;省略的字段则不属于(第 8.4.1 节)。选定一种写法,并保证签名时与对外提供时完全一致。
3. 描述技能
一个技能需要 id、name、description,以及 tags 中至少一项;examples、inputModes、outputModes 和 securityRequirements 是可选的。每张卡片都值得做三项检查:
skills为空列表的卡片无效,没有tags的技能也无效。标记为必填的数组必须至少包含一个元素(第 5.7 节)。- 保证
id在整张卡片内唯一。proto 把它描述为唯一标识符,校验程序遇到重复会给出警告。 - 技能自己的
inputModes和outputModes会替换该技能的卡片默认值,所以要如实声明。
技能是写给阅读卡片的人或智能体看的描述,没有任何 A2A 操作会选择某个技能。细节和规范中的示例见 AgentSkill 。
4. 声明安全方案
securitySchemes 是一个映射,键是您自定义的名称,值是一个方案,每个方案恰好设置五个键中的一个:apiKeySecurityScheme、httpAuthSecurityScheme、oauth2SecurityScheme、openIdConnectSecurityScheme 或 mtlsSecurityScheme。securityRequirements 列出客户端必须满足哪些方案以及哪些 scope:
"securityRequirements": [{ "schemes": { "google": { "list": ["openid", "profile", "email"] } } }]- 要求中用到的每个名称,无论在卡片上还是在技能上,都必须在
securitySchemes中定义。 oauth2SecurityScheme恰好设置一种流程。新卡片不要声明implicit和password:v1.0.1 的 proto 把二者都标记为deprecated。- 如果设置了
extendedAgentCard: true,至少要声明一个方案。扩展卡片面向已认证的客户端,没有任何方案,客户端就没有可用来认证的手段。 - 每种方案和流程内部的必填字段见文末的表格 。
各方案类型和 OAuth 2.0 流程的说明见 SecurityScheme 。
5. 校验卡片
把下面的程序保存为 agent-card-lint.jq。它只读取卡片,不发起任何网络请求,应用前面各节的规则,再加上 v1.0.1 a2a.proto 中的 REQUIRED 字段。
# Agent Card lint for A2A v1.0.1.
# Usage: jq -r -f agent-card-lint.jq agent-card.json
# Checks come from the REQUIRED annotations in a2a.proto, specification sections 5.7, 8.3 and 8.4,
# and the v0.3 -> v1.0 renames. ERROR lines break the specification; WARN lines are advisory.
def blank: . == null or . == "" or . == [];
# Required strings, arrays (at least one element, section 5.7) and objects.
def need($o; $path; $strs; $arrs; $objs):
if ($o | type) != "object" then "ERROR \($path): expected a JSON object"
else
($strs[] as $k | select($o[$k] | blank) | "ERROR \($path)/\($k): required string is missing or empty"),
($arrs[] as $k | select(($o[$k] | type) != "array" or ($o[$k] | length) == 0)
| "ERROR \($path)/\($k): required array is missing or empty"),
($objs[] as $k | select($o[$k] == null) | "ERROR \($path)/\($k): required object is missing")
end;
def v03: {
"url": "supportedInterfaces[0].url",
"preferredTransport": "supportedInterfaces[].protocolBinding",
"additionalInterfaces": "supportedInterfaces[]",
"protocolVersion": "supportedInterfaces[].protocolVersion",
"supportsAuthenticatedExtendedCard": "capabilities.extendedAgentCard",
"security": "securityRequirements"
};
def card_fields: ["name", "description", "supportedInterfaces", "provider", "version", "documentationUrl",
"capabilities", "securitySchemes", "securityRequirements", "defaultInputModes", "defaultOutputModes",
"skills", "signatures", "iconUrl"];
# Scheme type -> [required strings, required objects]; flow type -> same.
def scheme_req: {
"apiKeySecurityScheme": [["location", "name"], []],
"httpAuthSecurityScheme": [["scheme"], []],
"oauth2SecurityScheme": [[], ["flows"]],
"openIdConnectSecurityScheme": [["openIdConnectUrl"], []],
"mtlsSecurityScheme": [[], []]
};
def flow_req: {
"authorizationCode": [["authorizationUrl", "tokenUrl"], ["scopes"]],
"clientCredentials": [["tokenUrl"], ["scopes"]],
"deviceCode": [["deviceAuthorizationUrl", "tokenUrl"], ["scopes"]],
"implicit": [[], []],
"password": [[], []]
};
def requirement_names($path):
(. // []) | to_entries[] | .key as $i | .value
| if (type == "object" and (.schemes | type) == "object") then (.schemes | keys[] | {name: ., at: "\($path)/\($i)"})
else {bad: "\($path)/\($i)"} end;
. as $card
| if type != "object" then "ERROR /: an Agent Card must be a JSON object" else
[
need($card; ""; ["name", "description", "version"];
["supportedInterfaces", "defaultInputModes", "defaultOutputModes", "skills"]; ["capabilities"]),
(keys[] | select(. as $k | card_fields | index($k) | not) | . as $k
| if v03[$k] then "ERROR /\($k): v0.3 field, use \(v03[$k]) in v1.0"
else "WARN /\($k): not an AgentCard field in v1.0.1 (typo?)" end),
(($card.supportedInterfaces // []) | to_entries[] | .key as $i | .value as $v
| need($v; "/supportedInterfaces/\($i)"; ["url", "protocolBinding", "protocolVersion"]; []; []),
(select(($v | type) == "object") | $v
| (select((.url // "https://") | startswith("https://") | not)
| "WARN /supportedInterfaces/\($i)/url: production URLs must be absolute HTTPS"),
(select((.protocolBinding // "JSONRPC") as $b | ["JSONRPC", "GRPC", "HTTP+JSON"] | index($b) == null and ($b | test("^[a-z][a-z0-9+.-]*:") | not))
| "WARN /supportedInterfaces/\($i)/protocolBinding: not JSONRPC, GRPC or HTTP+JSON; custom bindings should use a URI (section 5.8)"),
(select((.protocolVersion // "1.0") | test("^[0-9]+\\.[0-9]+$") | not)
| "WARN /supportedInterfaces/\($i)/protocolVersion: expected major.minor such as 1.0"))),
(select($card.provider != null) | need($card.provider; "/provider"; ["url", "organization"]; []; [])),
(($card.skills // []) | to_entries[] | .key as $i | .value as $s
| need($s; "/skills/\($i)"; ["id", "name", "description"]; ["tags"]; []),
(select(($s | type) == "object" and ($s | has("security"))) | "ERROR /skills/\($i)/security: v0.3 field, use securityRequirements in v1.0")),
((($card.skills // []) | map(select(type == "object") | .id) | group_by(.)[] | select(length > 1 and .[0] != null) | .[0])
| "WARN /skills: id \"\(.)\" is used more than once"),
(($card.securitySchemes // {}) | to_entries[] | .key as $n | .value as $v
| "/securitySchemes/\($n)" as $p
| ($v | if type == "object" then keys else [] end) as $ks
| if ($ks | length) != 1 or (scheme_req | has($ks[0]) | not)
then "ERROR \($p): set exactly one of \(scheme_req | keys_unsorted | join(", "))"
else $ks[0] as $t
| need($v[$t]; "\($p)/\($t)"; scheme_req[$t][0]; []; scheme_req[$t][1]),
(select($t == "oauth2SecurityScheme" and ($v[$t].flows | type) == "object")
| ($v[$t].flows | keys) as $fk
| "\($p)/\($t)/flows" as $fp
| if ($fk | length) != 1 or (flow_req | has($fk[0]) | not)
then "ERROR \($fp): set exactly one of \(flow_req | keys_unsorted | join(", "))"
else need($v[$t].flows[$fk[0]]; "\($fp)/\($fk[0])"; flow_req[$fk[0]][0]; []; flow_req[$fk[0]][1]),
(select(["implicit", "password"] | index($fk[0])) | "WARN \($fp)/\($fk[0]): deprecated in v1.0.1")
end)
end),
([$card.securityRequirements | requirement_names("/securityRequirements"),
(($card.skills // []) | to_entries[] | .key as $i | .value | select(type == "object")
| .securityRequirements | requirement_names("/skills/\($i)/securityRequirements"))][]
| if .bad then "ERROR \(.bad): a requirement is an object with a \"schemes\" map"
elif (.name as $n | ($card.securitySchemes // {}) | has($n) | not) then "ERROR \(.at): scheme \"\(.name)\" is not defined in securitySchemes"
else empty end),
(select($card.capabilities.extendedAgentCard == true and (($card.securitySchemes // {}) | length) == 0)
| "WARN /capabilities/extendedAgentCard: GetExtendedAgentCard needs authentication, but no securitySchemes are declared"),
(($card.capabilities.extensions // []) | to_entries[] | select(.value.uri | blank)
| "WARN /capabilities/extensions/\(.key): extension without a uri"),
(($card.signatures // []) | to_entries[] | .key as $i | .value as $g
| need($g; "/signatures/\($i)"; ["protected", "signature"]; []; []),
(select(($g | type) == "object" and ($g.protected | blank | not)) | $g.protected
| (try (gsub("-"; "+") | gsub("_"; "/") | @base64d | fromjson) catch null) as $h
| if $h == null then "ERROR /signatures/\($i)/protected: not base64url-encoded JSON"
else (("alg", "kid") as $k | select($h[$k] | blank) | "ERROR /signatures/\($i)/protected: header needs \($k)"),
(select($h.typ != "JOSE") | "WARN /signatures/\($i)/protected: typ should be JOSE") end))
] | if length == 0 then "OK: no findings" else .[] end
end对您的卡片运行它。ERROR 行表示卡片无效或按现有写法无法工作:缺少必填字段、残留 v0.3 字段、引用了未声明的内容。WARN 行只是建议。
jq -r -f agent-card-lint.jq agent-card.json在 CI 中,遇到任何 ERROR 就让构建失败:
if jq -r -f agent-card-lint.jq agent-card.json | grep -q '^ERROR'; then exit 1; fi它对官方示例卡的判断
想拿一张卡片来试,可以复制规范第 8.5 节的 JSON,或者从 a2aproject/A2A 的克隆中提取:
git show v1.0.1:docs/specification.md \
| awk '/^### 8\.5\. Sample Agent Card/{f=1} f&&/^```json/{g=1;next} g&&/^```/{exit} g' > agent-card.json对这份示例的两个版本运行,校验程序给出不同的结论:
| 卡片 | 结果 |
|---|---|
标签 v1.0.1 中的第 8.5 节示例 | ERROR /security: v0.3 field, use securityRequirements in v1.0 |
main 分支(提交 98279d6,2026-10-20)中的第 8.5 节示例 | OK: no findings |
两份示例只差一行:标签版本仍用 v0.3 的写法书写安全要求,main 已经更正。如果您从旧版本的规范复制卡片,请先检查这个字段。SecurityScheme
对此有更详细的说明。
故意改坏它
检查程序只有在该失败时真的失败,才有价值。下面每一行在 main 示例上只改一处,并给出由此产生的唯一一条结果:
jq '<edit>' agent-card.json | jq -r -f agent-card-lint.jq| 修改 | 结果 |
|---|---|
del(.name) | ERROR /name: required string is missing or empty |
del(.skills[0].tags) | ERROR /skills/0/tags: required array is missing or empty |
.supportedInterfaces[0].protocolBinding="REST" | WARN /supportedInterfaces/0/protocolBinding: not JSONRPC, GRPC or HTTP+JSON; custom bindings should use a URI (section 5.8) |
.securityRequirements=[{"schemes":{"nope":{"list":[]}}}] | ERROR /securityRequirements/0: scheme "nope" is not defined in securitySchemes |
.securityRequirements=[{"google":["openid"]}] | ERROR /securityRequirements/0: a requirement is an object with a "schemes" map |
.securitySchemes.google.openIdConnectSecurityScheme={} | ERROR /securitySchemes/google/openIdConnectSecurityScheme/openIdConnectUrl: required string is missing or empty |
.capabilities.extensions=[{"required":true}] | WARN /capabilities/extensions/0: extension without a uri |
.signatures[0].protected="@@@" | ERROR /signatures/0/protected: not base64url-encoded JSON |
它不检查什么
- URL 是否有响应,以及服务器是否真的实现了卡片声明的内容。
- 密码学意义上的签名。它只检查
protected头能否解码并包含alg和kid,仅此而已。 - v1.0.1 之后的规则。这个程序针对
v1.0.1的 proto 编写,并在 jq 1.8.1 上测试过。
迁移 v0.3 卡片
校验程序会指出它发现的每一个 v0.3 字段,以及替代它的 v1.0 字段:
| v0.3 字段 | v1.0 的替代 |
|---|---|
url | supportedInterfaces[0].url |
preferredTransport | supportedInterfaces[].protocolBinding |
additionalInterfaces | supportedInterfaces[] |
protocolVersion | supportedInterfaces[].protocolVersion |
supportsAuthenticatedExtendedCard | capabilities.extendedAgentCard |
security(卡片与技能) | securityRequirements |
这些字段同样出现在 Agent Card 的迁移表中,protobuf 字段编号见 Agent Card Proto 字段编号 。
6. 给卡片签名(可选)
签名让客户端确认卡片真实且未被改动。规范的规则(第 8.4 节):
- 先用 JSON 规范化方案(RFC 8785)对卡片做规范化,再按 JSON Web Signature(RFC 7515)签名。
signatures不参与签名内容。- 遵守字段在场性:省略您没有设置的可选字段和持有默认值的字段,但必填字段和显式设置的可选字段必须保留。
- 在 protected 头中放入
alg、typ和kid;typ应当是JOSE。头中还可以带jku,即密钥集的地址。
不依赖密码学库就能检查的一步,是规范化后的形式。下面这条 jq 命令完成排除 signatures 和键排序,并去掉在场性规则要求丢弃的空 extensions 数组:
jq -S -c 'del(.signatures) | del(.capabilities.extensions | select(. == []))' agent-card.json对规范中规范化示例所用的最小卡片运行它(同一张卡片在 Agent Card 页面上),输出与规范给出的规范化字符串完全一致:
{"capabilities":{"pushNotifications":false,"streaming":false},"defaultInputModes":["text/plain"],"defaultOutputModes":["text/plain"],"description":"An example agent that echoes messages.","name":"Example Agent","skills":[{"description":"Echoes the input.","id":"echo","name":"Echo","tags":["echo"]}],"supportedInterfaces":[{"protocolBinding":"JSONRPC","protocolVersion":"1.0","url":"https://example.com/a2a/v1"}],"version":"1.0.0"}这个示例在规范的 main 分支中;v1.0.1 标签使用的是更短的片段,同一条命令同样能复现它的预期输出。
只把它当作"签名时的内容与对外提供的内容"之间的快速比对。jq 不是 RFC 8785 的实现。在 jq 1.8.1 上,它把 1e2 输出为 1E+2,而 RFC 8785 给出 100;它保留 1.0,而 RFC 8785 给出 1;它按码点排序键,而 RFC 8785 按 UTF-16 码元排序,所以包含 U+FFFF 的键会排在包含 emoji 的键之前,与 RFC 8785 正好相反。Agent Card 很少含有数字,但 AgentExtension.params 是自由结构,可能含有。真正的签名请使用您所用语言的 JCS 库和 JWS 库生成。
7. 发布并设置缓存
把卡片放在 https://{server_domain}/.well-known/agent-card.json(第 8.2 节)。Agent Card 很少变化,所以规范要求服务器帮助客户端缓存(第 8.6.1 节):
- 发送
Cache-Control,其max-age与卡片的更新频率相匹配(应当)。 - 发送由
version或卡片内容哈希派生的ETag(应当)。Last-Modified是可选的。
检查您的服务器实际发送了什么:
curl -sI https://<your-domain>/.well-known/agent-card.json | grep -i -E 'cache-control|etag|last-modified'客户端应当遵循 HTTP 缓存(RFC 9111),缓存过期后发送带 If-None-Match 或 If-Modified-Since 的条件请求。没有 ETag 或 Last-Modified,就没有可供比较的条件,客户端只能重新下载整张卡片。
8. 出问题时
大多数卡片问题表现为智能体返回的错误,或客户端无法继续。下表把每种现象对应到背后的卡片错误。
| 您看到的现象 | 可能的卡片原因 | 修复 |
|---|---|---|
SendStreamingMessage 或 SubscribeToTask 返回 UnsupportedOperationError | capabilities.streaming 为 false 或缺失 | 流式传输可用后再声明 true;否则客户端只能使用非流式的 SendMessage |
任一推送配置操作返回 PushNotificationNotSupportedError | capabilities.pushNotifications 为 false 或缺失 | 四个操作都可用后再声明 true |
GetExtendedAgentCard 返回 UnsupportedOperationError | capabilities.extendedAgentCard 为 false 或缺失 | 声明 true 并配置扩展卡片 |
ExtendedAgentCardNotConfiguredError | 声明了 extendedAgentCard: true,但没有配置扩展卡片 | 配置扩展卡片,或声明 false |
ExtensionSupportRequiredError | 某个扩展是 required: true,而客户端没有在 A2A-Extensions 中声明 | 让客户端声明它;如果智能体没有它也能工作,就改为 required: false |
| 客户端选不出端点,或调用了错误的端点 | 只有 v0.3 的 url、supportedInterfaces 为空,或条目顺序错误 | 运行校验程序;按优先顺序列出接口 |
| 凭据正确但认证失败 | 某条要求引用了不在 securitySchemes 中的方案,或写成了 v0.3 的形状 | 运行校验程序;使用 {"schemes": {"name": {"list": []}}} |
| 签名验证失败 | 签名后内容被改动、signatures 没有被排除、可选字段的在场性不一致,或序列化器不符合 RFC 8785 | 用上面的 jq 命令比对规范化形式,并用 JCS 库签名 |
| 客户端不断重新下载整张卡片 | 没有 Cache-Control 或 ETag | 二者都加上(第 8.6.1 节) |
必填字段速查
下面是 v1.0.1 a2a.proto 中标记为 REQUIRED 的字段,校验程序检查的正是这些。这些表格是由脚本与 proto 比对的,而不是靠肉眼。
| 对象 | 必填字段 |
|---|---|
AgentCard | name, description, version, supportedInterfaces, capabilities, defaultInputModes, defaultOutputModes, skills |
AgentInterface | url, protocolBinding, protocolVersion |
AgentProvider | url, organization |
AgentSkill | id, name, description, tags |
AgentCardSignature | protected, signature |
apiKeySecurityScheme | location, name |
httpAuthSecurityScheme | scheme |
oauth2SecurityScheme | flows |
openIdConnectSecurityScheme | openIdConnectUrl |
authorizationCode 流程 | authorizationUrl, tokenUrl, scopes |
clientCredentials 流程 | tokenUrl, scopes |
deviceCode 流程 | deviceAuthorizationUrl, tokenUrl, scopes |
AgentCapabilities、AgentExtension、mtlsSecurityScheme,以及已弃用的 implicit 和 password 流程没有必填字段。
相关页面
- Agent Card 、AgentSkill 与 SecurityScheme :本指南所填写的对象。
- A2A 智能体发现 :客户端如何发现并信任卡片。
- A2A 错误码 :上面各表使用的错误名称。
- A2A v1.0 有哪些变化 :完整的 v0.3 到 v1.0 迁移指南。
来源:A2A 官方规范:Capability Validation(3.3.4)、Field Presence and Optionality(5.7)、Protocol Binding 标识(5.8)、Discovery Mechanisms、Protocol Declaration Requirements、Agent Card Signing、Sample Agent Card 与 Caching(8.2 至 8.6)
;a2a.proto v1.0.1
;规范在 v1.0.1 标签与 main 提交 98279d6
的示例卡