mentionable.dev

Identity & Auth Guide

Single-line summary: Mentionable carries identity in two layers — transport-verified auth_method/verified and the extensible sender.identities surface — with a 3-step trust chain (Connector verifies → issuer trust policy → Agent ACL). Cross-boundary identity is now issued as W3C PlatformIdentityCredential v0.2; the custom IdentityEvidence signed-attestation wire is legacy.

See also: normalized-message-guide, building-a-connector, slack-identity-acl-cookbook, policy-part-guide, glossary

Spec sources: docs/spec/platform-identity-credential-v0.2.md (current cross-boundary wire), docs/spec/identity-evidence-v0.1.md (legacy envelope, still the internal sender.identities shape). Type source: packages/core/src/identity.ts.

Two Layers

┌────────────────────────────────────────────────────────┐
│  Layer 1: Transport verification                       │
│  → sender.auth_method (compact v0.1 view)              │
│  → sender.verified                                     │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│  Layer 2: Extensible identity envelopes                │
│  → sender.identities: IdentityEvidence[]               │
│    (platform, delegated, OAuth, wallet, agent-signed)  │
└────────────────────────────────────────────────────────┘

Layer 1 is “did the wire credential check out”. Layer 2 is “what verified principals are claiming this turn, possibly across trust boundaries”. Agents authorize on Layer 2.

PlatformIdentityCredential v0.2 — What Changed

Since issue #613, the wire format for cross-boundary identity is no longer the custom IdentityEvidence envelope with a signed-attestation proof. It is a profile of W3C Verifiable Credentials 2.0: the PlatformIdentityCredential, signed with a standard DataIntegrityProof (eddsa-jcs-2022 — the same RFC 8785 JCS + Ed25519 combination the custom proof used, now standardized). Spec: platform-identity-credential-v0.2.md.

The moving parts, guide-level:

v0.1 remains as legacy. The IdentityEvidence shape below is still the internal sender.identities surface, and the metadata.mentionable.identity_evidence carrier stays alive during the compatibility window for v0.1-only recipients (explicitly gated by recipient capability — the same fact never travels in both carriers at once). New Connector issuance targets v0.2.

sender.auth_method Values

ValueLayer 1 verifierNotes
ap-http-signatureFedify HTTP SignaturesThe default for ActivityPub inbox delivery.
ap-object-integrity-proofFedify object proofObject-level signature over the activity body.
a2a-jwtA2A JWT validatorbearer-jwt agent-card auth scheme.
a2a-oauthA2A OAuthoauth2 agent-card auth scheme.
email-dkimDKIM signatureFirst-class for inbound email.
email-dmarcDMARC alignmentStronger inbound signal than raw DKIM.
none(no verification)The adapter could not verify. verified: false.

IdentityEvidence Shape

type IdentityEvidence = {
  id?: string // jti / replay-detection key
  subject: string // mailto:, acct:, slack:, eip155:, @agent@domain
  issuer: string // who minted this evidence
  method: string // open token; e.g. urn:mentionable:auth:slack-workspace-member:v0.1
  assurance: 'platform' | 'domain' | 'address' | 'agent' | 'oauth' | 'wallet' | 'delegated' | string
  audience: string | string[] // canonical recipient(s) this evidence is bound to
  issued_at: string
  not_before?: string
  expires_at?: string
  on_behalf_of?: string[] // delegation chain, immediate caller first
  claims?: Record<string, unknown> // safe non-secret claims; profile under claims.profile
  source?: { transport?: string; transport_module?: string; connector?: string; channel?: string }
  proof: IdentityProof
}

Four Proof Types

proof.typeWhat it provesWhere it’s valid
transportNative transport verifier checked the proof; carries verified_by and optional key_id.Inside the same runtime/transport boundary. NOT portable across origins unless re-wrapped.
signed-attestationIssuer-signed (Ed25519/JWS) over canonical evidence with proof omitted. Carries alg, kid, value, optional canonicalization.Across origins, audience-bound, fresh, when receiver’s Trusted Connector Issuer policy accepts the issuer.
bearer-tokenReceiver verified an OAuth/bearer token against an issuer. Carries verified_by, optional token_type, key_id.Trust scope of the verifying component.
siweSign-In with Ethereum proof verified by the adapter/connector. Carries verified_by, chain_id, address.Same as bearer-token.

The proof.type field is open — a (string & {}) escape hatch lets implementations carry namespaced future proof systems.

3-Step Trust Chain

A signed Connector attestation does NOT grant authority by itself. The chain is:

  1. Connector verifies the native platform. Slack signature, Discord interaction signature, OAuth ID token, SIWE, etc. The Connector knows what it just saw.
  2. Receiver’s Trusted Connector Issuer policy. A local policy entry says “this receiver trusts mentionable-slack.example to issue urn:mentionable:auth:slack-workspace-member:v0.1 evidence at assurance: platform for subjects starting slack:”. Without this policy, even a cryptographically valid attestation is just a claim from an unknown party.
  3. Per-agent ACL. The agent (or its system prompt) decides whether the verified subject is allowed to perform this specific action. See slack-identity-acl-cookbook.

Cryptographic validity is necessary but not sufficient. Agents that skip step 2 effectively trust anyone who signs anything.

Profile Claims

IdentityEvidence.claims.profile carries platform display facts (display_name, username, avatar, locale, timezone, provider, provider_subject, extensions). The receiver MAY project these into NormalizedMessage.sender.profile ONLY after the evidence’s signature, audience, freshness, and Trusted Connector Issuer policy all pass.

If verification fails for any reason, profile claims do not appear on sender.profile. This invariant is what makes sender.profile safe to render in an LLM Harness — anything visible there has been gated through the trust chain.

Well-Known Methods

Method URIIssuerSubject shape
email-dkim(transport)mailto:user@domain
email-dmarc(transport)mailto:user@domain
ap-http-signature(transport)acct:user@host or actor IRI
ap-object-integrity-proof(transport)actor IRI
a2a-jwt / a2a-oauth(transport)issuer-defined subject
urn:mentionable:auth:slack-workspace-member:v0.1Slack Connector instanceslack:T<workspace>/U<user>
urn:mentionable:auth:agent-self-sign:v0.1the agent itself (AgentCard signing_key)@agent@domain
urn:mentionable:auth:oauth:v0.1OAuth issuer URLissuer-defined subject
urn:mentionable:auth:siwe:v0.1(Connector or transport)eip155:<chainId>:<address>

The method registry is open. Anyone can mint a new method URI; receivers control whether they trust it via Trusted Connector Issuer policy.

Security Rules

Chained Agent Calls — on_behalf_of

When agent A calls agent B on behalf of user U, the upstream user authority travels in on_behalf_of:

{
  subject: '@agent-a@example.com',
  on_behalf_of: ['mailto:u@example.com'],  // immediate caller first
  ...
}

A multi-hop chain has multiple entries; the immediate caller is at index 0. Agent B’s authorization layer decides whether the chain (subject + delegation) collectively authorizes the action. This composes with [[multi-agent-composition|AgentChain hop tracking]] but is conceptually independent — AgentChain bounds chain depth, on_behalf_of carries authority.

Identity Policy

IdentityPolicy lets receivers express purpose-scoped rules:

type IdentityPolicy = {
  default?: 'deny-by-default' | 'accept-any-valid-evidence'
  accepts?: Array<{
    issuers?: string[]
    methods?: string[]
    subjects?: string[]
    assurance?: IdentityAssurance[]
    purposes?: IdentityPurpose[] // basic-use | terms-invocation | account-linking | payment | delegation | destructive-action | sensitive-data
  }>
  step_up_required_for?: IdentityPurpose[]
}

Default to deny-by-default for any non-trivial purpose. Use step_up_required_for: ['payment', 'destructive-action'] to require a fresh PolicyPart-driven step-up before risky operations.