Revisión de coherencia de todo lo que entró. Cinco cosas mal, ninguna
detectable compilando:
1. LA PUERTA DE TOOLS DECIDÍA AL REVÉS. executeUmindTool encolaba antes de
clasificar, así que en un canal público CUALQUIER nombre de herramienta
terminaba como pendiente de aprobación — incluidos los que el modelo
inventa y los internos, cuyos guards ("solo el dueño") quedaban
inalcanzables porque vivían más abajo en el flujo. Un visitante del widget
podía llenarle al dueño la bandeja de solicitudes para ejecutar cosas que
no existen.
Ahora se clasifica primero: lectura ejecuta, interna se rechaza si el
pedido es público, acción se encola, desconocida se rechaza. Verificado
inyectando la falla: el test falla y vuelve a pasar al arreglarlo.
2. El enlace "Ver el documento" de la bandeja estaba muerto: archivo_id no lo
escribía nadie. Ahora la acción aprobada engancha el PDF que produjo.
3. Los PDF generados esquivaban la cuota de almacenamiento. El tope del plan
se evitaba pidiéndole documentos al agente en vez de subiendo archivos.
4. La vista de Consumo no conocía el tipo "documento" — el rubro nuevo salía
sin nombre ni color. Y el icono que le puse no existía en el set, así que
habría quedado invisible: el mismo bug de los iconos de hace unas semanas,
que compila y se ve vacío.
5. Las seis lecturas nuevas se registraron con w() en vez de r(), exigiendo
SoloAdmin para leer. No era una fuga —era más estricto— pero un usuario
staff sin admin veía la lista de agentes y 403 en sus avisos.
Y el guard que evita el próximo: TestTodoHandlerUmindValidaAlcance recorre los
65 handlers de uMind y exige que cada uno valide el alcance del tenant, o esté
en una lista de exenciones con el motivo escrito. Los mismos handlers se montan
para staff y para clientes; el middleware de scope deja los tenants permitidos
en el contexto pero no mira el :id de la URL — eso lo hace cada handler, y el
que se olvide compila igual. Al escribirlo el propio test encontró que mi lista
de exenciones sobraba en dos, y verifiqué con un handler con fuga deliberada
que lo detecta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El motor de PDF ya existía entero para el staff — Chrome headless, text/template
con {{range .Items}}, y hasta el importador que convierte un Word en plantilla
con IA. Lo único que faltaba era que fueran de cada cliente.
PlantillaDocumento gana TenantID *uint: nulo = global del staff (lo de
siempre), con valor = del espacio. Mismo patrón exacto que AiConfig, el que ya
tiene su lección aprendida. Y GetPlantillaDocumentoActiva ahora filtra
tenant_id IS NULL explícitamente: sin eso, la plantilla que un cliente escribe
para su propio contrato podía salir en un documento de la empresa. Hay test.
Un cliente sin plantilla propia cae a la global, así puede emitir una
cotización desde el primer día y personalizarla cuando quiera. Guardar crea
versión nueva en vez de pisar la vieja: si la nueva sale mal, la anterior sigue
ahí. Y se valida que compile ANTES de guardar — una plantilla rota descubierta
al generar deja al cliente esperando un PDF que nunca llega.
La tool solo se le ofrece al modelo si hay alguna plantilla disponible:
prometerle una capacidad que después falla es peor que no tenerla.
El semáforo de tres: cada PDF levanta un Chrome entero, y cien clientes
generando a la vez son cien navegadores. Eso tira el servidor mucho antes que
cualquier consulta a la IA, así que va desde el día uno y no cuando se caiga.
El JSON de ítems mal formado no tumba la generación — sale el documento sin la
tabla, que todavía se puede corregir a mano. Y sin cantidad se asume 1: el
modelo la omite seguido, y un total en cero es peor que uno aproximado.
Cada documento se cobra (levanta un Chrome) y queda en los archivos del
espacio, descargable como cualquier otro.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>