Skip to content

Usuarios identificados ​

Por defecto, el chat de un agente es anónimo: el navegador se inventa una identidad de conversación y nadie sabe quién escribe. Si tu aplicación ya tiene usuarios con sesión, el agente puede saber quién es cada uno. Tu backend firma un token corto (un JWT) para ese usuario, el widget o el SDK lo mandan en cada petición y ur comprueba la firma antes de hacer nada.

Con eso:

  • la conversación es de ese usuario, y otro no la puede leer aunque conozca su subjectId;
  • las tools saben quién pregunta y pueden reenviar su token a tu API;
  • puedes dar tools y un prompt distintos a quien ha iniciado sesión y a quien no;
  • el prompt puede llevar sus datos (su nombre, su email, cualquier claim del token).

Qué hace ur y qué no ​

ur no autentica a nadie ni firma tokens. No tiene login para tus usuarios ni guarda sus contraseñas: quien sabe quién es el usuario es tu aplicación, que ya tiene su sesión. ur solo comprueba que el token lo ha firmado tu aplicación, que es para este agente y que no ha caducado, y se fía de lo que diga.

El recorrido de una petición:

  1. El usuario inicia sesión en tu aplicación, como siempre.
  2. El navegador le pide un token a tu backend (por ejemplo, GET /ur-token con la cookie de sesión de tu aplicación).
  3. Tu backend comprueba la sesión y firma un JWT con el id del usuario en sub, válido unos minutos.
  4. El widget o el SDK lo mandan a ur en la cabecera Authorization: Bearer.
  5. ur verifica la firma, la audiencia, el emisor y la caducidad. Si algo no cuadra, responde 401 sin abrir la conversación ni llamar al modelo.

La clave de firmar no sale de tu backend

La clave privada (o el secreto HS256) vive solo en tu backend. El navegador no firma nada: le pide a tu backend un token ya firmado, y tu backend decide si hay sesión. Una clave en el JavaScript de tu web la puede leer cualquiera, y con ella se puede firmar un token a nombre de cualquier usuario.

Las claves públicas se pegan en el panel. ur no las descarga de una URL (JWKS) ni las descubre de un proveedor OpenID Connect: si usas uno, copia su clave pública al emisor.

Crear el emisor ​

Un emisor es la aplicación que firma los tokens: con qué clave se verifican y qué tienen que decir. Se crea en Ajustes → Sistema → Emisores de JWT, y solo lo puede hacer un administrador. Varios agentes pueden usar el mismo, y también una inferencia que pide JWT en vez de un token de acceso.

CampoQué es
NombrePara reconocerlo en el panel.
ModoClave pública (recomendado) o Secreto compartido (HS256). No se cambia después: para pasar de uno a otro, crea otro emisor.
Audiencia (aud)Obligatoria. El token tiene que llevarla. Usa un valor propio de este uso, por ejemplo ur-soporte, para que un token que tu aplicación firma para otra cosa no valga aquí.
Issuer (iss)Opcional. Si lo rellenas, el token tiene que llevar exactamente ese valor.
Vida máxima del tokenDe 1 minuto a 24 horas; por defecto, 1 hora. Un token que dure más (exp − iat) se rechaza aunque esté bien firmado.
Qué se guarda del usuario en sus conversacionesSolo el sub (por defecto), los claims que elijas o todos. Ver Qué se guarda del usuario.

Con clave pública ​

Tu backend firma con su clave privada y ur guarda solo la pública. Así, aunque alguien leyera la base de datos de ur, no podría firmar tokens. Para generar un par de claves ES256:

bash
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out privada.pem
openssl pkey -in privada.pem -pubout -out publica.pem

privada.pem va a tu backend, tratado como cualquier otro secreto (una variable de entorno, tu gestor de secretos). En el emisor pegas el contenido de publica.pem.

genpkey, no ecparam

openssl ecparam -genkey escribe la clave privada en otro formato (BEGIN EC PRIVATE KEY) que algunas librerías no leen para firmar, jose entre ellas. genpkey la escribe en PKCS#8 (BEGIN PRIVATE KEY), que leen todas.

Se acepta una clave pública en PEM, un certificado X.509 en PEM o una JWK. El algoritmo lo decide el tipo de clave:

ClaveAlgoritmo
EC P-256ES256
EC P-384ES384
Ed25519EdDSA
RSA de 2048 bits o másRS256

Cada clave tiene un kid: el que escribas al pegarla, el de la JWK si lo trae o, si no, su huella (el thumbprint de RFC 7638). Si el token lleva kid en la cabecera, se verifica solo con esa clave; sin kid, se prueban todas.

Un emisor admite hasta cinco claves, para rotar sin cortes: añade la nueva, haz que tu backend firme con ella y quita la vieja cuando hayan caducado los tokens que firmó.

Si pegas una clave privada por error, se rechaza y no se guarda.

Con secreto compartido (HS256) ​

Al crear el emisor, ur genera un secreto aleatorio de 32 bytes y lo enseña una sola vez. Cópialo a tu backend en ese momento: después el panel solo dice que existe.

Úsalo tal cual, como texto. No lo decodifiques de base64: el secreto es la cadena entera, y con los bytes decodificados la firma no coincide.

Rotar el secreto genera uno nuevo, y el anterior deja de valer en el acto: hasta que tu backend tenga el nuevo, sus tokens dan 401.

Es más simple de montar, pero el mismo secreto que verifica también firma: quien lo consiga, en tu backend o en ur, puede firmar tokens a nombre de cualquiera. Por eso es mejor la clave pública.

Probar un token ​

La ficha del emisor trae Cómo firmar el token, con el código de ejemplo para su modo. Y en el listado de emisores, cada uno tiene su botón Probar un token (▶): pegas un JWT y te dice si pasaría y, si no, por qué (la firma, la audiencia, el emisor, que ha caducado, que no lleva exp, que dura demasiado, un kid que no conoce…). Se prueba contra lo guardado, así que guarda antes los cambios del emisor. Úsalo mientras montas el backend: por el canal público el motivo no se da nunca.

Pedir el usuario en el agente ​

En la ficha del agente, sección Autenticación, eliges quién puede hablar con él y el Emisor de los tokens. Hay tres modos (en la API y en esta documentación, por su valor):

ModoSin tokenCon un token válidoCon un token que no pasa
Abierto (off, por defecto)AnónimoSe ignora: anónimoSe ignora: anónimo
Parcial (optional)AnónimoIdentificado401
Obligatorio (required)401Identificado401

En Parcial, un token que no pasa no cae a anónimo: si la sesión de tu aplicación ha caducado, el cliente tiene que enterarse en vez de seguir hablando como otro.

El token se comprueba en /chat, /history y /start. /config, que el cliente pide al arrancar, sigue siendo anónimo y dice el modo en authMode (con off no viene).

  • Un emisor que usa algún agente no se puede borrar.
  • Pasar el agente a Abierto conserva el emisor elegido, para volver sin elegirlo otra vez.
  • Como el resto de la ficha, el cambio tarda hasta 30 segundos en verse en /embed/*.

Firmar el token en tu backend ​

Tu backend expone una ruta, detrás de su propia sesión, que devuelve un token recién firmado para el usuario que la pide. Lo que tiene que llevar:

Claim
subObligatorio. Quién es el usuario en tu aplicación: su id, de 1 a 256 caracteres. Mejor un id que no cambie que un email: si cambia el sub, cambia la conversación.
audObligatorio. La audiencia del emisor (o una lista que la incluya).
issSi el emisor lo pide, exactamente ese valor.
expObligatorio. Un token sin caducidad se rechaza. Unos 10 minutos está bien: el cliente pide otro cuando caduca.
iatRecomendado. La vida del token (exp − iat) no puede pasar de la máxima del emisor; sin iat, se cuenta desde el momento de la petición.
nbfOpcional.
Los tuyosLo que quieras que lean las tools: email, plan, tenantId…

ur deja 60 segundos de margen de reloj en exp, nbf e iat. En la cabecera, alg tiene que ser el del emisor: el algoritmo lo fija el emisor, y el del token solo se compara con él. Si mandas typ, que sea JWT. El token entero no puede pasar de 8192 caracteres.

ts
import { SignJWT, importPKCS8 } from 'jose';

// El contenido de privada.pem, desde el entorno o tu gestor de secretos.
const privateKey = await importPKCS8(process.env.UR_JWT_PRIVATE_KEY!, 'ES256');

export function signUrToken(user: { id: string; email: string }): Promise<string> {
  return new SignJWT({ email: user.email })
    .setProtectedHeader({ alg: 'ES256', kid: 'mi-app-2026' }) // el kid de la clave en el emisor
    .setSubject(user.id)
    .setAudience('ur-soporte')
    .setIssuer('https://mi-app.example.com') // solo si el emisor pide un iss
    .setIssuedAt()
    .setExpirationTime('10m')
    .sign(privateKey);
}

// Con Express: la ruta va detrás de tu sesión, como cualquier otra.
app.get('/ur-token', requireLogin, async (req, res) => {
  res.type('text/plain').send(await signUrToken(req.user));
});
ts
import { SignJWT } from 'jose';

// El secreto que enseñó ur al crear el emisor, como texto: sin decodificarlo.
const secret = new TextEncoder().encode(process.env.UR_JWT_SECRET!);

export function signUrToken(user: { id: string; email: string }): Promise<string> {
  return new SignJWT({ email: user.email })
    .setProtectedHeader({ alg: 'HS256' })
    .setSubject(user.id)
    .setAudience('ur-soporte')
    .setIssuedAt()
    .setExpirationTime('10m')
    .sign(secret);
}
php
<?php
use Firebase\JWT\JWT;

function signUrToken(array $user): string
{
    $now = time();
    $payload = [
        'sub'   => (string) $user['id'],
        'email' => $user['email'],
        'aud'   => 'ur-soporte',
        'iss'   => 'https://mi-app.example.com', // solo si el emisor pide un iss
        'iat'   => $now,
        'exp'   => $now + 600,
    ];
    // privada.pem (PKCS#8); el último argumento es el kid de la clave en el emisor.
    return JWT::encode($payload, file_get_contents('/ruta/segura/privada.pem'), 'ES256', 'mi-app-2026');
}

// En tu ruta /ur-token, detrás de tu sesión:
header('Content-Type: text/plain');
echo signUrToken($currentUser);
php
<?php
use Firebase\JWT\JWT;

function signUrToken(array $user): string
{
    $now = time();
    $payload = [
        'sub'   => (string) $user['id'],
        'email' => $user['email'],
        'aud'   => 'ur-soporte',
        'iat'   => $now,
        'exp'   => $now + 600,
    ];
    // El secreto que enseñó ur al crear el emisor, tal cual.
    return JWT::encode($payload, getenv('UR_JWT_SECRET'), 'HS256');
}

Los ejemplos de Node usan jose y los de PHP, firebase/php-jwt, pero vale cualquier librería de JWT. Si no hay sesión, que la ruta conteste un error (un 401, por ejemplo): el cliente de abajo lo trata como «sin token».

Pasarlo desde el cliente ​

Con el SDK ​

La opción getUserToken es una función que devuelve el token:

ts
const client = new UrAgentClient({
  agent: '<id-del-agente>',
  apiBase: 'https://<tu-backend-ur>',
  getUserToken: async () => {
    const res = await fetch('/ur-token', { credentials: 'same-origin' });
    return res.ok ? res.text() : null; // sin sesión: habla anónimo
  },
});
  • Se llama antes de cada petición a /chat, /history y /start, nunca en /config. Lo que devuelva va como Authorization: Bearer <token>, sin los espacios ni saltos de línea de los extremos.
  • Si devuelve vacío, null o undefined, la petición va anónima: vale con optional y da 401 con required. getConfig().authMode dice cuál de los dos tiene el agente.
  • Cambiar de token (iniciar o cerrar sesión) es cambiar de conversación: ver La conversación es del usuario.

Se llama en cada petición. Si firmar te cuesta, reutiliza el token durante parte de su vida:

ts
let cached: { token: string; at: number } | null = null;

async function getUserToken(): Promise<string | null> {
  // Los tokens duran 10 minutos: cada uno se reutiliza durante 8.
  if (cached && Date.now() - cached.at < 8 * 60_000) {
    return cached.token;
  }
  const res = await fetch('/ur-token', { credentials: 'same-origin' });
  if (!res.ok) {
    return null;
  }
  cached = { token: await res.text(), at: Date.now() };
  return cached.token;
}

// Al cerrar sesión en tu aplicación: cached = null.

Vacía esa caché al cerrar sesión, o el chat seguiría hablando como el usuario anterior. Y que no devuelva un token ya caducado: el SDK solo reintenta con un token distinto del que falló.

Con el widget ​

El widget recibe la misma función en su propiedad getUserToken. Es solo propiedad de JavaScript, no atributo: una función no cabe en un atributo.

html
<script src="https://<tu-backend-ur>/widget.js" data-agent="<id-del-agente>"></script>
<script>
  document.querySelector('ur-agent-chat').getUserToken = async () => {
    const res = await fetch('/ur-token', { credentials: 'same-origin' });
    return res.ok ? res.text() : null;
  };
</script>

Asígnala justo después del <script> del widget. El elemento se crea en cuanto se ejecuta ese script y pide el historial cuando le llega la configuración, así que asignada ahí la primera petición ya lleva el token. Asignada más tarde, vale desde la petición siguiente. Si importas el widget como módulo, asígnala antes de poner agent o de insertar el elemento.

Sin token, en un agente que lo exige, el widget no recupera el historial y al escribir pinta el error en el chat.

Errores y reintento ​

Un token que no pasa recibe 401 con el motivo en error y la cabecera WWW-Authenticate:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
Content-Type: application/json

{ "error": "token_expired" }
errorCuándoQué hacer
token_missingEl agente es required y no llegó token.Que el usuario inicie sesión.
token_invalidLa firma, la audiencia, el emisor, el algoritmo, el kid, el sub, una vida demasiado larga, la falta de exp… También una cabecera Authorization que no es Bearer <token>.Revisa tu backend con Probar un token.
token_expiredHa caducado.Pedir otro. El SDK ya lo hace una vez.

El motivo es grueso a propósito: decirle a un cliente público si falló la firma o la audiencia solo ayuda a quien prueba tokens.

El 401 llega antes que todo lo demás: no se abre conversación, no se gasta turno, no pasa por los filtros de seguridad y no llama al modelo. Lo único que ya ha contado es el límite de peticiones por IP, que es lo que frena a quien prueba tokens a lo bruto.

Lo que hace el SDK:

  • Ante token_expired, vuelve a llamar a getUserToken y repite la petición una vez con el token nuevo. Si devuelve el mismo token, o ninguno, no repite.
  • Ante token_missing o token_invalid no reintenta: repetir daría lo mismo.
  • Lo que queda llega a onError(message, info), con info.status y, en un 401, info.code. loadHistory() rechaza con un UrAgentClientHttpError (con status y code), y createChatStore deja el motivo en errorCode. start() no falla hacia fuera.
ts
client.onError((message, info) => {
  if (info?.status === 401) {
    // info.code: token_missing | token_invalid | token_expired
    pedirQueInicieSesion(); // tu UI
    return;
  }
  console.warn(message);
});

La conversación es del usuario ​

Con un usuario identificado, la conversación ya no se identifica solo por agente, subjectId y canal: también por el usuario (su emisor y su sub). Eso quiere decir que:

  • Otro usuario que mande el mismo subjectId cae en otra conversación, vacía. Conocer un subjectId ya no da acceso a nada.
  • Un usuario identificado nunca se queda con una conversación anónima. Si alguien empieza anónimo y después inicia sesión (agente en optional), con el token empieza otra; al cerrar sesión, vuelve a la anónima.
  • El mismo usuario, en el mismo navegador, retoma su conversación al volver, también si estaba cerrada por el límite de turnos.
  • El límite de mensajes por hora y el de ráfaga se cuentan por usuario, no por la huella del navegador.

Cada navegador guarda su propio subjectId, así que en otro dispositivo el usuario empieza otra conversación. Todavía no hay forma de listar las conversaciones de un usuario para retomar una desde otro sitio.

En Conversaciones del agente, las de un usuario identificado llevan su sub junto al canal, y la cabecera de la conversación dice además de qué emisor era el token. El buscador también encuentra por sub. Si el emisor guarda más datos del token (ver Qué se guarda del usuario), la cabecera los enseña en Datos del token.

Qué se guarda del usuario ​

Por defecto, de un usuario identificado ur solo guarda su sub. En el emisor puedes elegir guardar más con cada conversación, para verlo en Conversaciones:

  • Solo el sub (por defecto).
  • Los que elijas: los claims que pongas, separados por comas (email, name). Un claim con espacio de nombres, como https://tu-app.com/roles, se escribe entero.
  • Todos los claims: todo lo que lleve el token.

El token en sí no se guarda nunca: mientras no caduca, quien lo tuviera podría hablar como ese usuario. Se guardan los datos del último mensaje de cada conversación. Los ve quien pueda ver las conversaciones del agente, aunque no sea administrador. Si después reduces lo que se guarda (o borras el emisor), lo que sobra se borra en el momento de todas sus conversaciones; si lo amplías, solo se nota desde el siguiente mensaje de cada usuario.

Sus datos en las instrucciones ​

En Parcial y en Obligatorio, las Instrucciones para usuarios identificados (sección Autenticación del agente) pueden llevar datos del token de quien pregunta: {{jwt.email}}, {{jwt.name}} o cualquier otro claim. En cada turno, ur los cambia por los de ese usuario y añade el texto al final del prompt del agente, en un bloque aparte.

text
Hablas con {{jwt.name}} ({{jwt.email}}), cliente de la tienda {{jwt.org.name}}.
Sus roles: {{jwt.roles}}.

Con un token cuyo payload es {"sub": "u_8124", "name": "Ana Pérez", "email": "ana@example.com", "org": {"name": "Bilbao"}, "roles": ["admin", "ventas"]}, el modelo lee:

text
Hablas con Ana Pérez (ana@example.com), cliente de la tienda Bilbao.
Sus roles: admin, ventas.
  • Cómo se escriben. {{jwt.<claim>}}, con dos llaves a cada lado (los espacios dentro valen: {{ jwt.email }}). Con un punto se entra en un objeto ({{jwt.org.id}}) o en una lista, por su posición ({{jwt.roles.0}}), y {{jwt}} es el payload entero en JSON. El nombre del claim, como en la tool HTTP: letras, números y _. Un claim con espacio de nombres (https://tu-app.com/roles) no se puede usar aquí.
  • Cómo sale cada valor. Un texto, tal cual y en una sola línea: los saltos de línea se cambian por espacios, para que un valor no pueda abrir una sección nueva del prompt. Un número o un booleano, como tal. Una lista de valores sueltos, separada por comas. Un objeto (o una lista con objetos), en JSON. Un claim que el token no trae, en blanco; si todo el texto se queda en blanco, no se añade nada.
  • Solo con un usuario identificado. En Parcial, un anónimo no recibe este texto, sino las instrucciones para anónimos, que no pueden llevar datos del usuario. Un token que no pasa no llega nunca al modelo: /embed contesta 401 antes. En el chat de pruebas del panel, los datos salen de la identidad de prueba; sin ella, el chat habla como anónimo.
  • Por qué no en el prompt del agente. Ese prompt es el mismo para todos y el proveedor del modelo lo cachea. Los datos de cada usuario van detrás, en un bloque aparte, así que la caché sigue funcionando. Un {{jwt.email}} en el prompt del agente se mandaría tal cual.
  • Adónde van. Al proveedor del modelo, en cada turno, aunque el emisor no guarde ninguno con la conversación (Qué se guarda del usuario habla de la base de datos de ur). El token, nunca: no tiene placeholder.
  • Mal escrito no se guarda. {{jwt.e-mail}}, {jwt.email} o el {{$user.claims.email}} de la tool HTTP: el panel lo avisa junto al campo y la API contesta 400. Cualquier otra cosa entre llaves ({{nombre}}) se manda tal cual.
  • La fecha y la hora también. {{now}}, {{time}} y sus versiones con zona ({{now.Europe/Madrid}}) valen aquí como en el prompt del agente (La fecha y la hora), también en las instrucciones para anónimos: las del inicio de la conversación. Se ponen antes que los datos del usuario, así que un claim que traiga {{now}} se manda tal cual.

Los valores los firma tu aplicación, pero alguno lo escribe el propio usuario (su nombre, por ejemplo). Preséntalos como datos y no como órdenes: «El nombre que ha dado el usuario es {{jwt.name}}», mejor que «{{jwt.name}}» suelto en mitad de las instrucciones.

Usarlo en tus tools ​

Si escribes tus propias tools sobre @nykk/ur-core, el usuario del turno sale de getEndUser(runConfig):

ts
interface EndUser {
  issuerId: string | null;         // el emisor; null en una identidad de prueba hecha solo de claims
  sub: string;                     // quién es (también en claims.sub)
  claims: Record<string, unknown>; // el payload entero del token
  token: string | null;            // el JWT tal cual, para reenviarlo a tu API
  verified: boolean;               // false solo con una identidad de prueba del panel
}

Devuelve null si el turno es anónimo, si el agente no pide usuario o si la tool corre fuera de un agente (una inferencia —también una que pide JWT para entrar: el token solo abre la puerta—, o Probar tool sin identidad de prueba). El usuario no llega al modelo: solo lo leen las tools.

ts
import { tool } from '@langchain/core/tools';
import { getEndUser, type ToolInstanceConfig, type ToolTemplate } from '@nykk/ur-core';
import { z } from 'zod';

export const myOrdersTool: ToolTemplate = {
  id: 'my_orders',
  name: 'My Orders',
  description: 'Los últimos pedidos del usuario identificado.',
  scope: 'app',
  // Sin usuario no sirve: no se engancha a un agente en `off` ni a una inferencia.
  requiresEndUser: () => true,
  functions: [
    {
      suffix: '',
      description: 'Los últimos pedidos del usuario que pregunta.',
      parametersSchema: z.object({}),
      factory: (instance: ToolInstanceConfig) =>
        tool(
          async (_input, runConfig) => {
            const user = getEndUser(runConfig);
            if (!user) {
              return 'Nadie ha iniciado sesión: para ver sus pedidos, el usuario tiene que hacerlo.';
            }
            // Una identidad de prueba del panel trae claims que alguien ha escrito a mano.
            if (!user.verified) {
              return 'Identidad de prueba: no se consultan pedidos reales.';
            }
            return JSON.stringify(await listOrders(user.sub)); // tu código
          },
          { name: instance.name, description: instance.description, schema: z.object({}) },
        ),
    },
  ],
};
  • verified solo es false con las identidades de prueba del panel. Una tool que decide permisos con el usuario tiene que mirarlo: en Probar agente, quien tenga acceso al agente puede escribir los claims que quiera.
  • requiresEndUser marca una plantilla que no sirve sin usuario. Una instancia suya no se puede enganchar a un agente en off ni a una inferencia, y en un agente en optional el modelo no la ve en los turnos anónimos. Puede depender de la configuración de la instancia: requiresEndUser: (config) => ….

Con la tool de petición HTTP ​

La plantilla http_request manda a tu API quién es el usuario sin escribir código: un claim del token en la ruta, la query, una cabecera o el cuerpo, o el token entero, que tu API verifica por su cuenta. El interruptor Reenviar el token del usuario añade Authorization: Bearer <token> a la petición.

Una instancia que usa al usuario solo se engancha a agentes en Parcial u Obligatorio, y en un turno sin usuario no sale ninguna petición: el modelo lee que el usuario tiene que iniciar sesión. Dónde se admite cada valor y el resto de reglas, en Tool de petición HTTP.

Modo parcial ​

Con el agente en Parcial (optional), los anónimos y los identificados hablan con el mismo agente, y puedes separar lo que recibe cada uno.

Tools solo para usuarios identificados. En la lista de tools del agente, cada una se puede marcar Solo usuarios identificados. En un turno anónimo, el modelo no la ve. Si aun así la llama (porque la vio antes en la conversación, por ejemplo), no se ejecuta: el modelo recibe que el usuario tiene que iniciar sesión. Las tools que necesitan usuario por sí mismas (una http_request que manda al usuario, una plantilla con requiresEndUser) van siempre marcadas. La marca vale en cualquier modo: en Abierto no hay usuario nunca, así que una tool marcada no la ve nadie.

Instrucciones por audiencia. Dos textos que se añaden al final del prompt del agente, según quién pregunte en cada turno: Instrucciones para usuarios identificados e Instrucciones para anónimos. Hasta 20 000 caracteres cada uno. Van detrás del prompt, en un bloque aparte, así que la caché del prompt del proveedor sigue funcionando. Las de los anónimos solo se aplican en Parcial, el único modo en que los hay. Las de los identificados, también en Obligatorio, y pueden llevar sus datos: ver Sus datos en las instrucciones.

Por ejemplo:

  • Para anónimos: «Si te piden algo de su cuenta, diles que inicien sesión en la web.»
  • Para usuarios identificados: «Puedes consultar sus pedidos con la tool mis_pedidos.»

En Parcial, la vista previa del prompt se puede ver como Anónimo o como Identificado: enseña el prompt y las tools que recibe el modelo en cada caso. En Obligatorio, se ve siempre como Identificado. Los datos del usuario salen sin sustituir: en la vista previa no hay token.

Probar en el panel ​

  • Probar un token, en la fila del emisor (Ajustes → Sistema → Emisores de JWT): si un JWT pasaría y por qué no.

  • Probar agente: si el agente pide usuario, el chat de pruebas tiene una Identidad de prueba. En ella pegas:

    • un JWT con Verificar la firma, que se verifica contra el emisor del agente, como en el canal real (si no pasa, dice por qué);
    • un JWT sin verificar: se leen sus claims sin mirar la firma ni la caducidad, y el token se reenvía tal cual, así que el backend que lo reciba lo verificará igualmente;
    • unos claims en JSON (con sub): un usuario inventado, sin token, así que una tool que reenvía el token del usuario no hace la petición y lo dice.

    Las dos últimas llegan a las tools con verified: false. Sin identidad, pruebas como anónimo, también en un agente Obligatorio (el canal real le contestaría 401).

    La conversación del chat de pruebas sigue siendo tuya: cambiar de identidad no abre la de ese usuario ni empieza otra, así que el modelo ve lo que hablaste con la identidad anterior. Para empezar de cero, reinicia la conversación.

  • Probar tool tiene la misma Identidad de prueba. Para verificar la firma hay que elegir el emisor, porque una tool no tiene agente del que sacarlo.

Qué dice la API ​

GET /api/agents, con un access token, dice de cada agente si pide usuario y qué token acepta: lo justo para que tu backend sepa firmarlo. Nunca claves ni secretos.

json
{
  "id": "0190c2a4-…",
  "name": "Soporte",
  "identifier": "soporte",
  "auth": {
    "mode": "optional",
    "issuer": {
      "issuer": "https://mi-app.example.com",
      "audience": "ur-soporte",
      "algorithms": ["ES256"]
    }
  },
  "tools": [
    { "name": "calculadora", "requiresEndUser": false },
    { "name": "mis_pedidos", "requiresEndUser": true }
  ]
}

Con mode: "off", issuer es null. Una tool con requiresEndUser: true solo existe para un usuario identificado.

Desde el MCP, get_agent y list_agents enseñan además el nombre del emisor y sus claves por kid, algoritmo y huella, nunca el secreto. El modo y el emisor de un agente se cambian en el panel: el MCP todavía no lo permite.