Saltar al contenido principal

0007 — Onboarding en 3 pasos y guardián de seguridad de los bots

Estado: aceptada · Fecha: 2026-09-19

Contexto​

El objetivo de producto: que un cliente nuevo tenga su bot respondiendo en su WhatsApp en tres pasos, sin ayuda. El asistente anterior (20260926000001) tenía ocho pasos (perfil, sedes, servicios, recursos, equipo, bot, canal, prueba), cada uno con vista previa y aplicación. El paso "canal" no conectaba nada: WhatsApp se conectaba en otra pantalla.

Abrir el alta a cualquiera exige cuidar lo que se activa. La auditoría del 2026-09-19 encontró:

  • No había moderación de contenido. Cualquier prompt se activaba, aunque el texto legal prometía moderación.
  • POST /api/bots/message, /api/bots/reload y /api/bots/:id/flow-versions no tenían guard. Con un tenantId y un botId cualquiera podía hacer hablar al bot de otro (y gastar LLM) o reescribir su flujo.
  • Meta prohíbe desde 2026 los chatbots de propósito general en la API de WhatsApp Business: un bot tiene que atender un negocio concreto.
  • Sin captcha ni límites, una cuenta recién creada podía usarse para enviar spam desde el primer minuto.

Decisión​

1. Tres pasos, todo en /onboarding​

Una pantalla completa (fuera del layout del dashboard) con tres pasos:

  1. Negocio: nombre, sector (industry_packs), qué ofrece y datos de contacto opcionales. La zona horaria y el país se detectan en el navegador. El pack siembra en silencio módulos, roles, servicios y una sede.
  2. Asistente: prompt prediseñado a partir del paso 1 (plantilla del pack), editable, con "Mejorar con IA" y un chat de prueba sin herramientas.
  3. WhatsApp: Embedded Signup con coexistencia (ADR 0006) sobre una cuenta de canal ya atada al bot.

El bot nace siempre como flujo LangGraph (buildSectorFlow) con su versión 1 en bot_flow_versions. "El prompt" que edita el cliente es el system_prompt del nodo llm marcado role: 'main_agent'. Sedes, servicios, equipo y horarios pasan a "siguientes pasos" opcionales.

2. Reglas de plataforma inmutables​

composeSystemPrompt añade, después del prompt del tenant y en tiempo de ejecución, unas reglas que el tenant no puede quitar porque no viven en el flow_definition: limitarse al negocio, no inventar datos, no pedir contraseñas ni tarjetas, derivar a una persona y reconocer que es un asistente virtual. Se aplica a todo nodo llm que conversa (no a los de output_mode: background).

3. Guardián de seguridad (backend/src/bot-safety/)​

Tres capas, gana la más severa:

  1. Patrones deterministas en español (safety-patterns.ts): gratis, sin red. Lo evidente se bloquea sin llamar a OpenAI. Cada patrón tiene su caso de "no molesta a un negocio legítimo".
  2. Moderación de OpenAI (omni-moderation-latest).
  3. Clasificador con salida estructurada contra la Business y la Commerce Policy de Meta, más "asistente de propósito general". El contenido va como dato entre etiquetas: no puede rebajar el veredicto de las otras capas.

Resultado: approved (activo), review (guardado pero inactivo hasta que un superadmin decida) o blocked (no se guarda; se explica el motivo). Sin ninguna capa de IA disponible, lo limpio va a review: nunca se afirma "limpio" sin haberlo comprobado.

Punto único de enforcement: la carga del registro. bots.safety_status vive en la base, y un trigger (bots_safety_guard) lo devuelve a pending en cuanto cambia el prompt o el flujo. El registro revisa los pending al cargarlos. Así queda cubierto todo camino que edita un bot: panel, Copilot, asistente de flujos, versiones y onboarding. Mismo trigger: el panel (authenticated) no puede escribir safety_status, y el backend solo conserva su veredicto si manda a la vez el safety_hash del contenido que revisó.

BOT_SAFETY_MODE=observe (por defecto) registra y avisa pero sigue sirviendo los bots existentes; enforce apaga lo no aprobado. El onboarding aplica enforce siempre. Los bots anteriores se aprobaron en el backfill.

Cola de superadmin: GET /api/bot-safety/queue y POST /api/bot-safety/:botId/decision. Una decisión humana para un contenido manda sobre las automáticas (caché por content_hash).

4. Anti-spam y abuso​

  • Endpoints cerrados. /api/bots/message exige sesión y pertenencia, salvo el widget de webchat heredado, que solo funciona si el tenant publicó ese bot en un webchat activo, sin poder elegir conversación ni variables y con 30 mensajes por minuto. /reload, /tenants/:id y /flow-versions exigen pertenencia; /tenants, superadmin.
  • Captcha Cloudflare Turnstile (NEXT_PUBLIC_TURNSTILE_SITE_KEY) en alta, login y recuperar contraseña. Supabase lo exige en los tres cuando se activa, así que no basta con el alta.
  • Correos desechables: trigger en auth.users contra blocked_email_domains, y aviso en el formulario con la misma lista (un test compara las dos).
  • Cuentas nuevas (tenants.trust_level = 'new'): tope diario de mensajes (BOT_NEW_TENANT_DAILY_CAP), tope diario de plantillas (NEW_TENANT_DAILY_TEMPLATES) y 20 mensajes por minuto por canal. Pasan a verified solas tras NEW_TENANT_TRUST_DAYS sin suspensión, o por un superadmin (POST /api/bot-safety/tenants/:id/verify). Las plantillas no se prohíben del todo porque los recordatorios de cita también lo son.

5. Enlace de configuración para tenants creados por un superadmin​

El alta pública está desactivada: los tenants los crea un superadmin. Antes tenía que inventar la contraseña del administrador y hacérsela llegar por fuera, con lo que Xenpia conocía la clave del cliente y esa clave viajaba por WhatsApp.

Ahora el alta no pide contraseña: crea el usuario con una aleatoria que nadie ve y devuelve un enlace de configuración de un solo uso (7 días) que el superadmin copia o manda por WhatsApp. Quien lo abre elige su contraseña, acepta las tres políticas (se registran con IP y navegador, igual que en el alta pública) y aterriza en los 3 pasos.

  • tenant_onboarding_invites guarda solo el sha256 del token, está cerrada a anon y authenticated, y es de un solo uso (used_at) con caducidad (expires_at). Generar un enlace nuevo revoca el pendiente.
  • Las acciones de superadmin resuelven user_is_superadmin por sesión antes de tocar nada: el tenantId que llega es entrada del cliente, no autorización.
  • La pantalla pública (/invite/setup/[token]) no revela nada sin el token; el token es el secreto.

Consecuencias​

  • Un cliente nuevo llega a tener el bot respondiendo sin tocar el panel.
  • Editar un bot existente lo manda a revisión automática al recargar. En observe no cambia nada para el cliente; pasar a enforce exige mirar antes la cola.
  • Cada revisión que no sale de caché cuesta dos llamadas a OpenAI (moderación + clasificador).
  • El widget de webchat antiguo (data-bot-id sin channelAccountId) solo sigue funcionando si el bot tiene un canal webchat activo.
  • El superadmin ya no conoce la contraseña de ningún cliente. Si el cliente pierde el enlace, se genera otro desde la lista de tenants.
  • Pendiente: la RPC create_tenant_with_admin del callback de alta (no versionada) puede crear un segundo tenant con el nombre de la organización. El paso 1 renombra el tenant personal; hay que confirmar en DEV qué hace esa RPC antes de quitar la llamada.