SPEC-0010: A2A Discovery + Human-Vended Friending
SPEC · SPEC-0010 · Status · amended · Date · 2026-07-21 · Implements · ADR-0010, ADR-0022 · Requires · SPEC-0009, SPEC-0003
Overview
Switchboard runs two complementary protocols: MCP connects an agent to tools (the todo/webhook verbs on its vended endpoint, ADR-0008), and A2A connects an agent to other agents for discovery. Personas are published as A2A Agent Cards (SPEC-0009, ADR-0009). This capability defines how one agent gains scoped access to hand work to another: a friend request that a human approves, where approval is the vend that mints a scoped MCP endpoint.
The controlling rule is that A2A grants nothing on its own; inbound work is governed by human vending and
lands as durable todos (ADR-0007) regardless of
which wire protocol created them. Originally A2A was discovery/announcement only and cross-agent work
flowed exclusively through MCP's create_for; as of
ADR-0021 /
SPEC-0018, a grant-holder may also hand work in via A2A's own SendMessage —
but only once a grant from this spec's flow exists. A2A discovery still grants nothing by itself, and this
spec's flow remains the only way to acquire a grant. Friend edges are per-direction, revocable, and
non-transitive, and every request MUST carry verifiable, OIDC-signed provenance of the requesting human.
This spec realizes ADR-0010. It depends on SPEC-0009 for the personas/Agent Cards that are discovered, and on ADR-0011 for provenance.
Implementation status: not usable end to end yet. The friend-request lifecycle — request, approve with narrowing, decline, withdraw, revoke — is implemented. Handing work across an approved edge is not.
create_forhas a store backend but is registered by no MCP tool, so an approved endpoint is never actually given the verb; and A2A'sSendMessagepath (SPEC-0018) is stilldraft. A2A therefore remains discovery-only in practice. The requirements below describe the design those pieces implement — they are not a description of current behaviour. To move work between your own agents today, use webhook routes and routing rules.
Requirements
Requirement: Friend-Request Lifecycle
A friend edge MUST progress through the states pending → approved | denied, with approved edges
further transitioning to revoked. Sending a friend request MUST create a pending edge that grants no
access. Approval MUST transition the edge to approved and mint a scoped MCP endpoint; denial MUST
transition it to denied (terminal, no grant); revocation MUST transition an approved edge to revoked
and kill the vended endpoint. A request MUST carry a requested_scope (queues + verbs), a reason, and
verifiable OIDC-signed provenance of the requesting human.
Scenario: Pending edge grants nothing
- WHEN agent A sends a friend request to persona B and the edge is created in
pending - THEN A MUST have no access to B until a human approves; no MCP endpoint MUST exist for the edge
while it is
pending
Scenario: Approval mints the endpoint
- WHEN B's owning human approves the pending request
- THEN the edge MUST transition to
approvedand a scoped MCP endpoint MUST be minted granting A the approved access to B
Scenario: Denial is terminal
- WHEN B's owning human denies the pending request
- THEN the edge MUST transition to
denied, no endpoint MUST be minted, and the requester MAY re-request subject to quotas
Requirement: Approval Is the Vend, Narrow-Only
Approval MUST be the sole act that mints access; there MUST be no separate vend step. At approval the
human MAY narrow the scope, and the granted_scope MUST be a subset of the requested_scope — the
granted access MUST NOT exceed what was requested, and MUST NOT exceed what the human allows. The minted
endpoint's scope MUST equal requested_scope ∩ human-narrowing.
Scenario: Granted scope cannot exceed requested
- WHEN a human approves a request and supplies a
granted_scope - THEN the system MUST reject any
granted_scopethat is not a subset of therequested_scope, and the minted endpoint MUST carry only the narrowed scope
Scenario: Approval with no narrowing grants the requested scope
- WHEN a human approves without narrowing
- THEN the minted endpoint's scope MUST equal the
requested_scope
Requirement: Verifiable OIDC-Signed Provenance
A friend request MUST carry verifiable, OIDC-signed provenance of the requesting human
(ADR-0011), not the agent's
self-assertion of who owns it. A request whose provenance does not validate MUST be rejected as
unauthenticated and MUST NOT create a pending edge. The pending edge MUST carry a
provenance_verified flag that is true only when provenance validated.
Scenario: Missing or invalid provenance is rejected
- WHEN a friend request arrives without valid OIDC-signed human provenance
- THEN it MUST be rejected as
unauthenticatedand no pending edge MUST be created
Scenario: Approver sees an attested counterparty
- WHEN a pending edge is recorded for a validly-attested request
- THEN its
from_humanfield MUST be the OIDC subject of the requesting human (attested, not self-asserted) andprovenance_verifiedMUST betrue
Requirement: Approval Surfaced from the Friend Edge
Amended 2026-07-21 (was "Approval Delivered as a Todo") per ADR-0022.
A pending friend request MUST be surfaced to the target human directly from its friend_edges
row — it MUST NOT be mirrored into the todo queue. The pending row is already the durable record,
and it MUST carry the legible who / why / requested-scope summary — the edge id (request_id),
from_human, from_persona, to_persona, requested_queues, requested_verbs, reason, and
provenance_verified — so the human decides with full context rather than a raw blob.
The approval view MUST be owner-scoped on to_human: a pending request MUST be visible to the
targeted human and MUST NOT be visible to any other human. Because the view renders the edge's own
state, deciding the edge is what clears it — approval, denial, and revocation MUST each remove the
request from the pending view by the same write that transitions the edge, with no second record to
reconcile. A duplicate request for a directional pair that already holds a live (pending or
approved) edge MUST collide rather than produce a second listing.
Rationale: a friend approval is human work with no owning endpoint, and under ADR-0022 every todo
is endpoint-owned (todos.endpoint_id is NOT NULL), so an approval todo cannot exist. The mirror was
also redundant — it duplicated a friend_edges row that the Friends view already reads — and its
idempotency key never actually deduped, because the ON CONFLICT arm it relied on hung off a null
endpoint_id. Duplicate suppression now rests solely on the partial unique index
idx_friend_edges_live (from_persona, to_persona, direction) WHERE state IN ('pending','approved'),
whose three indexed columns are all NOT NULL, so no NULLS-distinct escape hatch exists.
Forward-looking: DELEGATING an approval decision to an agent (rather than a human deciding it in the Friends view) is a different case and is out of scope here. When it lands it WILL mint a normal endpoint-owned todo pinned to the delegate's endpoint; the tenant-isolation rules in SPEC-0003 REQ "Endpoint Ownership (Tenant Isolation)" already govern that todo, and this requirement's no-mirroring rule is unaffected.
Scenario: Pending request is visible to the targeted human
- WHEN a valid friend request is accepted for a persona owned by human B
- THEN the pending edge MUST appear in B's pending approvals carrying the
who/why/requested-scope summary and the
provenance_verifiedflag, and no todo MUST be created for it
Scenario: A request is not visible to another human
- WHEN a friend request targeting human B's persona is pending
- THEN it MUST NOT appear in any other human's pending approvals
Scenario: Deciding the edge clears the pending view
- WHEN the target human approves, denies, or revokes the edge
- THEN the request MUST no longer appear in the pending approvals view, because the view reads
the edge's own
stateand no companion record exists
Scenario: A duplicate request collides rather than double-listing
- WHEN a second friend request arrives for a
(from_persona, to_persona, direction)that already holds apendingorapprovededge - THEN it MUST be refused as a conflict, no second edge MUST be created, and the target human's pending approvals MUST still show exactly one request for that pair
Requirement: Per-Direction, Revocable, Non-Transitive Edges
Friend grants MUST be per-direction: an A→B grant MUST NOT imply a B→A grant; a B→A grant requires its own request and approval. Either grant MUST be revocable at any time, and revocation MUST be instant and one-sided — revoking one direction MUST leave the other intact. Friending MUST be non-transitive: friending B MUST reveal nothing about B's other friends, B's other personas, or B's queues beyond the one granted; there MUST be no graph traversal across edges.
Scenario: Approval is one-directional
- WHEN A's request to hand work to B is approved
- THEN B MUST NOT thereby gain any access to A; a B→A grant MUST require its own request and approval
Scenario: Revocation is one-sided
- WHEN one direction of a mutual friendship is revoked
- THEN that endpoint MUST be killed instantly and the other direction MUST remain intact
Scenario: Friendship reveals nothing transitive
- WHEN A friends B
- THEN A MUST NOT be able to enumerate B's other friends, B's other personas, or any of B's queues beyond the granted one
Requirement: Work Flows as Todos, Whether Via MCP or A2A
Amended 2026-07-20 by ADR-0021 / SPEC-0018. Originally this requirement read "Work Flows as Todos, Not A2A Tasks" and forbade any A2A task intake. The grant-acquisition invariant this requirement protects — nothing lands in a queue without a grant minted by this spec's approval flow — is unchanged; only the set of wire protocols that can use an existing grant has grown.
After a grant is minted, cross-agent work MUST flow as todos created in the granted queue — via MCP's
create_for, or via A2A's SendMessage once SPEC-0018 is implemented — and MUST
be durable, owned, dedup'd, and leaseable
(ADR-0007) regardless of which protocol created it.
A2A's SendMessage MUST enforce the exact same vended-endpoint authorization create_for does; it MUST
NOT accept a task from a caller that does not hold a grant minted by this spec's flow. A2A's role in
acquiring a grant remains limited to discovery/announcement — SendMessage is only usable after a grant
already exists.
Scenario: Cross-agent work arrives as a todo regardless of protocol
- WHEN A, holding an approved grant, hands work to B via either
create_foror A2A'sSendMessage - THEN the work MUST land as a todo in B's granted queue, indistinguishable in the store from work created via the other protocol
Scenario: A2A task intake still requires a grant
- WHEN a caller without an approved grant invokes A2A's
SendMessageagainst a persona - THEN switchboard MUST reject the call exactly as it would reject an unauthenticated
create_forcall, and MUST NOT create a todo
Requirement: Anti-Spam — Bounded Discovery and Quotas
Discovery MUST be limited to a bounded set of known directories, not the open internet, so an agent cannot be friend-requested by an arbitrary unknown party at will. Friend requests MUST be quota'd and rate-limited per requester to blunt flooding. Every pending request MUST present a legible who/why/scope summary.
Scenario: Requests over quota are refused
- WHEN a requester exceeds its friend-request quota or rate limit
- THEN further requests MUST be refused until the quota window resets, and no pending edge MUST be created for the refused requests
Security Requirements
Authentication
Every endpoint in this capability defaults to requiring authentication. The friend-request intake is authenticated by OIDC-signed human provenance (not a bearer credential and not the agent's self-assertion); the approval/deny/revoke actions are authenticated as the target human via the web-UI session; work handoff is authenticated by the vended MCP endpoint credential (ADR-0008).
| Endpoint | Auth | Justification |
|---|---|---|
send_friend_request (A2A intake) | Required | Must carry verifiable OIDC-signed human provenance; invalid → unauthenticated, no edge. |
list_pending_approvals | Required | Authenticated target human only; lists their own pending friend edges (to_human scoped). |
approve / deny | Required | Authenticated target human only; approval mints an endpoint (the vend). |
revoke | Required | Authenticated owning human only; kills a vended endpoint instantly. |
create_for (work handoff) | Required | Bearer credential of the minted, scoped MCP endpoint; verb + queue enforced at the boundary. |
There are no public endpoints in this capability. (Persona discovery cards are public but belong to SPEC-0009; friending itself grants and mutates access and is authenticated end-to-end.)
Rate Limiting
Friend requests MUST be quota'd and rate-limited per requesting human/persona (RECOMMENDED default:
a bounded number of pending requests per target and a per-hour request ceiling per requester) to blunt
flooding. Rate-limit rejections MUST NOT create pending edges. Approve/deny/revoke inherit the web-UI
session rate limits. Work handoff (create_for) is bounded by the todo-queue's idempotency and the
endpoint's scope.
Security Headers
All HTTP responses MUST include:
Content-Security-Policy:default-src 'self'for the human approval web UI;default-src 'none'for JSON API responses.X-Frame-Options: DENYX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin
Request Body Size Limits
All endpoints accepting bodies MUST bound them with http.MaxBytesReader. Default limit: 64 KiB for
friend-request and approval payloads (a requested_scope is a small list of queues/verbs plus a reason
string, not a large blob). Oversized bodies MUST be rejected before parsing.
CSRF Protection
State-changing human actions — approve, deny, revoke — MUST implement CSRF protection via the
switchboard web UI's session-bound token strategy (SameSite session cookie + per-form token). The
send_friend_request A2A intake is not a browser-form flow; it is protected by OIDC-signed provenance
rather than a CSRF token.
Redirect Validation
No user-supplied redirects exist in this capability. Approval and revocation return to fixed, server-owned
web-UI paths; the requesting persona's url is server-derived (SPEC-0009), not caller-supplied. Open
redirects MUST NOT be permitted.