Building a Connector
Audience: developers building a Slack/Teams/Discord/IDE/browser/custom client integration for Mentionable agents.
A Connector is a public integration point. It receives native platform events, normalizes them, and calls agents over a Mentionable Transport. The core safety rule is simple: the Connector may assert only what it has verified, and agents must decide whether they trust that Connector Instance.
Required Pieces
- Native verification. Verify the platform’s inbound request proof before producing any Mentionable message. Examples: Slack request signing, Discord interaction signatures, OAuth ID tokens, SIWE signatures, DKIM/DMARC, or ActivityPub HTTP signatures.
- Agent resolution. Resolve
@agent@domainthrough WebFinger and the AgentCard. Pick REST for ordinary HTTP clients or A2A for JSON-RPC agent-to-agent calls. - Message normalization. Convert the platform event into
NormalizedMessagesemantics or the target Transport’s request shape. - Identity Evidence. Attest only identities the Connector has verified.
Cross-boundary issuance now uses the W3C VC 2.0
PlatformIdentityCredentialv0.2 profile — recipient-bound (proof.domain), request-bound (proof.challenge= outer A2AmessageId), short-lived. The legacyIdentityEvidenceproof.type:"signed-attestation"wire remains only as an explicitly-gated fallback for v0.1-only recipients. - Connector Card. Publish the Connector Instance’s public metadata and
signing keys. The v0.1 compatibility route is
https://<connector-host>/.well-known/adapter-card. - PolicyPart handling. Render
consent_required,payment_required,unauthorized, and refusal parts in the native platform UI. Persist state so callback/resumption flows are single-use. - Operator documentation. Tell agents how to add the Connector Instance to
their Trusted Connector Issuer policy. For Slack-specific agent ACL examples,
see
slack-identity-acl-cookbook.md.
Starting From the Kit
Do not write pieces 2, 4, and 5 from scratch. @mentionable/connector-kit
(packages/connector-kit) is the platform-neutral half of a Connector,
extracted from the Slack reference and consumed by the X reference:
| Piece | Kit surface |
|---|---|
| Agent resolution | resolveAgentCard, selectTransport, agentCardCache |
| Calling the agent | callA2A / streamA2A, callRest / streamRest, reconstructPartsFromA2aResponse |
| Streaming ergonomics | makeAccumulator, FrameDebouncer, stream-timeout helpers |
| Identity carriers | agentCardSupportsPlatformIdentityCredentials, verifiableCredentials (v0.2) / identityEvidence (legacy v0.1) request fields |
| Signing keys | generateSigningKeyPair, signEd25519 / verifyEd25519, SigningKeyStore + InMemorySigningKeyStore + createPostgresSigningKeyStore, createEnvSigningKeyStore — from @mentionable/connector-kit/signing |
| Connector Card | buildConnectorCardBody({ keySet, self }), connectorCardKey, resolveConnectorHost — same subpath |
What stays yours is the platform-facing half: inbound request verification,
event routing, native rendering, OAuth/token storage, and the platform’s own
consent UI. Two small bindings are expected of every Connector — a kid
prefix (deriveDefaultKid(now, 'slack' | 'x' | …)) so key labels stay
attributable, and the self fields your Connector Card advertises next to the
keys (Slack passes { callback_url }, X passes { issuer }).
The issuer-side surface sits behind the /signing subpath because it pulls
in node:crypto; the default entry point stays fetch-only so a Connector
that only calls agents still loads on an edge/worker runtime.
Operators running more than one Connector Instance reuse the same
MENTIONABLE_ADAPTER_SIGNING_KEY_JSON /
MENTIONABLE_ADAPTER_PREVIOUS_SIGNING_KEYS_JSON env shape for each, but every
Instance MUST have its own keypair. A private key is never shared across
issuers — sharing one makes two issuers indistinguishable to every verifying
agent.
Identity Attestations
New Connector issuance uses the W3C VC 2.0
PlatformIdentityCredential v0.2 profile
(see the full credential examples there). The legacy v0.1 evidence shape
below remains for the compatibility window with v0.1-only recipients:
{
"subject": "slack:T123/U456",
"issuer": "mentionable-slack.example",
"method": "urn:mentionable:auth:slack-workspace-member:v0.1",
"assurance": "platform",
"audience": "@agent@example.com",
"issued_at": "2026-05-06T00:00:00.000Z",
"expires_at": "2026-05-06T00:05:00.000Z",
"source": {
"transport": "slack",
"connector": "slack-connector",
"channel": "C123"
},
"claims": {
"profile": {
"display_name": "JC",
"username": "jc",
"avatar": { "url": "https://avatars.slack-edge.com/..." },
"locale": "ko-KR",
"timezone": "Asia/Seoul",
"provider": "slack",
"provider_subject": "slack:T123/U456",
"extensions": {
"slack": {
"team_id": "T123",
"user_id": "U456"
}
}
}
},
"proof": {
"type": "signed-attestation",
"alg": "Ed25519",
"kid": "2026-05",
"canonicalization": "jcs",
"value": "<base64url signature>"
}
}
Do not use an email address, display name, or platform handle as a globally
linked account identifier unless the receiver has an explicit account-linking
policy. Put correlation hints in claims, not in authorization logic.
Sender Profile Facts
Connectors SHOULD populate the common sender.profile shape whenever the
platform exposes safe, user-visible facts. Keep this small and whitelisted:
- Good common fields:
display_name,username,url,avatar.url,locale,timezone,provider,provider_subject. - Good provider extensions: stable platform ids and public-ish workspace facts,
e.g.
extensions.slack.team_id,extensions.slack.user_id,extensions.slack.real_name. - Avoid by default: raw provider profile objects, emails, phone numbers, OAuth tokens, access scopes, private status text, and anything the platform treats as secret or administrator-only.
For Slack, the reference Connector reads users.info and signs a compact
snapshot alongside the workspace-member evidence. Display-name precedence is
display_name_normalized, display_name, real_name_normalized,
real_name, name, then the Slack user id. Avatar precedence is the largest
HTTPS image Slack exposes (image_512, image_192, image_72, …).
When forwarding history, attach profile facts to each HistoricalMessage.sender
when known. Historical sender profile is context for LLM attribution; unless a
fresh, audience-bound signed attestation is attached and verified, it remains
verified:false and must not drive authorization.
Lazy File Capabilities
When the source platform protects file bytes with connector-held credentials,
forward files as lazy descriptors instead of downloading them during message
dispatch. The descriptor should use the normal file part shape with a
connector-hosted bytes_ref.url:
{
"kind": "file",
"mime": "application/pdf",
"name": "report.pdf",
"size_bytes": 12345,
"bytes_ref": {
"kind": "url",
"url": "https://connector.example/api/slack/files/<signed-token>",
"expires_at": "2026-05-06T00:05:00.000Z"
}
}
The URL is a delegated capability, not identity evidence. Scope it to the source workspace/channel/file, the intended agent audience, the current turn, an expiration time, and a byte budget. The Connector should stream bytes only when the agent fetches this URL, and should fail closed when the token is expired, malformed, or out of scope.
Never forward provider credentials or private provider URLs. For Slack, do not
send url_private, url_private_download, thumbnail URLs, or bot/user OAuth
tokens to downstream agents; those URLs require a bearer token and must stay
behind the Connector boundary.
If the downstream Transport can carry typed parts, forward the descriptor as a
normal file part. A2A uses FilePart.file.uri; REST keeps the legacy
multipart user text fallback and MAY add a name="parts" JSON sidecar for
the current turn so receivers that understand Mentionable Part[] preserve
file metadata without downloading bytes.
Trust Policy
Receivers should default to deny-by-default for Connector-issued evidence:
[
{
"issuer": "mentionable-slack.example",
"methods": ["urn:mentionable:auth:slack-workspace-member:v0.1"],
"assurance": ["platform"],
"subject_prefixes": ["slack:"]
}
]
A Connector can be built by anyone, but trust is receiver-local. A valid signature from an unknown Connector proves only that the unknown Connector made the claim.
Security Checklist
- Canonicalize agent audiences as
@<local>@<domain>before signing. - Use short expirations for forwarded attestations; five minutes is the recommended default.
- Rotate Connector signing keys and publish previous keys only for the grace period.
- Never fetch Connector Cards for untrusted issuers as part of signature verification.
- Reject private, loopback, and non-HTTPS Connector Card hosts in production.
- Keep platform tokens and raw OAuth/SIWE credentials out of
claims. - Record replay IDs when the upstream platform gives you one.