Product changelog
Every Simmetric Chat release, from the newest version to the oldest.
Added
- Modelo de propiedad de los widgets (2026-10-05 "wown", quick 261005-scn). Los widgets ahora tienen propietarios (su creador) y usuarios (concesiones por widget): - La unión
widget_users(migración aditiva) + el topeusers.widgetQuotaLimitpor propietario; NULL/fail-closed 0 — las instalaciones intactas ven cero cambio de comportamiento (el idioma del retículomaxSponsoredUsersde la Phase-206). - El CRUD de widgets rearmado para PROPIETARIOS no administradores:POST /api/widgetsva por el nuevo permisowidget:manage+ el tope asignado por la administración (superar → 409{ error, quota: "widgets" }); las barreras de licencia a nivel de instalación (widget_enabled/max_widgets) siguen sin cambios. GET list/detail, PUT settings + las vinculaciones de workspace alcanzan creator | widget-user | admin (los ID ajenos permanecen como 404 no enumerantes); DELETE sigue siendo creator | admin. - La nueva sección de administración en el área de widgets (/widgets?tab=quotas, la pestaña de cuota): filas por usuario — user | owned | max-widgets — con escrituras assign / clear (null), búsqueda paginada, registrado en auditoría (user.widget_quota.updated). - La nueva pestaña "Usuarios" por widget (WidgetDetail, en modo de edición): el PROPIETARIO selecciona los usuarios del widget (una PUT de conjunto de reemplazo, la sonda del directorio de concesiones/widgets/owner-user-search— una superficie de elección de identidad, sin correos); los usuarios con concesión obtienen el brazo de solo lectura. - Pruebas: la matriz de servidor de 30 pines (widgetOwnership.test.ts), los brazos de frontend (WidgetUsersTab/WidgetQuotasTab), el recorrido e2e (widget-ownership.spec.ts).
Fixed
- Compartir un workspace establece el rol editor por defecto, no el de lector (2026-10-05 "viewer-default"). El síntoma reportado — "como usuario con concesión, no puedo crear chats nuevos ni escribir en los chats existentes del workspace compartido" — se remontó (vía una nueva sonda de integración de 4 pines,
granteeChatWriteAccess.test.ts) al ROL de la concesión, no a las barreras: conWORKSPACE_ROLE_ENFORCEMENT=true, una fila WorkspaceAccess editor pasa GET /chats (200) Y POST /chat (200) server-side; una fila VIEWER pasa el brazo de lectura y se evalúa 403 en el brazo de escritura POR DISEÑO, y un receptor sin fila obtiene el 404 no enumerante (SC-4). AMBAS superficies de concesión (el diálogo de compartir del propietarioWorkspaceAccessDialogy el panel de SettingsSettingsWorkspaceAccess) preseleccionaban lector en los selectores de rol individuales Y masivos — un administrador que concedía sin cambiar el menú producía concesiones de solo lectura. La configuración por defecto de la UI también contradecía la intención a nivel de esquema: el comentario de migración de la Phase 189 D-12 fija el relleno de las filas heredadas en editor ("NOT the spec's viewer suggestion"). Ambas superficies establecen editor por defecto (individual + masivo); los pines de cableado de roles (una prueba que elige explícitamente lector y luego verifica que lector atraviesa hasta el final) quedan intactos. Nota: los usuarios con concesión ANTERIORES a esta corrección conservan su rol persistido — una reconcesión (upsert del rol) los re-evalúa. - "Resume el archivo adjunto" ya no miente con la frase prefabricada de no-resultados (el cierre de la brecha adjunto-antes-de-ingesta, 2026-10-05). Adjuntar un archivo en el chat lo carga a través de la pipeline de ingesta ASÍNCRONA; el mensaje pregunta un instante después, el documento todavía tiene CERO chunks, así que la resolución por ruta de
attachedDocumentId(ambos brazos del chat en chat.ts) no inyectaba NADA: el agente caía enrag_search, el documento reciente no estaba aún en la tienda de vectores, y la regla 3 de buildSystemPrompt respondía con la frase prefabricada "Non ho trovato informazioni su questo argomento nei documenti del workspace. Verifica che i file pertinenti siano stati caricati e indicizzati." — una afirmación falsa (el workspace puede estar lleno; solo el adjunto reciente no está listo). Ambos sitios de resolución ahora detectan el brazo de cero chunks e inyectan un aviso veraz (buildAttachmentNoticeChunk(new utils/attachmentNotice.ts)): el brazo non-empty-ragContext del orchestrator eliminarag_search, de modo que el modelo responde desde el aviso — "todavía en procesamiento, reintenta en un momento" (pending/processing) o "procesamiento fallido/cancelado, vuelve a cargar" — y el aviso prohíbe explícitamente la frase prefabricada. Consciente de estado y progreso (el progreso 0-100 del collector aparece solo cuando > 0); el escaneo DLP del contexto RAG montado en el aviso sin cambios. 5 pines de unidad sobre el ayudante (los brazos pending/processing/failed/cancelled + la instrucción anti-regla-3). Continuación de frontend — el esperador de ingest del chip de adjunto: el chip consulta cada 1,5s el nuevo endpointGET /api/documents/:id/attachment-status(triple control IDOR de metadatos, la proyección{ name, status, progress }, sin chunks), muestra un indicador giratorio + el progreso%mientras ingesta, RETIENE la barrera de envío (canSend+ la vía de Enter víaattachmentPending) hasta "completed", y suelta el adjunto con un aviso emergente sobrefailed/cancelled(404 → caída silenciosa; los errores de red transitorios siguen consultando — un tropiezo del servidor no debe destruir una ingesta sana). La barrera es una cortesía del cliente: el aviso del servidor queda como respaldo veraz de las pestañas obsoletas. 4 pines de endpoint en documentIdor.test.ts (la proyección access-user + los brazos D-04 de administración); 2 pines de ChatInputArea (pending deshabilita el envío + el chip de progreso; completed lo vuelva a habilitar). - Settings → "Acceso a los workspaces": las filas de concesión muestran de nuevo el nombre del usuario (Q6-fast settings parity, 2026-10-05). El diálogo de acceso a los workspaces había corregido este mismo defecto (la cadena username → id), pero el panel de Settings mantenía la cadena PRE-Q6
user ? displayName(user) : userId— y el circuito D-15 nunca envía un objetouseradjunto (T-189-11), de modo que cada fila del panel expandida caía en el UUID crudo. El panel ahora refleja la cadena del diálogo (el usuario adjunto forma heredada →grant.username→ id) y su suite fija la verdadera forma del circuito: el simulador estático eliminó el campousernunca enviado, una prueba nueva verifica que el nombre de usuario se renderiza y el UUID jamás (+1 prueba). - El punto de entrada del workspace compartido: las filas de la página Workspaces ahora ABREN (2026-10-05 "open-row"). Un receptor de concesión a nivel de workspace (lector/editor SIN acceso a proyectos) nunca podía alcanzar el workspace compartido en la UI del chat: los selectores de la barra lateral son proyecto-primero — el menú de proyectos va por
GET /api/projects(lista solo los proyectos del creador / ProjectAccess, sin brazo de concesión de workspace) y el menú de workspaces estádisabledhasta que se seleccione un proyecto, filtrado porprojectId === selectedProjectId— mientras la página Workspaces listaba el workspace compartido (el filtro OR deGET /api/workspacesincluye el brazo de concesiónaccessGrants.some(userId)) pero sus filas eran solo de gestión, sin forma de entrar.WorkspaceRowgana una acción "Open" (primera en la columna de acciones, renderizada para CADA fila listada — la pertenencia a la lista implica al menos lector por el filtro OR del servidor; la barrera del cliente es solo UX, las rutas del chat reponen la barrera server-side por solicitud) que monta el workspace a través de ChatContext (setWorkspaceId(id)+ navigate("/") — persistelastWorkspaceId, así que la restauración al arranque vuelve al contexto al recargar sin los selectores). Un efecto espejo en App mantiene coherentes los menús de la barra lateral cuando el workspace activo cambia fuera de los selectores — SOLO cuando el proyecto padre está en la lista de proyectos del usuario; la superficie del chat es pilotada por el solocurrentWorkspaceId. Los selectores de la barra lateral ganan una entrada estática "Compartidos conmigo" (el valor centinela__shared__en el menú de PROJECT, renderizado solo cuando el conjunto de compartidos no está vacío): con un proyecto seleccionado la lista de workspaces se acota a ese proyecto como antes; elegir el centinela (o una selección vacía al arranque) hace que el menú de workspaces liste el conjunto invisible-por-proyecto (todos los workspaces alcanzables por el filtro OR cuyo padre del proyecto falta enGET /api/projects) — la lista de compartidos del receptor solo-workspace, antes inalcanzable porque el control requería primero un proyecto Y Radix Select no puede vaciar un ID de proyecto concreto una vez elegido (callejón sin salida unidireccional; la entrada nombrada es el camino siempre clicable de regreso). El marcador va guiado por el contenido a través del ayudante compartidoworkspaceSelectPlaceholderKeyensidebar/navModel.tsx(un centinela vacío obsoleto de una concesión revocada degrada a la formulación genérica en lugar de un alcance compartido habilitado-y-vacío;workspaceSelectDisabledbarrera los montajes deAppNavOverlay— elAppSidebarNavintegrado se renderiza solo sobre elementos anfitriones). Las claves i18nworkspace.open+sidebar.selectSharedWorkspace+sidebar.sharedProjectsañadidas a los 8 idiomas — la paridad verde. Pines nuevos: las filas de lector muestran Open y jamás delete/edit, unonOpenausente no renderiza ningún control, el pin acompaante de página verifica la escrituralastWorkspaceIden el localStorage vía el ChatProvider real (WorkspacesPage), el brazo compartido se renderiza desde los elementos anfitriones sin proyecto y se esconde cuando está vacío (AppSidebarNav), la elección del centinela muestra el marcador compartido mientras un centinela obsoleto degrada, y la matriz de barreras del overlay fija habilitado/deshabilitado por brazo + marcador en el escritorio y el móvil (AppNavOverlay). - El diálogo "Acceso" de los workspaces (el botón superior de la barra lateral) ya no desborda su propio borde (2026-10-05). Cada bloque de ancho completo se escapaba del borde redondeado del diálogo: el
DialogContentdel kit de UI es una rejillagridSINgrid-template-columnsexplícito, de modo que los hijos se sentaban en UNA única pista implícitaautoque crece hasta la contribución min-content del elemento más grande — una fila de concesión con un nombre de usuario UUID irrepartible de 36 caracteres aportaba 503–549px contra la caja de contenido de 488px (sm:max-w-[520px]), ensanchando la pista y ESTIRANDO cada elemento de ancho completo (el título, la fila de selectores, la lista de casillas, las<li>de concesión) más allá del borde. La corrección de dos capas: la base del kit de UI fija ahora una columna explícita de mínimo-cero (grid-cols-[minmax(0,1fr)]— toda la familia de Dialogs encoge los elementos a la caja de contenido limitada en vez de empujar la pista, en toda la aplicación) y el sitio del diálogo añadebreak-wordsal título interpolado +flex-wrapa las filas de concesión/masivas y a cada<li>de concesión (los botones de nowrap se envuelven a su propia línea por debajo de ~360px de pantalla). Verificado con Playwright: cero desbordamiento 380→1280px (los nombres de concesión se truncan con elipsis según lo diseñado, nada cruza el borde); el nuevoWorkspaceAccessDialog.test.tsxfija ambas capas — 174 suites / 1487 pruebas frontend verdes (+1 suite, +4 pruebas). - Las filas de concesión de "Acceso" muestran el NOMBRE del usuario de nuevo (el seguimiento rápido de Q6, 2026-10-05). La respuesta del servidor D-15 lleva
{userId, username}pero NO un objeto de usuario adjunto (eliminado con T-189-11 para dejar de filtrar correos/roles), mientras el diálogo mostraba el nombre ÚNICAMENTE desde ese campo opcionalusermuerto — cada fila de usuario asignado caía en el ID UUID. Las filas de concesión ahora resuelven el usuario adjunto (forma heredada) →username→ id;WorkspaceAccessGrantgana el campousernameque el circuito realmente envía. Pin nuevo: el nombre de usuario renderizado / el ID jamás filtrado / las filas heredadas con null como nombre de usuario conservan la cadena de reserva al ID (+1 prueba). - El enlace profundo de Settings restaurado (2026-10-05). Una carga completa de la página
/settings(marcador, entrada de la barra lateral, entradas de navegación heredadas) dejaba de abrir el diálogo de ajustes desde la revisión R-5 "settings as dialog": al primer commit las consultas de auth seguían en vuelo (user = null) y el efecto close-on-logout — declarado después del abridor en el mismo componente — lo volvía a cerrar en el mismo lote (open-then-cancel), y el pathname tras la carga ya estaba normalizado así que nada reabría. La navegación suave (el menú del usuario) no estaba afectada. El abridor ahora dispara solo sobre una sesión CARGADA y la normalización conserva la cadena de consulta, de modo que los enlaces profundos?tab=sobreviven al montaje del diálogo; los marcadores?tab=también ENTRAN ahora en la sección apuntada (el mismo brazo que un clic en una entrada de la barra: la página del grupo se abre y la entrada marcadata-active).e2e/settings-left-menu-nav.spec.tsestá verde de nuevo. - También verificado como no-un-error:
POST/DELETE /api/system/settings/branding/iconson las rutas del plugin ENTERPRISE (simmetric-enterprise/brandingRoutes, Phase 145,mountProtected+adminLicense("white_label")) — la compilación community responde 404 y la UI degrada con el aviso emergente documentado de mejora por diseño (la barrera grep D-12whiteLabelExtractionGuard); la batería EE 37 suites / 737 pruebas verdes, la barrera community verde. - El panel de Settings ya no desconecta a los usuarios no administradores que lo abren. El enrutador de los ajustes estaba bloqueado de punta a punta con
requireAdmin(el permisoadmin:settings) mientras el retículo de visibilidad por rol (SETTINGS_TAB_PERMISSIONS/RoleSectionVisibility) seguía renderizando secciones — Perfil, Apariencia, proveedores LLM, Avanzado — a los usuarios ordinarios. Abrir el panel (o una sección) hacía que la carcasa deSettingsPageconsultaraGET /api/system/settings, recibiera 403, y la página confundía 403 con 401: mostraba "Session expired. Please log in again.", borraba el token y redirigía a /login 3 segundos después — destruyendo una sesión enteramente válida por un simple hueco de permiso. Corrección en dos partes (2026-10-04 "settings-panel logout"): - Servidor:GET /api/system/settingsahora es consciente de permisos. Los administradores (portadores deadmin:settings) conservan la carga útil de consola completa byte-igual a la anterior; cada otro usuario autenticado recibe solo el subconjuntoNON_ADMIN_SETTINGS_READ_KEYS(compartido) — las claves no sensibles consumidas por las superficies alcanzables de los no administradores (BRANDING_* para la pestaña Apariencia / la cabecera de la app,ALLOW_NON_ADMIN_UPLOADpara la página de cargas) — cada entradareadOnly: true(la regla antidesfase: nunca renderizar un formulario que el servidor rechazaría con 403). Los secretos (SMTP_PASS,LLM_API_KEY,JWT_SECRET,DATABASE_URL,VECTOR_DB_API_KEY, ...) nunca pueden alcanzar una respuesta no administrativa. La vista de la organización (?organizationId=) y todas las rutas de escritura (PUT /,POST /mail-test) quedan solo para la administración (fail-closed). - Frontend: solo un 401 (fallo de autenticación) destruye la sesión — en el brazo de redirección de 3 segundos deSettingsPagey en el brazo de erroruseMedeApp.tsx. Un 403 ahora renderiza una banda distinta "No tienes permiso para ver estos ajustes." (settings.errorNoPermission, los 8 idiomas) y la sesión sobrevive. - La ingesta del Collector: los archivos cargados con una extensión de tipo texto (
.txt.md.csv) pero con unos bytes mágicos PDF ya no se indexan como UTF-8 crudo (el incidente AI-ACT.txt del 2026-10-01). Un PDF renombrado como texto alimentaba el índice de vectores solo con las entrañas del PDF sin comprimir (el diccionario de Información, los metadatos XMP, los diccionarios de objetos, los elementos de estructura) — 3321 chunks, ninguno de texto normativo — mientras el cuerpo dentro de los flujos comprimidos FlateDecode degradaba a basura U+FFFD.parseFileahora husmea la cabecera%PDF-(los primeros 1024 bytes, la ventana desde la basura de ISO 32000) en los archivos declarados texto y los dirige al analizador de PDF; los metadatos de los chunks ganan un selloroutedAsPdfy un falso positivo del husmeo (un archivo de texto que solo menciona "%PDF-" temprano) vuelve al analizador de texto en vez de hacer fallar la ingesta.
Added
- Conectores: los conectores existentes ahora pueden ser MODIFICADOS desde la interfaz de administración (Q1-quick) — el brazo preexistente
PUT /api/connectors/:idpor fin sale a la superficie: el menú de desborde de la tarjeta gana Edit (la primera entrada), que abre un diálogo prellenado sobre la superficie mutable (el nombre, la vinculación del archivo de conocimiento, los mensajes de bienvenida/respaldo, la localización de respaldo, la tasa limitada + el límite de sesiones diarias — vacío pone null,0= el valor centinela UNLIMITED del enrutador).isEnabledqueda en el interruptor de la tarjeta, elpollModede Telegram en los botones de webhook, y la plataforma/el botToken/el workspace siguen siendo inmutables por esquema. El brazo PUT también gana el evento de auditoríachat_connector.updated(a paridad con el brazo DELETE, D-21). - Phase 221 (INSTLIC) — la ranura de pegado de licencia de instancia (la mejora enterprise sin .env): una nueva superficie de administración
GET/PUT/DELETE /api/license/instance(requireAdmin + el middleware de inquilino; el apilamiento por ruta deja GET /info y /diagnose intactas) más el panel Settings → Security → Licencia. El pegado verifica primero por sonda un JWT RS256 de nivel ENTERPRISE no reclamado (una clave rechazada nunca persiste: la enumeración cerrada{ error, reason }— invalid / expired / plugin_bound / community_tier), guarda SOLO el cifrado AES-256-GCM en SystemConfig (la claveINSTANCE_LICENSE— la disciplina de costura de escritura de SMTP_PASS), invalida el caché de configuración Redis D-07, y se aplica a la instancia EN MARCHA por el MISMO resolutor que ejecuta el paso de arranque (setCachedLicense — los sobresalientes de límite inyectados por el cargador sobreviven; las banderas booleanas se activan de inmediato; borrar el pegado vuelve al estado del próximo arranque con paridad de arranque). La precedencia, el arranque y el runtime idénticos: la ranura → la fila de plugin verificada → la variable de entornoLICENSE_KEY→ Community. ElPUT /api/system/settingsmasivo rechaza la clave (la doctrina de la ruta de escritura dedicada para la retención del chat) y el listado de ajustes nunca expone el cifrado. - Phase 219 (SETP-01/02) — banderas de ver + modificar por rol para cada sección de Settings:
RoleSectionVisibility.canModifyaditiva (se resta del respaldoSETTINGS_SECTION_MODIFY_PERMISSIONS— jamás concede,admin:settingsterminal),settingsModifyenGET /api/roles/me/menu-sections, la barrera del servidor en las mutaciones de sección que devuelve403 { error, section }(las mutaciones/api/providers→llm;POST /api/agency/users→team) y una columna Modificar en el editor de visibilidad de roles (PUT /api/roles/:id/visibilityaceptacanModifypor entrada — dejar en el mismo estado cuando falta).
Fixed
- Los diálogos de confirmación de eliminación de la barra lateral ya no desbordan (Q5-quick): las confirmaciones de eliminación de chat/carpeta eran cajas
w-80cuyo botón del kit de UI eswhitespace-nowrap shrink-0POR DISEÑO — las etiquetas de acciones de carpeta localizadas largas ("Mantieni N chat…" / "Elimina N chat permanentemente") escapaban horizontalmente de la caja, y los nombres de carpeta interpolados largos desbordaban el título. Las cajas son ahoraw-[22rem] max-w-[calc(100vw_-_2rem)](más anchas + acotadas a la pantalla), las filas de botonesflex-wrap, las dos etiquetas de acción largas sobrescriben el nowrap (whitespace-normal text-left), y los títulos/textos interpolados llevanbreak-words. Las tres claves i18n que faltaban del diálogo de eliminación del chat (sidebar.deleteChat/.deleteChatConfirm/.deleteChatButton) se entregaron en los 8 idiomas — la interfaz italiana ya no muestra los respaldos insertados en inglés ahí. El seguimiento Q5: el primer límite nunca compilaba —calc(100vw-2rem)dentro de un valor arbitrario es CSS INVÁLIDO (Tailwind exige la idioma del subrayadocalc(100vw_-_2rem)para los espacios alrededor de los operadores) y la regla nunca se emitía silenciosamente, dejando la caja de 352px sin límite en las pantallas estrechas. El utilitario corregido se entrega en el CSS construido (max-width:calc(100vw - 2rem)verificado en el paquete dist). - Resolución del modelo: los turnos del chat ya no aterrizan sobre el predeterminado fijado en código
qwen2.5:3bcuando no existe un pin explícito (Q4-quick — la causa raíz del incidente de los cuatro chats: los chats nuevos servían un modelo local pequeño en lugar del predeterminado del workspace, 34-98s por turno). Dos correcciones: (1) los predeterminados de esquema deChat.modelyWorkspaceAgentConfig.modelcambiaron de"qwen2.5:3b"al centinela""(migración aditiva —ALTER COLUMN … SET DEFAULT, las filas existentes intactas): la cadena falsa (model || chat.model || undefined, orchestrator:388/1059) ahora cae a la cadena de configuración del agente del workspace exactamente como el centinela hermano"default"; (2) la preferencia de modelo por workspace en el localStorage ahora es SOLO-EXPLÍCITA — las elecciones de la paleta escribenexplicit: true, el re-persist automátic de la resiembra desapareció (un predeterminado resuelto por cadena no es una elección del usuario y no debe volverse pegajoso) y los valores heredados sin marca se limpian una vez y nunca se usan como candidatos. Una preferencia obsoleta (p. ej.qwen2.5:3b, sigue siendo válida-aunque-lenta en el registro) ya no puede sombrear el predeterminado del workspace en cada chat nuevo. - Chat: "un mensaje abre N chats" — el cliente adoptaba el chatId del servidor solo en el evento SSE
done(hasta ~90s más tarde en los turnos ricos en herramientas), de modo que un reintento del usuario en esa ventana re-enviaba sin chatId y el servidor creaba un chat NUEVO por reintento, mientras el turno en vuelo terminaba en segundo plano como una fila huérfana (Q3-quick). La ruta del flujo ahora emite un evento aditivochatque lleva el chatId al INICIO del turno (antes de cualquier token); el cliente lo adopta de inmediato (protegido + idempotente — un envío que ya lleva un chatId nunca re-adopta) y actualiza la barra lateral en el acto. Los reintentos ahora aterrizan en el MISMO chat. Byte-igual para los clientes previos a Q3 (los eventos SSE desconocidos se ignoran). - Recuerdo de memoria: un par (usuario, workspace) con CERO recuerdos almacenados ahora salta todo el gancho de recuerdo pre-LLM (Q2-quick) — una cuenta indexada de Postgres (
@@index([userId, workspaceId])) bloquea el gancho antes de cualquier trabajo: sin incrustación de consulta, sin ida y vuelta del collector, y sin más advertencia por turno[vector-store] Tabla "ws_user_memory_…" no encontradade una tabla LanceDB que legítimamente no existe hasta la PRIMERA escritura de recuerdo (elensureTablede vectorStore crea en la primera ingesta; el brazo de búsqueda falla abierto a []). Un tropiezo de la base en la cuenta falla abierto al recuerdo prebarrera (el contrato de mejor esfuerzo del gancho — nunca bloquear el chat). - shared: la guardia de regresión
FEATURE_FLAGSquedó rezagada por el traslado de los conectores a la EE (quick 260930-l4q) — el pin subió de 12 → 13 porconnectors_enabled.
Fixed
- Copia de seguridad/restauración: un ensayo en seco o una restauración que falla con "Unable to decrypt (no key in chain matched)" (credenciales de destino escritas bajo una
ENCRYPTION_KEYprevia) ahora devuelve un error guiado que nombra la causa, las palancas de recuperación (LEGACY_PREVIOUS_ENCRYPTION_KEYS/ unaENCRYPTION_KEYprevia, luego el reinicio), el número de claves probadas y el manual de recuperacióndocs/ENCRYPTION_KEY_ROTATION.md— que ganó una sección de recuperación de Docker "Backup restore dry-run fails". Solo la longitud de la cadena de claves se divulga, nunca el material de las claves.
- Phase 220 (SPLIT-01/02) — la coherencia de la escisión del repositorio:
docs/REPO_BOUNDARIES.md(la matriz de auditoría módulo→repositorio para 4 repos con la doctrina de clasificación de 7 criterios y las excepciones por diseño) + la barrera de fronteras del repositorio:scripts/repo-boundary-check.cjs(una barrera de grep con lista de permitidos — el núcleo no debe referenciar los paquetes de plugin privados fuera de las dos costuras de carga y de los sitios de contrato documentados) + la autopruebascripts/repo-boundary-check.test.cjs+ el trabajo de CIrepo-boundary-checkcableado enbuild.needs
Added
- UI: revisión completa de la carcasa (R-2 → R-5). El estado vacío del chat muestra una marca de palabra monocromática elegante (la animación de descomposición cian/magenta queda ahora confinada al tema hacker); la barra lateral se convierte en un raíl minimalista al estilo Claude (nuevo chat + lista de chats + menú del pie) con la navegación completa en un overlay cmdk a la medida del teclado (Cmd/Ctrl+/) que mantiene el filtrado RBAC exacto; el menú de usuario se mudó de la barra superior a un diálogo dedicado (cuenta, idioma, tema, ajustes, cerrar sesión) con un cajón a la izquierda en el móvil; Settings se abre como un gran diálogo maestro-detalle (menú a la izquierda + detalle a área completa con conmutador de raíl y miga de pan) en vez de una página enrutada — el área principal es ahora el chat solo en cada página. Los temas (claro "Pearl", oscuro, hacker, sistema) recibieron una pasada de coherencia con correcciones de contraste WCAG-AA para el texto atenuado y los acentos del chat, guardias
prefers-reduced-motionpara todos los overlays, y una marca aware del etiquetar blanco en el logotipo del texto. - Docker: el servicio de metasolicitud de búsqueda searXNG opcional añadido a la pila compose (infraestructura pura — nada depende de él aún; la etiqueta de la versión fijada, un comprobador de salud basado en wget, los ajustes auto-abastecidos por el punto de entrada de la imagen).
- Docker: el plugin privado
@simmetric-chat/saasahora se monta en el servidor compose como el plugin enterprise (un montaje por vínculo del repositorio hermano completo; el saasLoader no-ops sobreMODULE_NOT_FOUNDen el modo community).
Fixed
- Sembrado: un fallo de arranque durante la siembra de usuarios podía barrer los roles y bloquear al administrador; las siembras ahora se autosan — la concesión idempotente vuelve a aplicar el rol de administrador al usuario
adminsuperviviente en cada arranque (consignado en los elementos diferidos de STATE.md). - Tenencia:
POST /api/auth/registerahora consolida la creación del usuario, la concesión del rol predeterminado y la pertenencia de la organización por defecto en una sola transacción, y cada vía de siembra/arranque re-ejecuta el ayudante de pertenencia idempotente en su rama "el usuario ya existe" — un fallo a mitad del arranque/solicitud ya no puede dejar a un usuario sin pertenencia (la revisión de la Phase 182 WR-01); el resucitador de lápidas ahora aplica elroleInOrgde quien llama para que la eliminación degrade los privilegios (WR-02). - Las compilaciones Enterprise: la sección Settings → Advanced → Backup (y las rutas
/ssoy/logs) ya no quedan bloqueadas detrás de la tarjeta "requires Enterprise" tras un inicio de sesión en la aplicación o un acceso SSO. La consulta del manifiesto de los módulos enterprise (GET /api/enterprise/modules) ahora está bloqueada por la autenticación y es reactiva a la sesión: antes se consultaba una vez, sin autenticación, al arranque de la aplicación y se cacheaba para toda la sesión (retry: false+staleTime: Infinity), de modo que en la pantalla de inicio devolvía 401 y las funciones Enterprise permanecían ocultas hasta una recarga completa CON el token almacenado. El gancho ahora se suscribe auseMe()y vuelve a consultar el manifiesto cuando aparece una sesión (el inicio con contraseña, la entrega SSO?token=), y nunca consulta el endpoint sin sesión en absoluto. - Docker: el ayudante de abastecimiento de secretos de producción (
docker/provision-encryption-key.sh) ya no mete al servidor en bucles de fallo cuando el.envraíz se despliega con un marcador de posición del plantilla.env.examplesin llenar (<sostituire-con-valore-generato>) como valor deAPI_KEY_HMAC_SECRET/ENCRYPTION_KEY— un marcador de posición ahora se reconoce como "no establecido" (advertencia sonora) y el abastecimiento cae a las vías de restauración/generación. Cualquier otro valor inválido no vacío sigue fallando ruidosamente en la barrera del arranque. - Seguridad: el router de ayuda para las pruebas E2E (
/api/__tests__, un servidor eco MCP de inicio/parada sin autenticación) ya no se monta cuando el servidor corre conNODE_ENV=production— los arranques de producción responden 404 ahí, aplicando el contrato documentado de solo dev/prueba del router (ningún endpoint de engendrado de procesos sin autenticación en producción). - Seguridad: el control del callback del collector de importación de archivos (
PUT /api/archives/import/:jobId/callback) ahora compara la cabeceraX-Collector-Secreten tiempo constante (crypto.timingSafeEqualcon una guarda de longitud), alineando la disciplina de tiempo seguro ya usada por el callback de estado de los documentos y la barrera de secreto del collector — cerrándola última comparación de secreto en claro en el servidor. - Autenticación:
POST /api/auth/loginahora valida su cuerpo a nivel de ruta con el esquema compartidologinSchema(safeParse) — un cuerpo inválido o vacío devuelve400 { error, details }coherente con cada otra ruta de autenticación, en vez de un401con un mensaje Zod crudo. Las credenciales válidas-aunque-erróneas siguen devolviendo401 "Invalid credentials". - Autenticación:
loginahora ejecuta unbcrypt.compareseñuelo contra un hash desechable fijo cuando el usuario no se encuentra, así que el tiempo de respuesta ya no distingue nombres de usuario existentes de inexistentes (el endurecimiento de la enumeración). Ambas vías siguen devolviendo el mismo401 "Invalid credentials". - Autenticación:
POST /api/auth/admin-reset-passwordahora valida su cuerpo con el nuevo esquema compartidoadminResetPasswordSchema(userIdUUID +newPasswordde 8-128 caracteres,safeParse→400 { error, details }) — la última ruta de autenticación deja la validación de cuerpo desestructurada ad-hoc. - Autenticación: la rama de registro cerrado de
POST /api/auth/registerya no duplica la lógica de autenticación en línea — usaverifyToken/isTokenRevoked/isAdmin/getCachedUserWithRolesimportados estáticamente (sin unawait importdinámico, sin reviva comprobación de cabecera muerta, la búsqueda del usuario en caché) preservando cada forma de respuesta y el orden revocación-antes-de-búsqueda. - Seguridad: el endpoint de agotamiento de caché del widget (
POST /api/config/:widgetId/cache-bust) ahora compara la cabeceraX-Api-Keyen tiempo constante (crypto.timingSafeEqualcon una guarda de longitud) y falla cerrado si la clave esperada no está establecida, en lugar de la comparación en claro previa con la forma fail-openexpected &&— el control de la clave del widget ahora corresponde a la disciplina de tiempo seguro usada por las barreras de secreto del servidor y el collector. - La pipeline de publicación: las cinco imágenes de Docker ahora se construyen solo amd64. Las compilaciones
linux/arm64emuladas con QEMU se atascaban hasta ~5h (el widget primero, luego la compilación de fuentes pgvector todo-en-uno) y agotaban el disco de las minutos pagados de un repositorio privado;setup-qemueliminado, cada entrada de la matriz publicalinux/amd64, y la matriz de compilación se deserializa amax-parallel: 2. Los usuarios de arm64 pueden auto-compilar localmente (los Dockerfiles son seguros para la compilación cruzada) o adjuntar un corredor arm64 autohospedado — véase la nota arm64 dedocs/DEPLOYMENT.md.
Changed
- Herramientas: una barrera de código muerto ahora corre en CI (knip) y falla por archivos, dependencias o exportaciones no utilizadas fuera de las listas de permitidos documentadas; una barrida de todo el repositorio eliminó ~3400 líneas de código muerto, la generación de la clave API del widget ahora vive detrás del flujo estándar de administración, y los scripts i18n informan las claves de traducción definidas-pero-nunca-usadas como un indicio de limpieza no bloqueante.
- Widgets: el pin del modelo de respuesta por widget — la configuración de administración de cada widget ahora puede seleccionar el proveedor LLM + el modelo que sirve las respuestas del chat de ese widget (
responseProviderId/responseModel, migración aditivaadd_widget_response_model). El pin se resuelve server-side desde la fila de la base de datos del widget en cada solicitud de chat (nunca provista por el cliente — el proxy del widget y la interfaz del visitante quedan intactas), tiene prioridad sobre el predeterminado del workspace y los widgets sin establecer conservan la cadena de resolución workspace/global existente. Los chats del widget ahora registran el modelo que realmente los sirve.
Added
- RAG: el filtrado de metadatos para
rag_search— el agente ahora puede acotar la recuperación por tipos de documento (documentTypes: pdf/md/txt/csv/docx/xlsx) y fechas de ingesta (dateFrom/dateTo, ISO; un límite superior de solo fecha incluye todo el día), corrigiendo el modo de fallo "los documentos de 2023 para las preguntas de 2025". Los filtros atraviesan la búsqueda híbrida entrando en ambas patas de recuperación: la pata de vectores llega al collector como verdaderos prefiltros sobre los metadatos de ingesta estampados (los predicados JSONB de pgvector + las cláusulas must de la carga útil de Qdrant; LanceDB y Chroma ignoran las claves nuevas con una advertencia registrada), y la pata FTS gana predicados SQL parametrizados sobre la tabla de documentos. Un respaldo post-recuperación server-side contra la tabla authoritativa de documentos garantiza la corrección filtrada en cada proveedor. Los documentos injeridos de aquí en adelante son siempre filtrables; los vectores heredados (pre-estampado) se excluyen mientras un filtro está activo_y regained the filterability via admin re-embed — sin cambio de esquema, sin re-embed de los documentos existentes, y un comportamiento sin filtros sin cambios. - DLP: cuatro patrones integrados nuevos para los identificadores europeos (sobre todo italianos) más comunes, sembrados idempotentemente por una migración de solo datos y reflejados en el conjunto de reserva sin base de datos —
it_vat_iva(Partita IVA; ancorado a la etiqueta, así que un número desnudo de 11 cifras nunca coincide),it_codice_fiscale(el código fiscal de 16 caracteres; solo en mayúsculas, las palabras de prosa como "foschi" nunca coinciden),iban(el prefijo del país + longitud ≥ 15; las formas compacta y agrupada por espacios, tiempo lineal/a prueba de ReDoS), yeu_phone(teléfono IT/EU; sembrado DESHABILITADO para revisión de la administración, ya que cualquier regex telefónica estructurada señala las secuencias de dígitos de orden/referencia). Los integrados son ahora 10 en total; los despliegues existentes reciben las filas nuevas almigrate deploycon la seguridad de repeticiónON CONFLICT DO NOTHING. - Chat: los administradores ahora pueden revelar o re-ocultar los textos censurados por DLP en toda la conversación de una vez, vía un nuevo elemento "Show DLP texts" / "Hide DLP texts" en el menú más-acciones (+) de la entrada del chat (los iconos Eye/EyeOff, mostrados solo a los administradores puesto que solo ellos pueden expandir un aviso DLP). La preferencia dura toda la sesión, oculta por defecto y aplica a cada aviso, incluidos los mensajes recién llegados; el botón de ojo por aviso sigue funcionando para los ajustes manuales por mensaje entre las conmutaciones globales.
- La configuración de los patrones DLP: los seis patrones DLP ahora son respaldados por la base de datos (la tabla
dlp_patterns, sembrada idempotentemente por la migración para que los despliegues existentes reciban los integrados) en lugar de fijados en código. Los administradores los gestionan en Settings → Advanced → DLP Patterns: habilitar/deshabilitar cualquier patrón por fila, renombrar los integrados (su regex está congelada — los intentos de modificación devuelven 400), y añadir hasta 50 patrones personalizados (nombre snake_case, fuente regex con validación de compilación al guardar, banderas, reemplazo) con una vista previa de prueba en vivo que muestra los segmentos coincidentes y el texto censurado. Los escaneos que puedan cargar el conjunto de la base de datos —la entrada/salida del plugin de filtro y la descarga final a fin de respuesta— usan los patrones configurados a través de un cache TTL de 5 minutos con la puesta en caché de regex compilados por patrón, cayendo de vuelta al conjunto integrado cuando la base de datos es inalcanzable; la descarga progresiva token por token del flujo mantiene los patrones integrados (la limitación documentada de la v1). Todo el panel está localizado en los 8 idiomas. - El bypass de roles DLP: un nuevo ajuste del sistema
DLP_BYPASS_ROLES(un arreglo JSON de nombres de roles, por defecto[]) permite a los administradores eximir a los roles seleccionados de TODO escaneo y censura DLP — la entrada/salida del plugin de filtro y el núcleo de descarga progresiva del flujo (incluyendo el escaneo del contexto RAG y la censura del pensamiento, tanto para el chat JWT como para la cuenta de servicio del widget) honran todos la misma barrera. Cada corrida con bypass dispara un evento de auditoría fire-and-forgetdlp.bypassedque registra QUIÉN pasó por alto (los nombres de roles intersecados) y la superficie de origen — nunca el contenido escaneado. La interfaz de administración: una selección múltiple de roles con una advertencia de exposición en Settings → Advanced → DLP, y una etiqueta de acción "Con bypass" en el panel del historial de coincidencias DLP, localizados en los 8 idiomas. - El etiquetado y filtrado de la fuente de auditoría DLP: cada registro del evento
dlp.*llevasource: "chat" | "widget"en sus metadatos — el núcleo compartido de la corriente SSE deriva la marca de la cabeceraX-Widget-Iddel widget para que las conversaciones del widget se censuren como las del chat pero ahora discernibles en el panel del historial de coincidencias DLP (Settings → Advanced). El panel gana filtros del lado del cliente por fuente (Todo/Chat/Widget), el ID del chat y el usuario. - La interfaz de administración para la recoleadora de borradores de carga: una nueva sección en Settings → Maintenance expone el conmutador habilitado (
upload_draft_reaper_enabled) y la expresión cron de cadencia (upload_draft_reaper_cron) víaPUT /api/system/settings, con una precomprobación del lado del cliente de la forma cron de cinco campos (el servidor conserva su comportamiento de advertir-y-respaldo para los valores inválidos), avisos emergentes de éxito/error localizados y descripciones en los 8 idiomas..env.exampledocumenta las claves de la recoleadora como sobreescrituras de variables de entorno (upload_draft_reaper_enabled,upload_draft_reaper_cron,upload_draft_retention_days).
- Config: la configuración del entorno ahora se define una vez en un esquema Zod compartido (
@simmetric-chat/shared), la precedencia efectiva queda documentada y fijada por pruebas como DB > ENV > predeterminado (las ediciones del ajustes UI ganan de inmediato; una matriz de 8 casos), y las claves que se ignorarían porque una variable de entorno las sobrescribe se muestran con una placaenvOverriddenen la interfaz de ajustes. - Config: el
.envde la raíz del repositorio ahora es la única configuración de runtime para el servidor, el collector y el widget — un nuevo cargador de cero dependencias (loadRootEnv()) lo combina bajo las sobreescrituras.envpor paquete (heredadas), con una advertencia de obsolescencia una por arranque que enumera los nombres de claves tocados (nunca los valores); Docker y Tauri alineados (elenv_filedel compose apunta al archivo raíz, ninguna imagen hornea secretos). - Config: los tres archivos
.env.examplede los paquetes se regeneran desde sus esquemas Zod (83/15/6 claves, agrupadas, valores predeterminados sincronizados) con trampas de Jest que fallan cuando una futura clave del esquema queda sin documentar; las lecturas crudas deprocess.envconservadas fuera del esquema (HF/excentricidades de openai, las claves de cifrado/HMAC, LOG_LEVEL, las barreras de prueba) quedan fijadas por 44 sondas de guarda comportamentales. - UX: eliminar un workspace ahora pide un diálogo de confirmación estándar (era un miniconfirm en línea), los workspaces admiten la eliminación masiva multi-selección con un diálogo de confirmación y un aviso emergente de resultados que cuenta las filas saltadas (las filas sin permiso se saltan, nunca un fallo silencioso), la vista previa OCR lado a lado mantiene el texto extraído ligado a la misma porción de página que el panel de imagen, y el detalle de la corrida de síntesis muestra el botón de aprobación deshabilitado con la razón explícita en las corridas que todavía no se pueden aprobar.
- UX: las casillas de selección de workspaces y la eliminación masiva están localizadas en los 8 idiomas.
Changed
- La recoleadora de borradores de carga es totalmente configurable vía los ajustes del sistema: un conmutador habilitado/deshabilitado (
upload_draft_reaper_enabled, por defecto "true") y una sobrescritura de cadencia (upload_draft_reaper_cron, por defecto diaria 03:00 UTC), ambos gestionados víaPUT /api/system/settings. Los valores cron inválidos registran una advertencia y vuelven a la cadencia por defecto en vez de hacer fallar el arranque; el comportamiento por defecto no cambia.
Fixed
- Corregido un ciclo de fallo del arranque en producción Docker donde el arranque del servidor moría con un fallo de restricción única en
api_keys.prefix: la sembradora de la clave API del widget ahora deriva el prefijo de visualización del compendio HMAC y tolera las colisiones de prefijo (recontrol por compendio de clave, advertencia en vez de fallo), y la generación de la clave API reintenta con una clave nueva ante una colisión de prefijo (máximo 3 intentos). - Las citas del chat ahora reflejan las fuentes en las que una respuesta realmente se funda en vez de todo lo que las herramientas de búsqueda recolectaron: tras la desduplicación de citas, un filtro de fundamentación acota las citas por documento al top-2 por puntuación de relevancia y descarta las citas de fragmentos cuyo texto no comparte solapamiento léxico significativo con la respuesta final (una barrera conservadora — las páginas wiki/archivolos resultados web/memoria, las citas sin texto de fragmento y las respuestas muy cortas siempre se conservan). Las respuestas de "más información" ya no muestran "Fonti (5)" de un solo documento a ~1,5% de relevancia; la exención "Sin evidencia de apoyo" se juzga igualmente sobre el conjunto filtrado (tanto la vía de flujo como la de no flujo). Solo la pipeline de citas — ningún cambio de interfaz, i18n o esquema.
- Docker: el comprobador de salud del servicio ollama reemplazó la sonda
curl(la imagenollama/ollamano incluye curl/wget/nodo — el contenedor reportaba "unhealthy" por siempre) por una sondaollama ls; el comprobador de salud qdrant reemplazó la sondawget(la imagen no incluye wget/busybox) por un control de socket bash/dev/tcp— ambos servicios ahora reportanhealthy. Eldocker/entrypoint.shdel contenedor único ahora procesaprovision-encryption-key.sh(a paridad con la imagen separada): los arranques autónomos de producción auto-abastecenENCRYPTION_KEY+API_KEY_HMAC_SECRETen vez de fallar la barrera. Los cinco Dockerfile pasan limpios bajo hadolint (la política documentada de dependencias sin fijar + las renuncias de ENV de marcado). - Los eventos de cronología de auditoría DLP ya no muestran "sin detalles de coincidencia" cuando el contenido coincidente llegó en un segmento temprano del flujo: el evento de flujo
dlp.output_matchahora deriva sus tipos de coincidencia Y su lista de textos coincidentes de las coincidencias acumuladas en toda la corrida (todas las descargas progresivas más el escaneo final de la cola) en vez del escaneo del último segmento solo — las filas históricas podían almacenar un arreglomatchTypesvacío mientras la fila mostraba de todos modos una placa de tipo de coincidencia. El texto coincidente se almacena según el precedente del plugin de filtro DLP (dlp.input_match/dlp.output_matchno de flujo ya lo llevan); el panel de auditoría es solo para la administración y bloqueado por Enterprise. La vista expandida del panel ahora renderiza la lista de tipos detectados con una pista "de solo tipo" para los eventos que registran tipos de coincidencia sin coincidencias (localizado en los 8 idiomas), manteniendo el respaldo "sin detalles" solo para los metadatos verdaderamente vacíos. - Los archivos cargados quedan disponibles para reintentar: los callbacks de estado terminal del collector ya no borran los archivos de borrador almacenados de
storage/uploads/drafts/(el callback de cualquier documento hermano podía borrar el archivo del que depende cada reintento), y/retry//assignse autosan restaurando el archivo de borrador almacenado desde la copia OCR persistente (storage/ocr-sources/) cuando existe en vez de fallar con "ya no existe en el disco". - Eliminar un borrador de carga fallido y obsoleto ya no devuelve bloqueado (409) —
DELETE /api/uploads/:idahora trata los estados de pata terminal (completada o fallida) como borrables y solo rechaza los borradores con una pata genuinamente en vuelo (habilitada no terminal), y la eliminación masiva del frontend usa el mismo predicado en vuelo que los chips del filtro para que lo que la interfaz ofrece borrar sea exactamente lo que el servidor acepta. - El reintento RAG/KB de una carga cuyo archivo de borrador almacenado falta ahora devuelve un claro error de recarga (400) en vez de un aviso falso de éxito, las patas del collector fallidas ya no borran el archivo de borrador almacenado de
storage/uploads/drafts/(los reintentos futuros siguen siendo posibles — la recoleadora de 24h y la ruta DELETE quedan como los únicos borradores de archivos), y los avisos emergentes de reintento ahora reflejan el desenlace por pata: una pata rechazada sobre HTTP 200 muestra un aviso de error localizado y el recuento de reintentos masivos lo cuenta como fallido. - Los despliegues Docker de producción auto-abastecen una
ENCRYPTION_KEYpersistente (un volumen de almacenamiento del servidor,/app/storage/.encryption-key) en vez de meterse en un bucle de fallo sobre el predeterminado duro. El punto de entrada del servidor genera la clave una vez antes de cualquier paso de Prisma, la restaura en cada reinicio/recompilación, prefiere un valor suministrado por el operador y falla ruidosamente ante una clave persistida corrupta..env.exampleydocs/DEPLOYMENT.mdactualizados para marcar la variable como Requerida-en-producción. Véasedocs/ENCRYPTION_KEY_ROTATION.mdpara las notas de Docker. - Las páginas OCR de la KB ya no fallan cuando la corriente de visión de Ollama es cerrada por el motor de modelo sin el marcador final
donetras una transcripción completa (observado con el motor personalizado glm-ocr):ocrPageahora rescata la salida acumulada no vacía como el resultado de la página con una banderatruncatedy un registro de advertencia, en vez de desechar una transcripción real detrás de "Did not receive done or success response in stream." Una corriente sin done y sin contenido aún hace fallar la página. - Los resúmenes de los trabajos OCR son veraces sobre la salud de las páginas: las páginas que terminan con un marcador
[FAILED:se cuentan y almacenan comofailedPagesen el JSON de resultado del trabajo, y los registros de culminación emiten una advertencia[ocr] Trabajo completado con páginas fallidas, de modo que un trabajo COMPLETADO con páginas fallidas ya no es indistinguible de un éxito limpio (cero tokens + una puntuación de calidad al suelo por sí solos eran ambiguos). - Documentado y corregido el modo de fallo para los despliegues Enterprise: cuando
@simmetric-chat/sharedgana archivos de fuente NUEVOS, una instantánea fuera de fecha de la dependenciafile:enterprise puede hacer fallar el servidor al arrancar con "Cannot find module './env.schema'" — pnpm toma la instantánea del paquetefile:ennode_modules/.pnpmy los archivos creados después del últimopnpm installnunca alcanzan esa instantánea (los enlaces duros solo actualizan los archivos ya existentes), de modo que elschemas/index.jsde la instantánea exige un módulo que no existe del lado enterprise, y la política fail-loud del cargador convierte el fallo del registro del plugin en un bucle de fallo de arranque. La corrección viaja como documentación: el manual de reconstrucción del repositorio enterprise ahora cubre la distinción reinstala-no-recompila (refresca la instantánea conpnpm install, luegopnpm build), y el manual de instalación de aire comprimido endocs/ENTERPRISE_PLUGIN.mdgana la subsección de solución de problemas correspondiente (el síntoma, la causa, la corrección, el control de cordura) para que los operadores sin el repositorio privado también encuentren el remedio.
Barrida de deudas & higiene de release (era: v1.5.0)
Added
docs/LICENSE_DECISION.md— el análisis del modelo de licencia para la distribución doble (community + enterprise). Decisión: AGPL-3.0 (community) + comercial propietario (empresa).LICENSEreemplazado Apache-2.0 → GNU AGPL-3.0 (el repositorio community).LICENSE_EE.md— la licencia comercial propietaria del plugin enterprise (@simmetric-chat/enterprise, el paquete enterprise).NOTICE— la explicación de la doble licencia en la raíz del repositorio.CLA.md— el Acuerdo de Licencia de Colaborador (v1.0) — copyright + concesión de patente, consciente de la doble licencia. Requerido para toda contribución externa.- Cabeceras de copyright AGPL-3.0 añadidas a los 981 archivos de fuente a través de 5 paquetes (
.ts/.tsx). Aplicadas víascripts/add-headers.cjs(idempotente). docs/ENTERPRISE_LICENSE_TERMS.md— los términos comerciales/acuerdo de nivel de servicio a la medida del operador para los clientes enterprise..github/workflows/release.yml— GitHub Release + la publicación de imágenes GHCR al empujar el tag.CHANGELOG.md(este archivo) — el formato Keep-a-Changelog, sembrado desde la historia de los hitos..planning/milestones/v1.5-debt-sweep-ROADMAP.md— el alcance del hito de evaluación de deudas (el traslado E2E, las sesiones de depuración, la clasificación de tareas rápidas, la disciplina de sincronización de sellos de versión).
Changed
- La versión de
package.jsonsubió de0.17.0→1.4.0(sincronizada con el último tag gitv1.4). Las versiones por paquete quedan en0.22.0/0.23.0(paquetes privados, no publicados en ningún registro). - El campo
licensedepackage.json+ todos lospackages/*/package.jsonestablecido enAGPL-3.0-or-later. - La placa de licencia del
README.mdactualizada de Apache-2.0 → AGPL-3.0; la sección de licencia actualizada para describir el modelo de doble licencia; la sección Contributing añadida con el requisito del CLA. CONTRIBUTING.mdactualizado con las instrucciones de firma del CLA.
Escala horizontal (la culminación de la capa Redis) (era: v1.4)
Objetivo: cerrar los 4 elementos de SCALE diferidos (CSW-19 + SCALE-02/03/04) y probar el escalado horizontal con una prueba de integración multi-instancia real — habilitando el despliegue de servidor multi-instancia seguro detrás de un equilibrador de carga.
Entregado: 7 fases (161–167), 13 planes, 29 tareas. Los 22/22 requisitos satisfechos. Tag git: v1.4.
Added
- SCALE-01 (CSW-19): los 3 planificadores restantes envueltos con candado → luego los 8 migrados a trabajos cron pg-boss;
setInterval+isRunning+withDistributedLockeliminados de los 8 planificadores migrados. - SCALE-02: el respaldo
scryptSyncde ENCRYPTION_KEY eliminado en producción — un arranque fail-loud (logger.error+process.exit(1)) + una excepción de servicio de defensa en profundidad; dev/test conserva scrypt; el manual de rotación actualizado. - SCALE-03: la verificación de claves API de bucle bcrypt → una búsqueda de compendio O(1)
findUnique({key_hash})con HMAC claveado; una migración de rupturaDROP COLUMN hashedKey+ADD COLUMN key_hash; la variable de entornoAPI_KEY_HMAC_SECRET; el manual de migración para el operadordocs/API_KEY_MIGRATION.md. - SCALE-04: 8 planificadores
setIntervalmigrados a pg-boss (una cola de trabajos con respaldo de Postgres, amiga del aire comprimido); 2 sondeadores de 10s sensibles a la latencia (OCR + síntesis) quedan comosetInterval; el singleton pg-boss (jobQueue.ts) con degradación amable. - La verificación del fan-out SSE: el aplazamiento entre instancias cerrado — 4 pruebas entre instancias basadas en simuladores que prueban el relé SSE Redis pub/sub (A publica → B retransmite, el salto de origen, el desmontaje, la degradación).
- La documentación del operador:
docs/SCALING.md(377 líneas) — la topología del despliegue multi-instancia, el requisito REDIS_URL, la dependencia de Postgres de pg-boss, los referentes cruzados SCALE-02/03, los sondeadores de 10s, la degradación amable.
Cambios de ruptura — acción del operador requerida
- `ENCRYPTION_KEY` debe establecerse en el
.envde producción (base64, 32 bytes) o el servidor se niega a arrancar. Véasedocs/ENCRYPTION_KEY_ROTATION.md. - Las claves API pasaron de bcrypt (
apiKeyHash) → HMAC-SHA256 (key_hash). Los operadores deben re-emitir las claves API del widget/collector. Véasedocs/API_KEY_MIGRATION.md.
Infraestructura
- Nueva dependencia:
pg-bossv12.28.0 (servidor). Nueva variable de entorno:API_KEY_HMAC_SECRET. El orden de arranque cambió — la inicialización de pg-boss es ahora el 7.º paso de init asíncrono con un desmontaje de apagado.
La barrida de CONCERNS (era: v1.3)
Objetivo: barrer la auditoría CONCERNS.md del 2026-08-26 — corregir todas las victorias fáciles directamente, abordar los elementos de esfuerzo medio en fases enfocadas, y evaluar (aplazar o documentar) los puntos bloqueantes/grandes esfuerzos.
Entregado: 7 fases (154–160), 14 planes. Un hito de endurecimiento brownfield; ninguna función nueva visible al usuario.
Added
- la corrección del gitignore de la doc, el mapa de archivos de chat.ts, los volteos de reglas de lint.
- las victorias fáciles de endurecimiento de código (el tope take:10 de apiKeyMiddleware, etc.).
- la reducción de la deuda de lint del frontend (3 planes, los volteos advertencia→error).
- la advertencia de arranque de producción de ENCRYPTION_KEY (CSW-11) + la generación en lote de título+etiquetas+seguimientos.
- las divisiones de módulos (la extracción del analizador de proveedores, los registros de capacidades).
- la cobertura de pruebas (chatImportService, chatExportService, apiKeyService, otros 5 servicios; el collector + el widget cableados en la configuración raíz de jest).
- evaluado y documentado (los derechos JWT de licencia del .env, el manual de rotación de la clave de firma de licencia, la corrección de la afirmación obsoleta de AGENTS.md).
Refinamientos (era: v1.2)
Objetivo: marca, descargo de IA, la reimplementación en TypeScript del algoritmo de grafo del wiki, correcciones de búsqueda RAG, endurecimiento MCP, el primer arranque del Asistente de Configuración.
Entregado: 6 fases (149–153 + 153.1).
Added
- BRAND-01/02: el favicon SVG del monograma S, el componente Monograma reutilizable, el descargo de IA localizado y atenuado debajo de cada mensaje del asistente (7 idiomas).
- MCP-01/02/03: el endurecimiento MCP — el mapa de transporte SSE por sesión, la barrera de auth Bearer
MCP_API_KEY, la IDOR de list_workspaces cerrada,getMCPToolsForWorkspacecableado en resolveSkillsForChat. - RAG-01/02/03: la corrección de la localidad FTS (la tokenización multi-idioma para it/ru/de/es/fr/zh), la desduplicación de los resultados de respaldo rag_search↔wiki_query, el prompt de asequibilidad LLM para rag_search vs wiki_query.
- WIKI-01: la reimplementación en TypeScript del algoritmo Graphify del wiki (detección de comunidad + la generación markdown del wiki, una reimplementación limpia de Apache-2.0, ~280 líneas Python → TS).
- WIZ-01/02: la interfaz de primer arranque del Asistente de Configuración (multipaso: cuenta de administración + configuración del proveedor + la base de vectores, el consumo de /api/system/initialize); SEED_BOOTSTRAP_ADMIN deshabilitado cuando el asistente está activo.
- .1: la limpieza de la deuda técnica de v1.2 (el volteo advertencia→error del React Compiler).
La arquitectura del plugin Enterprise (era: v1.1)
Objetivo: refactorizar el modelo de licencias de "banderas de funciones a la vista en la fuente pública" a un "plugin enterprise privado separado": las funciones enterprise (SSO, registro de auditoría, etiquetar en blanco, copia de seguridad) viven en un paquete privado entregado solo a los clientes pagantes; el repositorio community no contiene código de funciones enterprise. Las funciones mercancía se vuelven siempre-ACTIVAS en la comunidad.
Entregado: 9 fases (140–148), 19 planes, 26 tareas.
Added
- EPA-01: los tipos
PluginContext/EnterprisePluginen shared; la costura del cargadorloadEnterprisePlugin(app)/shutdownEnterprisePlugin()conrequire.resolve→requireen dos pasos (MODULE_NOT_FOUND = el no-op de la comunidad, otros errores =process.exit(1)fail-loud). - EPA-02: 9 banderas de mercancía eliminadas de
FEATURE_FLAGS(20→10): web_search, webhooks, push_notifications, memory, lead_export, widget_analytics, auto_title, synthesis_rate_limit. Las rutas de mercancía siempre-ACTIVAS. - EPA-03: SSO (SAML + OIDC + SCIM 2.0) extraído al plugin enterprise.
- EPA-04: el registro de auditoría (la tabla
event_loginmutable + un rol de base de datos de solo INSERT) extraído al plugin enterprise. - EPA-05: la marca blanca (el gancho IoC del validador de clave de configuración) extraída al plugin enterprise.
- EPA-06: la Copia de seguridad (~1400 líneas: backupService, el registro de proveedores, el planificador Bree, la restauración, las rutas, la interfaz) extraída al plugin enterprise.
- EPA-07: el resolutor
overrideFeatureLimitcon revocación reactiva; el manifiestoGET /api/enterprise/modules; la carga condicionalReact.lazyde 4 paneles enterprise. - EPA-08/09/10:
priority_supporteliminado;custom_agents= un límite numérico (community 3, enterprise el Infinito);licensePayloadSchemabyte-idéntico (RS256). - EPA-12: la alineación de documentos/entorno (
docs/ENTERPRISE_PLUGIN.md, el montaje de volumen de docker-compose, la división de.env.example), el perfil CI de aire comprimido (una barrera de grep en el servicio de licencia para las primitivas HTTP).
El candidato de lanzamiento público (simmetric-chat) (era: v1.0)
Objetivo: preparar y publicar el proyecto como un repositorio público limpio, renombrado a simmetric-chat, sin secretos/datos personales/rutas locales, con la verificación de licencias de las dependencias, el generador de claves enterprise (separado, no publicado) y la documentación completa.
Entregado: 8 fases (132–139), 12 planes, 16 tareas. El repositorio público publicado, el cambio de nombre completo, la auditoría de licencias, el generador de claves enterprise (HS256 → migrado a RS256 tras la v1.0), los documentos canónicos, el CI de GitHub Actions, un repositorio nuevo.
La interfaz del widget, i18n & Fiabilidad
Entregado: 7 fases (125–131), 32 planes, 75 tareas, 16/16 requisitos satisfechos.
Added
- WID-ADMIN: la subpágina con pestañas de creación/edición del widget (
/widgets/:id+/widgets/new), el panel de vista previa en vivo, la guarda de suciedad, la pestaña de clientes potenciales. - WID-I18N: el transporte por bloques JSON (mata el fallo
%20), la cadena de respaldo visitante→predeterminado-del-widget→en, la localización de la carcasa + el contenido a 7 idiomas, el hilado de la localización widget→proxy→servidor→orchestrator. - WID-QUEST: las preguntas sugeridas de tres estados (Predeterminado/Ninguna/Personalizadas), los grupos de 7 idiomas, la corrección del desbordamiento line-clamp.
- WID-CREDITS: los créditos del pie + la placa de IA, la eliminación bloqueada por la marca blanca, el relé de popup de incrustación real verificado.
- WID-BUGS: los botones de entrada de 44px, la ida y vuelta anfitrión-FAB, el contrato de iconos solo-SVG, el cartel de contacto, el bucle del wiki, la persistencia de los clientes potenciales, el i18n del RAG degradado.
Diagnóstico & Deudas
Entregado: 5 fases (120–124). Los diagnósticos de licencia (los registros del arranque, un endpoint de diagnóstico solo para la administración, el script pnpm license:check), la deuda de pruebas (la deriva ESM, LoginPage QueryClientProvider, la especificación de la forma de los simuladores), la evaluación de la deuda técnica (el pinchazo de la capa de escala Redis — aplazado a la v1.4).
Estabilización
Entregado: 4 fases (116–119).
Empresa & Acabado
Entregado: 8 fases (108–115).
Los hitos anteriores (v0.1–v0.16) están documentados en .planning/PROJECT.md (la sección de Historia de Hitos). La historia completa a nivel de fase vive en .planning/MILESTONES.md.