Skip to content

feat(transport): A2A extension adapter for PeerRequest#48

Open
Susanpdl wants to merge 3 commits into
agentrust-io:mainfrom
Susanpdl:feat/a2a-transport-adapter
Open

feat(transport): A2A extension adapter for PeerRequest#48
Susanpdl wants to merge 3 commits into
agentrust-io:mainfrom
Susanpdl:feat/a2a-transport-adapter

Conversation

@Susanpdl

Copy link
Copy Markdown

Summary

  • Adds ca2a_runtime.transport to parse A2A SendMessage metadata into PeerRequest, and attach the same fields on the way out.
  • Uses extension URI https://agentrust.io/extensions/ca2a/v0.1. Malformed cA2A metadata fails closed (TRANSPORT_ERROR); no cA2A keys means ordinary A2A (None).
  • Updates docs/spec/transport.md and LIMITATIONS.md so this is clearly parse/attach only. No HTTP server, ca2a start, or seal-to-verified-measurement binding.

Closes the first Tier 2 checkbox on #47 (adapter only), matching the acceptance notes from @carloshvp.

Test plan

  • ruff check src/ tests/
  • mypy src/ca2a_runtime/ src/ca2a_verify/
  • pytest tests/unit/ (141 passed locally)
  • CI green on this PR

Add ca2a_runtime.transport to map A2A SendMessage metadata to PeerRequest
(and reverse) under https://agentrust.io/extensions/ca2a/v0.1. Fail closed
on malformed cA2A fields; absence of all keys returns None (ordinary A2A).
Docs and LIMITATIONS keep the claim boundary: no live server or attestation.

Signed-off-by: Susan Poudel <susanpdl77@gmail.com>

@carloshvp carloshvp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on one transport-boundary issue.

The new adapter promises that malformed cA2A metadata fails closed with TRANSPORT_ERROR, but sealed_payload currently uses base64.urlsafe_b64decode() without alphabet validation. Python's default decoder can ignore some non-base64url characters, so malformed extension metadata can be accepted and converted into opaque bytes instead of failing closed.

Repro against this PR head:

PYTHONPATH=src python3 - <<'PY'
from ca2a_runtime.transport import (
    KEY_DELEGATION_CHAIN,
    KEY_PARENT_RECORD_HASH,
    KEY_RECORD_ID,
    KEY_REQUESTED_CAPABILITY,
    KEY_SEALED_PAYLOAD,
    parse_peer_request,
)
from tests.unit.test_a2a_adapter import _cred_dicts

meta = {
    KEY_DELEGATION_CHAIN: _cred_dicts([frozenset({"read", "write"}), frozenset({"read"})]),
    KEY_REQUESTED_CAPABILITY: "read",
    KEY_RECORD_ID: "rec-invalid-b64",
    KEY_PARENT_RECORD_HASH: None,
    KEY_SEALED_PAYLOAD: "abcd?",
}

try:
    req = parse_peer_request({"metadata": meta})
    print("ACCEPTED", req.sealed_payload)
except Exception as exc:
    print("RAISED", type(exc).__name__, str(exc), getattr(exc, "code", None))
PY

Actual result: ACCEPTED b'i\xb7\x1d'.

That conflicts with the PR's fail-closed contract for any present-but-malformed cA2A metadata, and it leaves the parser accepting a value that is not actually base64url. Please make the decode strict, for example by validating the URL-safe alphabet/padding before decoding or by translating to standard base64 and using base64.b64decode(..., validate=True), and add a regression test for a character outside the base64url alphabet such as abcd?.

Validation performed: inspected the PR diff, ran ruff check src/ tests/ successfully, ran pytest tests/unit/test_a2a_adapter.py tests/unit/test_errors.py successfully, reproduced the malformed sealed_payload acceptance above. mypy src/ca2a_runtime/ src/ca2a_verify/ did not complete in my local temp checkout because types-PyYAML is not installed; pytest tests/unit/ also did not complete locally because cedarpy is not installed. GitHub CI for the PR is green except the maintainer-approval gate.

base64.urlsafe_b64decode silently ignores characters outside the alphabet,
so a value like "abcd?" was accepted as opaque bytes instead of failing
closed. Validate the base64url alphabet before decoding and add regression
tests for characters outside it.

Signed-off-by: Susan Poudel <susanpdl77@gmail.com>
@Susanpdl

Copy link
Copy Markdown
Author

Good catch, thanks. Fixed in 9e5a2b7.

_b64url_decode now validates the base64url alphabet before decoding, so a present-but-malformed sealed_payload raises TRANSPORT_ERROR instead of being accepted as opaque bytes. Your exact repro ("abcd?") now raises TRANSPORT_ERROR.

Added a parametrized regression test covering characters outside the alphabet: abcd?, ab cd, ab==cd, a+b/c, %%%%. Full unit suite passes (146), ruff and mypy clean.

@carloshvp carloshvp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on one remaining adapter-shape issue.

The strict sealed_payload fix on this head addresses my previous blocker. The remaining problem is in attach_ca2a_metadata(): it only writes to out["metadata"]. If a caller passes the full A2A JSON-RPC message/send / SendMessage envelope that parse_peer_request() explicitly accepts, the helper attaches cA2A fields at the envelope root instead of under params.message.metadata or params.metadata. That means the helper can produce an object that the companion parser immediately treats as ordinary A2A.

Repro against 9e5a2b76ce40c523ac3599edb2cd37918f1f01dd:

PYTHONPATH=src python3 - <<'PY'
from ca2a_runtime.peer import PeerRequest
from ca2a_runtime.transport import attach_ca2a_metadata, parse_peer_request, KEY_DELEGATION_CHAIN
from tests.unit.conftest import build_chain

request = PeerRequest(
    chain=build_chain([frozenset({"read", "write"}), frozenset({"read"})]),
    requested_capability="read",
    record_id="rec-envelope",
    sealed_payload=None,
    parent_record_hash=None,
)
envelope = {
    "jsonrpc": "2.0",
    "id": "1",
    "method": "message/send",
    "params": {
        "message": {
            "messageId": "msg-1",
            "role": "user",
            "parts": [{"text": "task"}],
            "metadata": {"https://example.com/ext/other/v1/note": "keep"},
        }
    },
}

attached = attach_ca2a_metadata(envelope, request)
print("top_level_has_metadata", "metadata" in attached)
print("message_has_ca2a", KEY_DELEGATION_CHAIN in attached["params"]["message"].get("metadata", {}))
print("parse_attached_returns", parse_peer_request(attached))
PY

Actual result:

top_level_has_metadata True
message_has_ca2a False
parse_attached_returns None

That is a transport-boundary bug, not just a test-shape nit: the PR advertises a parse/attach helper for A2A SendMessage-shaped data, but the attach half can silently drop the cA2A envelope from the actual A2A metadata location. Please either make attach_ca2a_metadata() mirror collect_metadata()'s supported shapes and attach under params.message.metadata / params.metadata for envelopes, or narrow the API/docs/tests so it only accepts a message object and fails closed on a JSON-RPC envelope instead of producing a misleading root-level metadata field. Add a round-trip regression for the full envelope shape.

Validation performed: inspected the updated PR diff; ran ruff check src/ tests/ successfully; ran pytest tests/unit/test_a2a_adapter.py tests/unit/test_errors.py -q successfully; reproduced the envelope round-trip failure above in a temp checkout. GitHub CI is green except the maintainer-approval gate.

attach_ca2a_metadata only wrote to the root metadata, so a full JSON-RPC
message/send envelope got cA2A fields at the root while parse_peer_request
reads params.message.metadata / params.metadata. The result was an object
the parser treated as ordinary A2A. Attach now mirrors collect_metadata's
shapes so attach then parse round-trips for both bare messages and
envelopes. Add round-trip regressions for the envelope shapes.

Signed-off-by: Susan Poudel <susanpdl77@gmail.com>
@Susanpdl

Copy link
Copy Markdown
Author

Fixed in a321bb6.

attach_ca2a_metadata() now mirrors the shapes collect_metadata() reads, so it attaches under params.message.metadata (or params.metadata when there is no nested message) for JSON-RPC envelopes, and top-level metadata for a bare message. Attach then parse now round-trips for both shapes.

Your repro now prints:

top_level_has_metadata False
message_has_ca2a True
parse_attached_returns True

Added round-trip regressions for the full message/send envelope and for the params.metadata (no nested message) case. Full unit suite passes (148), ruff and mypy clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants