Puentes de chat
Un puente de chat convierte un producto de chat externo —hoy Slack— en una vía de primer nivel hacia un espacio de trabajo de Control Center. Menciona al bot y un agente se despierta; responde en el hilo de Slack y la conversación continúa; escribe un comando slash y se registra un ticket. Por debajo, los mismos espacios, transcripciones, tickets y atribución que usa la aplicación de escritorio. El chat es una superficie del espacio de trabajo, no una copia de él.
Slack es el único proveedor incluido. No hay ninguna implementación de Discord, Telegram, Teams o Matrix en todo el producto. La función está escrita deliberadamente sin conocer el proveedor —añadir un segundo producto es un valor de enum, un adaptador y un plugin—, pero hoy «chat» significa Slack.
Dos superficies lo cubren y pertenecen a personas distintas. Conectar un espacio de trabajo a Slack y personalizar el bot son acciones de administrador, en Ajustes → Espacio de trabajo → General. Vincular tu propia cuenta de chat con tu identidad de Control Center es una acción de miembro, en Ajustes → Espacio de trabajo → Perfil e identidad.
Tres decisiones dan forma a toda la función: en qué dirección corre la conexión, qué autoriza un mensaje que llega desde fuera y dónde puede vivir el conocimiento específico de cada producto.
Saliente por diseño
Sección titulada «Saliente por diseño»El puente nunca escucha al proveedor. Se llega a Slack mediante Socket Mode: el servidor pide a Slack una URL de socket y establece la conexión saliente; después recibe menciones, mensajes directos y comandos slash por esa conexión.
La consecuencia es precisamente lo importante. Un portátil detrás de NAT, un servidor doméstico sin dominio, una máquina que cambia de red: todos pueden ejecutar el puente sin puerto de entrada, sin túnel, sin certificado TLS y sin ninguna URL pública que rotar cuando cambie. No hay ningún endpoint de webhook que proteger porque no hay ningún endpoint de webhook. Cuando Control Center compone la definición de la aplicación, impone Socket Mode y elimina cualquier URL de solicitud, porque una aplicación configurada para ambas cosas sería una aplicación que funciona a medias.
El coste es una conexión que mantener viva, y eso es tarea del servidor: la reconexión usa retroceso exponencial con fluctuación aleatoria acotado a 30 segundos, una rotación de socket iniciada por el proveedor se reconecta sin aumentar ese retroceso y una credencial rechazada es terminal — reintentar para siempre un token revocado solo conseguiría un límite de velocidad, así que el puente se detiene y espera tokens nuevos mientras muestra en los ajustes el error del propio proveedor, literalmente.
Un espacio de trabajo sin aplicación de chat no cuesta absolutamente nada: ni socket, ni temporizadores, ni tráfico. Y todo arranca después del mensaje de servidor listo, porque una aplicación de chat que tarda en responder no debe retrasar nunca el arranque.
Autoriza la pertenencia, no la posesión del token
Sección titulada «Autoriza la pertenencia, no la posesión del token»Quien instaló la aplicación no es quien envía el mensaje. Por eso cada evento entrante se resuelve a un Principal antes de que ocurra nada, y falla en cerrado con una explicación:
- Una cuenta de chat sin vincular recibe instrucciones para vincularse, en el propio hilo de Slack donde escribió.
- Una cuenta vinculada cuyo usuario no es miembro de este espacio de trabajo recibe el aviso de que pida una invitación a un administrador.
- Un miembro cuyo rol es de solo lectura (
viewer,guest) recibe el aviso de que su rol no puede iniciar trabajo.
Ninguno de esos casos continúa. Un mensaje de chat nunca se atribuye a quien no lo envió, y eso es lo que mantiene honestos el registro de auditoría, los límites de velocidad por Principal y el autor de cada ticket. Consulta Multijugador — identidad, pertenencia y presencia para ver el modelo de pertenencia en el que se apoya.
Vincular es resolver una identidad, no conceder acceso. La pertenencia se vuelve a comprobar en cada evento, así que un vínculo que pertenece a alguien que desde entonces ha sido eliminado se resuelve en una negativa, no en sus antiguos privilegios. Hay dos vías para establecer un vínculo: un código de un solo uso y corta vida que el miembro escribe en el chat, o una coincidencia automática con el correo verificado del proveedor; y la vía del correo sigue exigiendo que el usuario coincidente ya sea miembro, así que un desconocido cuya dirección resulte existir en el servidor no queda vinculado a nada.
Los códigos se mantienen deliberadamente en memoria, con un tiempo de vida de 15 minutos. Persistir uno crearía una fila duradera con forma de credencial para algo destinado a evaporarse, y que reiniciar el servidor invalide todos los códigos pendientes es el comportamiento correcto, no una regresión — el miembro vuelve a pulsar «vincular».
Una negativa se responde en el hilo de Slack que abrió el mensaje, como una respuesta real. Una respuesta en hilo etiqueta el mensaje al que contesta, llega a la única persona que la necesita y aun así mantiene «no estás vinculado» fuera del propio canal. Además, publicar un mensaje es la única capacidad que tienen todos los proveedores, así que la explicación nunca depende de una función que la aplicación pueda no incluir.
Procedencia y el problema del eco
Sección titulada «Procedencia y el problema del eco»Un puente que replica en ambos sentidos entrará en bucle a menos que algo
distinga las dos direcciones. Cada mensaje que el puente trae se marca con
metadata['chat'] — proveedor, equipo, conversación, hilo, mensaje y remitente.
La réplica saliente omite cualquier mensaje que lleve esa marca.
Esa única regla es lo que hace segura la réplica bidireccional: un mensaje humano escrito en Control Center se retransmite al hilo vinculado (con el nombre del autor como prefijo), mientras que un mensaje que llegó desde el chat se detiene donde entró.
Lo que cruza hacia fuera es deliberadamente estrecho. La respuesta del agente
se retransmite como texto de chat normal. Un pensamiento corto puede aparecer en
la fila Thinking… de la tarjeta. La salida de las herramientas es un muro de
diffs y se queda en Control Center, que es también adonde apunta una respuesta
truncada.
Una tarjeta informa del trabajo; el texto sigue siendo el mensaje
Sección titulada «Una tarjeta informa del trabajo; el texto sigue siendo el mensaje»Donde el proveedor las representa, un turno lleva una tarjeta de tarea, editada en su sitio mientras dura el turno y situada encima de la respuesta que explica.
La tarjeta es un plan agrupado, no un párrafo apelmazado. Slack concatena
details y output en la misma tarea, así que los detalles de una fila se envían
una sola vez (el pensamiento en Thinking…) y output nunca se transmite en
streaming. La preparación es una fila cuyo título se reemplaza
(Working on it…, Cloning …). El razonamiento añade una fila Thinking…. Cada
herramienta es su propia fila (Read lib/main.dart). La respuesta es texto
normal bajo la tarjeta — la vista de plan de Slack la lee de un añadido de
markdown posterior, no de un fragmento mezclado con las tareas. Nada más: ni
salida de herramientas ni más argumento de herramienta que los de una breve lista
de permitidos, porque los argumentos de una herramienta desconocida pueden ser el
cuerpo entero de un archivo y una fila es una sola línea.
La tarjeta también cubre la espera antes del turno. Una primera mención crea la
conversación, y crearla clona todos los repositorios en los que trabajará el
agente, lo que en un repositorio real son minutos en los que todavía no se le ha
pedido nada al agente y el hilo parece como si el bot hubiera ignorado la
petición. Así que la tarjeta se abre en el momento en que se acepta la mención
(Working on it…) y luego esa fila de preparación se actualiza con las mismas
palabras que muestra la aplicación de escritorio — Cloning acme/widgets…,
Setting up Reviewer…, Starting the agent… —, después aparece una fila
Thinking… en cuanto el agente empieza a razonar y luego una fila por
herramienta. Una tarjeta, un mensaje, desde el «te he oído» hasta la respuesta
como texto normal debajo. La preparación solo se informa donde alguien del chat la
está esperando: una conversación que se está preparando en la aplicación, o una
que quedó abandonada y se reanuda al arrancar el servidor, se aprovisiona sin
ninguna tarjeta.
Lleva lo único que el chat no puede dar al lector: un enlace
View in Control Center a la conversación que hay detrás de la respuesta. Los
productos de chat solo aceptan http(s) en un enlace y nada más, así que el botón
apunta a una pequeña página que sirve este servidor, la cual redirige el
navegador al enlace control-center:// de la aplicación de escritorio. Esa
página no lee nada ni resuelve nada — los identificadores de su URL se reflejan en
un enlace profundo y en ningún otro sitio —, y un servidor sin una URL pública
accesible simplemente produce una tarjeta sin botón, no una tarjeta rota.
Las tarjetas son una capacidad como cualquier otra. Un proveedor que no las tenga
se comporta exactamente como antes, solo texto, y un proveedor que rechace una en
el momento de la llamada conserva su transmisión: la respuesta siempre tiene
prioridad sobre la tarjeta que la describe. Cuando el streaming está totalmente
desactivado, la tarjeta terminada viaja en el único mensaje publicado. Un ticket
registrado recibe el mismo trato — la confirmación de /cc ticket es una tarjeta
completada cuya razón de ser es el enlace al ticket recién creado.
Las capacidades se negocian y luego se recuerdan
Sección titulada «Las capacidades se negocian y luego se recuerdan»Los productos de chat difieren en lo que pueden representar, así que el puente está escrito contra la superficie más rica — una respuesta que crece en directo dentro de un hilo, un estado transitorio «is thinking…», un hilo con título, una respuesta efímera a un comando slash — y degrada una capacidad cada vez. Un proveedor que no tenga nada de eso sigue funcionando; las respuestas simplemente llegan como un solo mensaje.
Anunciar una capacidad no es prometerla. Las respuestas en streaming de Slack necesitan una función de plan de pago, y Slack solo la rechaza en el momento de la llamada. Un rechazo marcado como permanente se recuerda durante toda la vida de la conexión: el puente deja de intentarlo y publica respuestas completas a partir de entonces, y los ajustes lo dicen con claridad, porque «mis respuestas dejaron de llegar en streaming» debería poder responderse sin leer un registro. Las tarjetas de tarea se descartan igual si Slack rechaza una.
Todos los límites pertenecen al proveedor, no al relé: el máximo de mensajes, el máximo de fragmentos de la transmisión y si una transmisión necesita un anclaje de hilo los declara el adaptador, y una respuesta larga se divide por párrafo, línea o límite de palabra en lugar de a mitad de palabra.
Un puente, un adaptador por producto
Sección titulada «Un puente, un adaptador por producto»Todo lo que tiene forma de producto vive detrás de un puerto. El núcleo genérico
conoce el markdown, las conversaciones de chat, los hilos de chat y los miembros;
no sabe nada de los sobres de Slack, mrkdwn ni las intenciones de la pasarela.
Tres piezas lo hacen posible:
- El adaptador normaliza un producto: su transporte, sus llamadas a la API,
su códec de texto y sus peculiaridades de sobre. Las de Slack son
aleccionadoras — una mención de canal llega dos veces (como
app_mentiony comomessage) y la mayoría de los subtipos de mensaje son elementos de interfaz y no peticiones nuevas. Ambas cosas se resuelven en el adaptador, así que el núcleo nunca se entera de que Slack entrega por duplicado. - El descriptor es la forma pública del proveedor: qué credenciales necesita, con qué formato, qué URL ayudan al usuario a encontrarlas, qué puede hacer y si su aplicación se puede crear o editar desde aquí. La tarjeta de ajustes, el diálogo de conexión y la validación de argumentos se generan todos a partir de él — el cliente devuelve un proveedor más un mapa con clave por identificador de campo.
- El plugin es la composición en el lado del servidor: cómo se convierten las credenciales en una conexión verificada, cómo se construye un adaptador y cómo se gestiona la aplicación del lado del proveedor.
Así que añadir un producto es un valor de enum, un adaptador y un plugin. Ni una operación RPC nueva, ni un cambio de esquema, ni un cambio en el cliente — y el cliente se mantiene honesto, porque un campo del que nunca ha oído hablar se sigue representando con la etiqueta que proporcionó el servidor.
Dos detalles evitan que la abstracción se filtre hacia arriba. Los pasos de gestión de la aplicación para los que un proveedor no tiene API (generar un token, instalar la aplicación, reinstalarla tras un cambio de permisos) se devuelven como pasos con nombre y sus propios enlaces, de modo que la pantalla de finalización de la configuración los muestra sin saber de qué proveedor se trata. Y todas las instrucciones que escribe el puente nombran al proveedor y el comando que la aplicación usa realmente, así que un comando renombrado o un segundo proveedor se lee correctamente sin ninguna cadena nueva.
Dónde viven las credenciales
Sección titulada «Dónde viven las credenciales»Los tokens de chat de un espacio de trabajo están en el directorio propio de
ese espacio de trabajo, junto a su base de datos — como JSON plano en
<dataDir>/<workspaceId>/chat_credentials/<provider>.json, no en la base de
datos y deliberadamente tampoco en el llavero del sistema operativo. Dos
consecuencias se pierden fácilmente de vista más adelante: eliminar un espacio de
trabajo desvincula el directorio y sus tokens se van con él, y exportar o hacer
copia de seguridad de un espacio de trabajo copia workspace.db, de modo que un
espacio de trabajo entregado a otra persona no lleva dentro tokens de bot activos.
El archivo hereda la frontera de confianza del sistema de archivos del host del
servidor — cualquier cosa con acceso de lectura al directorio de datos puede leer
un token de bot, que es la misma frontera tras la que están las bases de datos.
Un archivo por proveedor, escrito de forma atómica y solo para el propietario: una aplicación revocada se elimina desvinculando un único archivo, y un archivo corrupto cuesta un proveedor en lugar de todos. Las credenciales entran hacia dentro con la operación de conexión y nunca vuelven a salir — las lecturas de estado responden solo con metadatos de la conexión.
Lo que deliberadamente no se ofrece
Sección titulada «Lo que deliberadamente no se ofrece»- Ajustes estructurales de la aplicación. El transporte, los ámbitos y las suscripciones a eventos se imponen, no se exponen. Una aplicación que se puede configurar hasta dejarla en silencio desde una pantalla de ajustes es un ticket de soporte, no una función.
- Conversación y comandos slash. La superficie de Slack son mensajes, respuestas en hilo y comandos slash.
- Una segunda fuente de verdad. El chat no es un lugar donde se almacene trabajo. El hilo de Slack es un espacio, la respuesta es una transcripción, el ticket es un ticket — todo ello en el espacio de trabajo y accesible desde cualquier cliente.
Consulta también
Sección titulada «Consulta también»- Multijugador — identidad, pertenencia y presencia: los roles y las identidades Principal por los que resuelve el puente
- Espacios de trabajo y aislamiento: por qué los vínculos y las credenciales son por espacio de trabajo
- El modelo de agentes: qué se despierta cuando llega una mención
- Tickets y delegación: qué registra
/cc ticket - Arquitectura: dónde se sitúa el puente entre el servidor y los clientes
- Conecta Slack a tu espacio de trabajo
- Configurar la integración con Slack
- Vincular tu cuenta de Slack
- Personalizar el bot de chat
- Conversaciones de PR: las mismas tres decisiones, tomadas para una forja