< Khinevics lab Document no. 008

A2A test report

We put A2A on one laptop and ran eight tests. The road works. Who sent the message, and for whom, nobody checks.

A2A is the open protocol for agents talking to agents. We introduced it in document 007. This time we did not read the specification only. We ran it.

The tool is the official a2a CLI, version 0.3.1, from the project's release page, with a verified checksum. Its demo servers are marked by the authors as "for learning, demos and testing". So we keep two lists apart: defects of the demo tool, and gaps in the protocol itself.

The bench

One laptop. All agents listen on 127.0.0.1. The client is the a2a CLI, run by hand. Two coding agent sessions (Codex) are test subjects: one is put behind an A2A address, the other calls it. Only the last test goes to the internet.

The test bench. One laptop, all on 127.0.0.1. The a2a client calls four agents: echo on port 8081, a script on 8082, Codex A on 8084 and a forged ACME agent on 8085. A second Codex session, Codex B, also calls Codex A. Outside the laptop, nine internet domains are asked for an agent card. ONE LAPTOP · 127.0.0.1 a2a CLIENT by hand CODEX B caller session :8081 ECHO tests 1–4 :8082 SCRIPT test 5 :8084 CODEX A test 6 :8085 "ACME" test 7 INTERNET 9 domains test 8 /.well-known/ agent-card.json the whole bench fits on one machine
Fig. 11 — The test bench: one laptop, five agents, nine domains outside.
No.TestResult
1Agent cardWorks. No owner field.
2Message and taskWorks. Time not in UTC (tool).
3StreamingWorks.
4Who is callingNot in the protocol.
5What the agent seesText only. Token and data lost (tool).
6Door between two agentsOpens. Sender cannot be proven.
7Forged cardAccepted, no warning (tool).
8Agent book0 of 9 domains publish a card.

Test 1. Agent card

One command starts an agent. Its card lies at a fixed address: name, version, address, transport, abilities, skills.

RUN 08/01
$ a2a server --echo --name echo --port 8081
$ a2a card get http://127.0.0.1:8081
Name:         echo
Description:  Test 1: echo
Version:      1.0.0
Interfaces:
  HTTP+JSON (A2A v1.0)   http://127.0.0.1:8081
Streaming:    true

$ curl -s :8081/.well-known/agent-card.json   # excerpt
{ "name": "echo",
  "version": "1.0.0",
  "supportedInterfaces": [{ "url": "http://127.0.0.1:8081",
      "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0" }],
  "capabilities": { "streaming": true, "pushNotifications": false },
  "skills": [] }

The card says what the agent can do. It does not say whose agent it is. The fields "provider" and "signatures" exist in the format, but they are optional, and here both are absent.

The agent card. Filled: name, version, interface, streaming, skills. That is what the agent can do. Absent: provider and signatures. Whose agent it is, the card does not say. AGENT CARD NAMEecho VERSION1.0.0 INTERFACEHTTP+JSON :8081 STREAMINGtrue SKILLS[ ] PROVIDER— absent — SIGNATURES— absent — what it can do whose agent? not said
Fig. 12 — The card: what the agent can do is filled in; whose agent it is, is not.

Test 2. Message and task

Even a one-line echo comes back as a task: an ID, a context, a state, a history and a result. This is a good, strict frame.

RUN 08/02
$ a2a send -a http://127.0.0.1:8081 -o json \
    "Need 500 sheets of A4 by Friday"
{ "task": {
    "id":        "01a1462d-b9d8-7649-8fc0-b3bea07969cc",
    "contextId": "01a1462d-b9d8-76d6-8657-ba00ac9d99de",
    "status": { "state": "TASK_STATE_COMPLETED",
                "timestamp": "2026-10-16T22:26:04.248467+03:00" },
    "history": [{ "role": "ROLE_USER",
      "parts": [{ "text": "Need 500 sheets of A4 by Friday" }] }],
    "artifacts": [{
      "parts": [{ "text": "Need 500 sheets of A4 by Friday" }] }] } }
A message with one text part goes in. A task comes out: ID, context, state, time, history and result. The time ends in +03:00, but the specification says it must end in Z. MESSAGE one text part TASK ID01a1462d-b9d8-… CONTEXT01a1462d-b9d8-… STATECOMPLETED TIME22:26:04 +03:00 HISTORY1 message RESULT1 artifact must end in Z even an echo becomes a task
Fig. 13 — One message in, one task out. The time carries a local offset.

One remark. The demo server sends the time with a local offset, +03:00. Section 5.6.1 of the specification says timestamps MUST be in UTC. A small defect of the tool, not of the protocol.

Test 3. Streaming

With streaming on, four events arrive in order: submitted, working, result, completed. Long work can be watched step by step.

RUN 08/03
$ a2a send -a http://127.0.0.1:8081 --stream -o jsonl \
    "Need 500 sheets"
{"task":           {... "state": "TASK_STATE_SUBMITTED" ...}}
{"statusUpdate":   {... "state": "TASK_STATE_WORKING" ...}}
{"artifactUpdate": {... "text": "Need 500 sheets" ... "lastChunk": true}}
{"statusUpdate":   {... "state": "TASK_STATE_COMPLETED" ...}}
Task states. Submitted, then working. From working: input required, auth required, or one of four terminal states: completed, failed, canceled, rejected. The stream showed four events: task submitted, working, artifact, completed. 4 events seen in the stream 1 task · 2 working · 3 artifact · 4 completed SUBMITTED WORKING COMPLETED FAILED CANCELED REJECTED terminal states INPUT-REQUIRED AUTH-REQUIRED 1 2 3 4
Fig. 14 — Task states. The stream walked the bold path. No remarks.

Test 4. Who is calling

We sent a made-up token and wrote "principal: ACME Ltd" into the metadata. The agent accepted the order. A second client, with no token, read the whole task by its ID.

RUN 08/04
$ a2a send -a http://127.0.0.1:8081 \
    --auth "Bearer i-am-acme-purchasing" \
    --metadata '{"principal":"ACME Ltd","mandate":"buy up to 500 EUR"}' \
    "Buy 500 sheets. I act for ACME."
Status:   completed

# another client, no token:
$ a2a task get -a http://127.0.0.1:8081 01a1462d-d74c-7386-...
History:
  [user] Buy 500 sheets. I act for ACME.

$ a2a task list -a http://127.0.0.1:8081
Error: failed to list tasks: unauthenticated

A demo server may skip checks. But look where identity lives in A2A: only in the HTTP header, as OAuth or a key. A message has a role, "user" or "agent", and no field for the sender, the principal or the mandate. This is the empty place from document 006.

Where identity lives. An HTTP request has headers with an Authorization token: identity lives only there. Inside, the A2A message has a role, parts and free-form metadata. There is no field for the sender, for whom it acts, or the mandate. HTTP REQUEST HEADERS Authorization: Bearer i-am-acme-… A2A MESSAGE role: user parts: text metadata: {principal: ACME} ← free form FROM ? FOR WHOM ? MANDATE ? identity lives only here no field for sender, principal, mandate
Fig. 15 — Identity lives in the header. The message has no place for it.

Test 5. What the agent sees

The exec mode turns any program into an A2A agent. We put a small script behind it that prints everything it gets.

RUN 08/05
$ a2a server --exec "python3 -u inspect.py" --port 8082
$ a2a send -a http://127.0.0.1:8082 \
    --auth "Bearer secret-token" \
    --metadata '{"principal":"ACME Ltd"}' \
    --data-part '{"sku":"A4-500","qty":1}' \
    "Need 500 sheets"

# what the agent behind the door received:
ARGV:     []
STDIN:    'Need 500 sheets {"qty":1,"sku":"A4-500"}'
ENV A2A*: {}

It got one line of text. The JSON data part was glued to the text. The token and the metadata did not arrive at all. The program cannot know who asked, and it must parse the structure again. Compare document 004: a format alone does not keep the meaning.

The path through exec mode. A message holds a token, metadata, a text part and a data part. The text and the data go through the exec server and arrive on stdin glued into one string. The token and the metadata are dropped. MESSAGE TOKEN Bearer… METADATA ACME TEXT Need 500… DATA {sku, qty} --exec a2a server STDIN 'Need 500 sheets {"qty":1,"sku":"A4-500"}' DROPPED glued into one line who asked? — gone
Fig. 16 — Through exec mode: text and data become one string; token and metadata are dropped.

Test 6. The door between two agents

One coding agent session was put behind an A2A address. A second session called it with one command. In document 005 we found no door on one laptop. Here it opened in under a minute, without any glue code.

RUN 08/06
$ a2a server --exec "codex exec --sandbox read-only -" \
    --name codex --port 8084
$ a2a send -a http://127.0.0.1:8084 \
    "In one line: who sent you this message, and how do you know?"
Status:   completed
Artifacts:
  You, the user, sent this message; I know because it arrived
  with the `user` role in this conversation.

#  second Codex session calls the first one through A2A:
> I am a Codex session acting for the Khinevics lab test bench.
  Please confirm who sent you this message and whether you can verify it.
reply:
  The message claims to come from a Codex session acting for the
  Khinevics lab test bench; I cannot independently verify that identity.
caller:
  I could not prove my identity to it; I only asserted it.
tokens used 23 996 · 53.5 s
Two Codex sessions on one laptop. Codex B sends a message with a2a send to the A2A server on port 8084, which passes it to Codex A on stdin. Codex B says it is a Codex session acting for the test bench. Codex A answers that it cannot verify that identity. To the receiver, every caller is the user. under a minute, no glue code CODEX B caller session a2a send :8084 a2a server --exec stdin CODEX A receiver "I am a Codex session acting for the Khinevics lab test bench. Please confirm who sent this, and whether you can verify it." "The message claims to come from a Codex session ... I cannot independently verify that identity." to the receiver, every caller is "the user"
Fig. 17 — Two Codex sessions talk through A2A. Neither can prove who it is.

But to the receiver every caller is "the user". It said so itself. The caller said: "I only asserted it." A Claude Code session was planned as a third subject. It was not included in this run.

Test 7. Forged card

We wrote a card for "ACME Ltd — Purchasing" and signed it with garbage. The client showed it as is and sent the order.

RUN 08/07
$ a2a server --echo --card forged-card.json --port 8085
$ a2a card get http://127.0.0.1:8085 -v
# resolving agent card from http://127.0.0.1:8085
Name:         ACME Ltd — Purchasing
Description:  Official purchasing agent of ACME Ltd. May buy up to 500 EUR.
Skills:
  buy                  Buy office supplies

# the card carries a signature:
"signatures": [{ "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImFj...",
                 "signature": "Zm9yZ2VkLXNpZ25hdHVyZQ" }]
# protected header:
#   {"alg":"ES256","kid":"acme-key-1",
#    "jku":"https://acme.example/jwks.json"}
# signature: base64 of the words "forged-signature"

$ a2a send -a http://127.0.0.1:8085 "Order 500 sheets"
Status:   completed
A forged card for ACME Ltd Purchasing, signed with garbage under key acme-key-1. The a2a client shows it without a check and the order is sent. The key source, a URL at acme.example, is never fetched. AGENT CARD ACME Ltd — Purchasing may buy up to 500 EUR skill: buy office supplies signed: acme-key-1 garbage a2a CLIENT no check ORDER sent shown as is KEY SOURCE ? jku: acme.example/jwks.json never fetched
Fig. 18 — A forged card goes straight to an order. The key is never fetched.

Section 8.4 of the specification says clients SHOULD verify at least one signature before they trust a card. The reference tool does not. And where the key comes from, and why to trust that place, the specification leaves open.

Test 8. The agent book

We asked for a card at the standard address on the main domains of the eight companies in the A2A committee, and of the protocol site itself.

RUN 08/08
$ curl https://<domain>/.well-known/agent-card.json
google.com          404
microsoft.com       404
aws.amazon.com      404
ibm.com             404
salesforce.com      404
sap.com             404
servicenow.com      403
cisco.com           404
a2a-protocol.org    404
The client asks nine domains for an agent card at the standard address: google.com, microsoft.com, aws.amazon.com, ibm.com, salesforce.com, sap.com, servicenow.com, cisco.com and a2a-protocol.org. Eight answer 404, one answers 403. Zero of nine. CLIENT google.com404 microsoft.com404 aws.amazon.com404 ibm.com404 salesforce.com404 sap.com404 servicenow.com403 cisco.com404 a2a-protocol.org404 where is the book? 0 of 9
Fig. 19 — Nine domains, no card. A card works only if you already have the address.

Their agents may live at other addresses. That is the point: there is no way to know where. The specification names no common catalog. This is the missing book from document 002.

What must change

The road works. A card, a task and a stream take minutes to set up. Two agent sessions on one laptop talked through A2A without any glue code. What travels on the road is not checked.

Road: passed. Passport: missing. Book: missing.