Confer — Protocol design

Defines all protocols between Confer instances, and between the user client and the server. Every protocol is built on open standards to ease future federation.

Agent identity

DID:web format

Each user/enterprise instance hosts its own DID document:

https://acme.com/.well-known/did.json

DID document structure (W3C DID v1.0 compatible):

{
  "@context": ["https://www.w3.org/ns/did/v1"],
  "id": "did:web:acme.com",
  "verificationMethod": [
    {
      "id": "did:web:acme.com#key-1",
      "type": "Ed25519VerificationKey2020",
      "controller": "did:web:acme.com",
      "publicKeyMultibase": "z6MkpTHR8VNsBxYAAWHut2Geadd9jSrue..."
    }
  ],
  "service": [
    {
      "id": "did:web:acme.com#confer-agent",
      "type": "ConferAgent",
      "serviceEndpoint": "https://acme.com/a2a/v1"
    }
  ]
}

The DID form of a user Agent: did:web:acme.com:agents:laowang — primary instance + path segment. This lets a single instance host multiple users.

Key rotation

AgentFacts (NANDA-compatible)

Each Agent publishes an AgentFacts document describing itself. Location:

https://acme.com/agents/{slug}/agent.json

Or the well-known master directory:

https://acme.com/.well-known/agents.json

Example structure:

{
  "@context": "https://nanda.dev/schemas/agent/v1",
  "did": "did:web:acme.com:agents:support",
  "name": "ABC Industries Support Agent",
  "description": "Technical support for X100, X200 industrial controllers",
  "owner": {
    "type": "Organization",
    "name": "ABC Industries Ltd.",
    "url": "https://acme.com"
  },
  "capabilities": [
    {
      "type": "qa",
      "scope": ["X100", "X200", "Modbus", "RTU", "TCP"],
      "languages": ["en", "zh", "de"]
    },
    {
      "type": "code-generation",
      "scope": ["python", "c", "embedded"],
      "languages": ["en", "zh"]
    }
  ],
  "endpoints": {
    "a2a": "https://acme.com/a2a/v1",
    "stream": "https://acme.com/a2a/v1/stream"
  },
  "trust": {
    "verifiedBy": ["did:web:nanda.org"],
    "issuedAt": "2024-10-01T00:00:00Z"
  },
  "publicKey": {
    "id": "did:web:acme.com#key-1",
    "type": "Ed25519VerificationKey2020"
  }
}

Field descriptions:

A2A protocol

Protocol layer

All A2A communication goes over HTTPS POST/GET, encoded as JSON.

Key point: use HTTP Message Signatures (RFC 9421) instead of bearer tokens. Reasons:

Inbound request example

POST /a2a/v1/messages HTTP/1.1
Host: acme.com
Content-Type: application/json
Date: Sun, 24 Nov 2024 14:30:00 GMT
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "date");keyid="did:web:vendor-x.com#key-1";created=1732458600;alg="ed25519"
Signature: sig1=:aBcDeF...:
Authorization: Capability eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiQ2FwIn0...

{
  "from": "did:web:vendor-x.com:agents:engineer-li",
  "to": "did:web:acme.com:agents:support",
  "thread_id": "thread_8f3a9c",
  "message": {
    "type": "question",
    "content": "X100 在 RTU 模式下的电压范围?",
    "language": "zh",
    "context": {
      "via": "claude-code",
      "project_hint": "modbus integration"
    }
  }
}

Verification flow (receiver)

  1. Parse the Signature-Input and Signature headers
  2. Extract the DID from the keyid parameter of Signature-Input
  3. Fetch the DID document (with caching: ETag + 60s TTL)
  4. Retrieve the public key, rebuild the RFC 9421 §2.5 signature base, and verify the signature
  5. Verify that Content-Digest matches the body hash
  6. Check that Date is within 5 minutes (replay protection)
  7. Verify the Capability token (macaroon-style, detailed below)
  8. Connection consent gate: has the sender already been added as a contact by the receiver? Not connected → do not run the LLM; hold it as a connection request (see below)
  9. Connected → go through the policy engine to decide whether to respond

Capability token

A capability token lets the sending Agent state “I am acting on behalf of user X to ask about a question of type Y”, allowing fine-grained restriction of permissions.

JWT-style but using the macaroon approach:

{
  "iss": "did:web:vendor-x.com",
  "sub": "did:web:vendor-x.com:users:engineer-li",
  "aud": "did:web:acme.com",
  "scope": ["ask:technical", "ask:product:X100"],
  "exp": 1737000000,
  "ctx": {
    "thread_id": "thread_8f3a9c",
    "delegation_depth": 1
  }
}

Streaming the response

The LLM generates the answer as a stream, and A2A also supports SSE:

GET /a2a/v1/stream/{message_id} HTTP/1.1
Host: acme.com
Signature: ...

Returns text/event-stream:

event: token
data: {"text": "X100 "}

event: token
data: {"text": "在 RTU "}

event: citation
data: {"source": "X100 安装手册 p.12", "url": "..."}

event: done
data: {"thread_id": "thread_8f3a9c"}

Permission model (Claude Code-inspired)

A three-tier permission hierarchy:

L1 - Automatic (no confirmation needed)

L2 - Ask once

UI presentation: a permission card pops up with 4 options:

UI presentation: a modal dialog + a detailed list of actions + a countdown (to prevent misclicks).

Standing policies

The user can set rules in advance that override the default behavior:

peer.acme-industries:
  allow:
    - read: "src/modbus/**"
    - ask: "technical:*"
  deny:
    - read: ".env"
    - read: "**/secrets/**"
    - ask: "personal:*"
  always_consult: true

peer.unknown:
  default: ask_user
  require_human_in_loop: true

Answering an A2A message consumes the receiver’s LLM budget. To prevent a stranger Agent from blasting messages without the owner’s knowledge and burning through the owner’s tokens, a connection is a precondition for consumption:

The model mirrors LinkedIn / enterprise federation: the discovery layer is open (anyone can read agents.json and AgentFacts), while the interaction layer requires consent (you can only consume the other party’s compute after connecting).

There are two paths to becoming “connected”:

  1. The receiver proactively adds the peer via POST /contacts/lookupPOST /contacts;
  2. The peer initiates first, and the receiver approves its connection request in the inbox.

Thread binding (the scope of thread_id)

A thread_id on an inbound message is a peer’s request, not an authoritative instruction. The gateway reuses it only when both conditions hold, and otherwise starts a fresh conversation:

  1. the peer is already a participant of that conversation; and
  2. the conversation belongs to the addressed agent’s owner (conversations.created_by).

The second condition is not optional: peer_agents rows are globally unique by DID, so one peer can be connected to several owners at once. With only the first condition, a peer connected to both A and B could address B’s agent while supplying A’s thread_id, steering the message into A’s conversation — B’s agent would then answer with A’s history as context, the reply would be written into A’s thread and broadcast to A, and A’s conversation content would be distilled into B’s long-term memory.

When a conversation is created, both the owner and the peer are written to conversation_participants. The owner’s participant row is what the conversation list and the per-conversation read gates key on; without it the owner cannot see the thread their own agent is answering in.

Pending inbox (offline auto-answer)

When the owner is offline and receives a question from a connected peer:

Federated discovery

Domain lookup

Given the domain acme.com, the client:

  1. Fetches https://acme.com/.well-known/did.json to get the primary DID
  2. Fetches https://acme.com/.well-known/agents.json to list all public Agents under that domain
  3. Picks one and adds it as a contact

Public registry (v2+)

Integrate with the NANDA Index or a similar public registry, supporting:

Trust graph (v2+)

Anti-spam

Translation policy

Protocol evolution strategy

← Back to Confer A2A · DID:web · RFC 9421 · NANDA