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; contextId chains 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": true to 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:

  1. omicOS as the hub (recommended): Claude Code calls omicOS the way §14.3 describes; omicOS delegates to a wrapped Codex through the a2a__codex tool. Auth, rate limiting, auditing, and webhooks are all handled centrally by omicOS, with zero changes needed on either end.
  2. Wrap it yourself with the official SDK: use a2a-sdk (Python, roughly 40 lines) to wrap codex exec --full-auto as an A2A server and register it with omicOS (remember allow_private: true for local use). The same wrapper, with a different command, can just as easily wrap claude -p — which opens up the reverse direction too. See the official A2A samples for skeleton code.
  3. 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

  1. A key is the sole credential; omicos a2a key revoke takes effect immediately — issue a separate key per collaborator
  2. A key is mandatory before exposing beyond loopback (enforced by the program); for public deployments, put a TLS reverse proxy in front as well
  3. 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
  4. A2A sessions are isolated from your own chat sessions; the peer can't enumerate your chat history
  5. Execution-type operations go through approval (INPUT_REQUIRED) by default and won't run without confirmation

results matching ""

    No results matching ""