Transport Binding

August 23, 2026 · View on GitHub

cA2A is a profile on A2A, not a competing transport. A2A moves tasks and context between agents and authenticates a peer's domain with the Signed Agent Card. cA2A adds a trust envelope around a delegated task and leaves the wire protocol untouched. This page states how the profile attaches to the transport and what a peer does with the attached data. For the higher-level statement of what the profile adds and where, see the A2A profile binding.

What ships today versus what does not

PieceStatus
Extension URI + namespaced metadata keys (below)Specified
ca2a_runtime.transport adapter: A2A metadata ↔ PeerRequestImplemented (parse/attach only)
Hand-off into handle_peer_request once a PeerRequest existsImplemented in-process; callers invoke it after parsing
Reference HTTP server/client (ca2a_runtime.transport.server/client)Implemented, software mode. A reference transport, not part of the profile
Live attestation handshake on an inbound callImplemented in software mode (ca2a_runtime.attestation, assurance="none")
Callee appraises the caller (mutual attestation)Implemented in software mode: challenge on the handshake, caller_offer on the request, appraised before the payload is opened. Off by default; see mutual-attestation.md
Seal gated on the appraised channel key on a live callImplemented in software mode
Seal bound to a hardware-verified measurementNot yet: needs a real quote via the verifier seam (Tier 3)
ca2a start CLI listenerImplemented: builds a PeerNode from a config file and serves it over the reference transport
Bridge to the official a2a-sdkImplemented (ca2a_runtime.transport.a2a_sdk, optional extra ca2a-runtime[a2a-sdk]): contributes cA2A's declaration to an operator-owned Agent Card, inspects a peer card without imposing policy, and converts SDK protobuf metadata to the mapping the adapter already parses

The reference HTTP server/client run a live call end to end in software mode (assurance="none"). That is progress on Tier 2 transport wiring and a convenience for running the peer path off hardware. It is not evidence that cA2A is attested across trust domains: that needs the hardware verifier seam driven by a real quote. See LIMITATIONS.md and ROADMAP.md.

The reference server bounds request bodies to 1 MiB, requires a decimal Content-Length, applies a ten-second body-read timeout, and rejects malformed JSON or UTF-8 with a structured 400 response. Its handshake accepts exactly one non-empty nonce of at most 256 characters. Production deployments should also apply their platform's connection and request-rate limits in front of it.

Overlay, not fork

cA2A does not define its own transport, message framing, or handshake. It rides inside A2A. Two pieces of cA2A data travel with a task:

  • The delegation credential (or the chain root-to-leaf), naming issuer, subject, scope, depth, and parent link. See the delegation chain.
  • The sealing metadata, carrying an opaque sealed ciphertext when the caller seals the task payload. Sealing crypto is implemented; binding that seal to a verified peer measurement on a live call is still Tier 2/3. See the sealed channel.

Both ride in A2A extension fields. The profile claims no new wire format and no new endpoint: removing every cA2A field leaves a valid A2A task. The reference HTTP transport (ca2a_runtime.transport.server) does expose convenience endpoints, including an attestation-handshake path for fetching the callee's channel key, but those belong to the reference transport, not the profile. They are one way to carry the extension fields, not a requirement of the profile, and any A2A server can carry them instead.

Extension URI and metadata keys

Per A2A v1.0 extensions, agents advertise support in the Agent Card and clients opt in with the A2A-Extensions header (HTTP/JSON-RPC) or equivalent binding metadata.

ItemValue
Extension URIhttps://agentrust-io.com/extensions/ca2a/v0.1
Opt-in headerA2A-Extensions: https://agentrust-io.com/extensions/ca2a/v0.1

Declaring the extension with the official SDK

An A2A application owns its Agent Card: its identity, interfaces, skills, documentation, publication route, and signing policy. cA2A therefore supplies an AgentExtension to merge rather than a competing complete card. The declaration is derived from the same PeerNode that enforces inbound calls, so its caller-attestation requirement cannot drift into a second configuration:

from a2a.types import AgentCard

from ca2a_runtime.transport.a2a_sdk import merge_agent_card

# Build the complete card in the A2A application, then add cA2A before signing.
operator_card = AgentCard(...)  # identity, interface, skills, and URL are yours
published_card = merge_agent_card(operator_card, peer_node)

The resulting capability entry has this shape:

{
  "uri": "https://agentrust-io.com/extensions/ca2a/v0.1",
  "required": false,
  "params": {"require_caller_attestation": "any"}
}

merge_agent_card returns a copy, preserves unrelated fields and extensions, and replaces any stale cA2A declaration so there is exactly one. It refuses a card that already has signatures: adding an extension changes the signed content, and cA2A neither verifies nor re-signs the operator's card. Merge first, then apply the operator's normal Agent Card signing and publication flow. The reference cA2A server does not publish a synthetic Agent Card because it does not own honest values for the required A2A identity, interface, or skill fields.

On the client side, fetch the Agent Card through the official SDK and pass it to inspect_agent_card. The returned AgentCardDiscovery records whether the URI was advertised, the declared caller-attestation requirement, and any warning (missing declaration, duplicate declaration, required=true, or an unknown parameter). Inspection is permissive in this Developer Preview and does not decide whether a call proceeds; deployments can record the result now and choose their own rollout policy. Binding this declaration to a Signed Agent Card and to attestation identity remains separate work.

Namespaced keys on A2A metadata (message and/or params):

Metadata keyJSON typeMeaning
https://agentrust-io.com/extensions/ca2a/v0.1/delegation_chainarray of credential objectsRoot-to-leaf delegation chain
https://agentrust-io.com/extensions/ca2a/v0.1/requested_capabilitystringCapability the callee must grant
https://agentrust-io.com/extensions/ca2a/v0.1/record_idstringProvenance record id for this hop
https://agentrust-io.com/extensions/ca2a/v0.1/parent_record_hashstring or nullParent TRACE/provenance hash; null for a root hop
https://agentrust-io.com/extensions/ca2a/v0.1/sealed_payloadstring (base64url) or omittedOpaque sealed ciphertext only, not a verified measurement binding
https://agentrust-io.com/extensions/ca2a/v0.1/caller_offerchannel-offer object or omittedThe caller's own attested channel key, bound to a challenge the callee issued (mutual attestation)

caller_offer is optional, unlike parent_record_hash: a caller that cannot attest omits the key entirely rather than sending null, and a callee serves it by default. A key that is present but is not a well-formed offer is malformed and fails closed, so a caller cannot get itself treated as unattested by sending rubbish. Its object shape is the same one the handshake endpoint returns.

Constants and helpers live in ca2a_runtime.transport.

Handshake response

GET /.well-known/ca2a/channel?nonce=<n> returns the callee's attested channel key and, additively, the challenge for the other direction:

{
  "channel_public_key": "...",
  "attestation": {"platform": "...", "measurement": "...", "public_key": "...", "nonce": "<n>"},
  "challenge": "v1.<expiry>.<random>.<mac>"
}

challenge is omitted by a callee that issues none, so a caller written against the older response sees exactly what it saw before and simply does not attest.

Attachment points

The credential and sealing metadata are carried in A2A metadata maps on the task message (and optionally params-level metadata), alongside the payload A2A already moves. cA2A does not rewrite the A2A message, change its routing, or interpose a new transport under it. cA2A does not yet verify the Agent Card signature or bind a delegation credential's subject or a peer attestation measurement to the card. Extension discovery must not be treated as either binding; those checks remain separate design work.

Ignore versus enforce

Carrying the trust envelope in extension fields is what keeps the profile an overlay:

  • A non-cA2A peer does not understand the extension fields and ignores them. The task is handled as a plain A2A task.
  • A cA2A-aware peer that sees no cA2A keys treats the message as ordinary A2A input (parse_peer_request returns None). It must not invent a partial trust state.
  • A cA2A-aware peer that sees any cA2A key fails closed on malformed or incomplete cA2A metadata (TransportError), then, once parsed, runs the inbound enforcement pipeline.

Adapter API

from ca2a_runtime.peer import handle_peer_request
from ca2a_runtime.policy import LocalPolicy
from ca2a_runtime.transport import attach_ca2a_metadata, parse_peer_request

# Inbound: A2A SendMessage JSON (or message dict) -> PeerRequest | None
request = parse_peer_request(send_message_body)
if request is None:
    # ordinary A2A — no cA2A trust envelope
    ...
else:
    result = handle_peer_request(request, policy=LocalPolicy.of(["read"]))

# Outbound: attach the same namespaced fields without changing parts/routing
message = attach_ca2a_metadata(message, request)

sealed_payload is base64url-decoded into opaque bytes on PeerRequest. That decode step does not imply the ciphertext is bound to a verified attestation report.

Running the reference transport

A library caller builds a PeerNode and hands it to ca2a_runtime.transport.server.serve. ca2a start is the same thing driven from a config file: it resolves the policy (local_policy or a Cedar policy_bundle_path) and the attestation provider, builds the node, and binds listen_addr.

ca2a start --config examples/minimal/ca2a-config.yaml

Provider selection fails closed rather than downgrading. software-only carries no hardware guarantee, so auto refuses to start when no confidential-computing platform is detected instead of quietly selecting it, and a named hardware provider whose device node is absent is a startup error. The node generates its own X25519 channel keypair and publishes the public half through the handshake, so there is no private key in the config file. See configuration.

What a cA2A peer enforces on inbound

Once a PeerRequest has been parsed, the intended inbound order is:

  1. Verify the delegation chain (verify_chain / handle_peer_request). Any violation denies the call with the specific error: INVALID_CREDENTIAL, BROKEN_DELEGATION_LINK, SCOPE_ESCALATION, DELEGATION_DEPTH_EXCEEDED, or CREDENTIAL_REPLAY. See error codes.
  2. Check the peer's attestation measurement against the expected value. Not yet wired on the live path. See attestation.
  3. Intersect the delegated scope with the local policy (allow-set or Cedar). Implemented in handle_peer_request. See Cedar policy.
  4. Open any sealed payload with the enclave-held key. Crypto implemented; live seal-to-verified-measurement binding is not. See the sealed channel.
  5. Emit a linked provenance/TRACE record referencing the parent record hash and credential_id. See the TRACE A2A profile.

Steps 1, 3, and 5 (and sealed open when a key is supplied) run today inside handle_peer_request after the adapter produces a PeerRequest. Steps 2 and the measurement-bound seal are remaining Tier 2/3 work.

Enforcement is a peer decision

How strictly a cA2A peer acts on the extension fields is local configuration, not a transport-level flag. Ca2aConfig.enforcement_mode selects the behavior:

  • enforcing: an unverifiable chain or a missing required credential denies the call. This is the default and the fail-closed posture the profile calls for.
  • advisory: the failure is recorded but the call proceeds.
  • silent: the check runs without a visible signal.
# ca2a config
attestation:
  provider: auto
  enforcement_mode: enforcing
max_delegation_depth: 8
listen_addr: "127.0.0.1:8443"

max_delegation_depth bounds the chain length a peer will accept and is passed through to verify_chain. listen_addr is the address ca2a start binds for the reference transport; the adapter itself does not open a socket. See failure modes and configuration.

Transport stability

This binding targets A2A v1.x extension points. A2A is now stable at v1.x. Confirming the specific extension fields cA2A occupies remain stable across A2A point releases is tracked on issue #18 and ROADMAP.md.