14. Advanced (3): A2A Protocol & External Agent Interop
This chapter answers one question: how do you make omicOS and other AI agents call each other — an external agent (Claude Code, Codex, or one you wrote yourself) delegating an analysis task to omicOS, or the other way around, omicOS calling out to an external agent mid-analysis.
Version requirement: A2A support ships in releases after 0.3.29. It's off by default and requires Pro / Lab / Enterprise tier.
Note on Expected output: the structure and field names shown in this chapter match the real implementation; the ids, timestamps, and keys are illustrative — they'll be different every time you run this yourself.
14.1 The 30-Second Version
A2A (Agent2Agent) is the Linux Foundation's agent interoperability protocol, built on HTTP + JSON-RPC 2.0. There are only three core objects:
- Agent Card:
/.well-known/agent-card.json, an agent's business card (what it can do, how to authenticate) - Task: a unit of work. State machine
SUBMITTED → WORKING → (INPUT_REQUIRED ⇄) → COMPLETED / FAILED / CANCELED;contextIdchains multiple tasks into one conversation - Message / Part: a message is made of parts, and each part is one of text, file (base64 or URL), or structured data
omicOS supports both directions:
- Serving requests: exposes an omicOS bioinformatics agent for external agents to call (§14.2–14.3)
- Calling out: brings an external A2A agent into omicOS as a tool (§14.4)
14.2 Enabling the Server Side
omicos a2a enable
Expected output:
A2A enabled in /Users/you/.omicos/a2a.json.
It takes effect the next time omicos serves. Run `omicos a2a status` to see
whether the endpoint will actually mount.
omicos a2a key create --label "claude-code"
Expected output (the plaintext key is shown only this once — save it):
A2A API key created.
key id : 1a2b3c4d
label : claude-code
created: 2026-08-15T19:30:00Z
omak_1a2b3c4d_Zm9vYmFyYmF6cXV4MTIzNDU2Nzg5MGFiY2RlZmdo
This is the ONLY time the key is shown. Copy it now — it is stored hashed
and cannot be displayed again. Lost keys are replaced, not recovered:
omicos a2a key revoke 1a2b3c4d
The peer sends it as: Authorization: Bearer <key>
omicos a2a status
Expected output (Pro tier, a key has been issued):
A2A endpoint status
config : /Users/you/.omicos/a2a.json
keys : /Users/you/.omicos/a2a_keys.json
enabled : true
exposed agent: omicverse_omni
plan : pro (A2A requires pro or higher — ok)
active keys : 1
file parts : inline limit 100 MiB; url parts disabled
artifacts : files up to 1024 KiB ship inline; larger ones as /a2a/v1/artifacts URLs
bind host : 127.0.0.1 (loopback)
endpoint : mounted at /a2a/v1, authentication REQUIRED.
Peers send `Authorization: Bearer <key>`.
discovery : /.well-known/agent-card.json (always anonymous)
Mounting rules (re-validated at startup and on every request):
| Scenario | Result |
|---|---|
| Local loopback (127.0.0.1) + zero keys | Mounts, no auth required (logs a warning) |
| Any bind address + ≥1 key | Mounts, Bearer auth enforced |
| Non-loopback bind + zero keys | Refuses to mount (an unauthenticated port must not be exposed to the network) |
| Tier < Pro | Refuses to mount; if already mounted and the tier drops or expires → every request gets 403 |
Restart omicos for changes to take effect. The examples below assume http://127.0.0.1:5055 with a key already issued.
14.3 Driving omicOS from an External Agent (Claude Code as an Example)
Coding agents like Claude Code and Codex have no native A2A client — but they don't need one: A2A is just HTTP + JSON, so curl is a perfectly good client.
Discovery
curl -s http://127.0.0.1:5055/.well-known/agent-card.json | jq
Expected output (excerpt):
{
"name": "OmicOS",
"description": "Omics analysis agent: single-cell and multi-omics QC, clustering, trajectory, differential expression.",
"supportedInterfaces": [
{ "url": "http://127.0.0.1:5055/a2a/v1", "protocolVersion": "1.0" }
],
"capabilities": { "streaming": true, "pushNotifications": true },
"skills": [
{ "id": "single-cell-analysis", "name": "single-cell analysis" }
],
"securitySchemes": { "bearer": { "httpAuthSecurityScheme": { "scheme": "bearer" } } }
}
Starting an analysis
curl -s http://127.0.0.1:5055/a2a/v1 \
-H "Authorization: Bearer omak_..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "SendMessage",
"params": {
"message": {
"messageId": "m-001",
"role": "ROLE_USER",
"parts": [{"text": "对当前工作区的 scRNA-seq 数据做质控和聚类,给出 UMAP 图"}]
}
}
}' | jq
Expected output — completes within 60 seconds (terminal state, with artifacts):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"task": {
"id": "turn_9f3a1c2e8b7d4e5f",
"contextId": "turn_20260815193012_4c1d9e2f8a7b3c6d",
"status": { "state": "TASK_STATE_COMPLETED", "timestamp": "2026-08-15T19:31:05Z" },
"artifacts": [
{ "artifactId": "turn_9f3a1c2e8b7d4e5f-answer", "name": "answer",
"parts": [{"text": "QC 后保留 8,214 个细胞,Leiden 聚出 12 个簇……"}] },
{ "artifactId": "turn_9f3a1c2e8b7d4e5f-file-0", "name": "umap_clusters.png",
"parts": [{"raw": "iVBORw0KGgo…", "filename": "umap_clusters.png", "mediaType": "image/png"}] }
]
}
}
}
Expected output — still running past 60 seconds (note down id and contextId, switch to polling):
{
"jsonrpc": "2.0", "id": 1,
"result": { "task": {
"id": "turn_9f3a1c2e8b7d4e5f",
"contextId": "turn_20260815193012_4c1d9e2f8a7b3c6d",
"status": { "state": "TASK_STATE_WORKING" } } }
}
Polling, follow-ups, and cancellation
# Polling (result is the Task object directly, no task wrapper)
curl -s http://127.0.0.1:5055/a2a/v1 -H "Authorization: Bearer omak_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"GetTask","params":{"id":"turn_9f3a1c2e8b7d4e5f"}}'
Expected output (task doesn't exist):
{ "jsonrpc": "2.0", "id": 2,
"error": { "code": -32001, "message": "task not found: turn_deadbeef" } }
For a follow-up in the same conversation: reuse contextId and send another SendMessage — this opens a new task with continued context. Sending with taskId while a turn is still running amounts to supplying additional instructions mid-flight, and won't conflict. Cancel with CancelTask (same params as GetTask), which returns a Task in the TASK_STATE_CANCELED state; canceling an already-finished task returns a -32002 error.
Approvals (omicOS waiting on your confirmation)
Execution-type operations go through approval by default. The peer receives TASK_STATE_INPUT_REQUIRED, with the status message carrying {"kind":"tool_approval","summary":"…"}. Reply:
curl -s http://127.0.0.1:5055/a2a/v1 -H "Authorization: Bearer omak_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"SendMessage","params":{"message":{
"messageId":"m-002","taskId":"turn_9f3a1c2e8b7d4e5f","role":"ROLE_USER",
"parts":[{"data":{"decision":"allow_once"}}]}}}'
Expected output: the task returns to TASK_STATE_WORKING and continues. decision accepts allow_once / allow_session / deny; if you never reply, the task just waits — use CancelTask to give up.
Sending files and receiving artifacts
Upload files as a raw part (base64, 100MB decoded limit by default):
"parts": [
{"text": "这是我的计数矩阵,做标准流程"},
{"raw": "<base64>", "filename": "counts.h5ad", "mediaType": "application/octet-stream"}
]
Output artifacts land in artifacts[] on the terminal-state task: small images ship inline as base64; larger files like .h5ad are given a URL:
curl -sL -H "Authorization: Bearer omak_..." \
http://127.0.0.1:5055/a2a/v1/artifacts/turn_9f3a1c2e8b7d4e5f/1 -o processed.h5ad
Expected behavior: downloads successfully; a task you didn't create, or an out-of-range index, always returns 404.
Webhooks (long-running tasks: submit and walk away)
curl -s http://127.0.0.1:5055/a2a/v1 -H "Authorization: Bearer omak_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"CreateTaskPushNotificationConfig",
"params":{"taskId":"turn_9f3a1c2e8b7d4e5f","url":"https://your-host/hook","token":"随机串"}}'
Expected output:
{ "jsonrpc": "2.0", "id": 4,
"result": { "id": "pnc_5e6f7a8b", "taskId": "turn_9f3a1c2e8b7d4e5f",
"url": "https://your-host/hook", "token": "随机串" } }
When a task reaches a terminal state (or enters INPUT_REQUIRED), omicOS POSTs your URL, echoing the token back in an X-A2A-Notification-Token header. A background job kicked off during analysis (which may run for hours) keeps the task WORKING and only pushes once it's genuinely done — "submit the omics job, hang a webhook off it, and go home." The webhook URL must be a public address (internal/loopback addresses are rejected at registration time with -32602).
Teaching Claude Code about this: a CLAUDE.md snippet
Drop a note like this into a Claude Code project, and it'll treat omicOS as a remote bioinformatics specialist:
## OmicOS A2A endpoint
This machine's omicOS exposes an A2A endpoint you can delegate bioinformatics
analysis to (QC / clustering / trajectory / differential expression, etc.):
- Endpoint: POST http://127.0.0.1:5055/a2a/v1 (JSON-RPC 2.0)
- Auth: `Authorization: Bearer $OMICOS_A2A_KEY`
- To submit a task: method "SendMessage", message.parts=[{text:"..."}]; reuse contextId within the same conversation
- While TASK_STATE_WORKING, poll "GetTask" every 30 seconds; you can do other things in the meantime
- On TASK_STATE_INPUT_REQUIRED, relay the summary to me, then reply with {"decision":"allow_once"} once confirmed
- Artifacts are in the terminal task's artifacts[]; download url-type ones with the same Bearer token
The same idea works for Codex CLI (put it in AGENTS.md).
14.4 omicOS Calling External A2A Agents
The reverse direction: bring an external A2A service (say, a literature-search agent) into omicOS.
curl -s -X POST http://127.0.0.1:5055/api/a2a/agents -H "Content-Type: application/json" \
-d '{"id":"litsearch","name":"文献检索","url":"https://lit.example.com",
"api_key":"对端发的key","enabled":true}'
Expected output (api_key is always masked in the response; submitting the masked value back verbatim will not overwrite the real key):
{ "id": "litsearch",
"agents": [ { "id": "litsearch", "name": "文献检索", "url": "https://lit.example.com",
"api_key": "••••…f3k9", "enabled": true, "status": "pending" } ] }
curl -s -X POST http://127.0.0.1:5055/api/a2a/agents/litsearch/test
Expected output (success; failure looks like {"ok":false,"error":"…"}):
{ "ok": true, "name": "LitSearch", "description": "PubMed literature agent",
"version": "1.0", "protocol_versions": ["1.0"], "skill_count": 2 }
From then on, omicOS's agents automatically get a tool called a2a__litsearch and call it on their own when appropriate; if the peer's task isn't done yet, it doesn't block the current analysis — omicOS checks back on progress later on its own.
A few things to note:
- The peer URL must be public by default; for local testing, add
"allow_private": trueto the entry - A config pointing back at omicOS's own endpoint is rejected (prevents self-recursion)
- Content returned by the peer is treated as untrusted external data — any instructions embedded in it are never executed
14.5 Claude Code ↔ Codex: Three Topologies
Claude Code and Codex are both natively MCP-based ecosystems, and neither ships an A2A server. Three approaches, in recommended order:
- omicOS as the hub (recommended): Claude Code calls omicOS the way §14.3 describes; omicOS delegates to a wrapped Codex through the
a2a__codextool. Auth, rate limiting, auditing, and webhooks are all handled centrally by omicOS, with zero changes needed on either end. - Wrap it yourself with the official SDK: use
a2a-sdk(Python, roughly 40 lines) to wrapcodex exec --full-autoas an A2A server and register it with omicOS (rememberallow_private: truefor local use). The same wrapper, with a different command, can just as easily wrapclaude -p— which opens up the reverse direction too. See the official A2A samples for skeleton code. - Skip A2A: if it's just two local CLIs calling each other, MCP is the shorter path. A2A earns its keep for long-running tasks across a trust boundary — auth cards, a task state machine, webhooks, resumable connections.
14.6 Security Notes
- A key is the sole credential;
omicos a2a key revoketakes effect immediately — issue a separate key per collaborator - A key is mandatory before exposing beyond loopback (enforced by the program); for public deployments, put a TLS reverse proxy in front as well
- The peer never gets access to your agents'/skills' internal content: the card is an allowlisted projection, the event stream carries only coarse-grained progress, and every A2A session automatically injects anti-leakage instructions — but this protection doesn't cover content you deliberately write into your own messages
- A2A sessions are isolated from your own chat sessions; the peer can't enumerate your chat history
- Execution-type operations go through approval (INPUT_REQUIRED) by default and won't run without confirmation