☰
Nasiko A2A Registry 设计解析:把“Agent 发现“本身做成一个 A2A Agent
2026/9/25 22:57:11 网站建设 项目流程

【免费下载链接】nasiko

Developer Control Plane for your AI Agents

项目地址:https://gitcode.com/gh_mirrors/na/nasiko
点击查看免费下载

在 Nasiko(Developer Control Plane for your AI Agents)中,Agent 之间的通信、发现与代理全部统一在 A2A(Agent-to-Agent)协议之下:控制平面在部署时只向每个 Agent 注入一个环境变量A2A_DISCOVERY_URL,Agent 通过标准的/.well-known/agent-card.json发现"注册表 Agent",再用SendMessage查询它来找到同类。本指南完整解读 docs/A2A_REGISTRY_DESIGN.md 中定义的注册表架构,并对照仓库源码(server/src/registry_a2a.rs、react-agent/src/registry.rs、agents/assistant-agent/main.py 等)说明其落地细节。读完你将掌握:A2A 规范下注册/发现的设计约束、控制平面如何注册与健康巡检 Agent、如何把一个"注册表"伪装成一个普通 A2A Agent、以及代理转发链路上的 ACL、限流与审计是如何串联的。

A2A 规范给注册表留下的设计约束

A2A 协议(Linux Foundation 维护,v1.0)本身对"注册表"几乎没有规定,Nasiko 的注册表设计完全建立在这些约束之上:

  • Agent Card 是注册单元——承载 name、skills、capabilities、security schemes 与 interfaces,即 docs/A2A_PROTOCOL.md 中描述的AgentCard结构;
  • Well-Known URL 是唯一的"标准"发现入口——/.well-known/agent-card.json,必须通过未认证的 GET提供,这是规范里唯一的固定 URL;
  • Extended Agent Card——认证后返回的更丰富卡片(Nasiko 目前未启用,各 Agent 的capabilities.extendedAgentCard均为false);
  • 没有标准注册表 API——注册(CRUD)、查询、发现的 API 完全由实现方自定义;
  • 协议是 pull 型(pull-based)——客户端主动去 well-known URL 拉卡片或查询注册表,规范没有定义 push/注册 webhook 或统一的发现查询格式。

结论很直接:既然规范不给答案,Nasiko 选择自己实现注册表,并且把它做成一个符合 A2A 协议的普通 Agent——这成为整个设计文档的核心决策(见下文"注册表本身就是一个 A2A Agent")。

关键设计决策

设计文档明确列出了五条顶层决策,它们是后续所有机制的前提:

  1. 所有 Agent 间通信都经过控制平面代理,不存在 Agent 直连 Agent;
  2. 每个部署的 Agent skills 不可变——更新 skills 就等于新部署(新版本);
  3. 按能力/标签发现——Agent 不需要按名字认识其他 Agent;
  4. Agent 永远不知道其他 Agent 的私网 IP——它只拿到代理 URL;
  5. 注册表本身就是一个 A2A Agent——Agent 用它们做其他一切事情的同一套 A2A 协议来发现彼此,没有私有 REST API。

第五条直接引出了仓库中的实现:server/src/registry_a2a.rs 的文件头注释明确写着"the control plane acting as the 'registry agent' from docs/A2A_REGISTRY_DESIGN.md",并说明平台 Agent 被注入唯一的 URL(A2A_DISCOVERY_URL),通过向{base}/a2a/v1POST JSONRPCmessage/send来发现同类,然后读取result.artifacts[0].parts[0].data.agents。

Agent 契约:平台强制什么、不强制什么

要在平台上运行,一个 Agent 容器只需满足三条:

  1. 在/.well-known/agent-card.json提供合法 AgentCard schema;
  2. 在声明的 interface URL 上实现A2A JSON-RPC(至少实现SendMessage);
  3. 响应健康检查(A2A 端点返回 HTTP 200)。

而控制平面不能也不试图强制:

  • 内部实现、框架或 LLM 选型;
  • 响应质量;
  • 声明的 skills 是否真的可用(那是开发者自己的责任)。

这套"最小契约"与仓库中各 Agent 的 Dockerfile/入口一致:例如 agents/assistant-agent/main.py 用 Starlette 挂载create_agent_card_routes与create_jsonrpc_routes(handler, rpc_url="/"),容器内统一监听 8000 端口(见PORT默认值),Agent 只负责"卡片合法 + JSON-RPC 可用 + 健康检查 200"。

注册流程:注入、健康检查、抓卡、入库

控制平面部署一个 Agent 容器时执行如下步骤:

  1. 创建容器并注入发现环境变量:
    A2A_DISCOVERY_URL=http://cp:8080 ← base URL of the registry agent
  2. 等待健康检查通过;
  3. 从 Agent 私网 IP 抓取/.well-known/agent-card.json;
  4. 校验卡片 schema;
  5. 将卡片存入注册表数据库;
  6. 卡片从此对其他 Agent 可见。

Skills 在这一刻被冻结——想更新 skills 就必须部署新版本。

仓库侧的证据非常完整:

  • server/src/seed.rs 中,种子 Agent 部署时构建环境变量:env.insert("A2A_DISCOVERY_URL", discovery_url),默认回退到http://host.docker.internal:8080(第 136–138 行);
  • 同一文件里,部署成功后调用crate::agents::utils::fetch_agent_card_with_retry(state.db, state.http_client, agent.id, agent_url)(第 200–206 行),实现"等待健康 → 抓卡 → 校验 → 入库"的完整链路;
  • config/src/lib.rs 中a2a_discovery_url也由A2A_DISCOVERY_URL环境变量提供(第 279 行),config/tests/config.rs对其有断言覆盖。

发现:注册表本身就是一个 A2A Agent

Agent 之间发现彼此不需要私有 REST API——它们对注册表说 A2A。注册表对外暴露标准卡片:

GET {A2A_DISCOVERY_URL}/.well-known/agent-card.json
{ "name": "Nasiko Agent Registry", "description": "Discovers and lists agents by capability, tags, or natural language query", "version": "1.0.0", "supportedInterfaces": [ { "url": "http://cp:8080/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" } ], "capabilities": { "streaming": false, "pushNotifications": false }, "defaultInputModes": ["application/json", "text/plain"], "defaultOutputModes": ["application/json"], "skills": [ { "id": "discover-by-capability", "name": "Discover Agents by Capability", "description": "Find agents that match given tags, capabilities, or natural language description", "tags": ["discovery", "registry", "a2a", "search"], "examples": [ "Find agents that can translate text", "Which agents support streaming?", "List all agents with tag: summarization" ], "inputModes": ["application/json", "text/plain"], "outputModes": ["application/json"] }, { "id": "get-agent-card", "name": "Get Agent Card", "description": "Retrieve the full Agent Card for a specific agent by ID or name", "tags": ["discovery", "registry", "lookup"], "inputModes": ["application/json", "text/plain"], "outputModes": ["application/json"] }, { "id": "list-agents", "name": "List All Agents", "description": "List all active agents with their skills and endpoints", "tags": ["discovery", "registry", "list"], "inputModes": ["application/json"], "outputModes": ["application/json"] } ] }

注意capabilities.streaming: false——注册表只做同步应答,不需要流式。这与 server/src/registry_a2a.rs 的实现一致:它只接受非流式方法并直接返回完整目录 JSON。

发现流程(Agent → Registry,全程 A2A)

Step 1:Agent 抓注册表的卡片(标准 A2A 发现)

GET http://cp:8080/.well-known/agent-card.json → 得到注册表 Agent 的卡片,得知它的 A2A 端点

Step 2:Agent 用 A2A SendMessage 查询注册表

结构化查询(程序化调用更推荐):

POST http://cp:8080/a2a/v1 A2A-Version: 1.0 { "jsonrpc": "2.0", "id": "req-1", "method": "SendMessage", "params": { "message": { "role": "ROLE_USER", "parts": [ { "data": { "action": "discover", "filter": { "tags": ["translation", "multilingual"], "capabilities": { "streaming": true } } } } ] } } }

自然语言查询同样可用:

POST http://cp:8080/a2a/v1 A2A-Version: 1.0 { "jsonrpc": "2.0", "id": "req-2", "method": "SendMessage", "params": { "message": { "role": "ROLE_USER", "parts": [ { "text": "find agents that can translate between languages" } ] } } }

Step 3:注册表返回匹配的 Agent(标准 A2A 响应)

{ "jsonrpc": "2.0", "id": "req-1", "result": { "task": { "id": "task-uuid", "contextId": "ctx-uuid", "status": { "state": "TASK_STATE_COMPLETED" }, "artifacts": [ { "parts": [ { "kind": "data", "data": { "agents": [ { "agent_id": "translation-agent-uuid", "name": "Translation Agent", "description": "Translates between 40+ languages", "skills": [ { "id": "translate-text", "name": "Text Translation", "tags": ["translation", "nlp", "multilingual"] } ], "capabilities": { "streaming": true }, "endpoint": "http://cp:8080/api/agents/translation-agent-uuid" }, { "agent_id": "deepl-agent-uuid", "name": "DeepL Agent", "description": "High-quality translation via DeepL", "skills": [ { "id": "deepl-translate", "name": "DeepL Translation", "tags": ["translation", "multilingual"] } ], "capabilities": { "streaming": false }, "endpoint": "http://cp:8080/api/agents/deepl-agent-uuid" } ] } } ] } ]} } }

Step 4:Agent 调用被发现的 Agent(标准 A2A,经由代理)

POST http://cp:8080/api/agents/translation-agent-uuid A2A-Version: 1.0 { "jsonrpc": "2.0", "id": "req-3", "method": "SendMessage", "params": { "message": { "role": "ROLE_USER", "parts": [ { "text": "Translate 'hello world' to Spanish" } ] } } }

仓库中的真实响应形状

设计文档里的响应是一个"理想化"的 A2A 1.0task包装;仓库实现 server/src/registry_a2a.rs 实际返回的是扁平结构——result.artifacts[0].parts[0].data.agents(agents_response函数,第 144–155 行)。文件头注释明确指出两个树内消费者都按这个形状解析:

  • Python 侧:agents/assistant-agent/main.py 的_discover_agents()遍历artifacts[].parts[],取kind == "data"的 part,读data.agents(第 132–138 行);
  • Rust 侧:react-agent/src/registry.rs 的discover_from_cp()用response.result.pointer("/artifacts/0/parts/0/data/agents")定位数组(第 108–111 行),再把每个条目反序列化为AgentInfo(要求id/name/description/endpoint/skills)。

registry_a2a.rs的测试用例还专门锁定了这两个消费者的读取路径:response_exposes_agents_at_the_react_agent_pointer断言 agents 数组出现在该指针位置,entries_satisfy_both_in_tree_consumers断言每个条目同时携带id/agent_id/name/description/url/endpoint且url == endpoint(第 165–219 行)——这正是设计文档示例中endpoint与agent_id字段在实现中合并归一的结果。

注册表端点的另一个实现细节:它刻意不做认证(Agent 不携带平台凭证——代理在转发前会剥掉authorization头),但做了全局限流;暴露的内容是设计文档本就声明公开的:运行中 Agent 的名字、描述、skills,以及运行时内部端点(VPC 私网 IP / localhost),这些在部署网络之外本就不可达(registry_a2a.rs头部注释)。

端到端:每条路径都是 A2A

┌──────────────────────────────────────────────────────────────────────┐ │ CONTROL PLANE │ │ │ │ ┌──────────────────┐ ┌───────────┐ ┌─────────────────────┐ │ │ │ Registry Agent │ │ Proxy │ │ Agent Card Store │ │ │ │ │ │ │ │ (PostgreSQL) │ │ │ │ /.well-known/ │ │ /api/ │ │ │ │ │ │ agent-card.json│ │ agents/ │ │ │ │ │ │ /a2a/v1 │ │ {id} │ │ │ │ │ └──────────────────┘ └───────────┘ └─────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────┘ Agent Developer's mental model: "There's one URL in my env var. I fetch its agent card. I talk to it via A2A to discover other agents. It gives me endpoints. I talk to those endpoints via A2A. Everything is A2A. I don't learn any proprietary API."

这个"开发者心智模型"在仓库中有一个完整的实现样本:agents/assistant-agent/main.py 的 Assistant Agent 正是这样工作的——读A2A_DISCOVERY_URL(第 49 行)→_discover_agents()用message/send查注册表 →_plan()让 LLM 从目录里挑 Agent 并生成[{"name","url","sub_query"}]计划 →_delegate()向每个 URL 发 A2A 1.0SendMessage(带A2A-Version: 1.0头)→_synthesize()汇总各 Agent 的响应。它甚至处理了 Docker 网络细节:把发现结果里的localhost重写为host.docker.internal(第 140–143 行),并过滤掉自己(name != "assistant-agent",避免自委托)。

Well-Known URL 代理(面向外部 A2A 客户端)

对于不在平台上运行的外部 A2A 客户端,文档规划了按 Agent 粒度的 well-known 代理,但明确标注尚未实现(not implemented yet):

GET https://platform.example.com/agents/{agent-id}/.well-known/agent-card.json ← not implemented

实现后它会返回 Agent Card,且supportedInterfaces[].url指向公共代理:

{ "supportedInterfaces": [ { "url": "https://platform.example.com/api/agents/{agent-id}", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" } ] }

外部客户端同样可以通过注册表发现 Agent:

GET https://platform.example.com/.well-known/agent-card.json ← registry's own card POST https://platform.example.com/a2a/v1 ← query the registry

也就是说:外部客户端面对的注册表面与平台内 Agent 完全一致——这也正是"注册表是一个 A2A Agent"这一决策的收益:无需为外部客户端单独设计一套发现协议。

代理通信流程:每一次调用都经过控制平面

┌──────────┐ ┌───────────────────┐ ┌──────────┐ │ Agent A │ │ Control Plane │ │ Agent B │ │ │ │ │ │ │ │ 1. Fetch registry card (A2A standard) │ │ │ │ ─────────────────► │ │ │ │ │ GET /.well-known/ │ │ │ │ │ agent-card.json │ │ │ │ │ │ │ │ │ │ │ 2. Query registry (A2A SendMessage) │ │ │ │ ─────────────────► │ │ │ │ │ POST /a2a/v1 │ │ │ │ │ "find translation" │ │ │ │ │ │ │ │ │ │ │ ◄─────────────────── 3. Returns agents │ │ │ │ [{endpoint: │ with proxy URLs│ │ │ │ "/api/agents/B"}]│ │ │ │ │ │ │ │ │ │ │ 4. Call Agent B (A2A SendMessage) │ │ │ │ ─────────────────► │ 5. Authenticate │ │ │ │ POST /api/agents/B │ 6. Check ACL │ │ │ │ │ │ 7. Log interaction│ │ │ │ │ │ 8. Forward ──────────────► │ │ │ │ │ POST /a2a/v1 │ │ │ │ │ │ │ │ │ │ │ │ ◄──────────────────── 9. Response │ │ ◄─────────────────── 10. Return to A │ │ │ │ │ │ │ │ │ └──────────┘ └───────────────────┘ └──────────┘ Every arrow is A2A protocol. Steps 1-4 from Agent A's perspective are indistinguishable from talking to any other A2A agent.

代理在每次调用中做的事(步骤 5–8)

  1. 认证调用方——转发前先识别调用 Agent 的身份;
  2. 检查 Agent 到 Agent 的 ACL——调用check_agent_acl(caller_agent_id, target_agent_id)查agent_acl表。设计文档定义的语义是 allowlist:调用方无任何行 = 不受限;有任何行 = 只能调用列出的目标。该检查实现在 server/src/acl.rs 的CpCallGuard::before_call()中。需要注意,当前代码实现比文档描述的语义更严:check_agent_acl的注释与实现为默认拒绝(default-deny)——调用方和目标必须在agent_acl中有一条显式记录才放行(allowed.unwrap_or(false)),并在 ACL 拒绝时返回"agent ACL denied: caller cannot invoke ..."。这与用户到 Agent 的访问控制(agent_grants/is_public)是两套独立机制,后者只约束 API 层访问;
  3. 记录交互——caller、target、时间戳、延迟、状态,形成审计轨迹;
  4. 限流——防止恶意 Agent 对另一个 Agent 发起洪泛;
  5. 转发请求——转发到目标 Agent 真实的私网 IP:port;
  6. 返回响应——剥掉内部头后返回给调用方。

CpCallGuard::before_call()(server/src/acl.rs 第 184–221 行)在 ACL 检查之后还会继续执行FlowGuard检查:深度、环检测、扇出、token 预算、超时(全部基于 Redis 的FlowGuard,即 flow/src/guard.rs 的实现),并在after_call()中记录 token 消耗与调用返回。ACL 拒绝、流控拒绝都会在转发发生之前拦截,因此每次 Agent 间调用都是"先认证、再授权、再计流控、最后转发"。

Registry 数据模型

实时 schema 位于migrations/。核心表如下(migrations/0001_schema.sql 中有完整定义):

-- Agent registry (one row per registered agent) -- agents.id is UUID (original schema); name is the human/A2A identifier. -- name is unique PER OWNER among active rows (partial unique index on -- (owner_id, name) WHERE deleted_at IS NULL) — NOT globally unique. -- search_vector is a GENERATED tsvector — never SELECT *. CREATE TABLE agents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, owner_id UUID NOT NULL REFERENCES users(id), url TEXT, -- agent's A2A endpoint (private IP) capabilities JSONB NOT NULL DEFAULT '{}', skills JSONB NOT NULL DEFAULT '[]', -- denormalised for fast card serialisation tags TEXT[] NOT NULL DEFAULT '{}', is_public BOOLEAN NOT NULL DEFAULT FALSE, -- replaces Redis agent:{id}:public status TEXT NOT NULL DEFAULT 'registered', -- registered|running|stopped|failed secrets_env JSONB NOT NULL DEFAULT '{}', -- {key: aes_gcm_ciphertext} encrypted with agent-scoped HKDF key deleted_at TIMESTAMPTZ, -- ... other columns omitted for brevity; see migrations/0001_schema.sql search_vector tsvector GENERATED ALWAYS AS (...) STORED -- exclude from SELECT lists ); -- Normalised skills (mirrors agents.skills JSONB for indexed querying) CREATE TABLE agent_skills ( id UUID PRIMARY KEY, agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, skill_key VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, tags TEXT[] NOT NULL DEFAULT '{}', examples JSONB NOT NULL DEFAULT '[]', UNIQUE (agent_id, skill_key) ); -- User grants for agent access -- grant_type: 'user' | 'public' -- grantee_id: UUID string for user grants, '*' for public -- (The Nasiko enterprise edition extends grants to teams, departments, -- and organization-wide access.) CREATE TABLE agent_grants ( id UUID PRIMARY KEY, agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, grant_type grant_type NOT NULL, grantee_id TEXT NOT NULL, granted_by UUID REFERENCES users(id), UNIQUE (agent_id, grant_type, grantee_id) ); -- Agent-to-agent invocation allowlist -- No rows for caller → unrestricted. Any rows → only listed targets. CREATE TABLE agent_acl ( caller_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, target_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE, granted_by UUID REFERENCES users(id), PRIMARY KEY (caller_agent_id, target_agent_id) );

与 migrations/0001_schema.sql 对照,有几个值得注意的落地点:

  • agents.skills是 JSONB 反规范化列(加速卡片序列化),同时agent_skills表保留了规范化副本(带UNIQUE (agent_id, skill_key)、tags 的 GIN 索引和强制小写的触发器),供索引查询与发现筛选使用;
  • agent_acl与agent_grants都带ON DELETE CASCADE,软删除的 Agent 通过deleted_at隔离;
  • search_vector是GENERATED ALWAYS AS ... STORED的 tsvector,由 name/description/tags 拼接生成,仓库注释特别提醒"never SELECT *"——发现查询应只取需要的列;
  • agents表上还有针对发现场景的索引:idx_agents_status、tags 的 GIN 索引、idx_agents_fts(search_vector),以及"每个 owner 每活跃名字唯一"的部分唯一索引(agents_owner_name_active_uniq)——保证同一 owner 下活跃 Agent 名字不冲突,但允许软删除后重名。

注册表查询本身在 server/src/registry_a2a.rs 的discoverable_agents()中实现:只选status = 'running' AND deleted_at IS NULL的 Agent,并动态补齐url——当agents.url为空(新部署的 Pod 尚未 Ready 时)会从state.runtime.endpoint(&ContainerId::from_uuid(row.id))实时解析,保证发现结果里不会出现"已知是死链"的 URL;仍未就绪的 Agent 保留在列表里(名字和描述对规划器仍有用),调用方会自行跳过没有 URL 的委托目标。

健康与活性

控制平面每 30 秒轮询一次所有活跃 Agent:

  • GET {private_url}/.well-known/agent-card.json
  • 连续 3 次失败→ 标记unhealthy,从发现结果中排除;
  • VM 被终止 → 标记removed;
  • 卡片内容不会被重读以捕捉 skills 变化(每个部署不可变)。

最后一条与"skills 冻结"决策呼应:健康轮询只关心"Agent 还活着",不关心"Agent 的卡片有没有变化"——因为卡片内容在部署时已固定,轮询去重读它毫无意义。

管理/UI 访问(非 A2A,内部)

Web UI 和 CLI 使用标准 REST 做管理操作(不走 A2A):

GET /api/agents ← list all agents (with status) GET /api/agents/{id} ← full agent details POST /api/agents ← register/deploy new agent PUT /api/agents/{id} ← update agent DELETE /api/agents/{id} ← stop and remove agent POST /api/agents/upload ← upload source for a server-side build /api/containers/* ← container ops (stop, start, restart, scale, logs)

设计文档给出的理由是:这些是控制平面内部 API,服务对象是平台运维人员而非 Agent,所以不需要 A2A。这与 A2A 协议的"无标准注册表 API"约束一致——注册/销毁/容器管理等操作本来就是实现自由区。

LLM Router 集成:绕开 A2A 的"内部快车道"

LLM Router 是控制平面的内部组件(不是独立 Agent),它直接读注册表数据(DB 查询,无 A2A 往返):

  1. 用户向控制平面发送消息;
  2. Router 查询 PostgreSQL 获取所有活跃 Agent 卡片;
  3. Router 把 Agent 卡片 + 用户消息发给 LLM;
  4. LLM 基于 skills/description 挑出最佳 Agent;
  5. 控制平面把请求代理给选中的 Agent;
  6. 响应流式回传给用户。

Router 之所以绕过 A2A 发现协议,是因为它在控制平面内部、有 DB 直连权限。只有外部 Agent 和客户端才走 A2A 发现路径。这是一个重要的架构分层:同一条注册数据,对外是 A2A 协议面,对内是 SQL 面——migrations/0001_schema.sql 中的router_request_log表和agent_selection_stats物化视图,正是这一"DB 直查"路径留下的审计与统计痕迹。

小结

Nasiko 的 A2A 注册表设计可以浓缩成三句话:

  • 对外统一协议面:注册表是一个 A2A Agent,Agent 只认识A2A_DISCOVERY_URL一个 URL,发现、查询、调用全部走SendMessage,没有私有发现 API;
  • 对内分层实现:注册表 =registry_a2a端点(A2A 面)+ PostgreSQL 数据模型(agents/agent_skills/agent_grants/agent_acl)+ 健康轮询(30s);管理面走 REST,LLM Router 走 SQL;
  • 安全收敛在代理:Agent 间通信永远经过控制平面代理,代理统一完成认证、agent_acl授权、审计日志、限流与 FlowGuard 流控,Agent 永远接触不到彼此的私网 IP。

对照仓库,这套设计不仅是文档,而是被 server/src/registry_a2a.rs(含针对两个树内消费者的形状测试)、react-agent/src/registry.rs、agents/assistant-agent/main.py 与 server/src/acl.rs 完整实现的真实系统——读者可以直接从这些文件继续深入,观察 A2A 注册表在真实流量下的行为。

【免费下载链接】nasiko

Developer Control Plane for your AI Agents

项目地址:https://gitcode.com/gh_mirrors/na/nasiko
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询