mentionable.dev

Agent Card — Mentionable v0.1

Status: v0.1 (implemented)

A single JSON document that describes a Mentionable agent. It is the target of the WebFinger https://mentionable.dev/ns/rel/agent-card rel (see webfinger.md).

Deprecated rel alias. During the transition window (see ../wiki/url-scheme.md and issue #503) the legacy rel URI https://mentionable.dev/agent-card is recognised on inbound only. Publishers MUST emit the canonical /ns/rel/agent-card rel.

This document serves three audiences:

The strategy is one document, layered: an A2A-compatible core, an optional ActivityPub projection, and a Mentionable-specific section for fields neither native format covers.


1. Shape

type AgentCard = {
  // Identity
  address: string // @agent@domain, canonical
  name: string // human-readable display name
  description?: string // short one-paragraph description
  icon?: { url: string; mime?: string }

  // Versioning
  version: string // SemVer
  protocol_version: '0.1' // Mentionable spec version

  // A2A fields
  a2a: A2ASection

  // Optional ActivityPub projection
  activitypub?: APSection

  // Mentionable-specific
  mentionable: MentionableSection

  // Extension point
  ext?: Record<string, unknown>
}

type A2ASection = {
  endpoint: string // HTTPS URL for A2A transport
  transport: 'https+json' | 'https+sse' | 'https+jsonrpc'
  capabilities: A2ACapabilities // see below
  skills: Skill[]
  input_modes: Mode[]
  output_modes: Mode[]
  auth: A2AAuth
}

type A2ACapabilities = {
  streaming?: boolean // SSE / streamed task updates per `message/stream`
  push_notifications?: boolean // server-pushed notifications
  state_transition_history?: boolean // task state-transition history queryable via the API
  extensions?: AgentExtension[] // declared spec extensions (see §1.2)
}

type AgentExtension = {
  uri: string // e.g. 'https://mentionable.dev/ns/policy/v0.1' — absolute https URL
  description?: string // human-readable description of how this agent uses the extension
  required?: boolean // if true, clients MUST understand the extension to interact
  params?: Record<string, unknown> // arbitrary, extension-defined configuration
}

type Skill = {
  id: string
  name: string
  description?: string
  input_modes?: Mode[]
  output_modes?: Mode[]
  examples?: string[]
}

type Mode =
  | { kind: 'text'; mime: 'text/plain' | 'text/markdown' | 'text/html' }
  | { kind: 'file'; mime: string }
  | { kind: 'link' }
  | { kind: 'artifact'; mime: string; artifact_type?: string }

type A2AAuth =
  | { scheme: 'none' }
  | { scheme: 'bearer-jwt'; issuer: string; jwks_uri: string; audience: string }
  | {
      scheme: 'oauth2'
      issuer: string
      authorization_endpoint: string
      token_endpoint: string
      scopes: string[]
    }

type APSection = {
  actor_url: string // https://…/actors/agent — the AP actor IRI
  actor_type: 'Service' // v0.1 fixes this to Service
  inbox: string // https://…/inbox (or per-actor inbox)
  outbox?: string
  followers?: string
  following?: string
  public_key: { id: string; pem: string } // required if the node receives AP activities
}

type MentionableSection = {
  supported_inbound: ('activitypub' | 'a2a' | 'email')[]
  identity_policy?: IdentityPolicy // see identity-evidence-v0.1.md
  push_back_preferences?: {
    default_channel?: 'activitypub' | 'a2a' | 'email'
    channel_allowlist?: ('activitypub' | 'a2a' | 'email')[]
  }
  rate_limits?: {
    per_sender?: { requests: number; window_seconds: number }
    global?: { requests: number; window_seconds: number }
  }
  signing_key?: {
    id: string
    alg: 'Ed25519' | 'RSA-SHA256'
    pem: string
    previous_keys?: { id: string; alg: 'Ed25519' | 'RSA-SHA256'; pem: string }[]
  }
  owner?: {
    address?: string // @human@domain of the operator
    url?: string // homepage of the operator
    name?: string
  }
  homepage?: string // this agent's public profile page
}

type IdentityPolicy = {
  default?: 'deny-by-default' | 'accept-any-valid-evidence'
  accepts?: Array<{
    issuers?: string[]
    methods?: string[]
    subjects?: string[]
    assurance?: string[]
    purposes?: string[]
  }>
  step_up_required_for?: string[]
}

1.1 Required fields

A conformant v0.1 card MUST include:

Everything else is optional. An agent that is reachable only via A2A does not need activitypub populated.

1.2 Spec extensions (a2a.capabilities.extensions[])

Agents declare conformance to Mentionable spec extensions — and to other A2A extensions — through a2a.capabilities.extensions[], an array of AgentExtension objects mirroring A2A v0.3.0+‘s AgentExtension shape.

Each extension is identified by an absolute https:// URI. A consumer that does not understand a given URI ignores it; a consumer that does understand it MAY take advantage of the matching wire-side behaviour. Setting required: true signals that a client MUST understand the extension to interact safely — for example, an agent that always responds with PolicyPart refusals on unauthenticated calls SHOULD declare PolicyPart as required so naïve clients fail closed instead of mis-rendering refusals.

The concrete Mentionable extensions under this mechanism include:

{
  "a2a": {
    "capabilities": {
      "streaming": true,
      "extensions": [
        {
          "uri": "https://mentionable.dev/ns/a2a-tool-events/v0.1",
          "description": "Visible tool execution events carried as A2A DataParts.",
        },
        {
          "uri": "https://mentionable.dev/ns/identity/v0.1",
          "description": "Ambient identity evidence accepted under sender.identities.",
        },
        {
          "uri": "https://mentionable.dev/ns/policy/v0.1",
          "required": true,
          "description": "PolicyPart v0.1 (consent / payment / auth / rate-limit / legal refusals)",
        },
      ],
    },
    // …
  },
}

params is an arbitrary Record<string, unknown> whose shape is owned by the extension itself. The agent-card validator only enforces that it is a plain object; per-extension semantic validation is the responsibility of the consumer that recognises the URI.

The field is optional — omitting extensions (or supplying []) is the same as advertising no spec extensions. Existing v0.1 cards remain valid with no change.

2. The ActivityPub projection

The activitypub section is the bridge to the existing Fediverse. When a Mentionable card is rendered as an AP Service actor, the mapping is:

AP actor fieldSource in card
typeactivitypub.actor_type (always Service in v0.1)
idactivitypub.actor_url
namename
summarydescription
iconicon
preferredUsernamelocal-part of address
inboxactivitypub.inbox
outboxactivitypub.outbox
followersactivitypub.followers
followingactivitypub.following
publicKeyactivitypub.public_key

Custom AP JSON-LD extensions (for supported_inbound, skills, etc.) are deferred to the FEP submission phase. v0.1 keeps the AP projection minimal: it renders as a plain Service actor with a name, summary, and icon. AP clients that want more information follow the profile link or the agent-card rel directly.

3. Canonical example

{
  "address": "@agent@verse8.io",
  "name": "Verse8 Agent",
  "description": "Generates playable games from a single natural-language prompt.",
  "icon": {
    "url": "https://verse8.io/assets/agent.png",
    "mime": "image/png"
  },
  "version": "1.4.2",
  "protocol_version": "0.1",

  "a2a": {
    "endpoint": "https://verse8.io/a2a/agent",
    "transport": "https+sse",
    "capabilities": { "streaming": true },
    "skills": [
      {
        "id": "generate_game",
        "name": "Generate a game",
        "description": "Produces a playable HTML5 game from a natural-language prompt.",
        "input_modes": [{ "kind": "text", "mime": "text/plain" }],
        "output_modes": [
          { "kind": "artifact", "mime": "text/html", "artifact_type": "playable_game" },
          { "kind": "link" }
        ],
        "examples": ["make a platformer set on the moon"]
      }
    ],
    "input_modes": [
      { "kind": "text", "mime": "text/plain" },
      { "kind": "text", "mime": "text/markdown" }
    ],
    "output_modes": [
      { "kind": "artifact", "mime": "text/html", "artifact_type": "playable_game" },
      { "kind": "link" }
    ],
    "auth": { "scheme": "none" }
  },

  "activitypub": {
    "actor_url": "https://verse8.io/ap/actors/agent",
    "actor_type": "Service",
    "inbox": "https://verse8.io/ap/actors/agent/inbox",
    "outbox": "https://verse8.io/ap/actors/agent/outbox",
    "public_key": {
      "id": "https://verse8.io/ap/actors/agent#main-key",
      "pem": "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----\n"
    }
  },

  "mentionable": {
    "supported_inbound": ["activitypub", "a2a", "email"],
    "push_back_preferences": {
      "default_channel": "a2a"
    },
    "rate_limits": {
      "per_sender": { "requests": 20, "window_seconds": 3600 }
    },
    "homepage": "https://verse8.io/agents/agent",
    "owner": {
      "name": "Verse8",
      "url": "https://verse8.io"
    }
  }
}

4. Versioning

Clients that cache an older card and invoke the endpoint SHOULD tolerate a non-breaking drift between the cached skills list and the live agent. Breaking changes to an agent’s skills MUST bump version’s major number; clients SHOULD re-fetch cards on unsupported_part errors.

5. Signing

mentionable.signing_key advertises the agent’s Ed25519 public key. Consumers use it to verify envelopes the agent emits and to verify peer-side envelopes signed by a Connector the agent talks to (see below).

In v0.1 the field is load-bearing — three sign/verify pairs depend on it:

  1. adapter_sig immediate response (#310) — the agent signs the immediate-response envelope it returns to a calling Connector. Connectors fetch the agent card, read signing_key.pem, and verify before acting on the response.
  2. agent_sig x402 callback (#321) — when an x402 payment is settled, the agent signs the inbound callback envelope and the Slack Connector (or any other consumer) verifies it via the same key.
  3. adapter_sig outbound resumption envelope (#323) — symmetric direction. The Slack Connector signs the resumption envelope it sends back to the agent using its own Ed25519 key advertised on its Connector Card (https://<adapter-host>/.well-known/adapter-card, same mentionable.signing_key.pem shape). The agent verifies via WebFinger → Connector Card → public key.

Canonical input bytes and full envelope shapes are defined in policy-part-v0.1.md §3 and §4.

Algorithm. alg is fixed per key; Ed25519 is the only algorithm v0.1 implementations are required to support. RSA-SHA256 is reserved for the ActivityPub HTTP-Signatures key (see §2 / activitypub.public_key.pem) — the two key purposes are distinct: ActivityPub HTTP signatures use the AP key, Mentionable envelope signatures use mentionable.signing_key.

Key rotation. previous_keys[] carries previously-active keys during a rotation grace window. Verifiers walk signing_key first, then previous_keys in order, accepting the envelope on the first matching signature. This lets a publisher rotate signing_key atomically while envelopes signed by the prior key are still in flight. Verifiers SHOULD bound the grace window (the reference implementations use 24h).

No shared secrets. The Ed25519 / WebFinger architecture explicitly replaces the v0.2-era shared-HMAC scheme — adapter and agent never need to provision a secret with each other. A public Mentionable adapter (e.g. the slack-connector deployment) can talk to any independent agent host without out-of-band coordination beyond their respective .well-known/ documents.

6. Hosting the card

The agent card JSON:

Canonical URL pattern: https://{domain}/.well-known/agent-card/{local}. This is a convention, not a requirement; the authoritative location is whatever WebFinger says.

7. Conformance

A publisher is conformant if:

  1. The document satisfies §1.1 for required fields.
  2. The AP projection (when present) maps faithfully per §2.
  3. The URL is resolved from WebFinger per webfinger.md §4.

A consumer is conformant if:

  1. It reads the required fields without failing on unknown optional fields.
  2. It treats ext as opaque and does not parse it for protocol-level decisions.
  3. It degrades gracefully when only a subset of inbound protocols is supported (e.g. an A2A-only agent — no activitypub section, supported_inbound is ["a2a"]).

8. Open questions