# Inventariar los agentes de Lelemon para una auditoría

Documento para el equipo de cumplimiento o de tecnología que tiene que armar, y mantener al día, el inventario de los agentes de Lelemon de su organización: qué agentes hay, cómo están configurados y, sobre todo, **a qué terceros viajan los datos de las personas que atienden**. Sirve, por ejemplo, para alimentar el registro de actividades de tratamiento (Ley 21.719). Alcanza para implementarlo sin preguntarnos nada.

Base de la API de Lelemon: `https://api.lelemon.cl`

## Qué resuelve esto

Un agente contesta, consulta conocimiento y, si tiene acciones, **manda datos a otros sistemas**: un ticket al sistema de soporte, un webhook a un n8n, un pedido a un ERP. Para el registro, lo que importa de cada agente es eso: qué datos puede mandar y hacia dónde.

Son tres lecturas, de la más general a la más fina:

1. **Listar** los agentes.
2. **Leer cada uno**: su prompt, su modelo, sus topes, sus canales, su conocimiento y las acciones que tiene concedidas, con el origen de cada destino.
3. **Mantenerlo al día**: releer sólo los agentes que cambiaron desde la última vez, mirando `updatedAt`.

Todo va con la credencial de organización en la cabecera `Authorization`:

```
Authorization: Bearer lel_a1b2c3d4e5f6_<...>
```

---

## Qué alcances pide cada cosa

| Qué quieren ver | Alcance |
|---|---|
| Los agentes, su configuración, su prompt y sus acciones concedidas (slug, nombre, descripción) | `agents:read` |
| Las colecciones de conocimiento de cada agente | `knowledge:read` además |
| **A qué origen sale cada acción** (HTTP, servidor MCP o conector), y el catálogo de acciones de un agente | `actions:read` además |

Para un inventario completo emitan una credencial con `agents:read`, `knowledge:read` y `actions:read`. Ninguna escribe: con esas tres no se puede cambiar nada en ningún agente.

El origen viaja con `actions:read` y no con un permiso aparte porque no abre ninguna puerta: el origen nunca fue el secreto. El secreto es el path (ver más abajo), y ese no sale con ningún alcance. Sin `actions:read`, el campo `destinationOrigin` no aparece en la respuesta: no viene vacío ni en `null`, simplemente no existe.

---

## 1. Listar los agentes

```
GET https://api.lelemon.cl/v1/external/agents?limit=50&offset=0
```

Alcance: `agents:read`. Devuelve una página:

```json
{
  "data": [
    {
      "id": "5f0b2f58-1d7e-4d0a-9d86-6b0a4a1f3c11",
      "slug": "ventas",
      "name": "Sofía",
      "description": "Atiende consultas de ventas por WhatsApp.",
      "status": "active",
      "audience": "external",
      "runtimeType": "text",
      "channels": ["whatsapp", "api"],
      "version": 3,
      "createdAt": "2026-08-02T14:10:00.000Z",
      "updatedAt": "2026-09-20T09:30:00.000Z"
    }
  ],
  "limit": 50,
  "offset": 0,
  "hasMore": false
}
```

Mientras `hasMore` sea `true`, repitan el pedido sumándole `limit` al `offset`. El techo de `limit` es 500.

La lista trae todos los estados (`draft`, `active`, `paused`, `archived`): un agente pausado sigue teniendo configurada la salida de datos, y conviene que el inventario lo incluya. No trae los borrados, ni **al asistente interno** de la organización: esa superficie no se administra por API, en ninguna ruta.

Sale del más nuevo al más antiguo. Si se crea un agente mientras recorren las páginas, uno puede aparecer dos veces: deduplíquenlo por `id`.

---

## 2. Leer cada agente

```
GET https://api.lelemon.cl/v1/external/agents/{agentId}
```

Alcance: `agents:read`. Con `knowledge:read` suma `knowledge`; con `actions:read`, cada acción suma `destinationOrigin`. Las acciones que salen en `actions` son las que el agente tiene concedidas hoy y puede usar: las nativas de Lelemon, las HTTP que registraron ustedes, las de sus servidores MCP y las de sus conectores.

```json
{
  "id": "5f0b2f58-1d7e-4d0a-9d86-6b0a4a1f3c11",
  "slug": "ventas",
  "name": "Sofía",
  "status": "active",
  "audience": "external",
  "channels": ["whatsapp", "api"],
  "version": 3,
  "systemPrompt": "Eres Sofía, la asistente de ventas de ...",
  "model": { "provider": "bedrock", "model": "claude-sonnet-4-6" },
  "maxIterations": 10,
  "maxTokens": 100000,
  "actions": [
    {
      "slug": "crear_ticket",
      "name": "Crear un ticket",
      "description": "Abre un ticket en soporte con lo que cuenta el cliente.",
      "destinationOrigin": "https://n8n.suempresa.cl"
    },
    {
      "slug": "buscar_conocimiento",
      "name": "Buscar en el conocimiento",
      "description": "Busca en los documentos del agente.",
      "destinationOrigin": null
    }
  ],
  "channelDetails": [
    { "slug": "whatsapp", "name": "WhatsApp", "connected": true }
  ],
  "knowledge": [
    { "id": "0c9d6a1e-...", "name": "Precios y garantías" }
  ]
}
```

### Cómo se lee `destinationOrigin`

| Valor | Significa |
|---|---|
| `"https://n8n.suempresa.cl"` | La acción manda datos a ese sistema. Es esquema, host y puerto si lo trae (`https://api.suempresa.cl:8443`). Vale para una llamada HTTP que registraron ustedes, para una acción de un servidor MCP que conectaron (es el origen del servidor) y para una de un conector (es el sitio que conectaron, como `https://suempresa.atlassian.net`, o el host de BigQuery). |
| `null` | Es una herramienta nativa de Lelemon (buscar en el conocimiento, recordar datos del caso, agendar, correo, herramientas del CRM) y **no tiene un destino que ustedes hayan configurado**. También vale `null` para una acción concedida que ya no existe, que el agente no puede ejecutar. Ver "Qué significa `null`". |
| `"(ilegible)"` | La acción sale a un destino —HTTP, servidor MCP o conector— pero su dirección guardada no se pudo interpretar. Hay que mirarla en el panel. |
| el campo no está | La credencial no tiene `actions:read`. |

### Qué significa `null`

`null` dice una sola cosa: **la acción es una herramienta de la plataforma y el destino no lo eligieron ustedes**. No dice que los datos no salgan de Lelemon. Algunas nativas usan proveedores que son de Lelemon y no varían por organización: el envío de correo, el calendario, la facturación electrónica. Esos proveedores son encargados de tratamiento de Lelemon, van en el registro como tal y no aparecen en este campo. Una acción que sí sale a un tercero que ustedes pusieron nunca viene en `null`: viene con su origen o con `"(ilegible)"`.

La lista de acciones que registraron ustedes (`GET /v1/external/actions`) trae el mismo dato, como `origin`, con el mismo alcance: es el mismo origen visto por acción en vez de por agente. La lectura por agente es la que responde "¿a dónde manda datos este agente?".

### El catálogo de un agente

```
GET https://api.lelemon.cl/v1/external/agents/{agentId}/actions
```

Alcance: `actions:read`. Devuelve el catálogo **entero** que ese agente puede tener, con `enabled` diciendo cuáles tiene concedidas hoy y `source` diciendo de dónde vienen (`Tus sistemas`, `Lelemon`, o el nombre del servidor MCP o el conector). Cada entrada trae el mismo `destinationOrigin`. Sirve para separar lo que el agente **tiene** de lo que **podría tener**: para el registro cuenta lo primero, y `enabled: true` es el filtro. Las acciones de servidores MCP y de conectores traen su origen igual que las HTTP.

---

## Qué entrega y qué no, a propósito

| Se entrega | No se entrega |
|---|---|
| El prompt, entero | La URL completa de cada acción, servidor MCP o sitio de conector |
| Modelo y proveedor, topes de vueltas y de tokens | El path y la query de la URL |
| Canales y si están conectados | Los valores de las cabeceras (credenciales del sistema de ustedes) |
| Las colecciones de conocimiento asignadas | Las llaves del proveedor del modelo |
| Las acciones concedidas: slug, nombre y descripción | Los datos de contacto que hay en el perfil del agente |
| El **origen** de cada acción (HTTP, MCP o conector), con `actions:read` | |

### Por qué el destino sale como origen y no como URL

Es el argumento central de esta guía, y no una limitación que vayamos a levantar: **el origen nunca fue el secreto. El secreto es el path.**

En un webhook de n8n, **el path es el token**. Quien tiene `https://n8n.suempresa.cl/webhook/a7f3-...` puede disparar el flujo de ustedes, sin más credencial. Entregar la URL completa a un inventario sería entregar la llave de cada integración a cualquiera que lea el inventario: el auditor, la planilla donde se copia, el correo donde se adjunta. Una URL puede además traer usuario y clave antes del host.

Y para el registro no hace falta. Lo que la ley pide saber es **a qué tercero** viajan los datos, y de ahí dónde está y quién es: eso lo dice el origen. Del path, lo único que se podría aprender es cuál flujo de ese tercero, y eso ya lo dice el nombre y la descripción de la acción.

Por eso ninguna credencial, con ningún alcance, recibe el path. El origen sí sale con `actions:read`: es justo el dato que una auditoría necesita y no sirve para entrar a nada.

---

## Lo que el inventario no cubre

- **Los proveedores de la plataforma.** Las herramientas nativas (correo, calendario, facturación electrónica) usan proveedores de Lelemon, que son los mismos para todas las organizaciones. Salen con `destinationOrigin: null` y van en el registro como encargados de tratamiento de Lelemon.
- **La finalidad y el país de destino** que se declararon al autorizar cada acción (art. 15 bis) no se leen por API. Quedan registrados en la organización.
- **Las conversaciones y los datos de las personas.** El inventario describe el agente, no lo que ha atendido.

---

## 3. Mantener el inventario al día

Un inventario que se armó una vez deja de ser verdad el día que alguien concede una acción nueva o cambia a dónde apunta una. Se mantiene mirando `updatedAt` de cada agente y releyendo sólo los que cambiaron.

### Qué mueve `updatedAt`

`updatedAt` de un agente cambia cuando cambia cualquiera de estas cosas:

- Sus ajustes: prompt, modelo, topes, canales, estado.
- Las acciones que tiene concedidas: se concede o se quita una.
- **Lo que hace una acción que tiene concedida**: se edita su dirección, su descripción o sus parámetros, se da de baja, o se borra. Una acción es de la organización y la comparten varios agentes, así que editarla mueve `updatedAt` de **todos** los que la tienen.
- **Lo mismo para un servidor MCP y para un conector**: cambiar la dirección del servidor, desconectarlo, que anuncie otro catálogo de herramientas, o cambiar el sitio o los límites del conector mueve a todos los agentes que tienen herramientas suyas.

Es un invariante de la base de datos: lo cumple el panel, la API y cualquier otra forma de escribir.

No lo mueven, a propósito, las llamadas de los agentes, la salud de un servidor MCP ni volver a verificar una credencial: nada de eso cambia a dónde van los datos.

Dos cosas que `updatedAt` **no** refleja, y que conviene releer de vez en cuando: las colecciones de conocimiento asignadas y si un canal está conectado. Ninguna de las dos cambia a dónde van los datos personales.

### `version` no sirve para esto

`version` es el candado de `expectedVersion` al modificar un agente con `PATCH`: sube cuando se modifica el agente (`PATCH`, activar, pausar, dar de baja) y sirve para que dos personas no se pisen. **No sube al conceder o quitar una acción, ni al editar una acción.** Si subiera al editar una acción, cualquier `PATCH` que alguien preparó leyendo el agente antes recibiría un `409` por un cambio que ese `PATCH` ni toca. Para saber si algo cambió, usen `updatedAt`.

### El ciclo

La primera vez se lee todo. Desde la segunda:

1. `GET /agents` y se siguen las páginas hasta `hasMore: false`. Cada agente trae su `id` y su `updatedAt`.
2. Se compara contra lo guardado la vez anterior, agente por agente:
   - un `id` que no estaba: es nuevo, se lee;
   - un `updatedAt` distinto del guardado: cambió, se lee;
   - el mismo `updatedAt`: no cambió, **no se lee**;
   - un `id` guardado que ya no está en la lista: se dio de baja o se borró, va a revisión.
3. De cada agente que se lee, `GET /agents/{agentId}`. Se guarda su `updatedAt` y lo que importa: `status`, `systemPrompt`, `model`, `channels`, `knowledge` y, por cada acción, `slug` y `destinationOrigin`.
4. Se compara con la lectura anterior de ese agente. Lo que cambió va a revisión de una persona.

Se compara el `updatedAt` contra el guardado de ese mismo agente, y no contra la hora del reloj de ustedes: así no hay que preocuparse por relojes desfasados.

Un cambio de origen es lo que más importa del ciclo: pasar de `n8n.suempresa.cl` a otro host es un tercero nuevo, y el registro hay que actualizarlo.

Una lectura completa cada semana, ignorando `updatedAt`, cubre lo que la fecha no refleja (las colecciones de conocimiento y la conexión de los canales).

### Cuánto cuesta

El techo es de 200 pedidos por minuto desde una misma dirección IP (y 3.000 por credencial). La primera vez, un inventario de N agentes son `1 + N` pedidos (y una página más cada 500 agentes). Después son `1 + K`, con K los agentes que cambiaron, que casi siempre son pocos. Con hasta 200 pedidos entra en un minuto; con más, espacien los pedidos y reintenten con un `429`. Una lectura diaria alcanza para la mayoría de las organizaciones.

### Ejemplo con curl

```bash
BASE=https://api.lelemon.cl/v1/external
AUTH="Authorization: Bearer $LELEMON_API_KEY"
GUARDADO=agentes-vistos.json   # {"<id>": "<updatedAt>", ...}; "{}" la primera vez

curl -s "$BASE/agents?limit=500" -H "$AUTH"   | jq -r --slurpfile visto "$GUARDADO"       '.data[] | select($visto[0][.id] != .updatedAt) | .id'   | while read -r ID; do
      curl -s "$BASE/agents/$ID" -H "$AUTH"         | jq '{id, updatedAt, slug, status, model, channels, acciones: [.actions[] | {slug, destinationOrigin}]}'
      sleep 2
    done
```

---

## Códigos de estado

| Código | Cuándo |
|---|---|
| `200` | Lectura correcta. |
| `401` | Credencial ausente, revocada o inválida. |
| `403` | A la credencial le falta `agents:read` (o `actions:read` en el catálogo). En la lectura de un agente, si le falta `actions:read` **no** da `403`: responde sin `destinationOrigin`. |
| `404` | El agente no existe, no es de la organización, o es el asistente interno. |
| `429` | Pasaron un techo por minuto: 3.000 pedidos por credencial o 200 por dirección IP. Esperen lo que diga `Retry-After`. |
