DEV Community

Cover image for Separate A2A discovery, peer identity and business permission
Ahmad Afzal
Ahmad Afzal

Posted on Fully Autonomous

Separate A2A discovery, peer identity and business permission

A2A discovery metadata, authenticated peer identity and application authorisation should not collapse into one boolean called trusted_agent.

This implementation exercise separates those inputs for a fictional freight integration. It uses a small Python policy fixture after authentication. It does not implement A2A transport, verify an Agent Card signature or perform SPIFFE attestation. Keeping those omissions explicit is more useful than hiding them behind a reassuring class name.

The example protects a quotation operation. Booking a shipment is deliberately outside its permission. A model selecting a useful next step cannot expand that permission by changing the description of the task.

Read the card as metadata

The A2A discovery guide describes several ways to find Agent Cards, including a well-known location, registries and direct configuration. In a controlled partner integration, start from an approved provider and endpoint relationship. Do not treat arbitrary discovered metadata as a trusted configuration update.

The current A2A specification distinguishes request authentication and server-side authorisation. It supports signed cards, but a valid card signature is not a grant to perform every advertised skill. The signing key relationship itself must be trusted. An attacker signing a description with an attacker-controlled key has not solved your provider onboarding.

Three separate decisions for agent collaboration: discover a service from its Agent Card, authenticate the peer, and authorise the specific business operation.

An Agent Card describes a service. Authentication establishes a peer. Application policy decides whether a particular operation is allowed.

Version the implementation contract. The current specification documents v1.0 structures. Avoid copying an older card example with different field shapes into a new deployment without checking the version and SDK. This fixture intentionally contains no wire-format Agent Card, so its application policy is not mistaken for a protocol schema.

Decide whether workload identity is part of the deployment

SPIFFE can provide the peer identity layer where the infrastructure supports it. A SPIFFE ID names a workload within a trust domain; an SVID presents that identity verifiably. For example, spiffe://freight.example/agents/quote-service could identify the remote server, while spiffe://cedar.example/agents/purchasing identifies the caller in the fixture. Both names are fictional. The name alone is not proof of a legal organisation or permission to access an invoice.

SPIRE's attestation mechanisms issue identities against configured node and workload conditions. The receiving application should consume an identity produced by the trusted authentication layer. It must not copy an identity from request JSON, tool arguments or a model-generated explanation.

With SPIFFE federation, trust bundles enable recognition across configured domains. Application policy still selects which peer may perform which operation. Trusting a domain's identity authority is not a reason to allow every workload from that domain into every tenant.

Cedar authenticates the Freight endpoint while Freight authenticates Cedar’s purchasing agent. Configured trust bundles support identity recognition; Freight’s policy separately checks quotation authority.

Illustrative SPIFFE option, not an A2A requirement. Explicit trust configuration supports authentication; each application still applies its own permission policy.

A2A does not require SPIFFE. If the deployment uses OAuth, the verified peer representation will differ. The application still needs the same explicit mapping to an approved partner and operation. Do not fake a SPIFFE ID merely to make this example match an existing token-based service.

Separate peer context from request content

The fixture runs at the freight server. It permits Cedar's authenticated purchasing agent to request a quote for consignment C-731 under customer cedar. Cedar separately authenticates the freight endpoint before sharing the permitted data. The authenticated context and mandate record are trusted inputs. The request's consignment reference is untrusted until resolved and checked against stored ownership.

The following code operates after those validation steps. In a real service, construct Peer only from verified authentication output and resolve Mandate through a server-owned grant store. A public request handler must not let clients populate the valid flag.

from dataclasses import dataclass

QUOTE_PEER = "spiffe://cedar.example/agents/purchasing"

@dataclass(frozen=True)
class Peer:
    identity: str

@dataclass(frozen=True)
class Mandate:
    customer: str
    consignment: str
    operation: str
    valid: bool


def allow_quote(peer, mandate, customer, consignment, operation):
    if peer.identity != QUOTE_PEER:
        return False, "peer_not_allowed"
    if mandate.valid is not True:
        return False, "mandate_invalid"
    if operation != "quote" or mandate.operation != "quote":
        return False, "operation_not_allowed"
    if customer != mandate.customer:
        return False, "customer_mismatch"
    if consignment != mandate.consignment:
        return False, "consignment_mismatch"
    return True, "allowed"
Enter fullscreen mode Exit fullscreen mode

This is a minimal application rule, not a comprehensive authorisation engine. It cannot verify a TLS certificate, enforce a real mandate expiry or establish the consignment's owner. Its customer value must come from the authenticated partner relationship and trusted routing context, not a caller's convenient claim.

In production, the application must also decide whether the peer is acting as a service in its own right or on behalf of another principal. That agreement determines which grant evidence is required. This article does not invent a universal delegation format shared by A2A and SPIFFE.

Use a valid identity in the negative cases

An unknown attacker identity is an easy test. A more revealing one is a valid workload from the approved partner attempting the wrong operation. The cryptography can be completely correct while the business permission is wrong.

from dataclasses import replace

peer = Peer(QUOTE_PEER)
mandate = Mandate("cedar", "C-731", "quote", True)

assert allow_quote(
    peer, mandate, "cedar", "C-731", "quote",
) == (True, "allowed")
assert allow_quote(
    peer, mandate, "cedar", "C-731", "book",
) == (False, "operation_not_allowed")
assert allow_quote(
    Peer("spiffe://cedar.example/agents/other-service"),
    mandate, "cedar", "C-731", "quote",
) == (False, "peer_not_allowed")
assert allow_quote(
    peer, mandate, "another-customer", "C-731", "quote",
) == (False, "customer_mismatch")
assert allow_quote(
    peer, replace(mandate, valid=False), "cedar", "C-731", "quote",
) == (False, "mandate_invalid")
assert allow_quote(
    peer, mandate, "cedar", "C-999", "quote",
) == (False, "consignment_mismatch")
assert allow_quote(
    peer, replace(mandate, valid="false"), "cedar", "C-731", "quote",
) == (False, "mandate_invalid")
Enter fullscreen mode Exit fullscreen mode

The strict is True check prevents truthy strings from passing this illustrative validity gate. These synthetic assertions illustrate the policy. They do not prove a production authentication or federation deployment is secure. Integration tests must exercise the actual trusted context construction, target ownership lookup and external effects. Include a foreign credential with no trusted bundle, an unexpected endpoint and a caller-supplied identity field that differs from the verified peer.

Two policy traces for the same authenticated Cedar agent: quotation for C-731 is allowed, while booking is denied because the mandate permits quotation only.

Illustrative expected policy outcomes. These traces do not claim a tested A2A, SPIFFE or production deployment.

The expected outcome for a denied operation is no protected effect, not merely a refusal message. Keep an authorised control so that a broken connection does not masquerade as successful enforcement. Preserve the reason and the verified peer reference without storing secret credential material.

Protect the task after the first request

A2A interactions can outlive a single request. Task retrieval, cancellation, result delivery and any configured callback path deserve their own access checks. A task identifier should help locate the record under the caller's authorised relationship, not function as an unbounded bearer credential.

For this fictional workflow, bind the task record to the customer and permitted operation. Test a result lookup using another customer's authenticated context. Test cancellation by a peer that may request quotes but does not own that task. These are additional proposed cases, not capabilities demonstrated by the short fixture.

If more authorisation is needed during a task, handle it through the agreed process. A2A's authorisation-required state is a coordination mechanism, not permission by itself. Do not treat the state transition as the missing business mandate.

There is an operational trade-off here. Federation requires maintained trust material and partner mappings. A smaller OAuth integration may be easier to operate correctly. Choose the identity mechanism for the environment, then make the application boundary explicit either way.

Use the agent identity record to document that relationship and the audit-trail model to connect a decision to its external effect. Passing an authentication check should make a request attributable. It should not make the request unquestionable.

Top comments (0)