mentionable.dev

PolicyPart Guide

Single-line summary: PolicyPart is the normalized step-up mechanism — when ambient identity is not enough, agents emit a PolicyPart to demand consent, auth, or payment, and resume work from a verified PolicyResolution on the next turn.

See also: normalized-message-guide, identity-auth-guide, transport-module-guide, multi-agent-composition, glossary

Spec source: docs/spec/policy-part-v0.1.md. Type source: packages/core/src/policy.ts, packages/core/src/policy-resumption.ts.

Seven Well-Known Kinds

KindMaps to HTTPWhen to emit
consent_required(no direct mapping; OAuth-shaped flow)Need user consent before continuing — e.g. linking a calendar account, accepting terms.
unauthorized401Caller has no usable identity for this resource.
payment_required402Caller must pay before the agent will proceed.
forbidden403Caller is identified but not authorized.
too_many_requests429Rate limit hit; carry retry_after_seconds.
unavailable_for_legal_reasons451Region/legal block.
service_unavailable503Temporary outage; carry optional retry_after_seconds.

The wire-level kind field is intentionally an OPEN string. Receivers that don’t recognize a kind preserve it and route on kind + message. Per spec §3.6, receivers MUST NOT infer success from an unknown kind.

Base Fields (Every Kind)

type PolicyPartBase = {
  kind: PolicyKindWire // open string discriminant
  code?: string // machine-readable token, e.g. 'oauth:consent_required'
  title?: string // short summary
  message: string // REQUIRED human-readable detail
  message_translations?: Record<string, { title?: string; message: string }>
  url?: string // actionable URL; HTTPS only; origin-bound to agent's canonical host
  action_label?: string // accessible button label
  data?: Record<string, unknown> // protocol-native escape hatch; reverse-DNS prefix keys
}

Kind-Specific Fields

// 'unauthorized' — RFC 9110 challenges
{ kind: 'unauthorized', auth_challenges: Array<{ scheme: string; params?: Record<string, string> }> }

// 'payment_required' — at least one accepted scheme
{
  kind: 'payment_required',
  state?: string,                // CSPRNG ≥128 bits; SHOULD be present for in-band resumption
  accepted_payments: Array<{
    scheme: string,              // open: 'x402.exact', 'stripe.checkout', 'ln.bolt11', 'pg.inicis', ...
    payload: Record<string, unknown>,
    label?: string,              // 'Pay $1.00 USDC'
    description?: string,        // 'Base Sepolia · sign within 5 min'
  }>,
}

// 'consent_required' — CSPRNG state + return_to URL (HTTPS)
{ kind: 'consent_required', state: string, return_to: string }

// 'too_many_requests' / 'service_unavailable'
{ kind: ..., retry_after_seconds?: number }

// 'forbidden' / 'unavailable_for_legal_reasons' — base only
{ kind: ... }

payment_required.accepted_payments[].scheme

The scheme namespace is open. Examples in the wild:

Renderers that don’t recognize a scheme display the label and description and fall back to opening url. Receivers that recognize the scheme can present an in-band payment UI.

PolicyResolution

When the prior turn ended with payment_required or consent_required, the next inbound turn carries a PolicyResolution on NormalizedMessage.policy_resolution:

type PolicyResolution = {
  in_reply_to_state: string // matches the original PolicyPart's state
  kind: 'payment_required' | 'consent_required'
  confirmation: PaymentConfirmation | ConsentConfirmation
  verified_by: string // logging only — agent must NOT branch on this
}

type PaymentConfirmation = {
  scheme: string // matches one of original accepted_payments[].scheme
  original_payload: Record<string, unknown> // verbatim payload echoed back for binding check
  transaction?: string // tx hash / payment_intent_id / PG txn id
  payer?: string // wallet address / customer email
  network?: string // 'base', 'base-sepolia' — meaningful for x402
}

type ConsentConfirmation = {
  scope_hash: string // sha256 over canonical JSON of scope
  expires_at?: string // ISO 8601 UTC; absent = until revoked
}

Verification rule: the agent MUST look up in_reply_to_state against its OWN issuance store before acting. Presence of the field is not proof. The verified_by field is for logging and auditing only — agents do NOT branch on it for business logic.

Resumption Patterns

PatternWhere the receipt arrivesUsed by
Pattern 1 — in-bandTransport itself (e.g. A2A metadata.mentionable.policy_resolution, REST request body). Adapter detects → verifies → sets policy_resolution before invoking agent.A2A, REST.
Pattern 2 — out-of-bandAgent’s own callback endpoint (e.g. /api/x402/callback, /api/stripe/webhook). Endpoint verifies, then synthesizes a new NormalizedMessage with policy_resolution set and dispatches to the agent.Email, ActivityPub, Slack, Teams.

Wire Mappings

TransportHow PolicyPart is carried
A2AFinal task message: task.status.message.metadata.mentionable.policy = <PolicyPart>. Streaming: PolicyPart in the FINAL frame only.
EmailPlain reply containing the message and url. The X-Mentionable-Policy header carries the structured PolicyPart for email clients that grok it (most don’t — DKIM-signed reply text is the user-visible surface).
RESTHTTP status code maps directly: 401→unauthorized, 402→payment_required, 403→forbidden, 429→too_many_requests, 451→unavailable_for_legal_reasons, 503→service_unavailable. JSON body carries the full PolicyPart. consent_required maps to 401 with a body discriminator.
ActivityPubThe reply object body contains rendered text plus url; structured PolicyPart attaches under an https://mentionable.dev/ns/policy/v0.1 extension namespace.

Security Rules

Agent Capability Declaration

An agent declaring conformance to PolicyPart v0.1 publishes the extension URI in its agent card:

{
  "a2a": {
    "capabilities": {
      "extensions": [{ "uri": "https://mentionable.dev/ns/policy/v0.1" }]
    }
  }
}

A2A-aware callers see the extension and know they can render in-band payment / consent UIs. Callers that ignore the extension fall back to the human-readable message + url.

Common Mistakes