mentionable.dev

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

  1. 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.
  2. Agent resolution. Resolve @agent@domain through WebFinger and the AgentCard. Pick REST for ordinary HTTP clients or A2A for JSON-RPC agent-to-agent calls.
  3. Message normalization. Convert the platform event into NormalizedMessage semantics or the target Transport’s request shape.
  4. Identity Evidence. Attach IdentityEvidence only for identities the Connector has verified. Cross-boundary evidence should use proof.type:"signed-attestation", be audience-bound, and expire quickly.
  5. 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.
  6. 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.
  7. 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.

Identity Attestations

Connector-issued identity evidence should follow this pattern:

{
  "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:

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