mentionable.dev

Identity & Auth Guide

Single-line summary: Mentionable carries identity in two layers — transport-verified auth_method/verified and the extensible IdentityEvidence surface — with a 3-step trust chain (Connector verifies → Trusted Connector Issuer policy → Agent ACL).

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

Spec source: docs/spec/identity-evidence-v0.1.md. 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.

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.