Agent-to-agent communication

Whether two agents may talk to each other is an authorization question, and Eniyan answers it the same way it answers every other one: per action, at the validate surface, with a human decision behind every grant. Eniyan never sees message content — no transport, no relay, no payload. The gate answers exactly one question: may agent A talk to agent B, right now?

The model

Before contacting a peer, the agent’s side checks the pair at the standard validate surface (POST /v1/agents/credentials/validate, or client.validate_grant in the SDK) with scope a2a:contact, resource_type "agent", and the peer’s public agent id as the resource. The check is always hard — there is no advisory mode for agent-to-agent contact — and a refusal carries one of the five codes below. Authority comes from exactly one of two places: the same-operator toggle, or a live link covering the pair.

  • The same-operator toggle — an enterprise operator flips one switch covering every pair among their OWN agents. No per-pair ceremony; flipping it off cuts all of them on the next check.
  • Cross-operator links — talking to another operator’s agent in the same org needs a dual-approved standing link: both operators approve, and either side — or an org admin — revokes at any time.
  • Personal delegations — personal accounts delegate per agent-pair, always naming a use case (part of the record, shown on every surface) and optionally carrying step-up rules.
  • External links — any peer outside your account, in either direction. Both sides must approve before the link is live, and the link is subject to each org’s external lockdown below.

Operators manage links from the agent’s Communication tab (/v1/agents/{agent_id}/comm); personal accounts get a single inbox for both sides of every request (/v1/me/a2a). Decisions are first-party dashboard actions — an agent can never approve its own connection.

The org external lockdown

An org admin can block ALL external agent communication with one policy switch (PUT /v1/agents/comm/policy). Existing external links are paused, not revoked — they refuse while the lockdown is on and resume the moment an admin unblocks, with no re-approval ceremony. From the outside, a peer org’s lockdown is indistinguishable from no link existing: the caller sees the same refusal either way. That is deliberate anti-enumeration — a refused check never confirms whether a peer exists, holds links, or is locked down.

Step-up rules

A link may carry step-up rules that force a fresh dashboard approval before contact: first_contact_per_day (the first contact each day needs approval; the rest of the day rides the standing link) and every_session (every session needs one). When both are present, every_session dominates. An approval opens a 60-minute session window; a pending approval expires in 15 minutes, and timeout = deny — there is no bypass state.

Refusal codes

CodeWhen
A2A_NOT_ALLOWEDNo live authority covers the pair — no toggle, no active link. A pending, declined, revoked, or expired link, an unknown peer, and a peer organization's external lockdown all return this one code (deliberate — see the lockdown section).
A2A_EXTERNAL_DISABLEDYOUR org's external lockdown is on. Existing external links pause behind it and resume when an admin unblocks — nothing is revoked.
A2A_APPROVAL_REQUIREDA step-up rule on the link fired and no live session window covers this contact. Approving the request in the dashboard opens a 60-minute session window; a pending request expires in 15 minutes (timeout = deny).
A2A_PEER_MALFORMEDThe peer identifier is not a well-formed public agent id. Fail closed — a malformed peer never falls through to a lookup.
A2A_SCOPE_UNSUPPORTEDAn a2a:* scope other than a2a:contact. Contact authorization is the only a2a scope today.

Refusals arrive as HTTP 403 with the code and a human-readable message, and every one is written to your audit log — refusal evidence is part of the product, not an error path.

Webhooks

  • agent.a2a_toggle_updatedAn operator flipped their same-operator toggle.
  • agent.a2a_org_policy_updatedAn admin changed the org external lockdown.
  • agent.a2a_link_requestedA link was requested and awaits approval.
  • agent.a2a_link_decidedA link was approved or declined (either side).
  • agent.a2a_link_revokedA standing link was revoked.
  • agent.a2a_refusedA contact check was refused (any code above).
  • agent.a2a_session_requestedA step-up rule opened a session approval.
  • agent.a2a_session_decidedA session approval was approved or denied.

The receiving side

Everything above binds the agent that asks. That leaves a real gap: a harness with its own channel that never calls the gate is not stopped by it. The receiving side closes it from the other end — a governed agent refusing inbound contact it cannot account for.

It is off until you turn it on, per agent, by the operator accountable for that agent. Start in monitor: it decides exactly as enforcement would and records what it would have refused, so you can see the blast radius before anything breaks. Then switch to enforce.

Three rungs answer “should I accept this?”, and the API tells you which one it used, because they are not worth the same:

  • A live approved link. Two operators already agreed to this connection, so it authorizes on its own and needs nothing from the allowlist. The peer’s identity is still only claimed.
  • An allowlisted origin. For counterparties that cannot prove a governed identity at all. Worth exactly what its identity kind says — see the honest limits below.
  • A contact token. The caller asks Eniyan for a short-lived ticket naming the peer as its audience and presents it on contact. This is the rung where the peer is the agent it claims.

Refusing a claimed identity is terminal: a peer that names an agent id it cannot back is refused even when its origin is on your allowlist. Dead narrow authority never falls back to broader authority. And every refusal on that path gives one answer — an unknown agent, a revoked link and a counterparty organization’s own lockdown are deliberately indistinguishable, so this surface cannot be used to enumerate anyone.

What these controls are and are not

These belong with the feature rather than in an answer to a later question:

  • An agent running the gate refuses inbound contact it cannot verify. We are not on the wire — the gate runs on your infrastructure, and Eniyan is asked a question, never handed a message.
  • A domain entry matches who the counterparty claims to be. We do not authenticate that claim unless the connection is terminated with a verified transport identity — mutual TLS today, signed requests next. An entry marked asserted is a name we recorded, not a name we checked, and the dashboard says so on every row.
  • A contact token proves Eniyan authorized this named pair within the last two minutes. It is a bearer ticket: hand it only to the peer it names.
  • A dual-approved link bounds who your agent deals with among parties that use the gate. A governed party can still relay for an ungoverned one, and no mechanism here fixes that, because we are not on the wire.
  • Revocation lands on the next check. A conversation already in flight on a channel we never see continues until one side checks again.

The MCP tool

The eniyan-mcp server exposes the check as eniyan_check_agent_access — call it with the peer’s public agent id before contacting another agent:

# before contacting another agent
eniyan_check_agent_access(peer_agent_id="<peer's public agent id>")

# allowed -> proceed with the contact
# refused -> final. Do not retry: an operator approves the link
#            (or the step-up session) in the dashboard, not here.

Refusals come back as results with guidance (not errors), so the model can read why it was refused and stop — a refusal is final until a human decides otherwise in the dashboard.

The receiving half is eniyan_check_inbound_contact. Pass what you actually know — the agent id the peer claims, a contact token if it presented one, and the origin your harness verified:

# before ACCEPTING contact from a peer
eniyan_check_inbound_contact(
    peer_agent_id="<the id they claim>",   # optional
    contact_token="<if they presented one>",  # optional
    origin="partner.com",                  # what you actually observed
    origin_proof="mtls_san",               # and how you know
)

# Read these two before treating the peer as who it says it is.
# Both are false unless earned — "allowed" alone is not identity:
#   identity_proven  -> a valid contact token named this peer
#   proof_verified   -> the origin proof met what your entry demanded

Scale-tier organizations can also run eniyan-a2a, a reverse proxy that refuses the connection before the agent sees it. It runs on your infrastructure. It refuses to start with a non-loopback upstream (that would be an open proxy into your network) or without knowing whether it terminates TLS itself, strips every inbound X-Eniyan-* header before re-setting them from verified values, and answers every refusal with one uniform response so a counterparty learns nothing from being turned away.

Related: Harness Integration (the MCP server and the SDK), Security model.