SMCP Agent-to-Agent (A2A) Spec
How SMCP nodes discover each other, delegate tasks, and forward client-authorized
requests across a federation. Builds on the wire protocol
(SMCP_PROTOCOL.md). References: smcp_a2a.py,
smcp_distributed_a2a.py, smcp_distributed_transport.py,
smcp_federated_auth.py, smcp_discovery.py.
1. Agents & capabilities
An agent is an SMCP node with an AgentInfo (id, name, specialties,
capabilities) and registered capability handlers. Capability names are unique
per node: registering a name that already exists is refused unless an explicit
override is requested (anti-shadowing). Capabilities are advertised via
capability_discovery.
2. Cross-node transport
Nodes talk over the authenticated SMCP WebSocket RPC — a node runs a receiving
server exposing the distributed_task_execute capability and calls peers with an
SMCPClient pool (PeerConnectionPool). Every cross-node RPC:
- is authenticated (the caller holds a session token for the capability),
- is encrypted + signed by the wire protocol,
- has a bounded timeout so a silent peer cannot wedge the caller.
distributed_task_execute
Params: {"task": {"task_id", "task_type", "task_data"}}. The receiver
authorizes the inner task_type against an allowlist (default: the node’s
application handlers, excluding control-plane capabilities such as
cross_server_delegate, distributed_workflow, federated_forward,
federated_key_exchange). A task_type outside the allowlist is refused — a
caller authorized only for distributed_task_execute cannot pivot to arbitrary
handlers.
3. Discovery
discovery_method selects a provider (smcp_discovery.py):
static— the configuredcluster.nodes.dns— SRV records fordiscovery_config.service_name(requires dnspython).consul— Consul health API (/v1/health/service/<name>?passing=1).etcd— etcd v3 range read overdiscovery_config.etcd_prefix; values are node JSON documents.
Discovery backends are security-sensitive: Consul/etcd fetches enforce TLS to non-loopback hosts and a request timeout, malformed entries are skipped (never crash discovery), and discovery may add nodes but must not repoint a statically-configured (trusted) node’s host/port. Routing prefers trusted (static) nodes over discovered ones for a given capability, so a poisoned backend cannot shadow a trusted provider.
4. Federated token forwarding
A node forwards a client’s request to a peer while proving the client authorized
it. Message flow (federated_forward capability):
- The forwarder builds a forwarding proof bound to
{client_jwt, forwarded_by, forwarded_to (the target node), task_hash, nonce, expires_at}. - It signs the proof (see §5), encrypts the request under a session key (§6),
and calls the target’s
federated_forwardwith{encrypted_request, from_node}. - The receiver derives/looks up the session key, decrypts, verifies the proof,
checks
from_node == proof.forwarded_by, validates the client JWT, checks the forwarder is trusted, then executes with the client’s permissions.
Client tokens carry the federation iss/aud binding and a forwarding_allowed
list; verification is algorithm-pinned. Multi-party federations SHOULD verify
client tokens with RS256 (issuer holds the private key, peers hold the public
key) via security.federation_jwt_* — this is independent of the transport JWT,
so a verify-only node still mints its own session tokens.
5. Forwarding proofs
Two schemes, selected by the signer’s configuration:
- PS256 (per-node asymmetric, recommended): the signer signs the canonical proof JSON with its own RSA private key (RSA-PSS, SHA-256); verifiers check against the signer’s registered public key. No shared-secret holder can forge.
- HS256 (shared-secret HMAC, demo/single-domain): HMAC over the canonical
proof JSON keyed by
jwt_secret.
Security rules (normative):
- The verifier MUST NOT let the message pick a weaker scheme: if the claimed
forwarded_byhas a registered public key, the proof MUST be PS256. - A node configured with its own proof key MUST reject all HMAC proofs.
forwarded_toMUST be non-empty and equal the receiver’s node id (target binding; an empty target does not opt out).- Each
nonceis single-use within a bounded, size-capped, lock-guarded replay cache;expires_atbounds validity.
6. Session keys
- Forward-secret (recommended,
crypto.perfect_forward_secrecy=true): an ephemeral ECDH exchange (federated_key_exchange, P-256) per session; the key is HKDF-derived with a salt bound to the exchange transcript (both ephemeral public keys), and ephemeral private keys are discarded. Under PFS afederated_forwardwith no established ECDH session is refused (no silent fallback to a long-term key). - Shared-secret (default): an HKDF key over
secret_keybound to the sorted node pair; deterministic on both sides, but not forward-secret.
Session payloads use AES-256-GCM with a fresh random 96-bit nonce and the
session_id (node pair) bound as GCM AAD, so ciphertext cannot be lifted onto a
different session.
7. Trust model summary
Transport session tokens authenticate client↔server within a node. Federated client tokens authenticate the end user’s identity across the federation (RS256 recommended). Forwarding proofs authenticate the forwarding node (per-node RSA recommended). Discovery establishes reachability, not trust — static configuration is authoritative.