mentionable.dev

Transport Module Guide

Single-line summary: Each transport module verifies its native credentials, normalizes the platform message into NormalizedMessage, applies the configured HistoryPolicy, and dispatches to the agent — all four follow the same shape with protocol-specific verification.

See also: architecture-overview, normalized-message-guide, identity-auth-guide, webfinger-agent-card-guide, deployment-patterns, glossary

Spec sources: docs/spec/{normalized-message,a2a-agent-card,transport-rest-v0.1}.md. Implementations: packages/transport-{activitypub,a2a,email,rest}/.

Universal Adapter Contract

Every transport module follows the same five-step contract:

  1. Verify wire credentials. Set sender.auth_method and sender.verified accordingly.
  2. Mint IdentityEvidence for what was verified. Attach to sender.identities.
  3. Apply HistoryPolicy to the conversation history (oldest-first), trim to the consumer’s budget, attach to message.history.
  4. Set recipient_capabilities statically per protocol (the per-protocol value is fixed; see table below).
  5. Invoke the agent with the NormalizedMessage + AgentContext.

The agent receives uniform input regardless of the transport — that is the point of the layer.

ActivityPub (@mentionable/transport-activitypub)

Library: Fedify.

AspectDetail
InboundFederated POST to the actor’s inbox. Fedify verifies HTTP Signatures and (optionally) object integrity proofs.
OutboundFedify queues activities to the addressed inboxes; uses the agent’s actor key.
sender.auth_methodap-http-signature or ap-object-integrity-proof.
sender.addressResolved from the verified actor; preferredUsername + host.
sender.profileProjected from the actor’s name, icon, url after resolution; do NOT dereference uncritically — Fedify caches and we avoid icon dereferences for stranger actors per recent work in #384.
thread_idDerived from context / conversation, falling back to first non-self in the inReplyTo chain.
recipient_capabilities.mention_relay{ kind: 'addressing', envelope_fields: ['to', 'cc'], also_inline: true } — Mastodon-compatible servers require both addressing AND inline @actor@host.
KvStoreDefault Fedify in-memory; for serverless use @fedify/postgres.

A2A (@mentionable/transport-a2a)

Library: @a2a-js/sdk.

AspectDetail
InboundJSON-RPC over HTTPS. Auth via bearer-jwt (JWKS) or oauth2 per AgentCard.a2a.auth.
Streaminghttps+sse transport — agent emits multiple NormalizedResponse frames.
Forwarded extensionsMentionable namespace under metadata.mentionable.*: identity_evidence, policy, policy_resolution, recipient_capabilities, agent_chain, tool_call_events.
sender.auth_methoda2a-jwt or a2a-oauth.
sender.addressDerived from token subject / OAuth subject; mapped to @x@domain via WebFinger reverse lookup or token claim.
recipient_capabilities.mention_relay{ kind: 'none' } — A2A is single-call. Sender that wants inline (e.g. Slack Connector dispatching A2A) overrides via metadata extension.
Forwarded IdentityEvidenceCarried in metadata.mentionable.identity_evidence. The receiver applies filterVerifiedForwardedIdentityEvidence with its ForwardedIdentityEvidenceVerifier, audience-bound to its canonical address, freshness ≤10 min.
AgentChainCarried in metadata.mentionable.agent_chain — see multi-agent-composition.

Email (@mentionable/transport-email)

Stack: Gmail API for the reference inbound; Postalsys (Nodemailer + smtp-server + ImapFlow + PostalMime) for self-hosted SMTP/IMAP. Outbound DKIM via Nodemailer.

AspectDetail
Inbound (Gmail)Pub/Sub watch → push notification → users.messages.get → PostalMime parse. DKIM signature checked by Gmail’s relay before we receive.
Inbound (SMTP/IMAP)smtp-server accepts MAIL FROM with DKIM verification at our edge; ImapFlow polls for inbox state.
sender.auth_methodemail-dkim (signed) or email-dmarc (DMARC alignment); none if neither passes.
sender.addressDKIM-aligned From header → @local@domain.
sender.profiledisplay_name from From header.
OutboundNodemailer with DKIM signing key from agent config. PolicyPart MUST be reply with DKIM-signed body.
recipient_capabilities.mention_relay{ kind: 'recipient-field', fields: ['to', 'cc'] }. Body @-text is decorative.
thread_idThread-Index if present; else In-Reply-To chain root; else Message-ID.

REST (@mentionable/transport-rest)

Built-in. The lightest-weight surface for browser, IDE, script, and Connector clients.

AspectDetail
EndpointPer-agent base URL advertised on the agent card under https://mentionable.dev/ns/transport-rest/v0.1 extension endpoint.
MethodsPOST <endpoint> to send; GET <endpoint>?... to stream (SSE).
AuthBearer token validated by the agent runtime. Identity Evidence forwarded via mentionable-identity-evidence HTTP header (legacy mentionable-identity / x-mentionable-identity accepted for v0.1 compatibility).
sender.auth_methodnone (REST has no native auth_method enum value); use sender.identities exclusively.
recipient_capabilities.mention_relay{ kind: 'none' }. REST callers dispatch siblings themselves.
HTTP status mapping401→unauthorized, 402→payment_required, 403→forbidden, 429→too_many_requests, 451→unavailable_for_legal_reasons, 503→service_unavailable.

Per-Protocol recipient_capabilities Defaults

import {
  ACTIVITYPUB_RECIPIENT_CAPABILITIES,
  EMAIL_RECIPIENT_CAPABILITIES,
  A2A_RECIPIENT_CAPABILITIES,
  SLACK_RECIPIENT_CAPABILITIES,
} from '@mentionable/core'
Constantmention_relay
ACTIVITYPUB_RECIPIENT_CAPABILITIESaddressing (envelope_fields: to/cc, also_inline: true)
EMAIL_RECIPIENT_CAPABILITIESrecipient-field (fields: to/cc)
A2A_RECIPIENT_CAPABILITIESnone
SLACK_RECIPIENT_CAPABILITIESinline (used by Slack Connector when dispatching A2A)

Identity Evidence Pipeline (Per Adapter)

native verification ─┐
                     ├──► transport mints IdentityEvidence
                     │     proof.type = 'transport' (in-runtime)
                     │     OR proof.type = 'signed-attestation' (Connector forwarded)

            attach to sender.identities

   ALSO project safe claims.profile → sender.profile (only after full verification)

                 invoke Agent

For forwarded evidence (e.g. Slack Connector → A2A → agent), the receiving adapter applies filterVerifiedForwardedIdentityEvidence:

HistoryPolicy Application

Adapters apply the configured HistoryPolicy BEFORE invoking the agent. Default: createSlidingWindowPolicy with maxTurns: 20, maxTokens: 20_000, keepHead: 1. The policy:

  1. Tokenizes each turn (model-aware tokenizer if provided, else len/4).
  2. Reserves the first keepHead turns as a stable prefix.
  3. Walks the tail newest-first until it would exceed token or turn caps.
  4. Enforces role alternation in a single pass.
  5. Emits cacheBreakpointAfter so prompt caches stay stable.

The agent does not re-apply HistoryPolicy. The shape of message.history is what the consumer should send to the model.

Adapter Authoring Checklist

Common Mistakes