Pontes de chat
Uma ponte de chat transforma um produto de chat externo — hoje, o Slack — em uma forma de primeira classe de entrar em um espaço de trabalho do Control Center. Mencione o bot e um agente desperta; responda no thread do Slack e a conversa continua; digite um comando slash e um ticket é aberto. Por baixo, os mesmos espaços, transcrições, tickets e a mesma atribuição que o app de desktop usa. O chat é uma superfície do espaço de trabalho, não uma cópia dele.
O Slack é o único provedor disponível. Não há nenhuma implementação de Discord, Telegram, Teams ou Matrix em lugar algum do produto. O recurso foi escrito de propósito sem conhecer o provedor — adicionar um segundo produto é um valor de enum, um adaptador e um plugin —, mas hoje “chat” significa Slack.
Duas superfícies cobrem isso, e elas pertencem a pessoas diferentes. Conectar um espaço de trabalho ao Slack e personalizar o bot são ações de admin em Configurações → Espaço de trabalho → Geral. Vincular sua própria conta de chat à sua identidade no Control Center é uma ação de membro em Configurações → Espaço de trabalho → Perfil e identidade.
Três decisões moldam o recurso inteiro: em que direção a conexão corre, o que autoriza uma mensagem que chega de fora e onde o conhecimento específico do produto pode morar.
Saída por design
Seção intitulada “Saída por design”A ponte nunca escuta o provedor. O Slack é acessado via Socket Mode: o servidor pede uma URL de socket ao Slack e estabelece a conexão de saída, e então recebe menções, mensagens diretas e comandos slash por essa conexão.
A consequência é justamente o ponto. Um laptop atrás de NAT, um servidor caseiro sem domínio, uma máquina que muda de rede — todos eles conseguem rodar a ponte sem porta de entrada, sem túnel, sem certificado TLS e sem URL pública para rotacionar quando ela muda. Não há endpoint de webhook para proteger porque não existe endpoint de webhook. Quando o Control Center monta a definição do app, ele afirma o Socket Mode e remove qualquer URL de requisição, porque um app configurado para os dois seria um app que funciona pela metade.
O custo é uma conexão a manter viva, o que é tarefa do servidor: a reconexão é backoff exponencial com jitter limitado a 30 segundos, uma rotação de socket iniciada pelo provedor reconecta sem escalar esse backoff e uma credencial rejeitada é terminal — tentar de novo um token revogado para sempre só renderia um limite de taxa, então a ponte para e espera novos tokens enquanto relata nas configurações o próprio erro do provedor, textualmente.
Um espaço de trabalho sem app de chat não custa nada: nenhum socket, nenhum timer, nenhum tráfego. E tudo isso começa depois do banner de pronto do servidor, porque um app de chat lento para responder nunca deve atrasar a inicialização.
A associação autoriza, não a posse do token
Seção intitulada “A associação autoriza, não a posse do token”A pessoa que instalou o app não é a pessoa que envia a mensagem. Por isso todo evento de entrada vira um Principal antes que qualquer coisa aconteça, e a falha é sempre fechada, com uma explicação:
- Uma conta de chat não vinculada recebe instruções de como vincular, no thread do Slack em que escreveu.
- Uma conta vinculada cujo usuário não é membro deste espaço de trabalho é orientada a pedir um convite a um admin.
- Um membro cuja função é somente leitura (
viewer,guest) é informado de que sua função não pode iniciar trabalho.
Nenhum desses casos prossegue. Uma mensagem de chat nunca é atribuída a alguém que não a enviou — é isso que mantém honestos a trilha de auditoria, os limites de taxa por Principal e os autores dos tickets. Consulte Multijogador — identidade, associação e presença para ver o modelo de associação no qual isso se apoia.
Vincular é resolução de identidade, não uma concessão de acesso. A associação é verificada de novo a cada evento, então um vínculo de alguém que desde então foi removido resulta em recusa, e não em seus antigos privilégios. Dois caminhos estabelecem um vínculo: um código de uso único e curta duração que o membro digita no chat, ou uma correspondência automática no e-mail verificado do provedor — e o caminho por e-mail ainda exige que o usuário correspondido já seja membro, de modo que um desconhecido cujo endereço por acaso existe no servidor não vincula nada.
Os códigos ficam deliberadamente em memória, com tempo de vida de 15 minutos. Persistir um deles criaria uma linha durável com cara de credencial para algo feito para evaporar, e um reinício do servidor que invalida todos os códigos pendentes é o comportamento correto, não uma regressão — o membro clica em “vincular” de novo.
Uma recusa é respondida no thread do Slack que a mensagem abriu, como uma resposta de verdade. Uma resposta em thread marca a mensagem que ela responde, chega à única pessoa que precisa dela e ainda mantém o “você não está vinculado” fora do canal em si. Publicar uma mensagem também é a única capacidade que todo provedor tem, então a explicação nunca depende de um recurso que o app pode não carregar.
Proveniência e o problema do eco
Seção intitulada “Proveniência e o problema do eco”Uma ponte que espelha nos dois sentidos entra em loop, a menos que algo distinga
as duas direções. Toda mensagem que a ponte traz é marcada com
metadata['chat'] — provedor, equipe, conversa, thread, mensagem e remetente.
O espelhamento de saída ignora qualquer mensagem que carregue essa marca.
Essa única regra é o que torna seguro o espelhamento bidirecional: uma mensagem humana digitada no Control Center é retransmitida para o thread vinculado (prefixada com o nome do autor), enquanto uma mensagem que veio do chat para no ponto em que entrou.
O que atravessa para fora é deliberadamente estreito. A resposta do agente é
retransmitida como texto comum de chat. Um pensamento curto pode aparecer na
linha Thinking… do cartão. A saída de ferramentas é uma parede de diffs e fica
no Control Center, que também é para onde aponta uma resposta truncada.
Um cartão relata o trabalho; o texto continua sendo a mensagem
Seção intitulada “Um cartão relata o trabalho; o texto continua sendo a mensagem”Onde o provedor os renderiza, um turno carrega um cartão de tarefa, editado no lugar enquanto o turno durar e posicionado acima da resposta que ele explica.
O cartão é um plano agrupado, não um parágrafo amassado. O Slack concatena
details e output na mesma tarefa, então os detalhes de uma linha são enviados
uma vez (o pensamento em Thinking…) e output nunca é transmitido em stream.
A preparação é uma linha cujo título é substituído (Working on it…, Cloning …). O raciocínio acrescenta uma linha Thinking…. Cada ferramenta é sua própria
linha (Read lib/main.dart). A resposta é texto comum abaixo do cartão — a visão
de plano do Slack a lê de um append de markdown posterior, não de um trecho
misturado às tarefas. Nada mais: nenhuma saída de ferramenta e apenas um
argumento de ferramenta de uma pequena lista de permissões, porque os argumentos
de uma ferramenta desconhecida podem ser o corpo inteiro de um arquivo e uma
linha é uma linha só.
O cartão também cobre a espera antes do turno. Uma primeira menção cria a
conversa, e criá-la clona todos os repositórios em que o agente vai trabalhar — o
que, em um repositório real, são minutos em que nada foi pedido ao agente ainda e
o thread parece que o bot ignorou o pedido. Então o cartão abre no momento em que
a menção é aceita (Working on it…) e depois essa linha de preparação é
atualizada com as mesmas palavras que o desktop mostra — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… —, depois uma linha
Thinking… assim que o agente começa a raciocinar, e então uma linha por
ferramenta. Um cartão, uma mensagem, de “eu ouvi você” até a resposta como texto
comum abaixo. A preparação só é relatada onde alguém no chat está esperando por
ela: uma conversa sendo preparada no app, ou uma conversa abandonada retomada na
inicialização do servidor, é provisionada sem cartão nenhum.
Ele carrega a única coisa que o chat não pode dar ao leitor: um link View in
Control Center para a conversa por trás da resposta. Produtos de chat aceitam
http(s) em um link e nada mais, então o botão aponta para uma pequena página que
este servidor serve, a qual redireciona o navegador para o link
control-center:// do app de desktop. Essa página não lê nada e não resolve nada
— os ids em sua URL são ecoados para um link direto e para nenhum outro lugar —, e
um servidor sem URL pública alcançável simplesmente produz um cartão sem botão, em
vez de um botão quebrado.
Cartões são uma capacidade como qualquer outra. Um provedor sem eles se comporta
exatamente como antes, só texto, e um provedor que recusa um cartão na hora da
chamada mantém seu stream: a resposta sempre tem prioridade sobre o cartão que a
descreve. Quando o streaming está totalmente desligado, o cartão final vai na
única mensagem publicada. Um ticket aberto recebe o mesmo tratamento — a
confirmação de /cc ticket é um cartão concluído cuja razão de existir é o link
para o ticket recém-criado.
As capacidades são negociadas e depois memorizadas
Seção intitulada “As capacidades são negociadas e depois memorizadas”Produtos de chat diferem no que conseguem renderizar, então a ponte é escrita contra a superfície mais rica — uma resposta que cresce ao vivo dentro de um thread, um status transitório de “está pensando…”, um thread com título, uma resposta efêmera a um comando slash — e regride uma capacidade de cada vez. Um provedor sem nada disso ainda funciona; as respostas simplesmente chegam como uma única mensagem.
Anunciar uma capacidade não é prometê-la. As respostas em stream do Slack exigem um recurso de plano pago e o Slack só recusa na hora da chamada. Uma recusa marcada como permanente é memorizada por toda a vida da conexão: a ponte para de perguntar e passa a publicar respostas inteiras, e as configurações dizem isso claramente, porque “minhas respostas pararam de vir em stream” deveria ser respondível sem ler um log. Cartões de tarefa são descartados da mesma forma se o Slack recusar um.
Todo limite pertence ao provedor, não ao relay: o teto de mensagens, o teto de trechos do stream e se um stream precisa de uma âncora de thread são todos declarados pelo adaptador, e uma resposta longa é dividida em um limite de parágrafo, linha ou palavra, nunca no meio de uma palavra.
Uma ponte, um adaptador por produto
Seção intitulada “Uma ponte, um adaptador por produto”Tudo que tem forma de produto vive atrás de uma porta. O núcleo genérico conhece
markdown, conversas de chat, threads de chat e membros; não sabe nada sobre
envelopes do Slack, mrkdwn ou gateway intents. Três peças fazem isso funcionar:
- O adaptador normaliza um produto: seu transporte, suas chamadas de API, seu
codec de texto e suas peculiaridades de envelope. Os do Slack são instrutivos —
uma menção em canal chega duas vezes (como
app_mentione comomessage) e a maioria dos subtipos de mensagem é enfeite, não pedido novo. Os dois são resolvidos no adaptador, então o núcleo nunca descobre que o Slack entrega em dobro. - O descritor é a forma pública do provedor: quais credenciais ele precisa, quais são seus formatos, quais URLs ajudam um usuário a encontrá-las, o que ele consegue fazer e se seu app pode ser criado ou editado daqui. O cartão de configurações, o diálogo de conexão e a validação de argumentos são todos gerados a partir dele — o cliente devolve um provedor mais um mapa com chaves por id de campo.
- O plugin é a composição do lado do servidor: como as credenciais se tornam uma conexão verificada, como um adaptador é construído e como o app do lado do provedor é gerenciado.
Então adicionar um produto é um valor de enum, um adaptador e um plugin. Nenhuma operação RPC nova, nenhuma mudança de schema, nenhuma mudança no cliente — e o cliente continua honesto, porque um campo de que ele nunca ouviu falar ainda é renderizado com o rótulo que o servidor forneceu.
Dois detalhes impedem que a abstração vaze para cima. As etapas de gerenciamento do app para as quais um provedor não tem API (gerar um token, instalar o app, reinstalar após uma mudança de permissões) são retornadas como etapas nomeadas com seus próprios links, para que a tela de concluir a configuração as mostre sem saber de que provedor se trata. E toda instrução que a ponte escreve nomeia o provedor e o comando que o app realmente usa, então um comando renomeado ou um segundo provedor são lidos corretamente sem nenhum texto novo.
Onde ficam as credenciais
Seção intitulada “Onde ficam as credenciais”Os tokens de chat de um espaço de trabalho ficam no diretório próprio desse
espaço, ao lado do banco de dados — como JSON puro em
<dataDir>/<workspaceId>/chat_credentials/<provider>.json, não no banco de dados
e deliberadamente não no chaveiro do sistema operacional. Duas consequências são
fáceis de perder de vista depois: excluir um espaço de trabalho desvincula o
diretório e seus tokens vão junto, e exportar ou fazer backup de um espaço de
trabalho copia workspace.db, então um espaço de trabalho entregue a outra
pessoa não carrega tokens de bot ativos dentro de si. O arquivo herda o limite de
confiança do sistema de arquivos do host do servidor — qualquer coisa com acesso
de leitura ao diretório de dados consegue ler um token de bot, que é o mesmo
limite atrás do qual os bancos de dados ficam.
Um arquivo por provedor, escrito atomicamente e somente para o proprietário: um app revogado é excluído desvinculando um arquivo, e um arquivo corrompido custa um provedor em vez de todos eles. As credenciais viajam para dentro pela operação de conexão e nunca voltam — leituras de status respondem apenas com metadados da conexão.
O que deliberadamente não é oferecido
Seção intitulada “O que deliberadamente não é oferecido”- Configurações estruturais do app. O transporte, os escopos e as assinaturas de eventos são afirmados, não expostos. Um app que pode ser configurado para o silêncio a partir de uma tela de configurações é um chamado de suporte, não um recurso.
- Conversa mais comandos slash. A superfície do Slack são mensagens, respostas em thread e comandos slash.
- Uma segunda fonte da verdade. O chat não é um lugar onde trabalho é armazenado. O thread do Slack é um espaço, a resposta é uma transcrição, o ticket é um ticket — todos eles no espaço de trabalho, acessíveis de qualquer cliente.
Veja também
Seção intitulada “Veja também”- Multijogador — identidade, associação e presença: os Principals e as funções pelos quais a ponte resolve
- Espaços de trabalho e isolamento: por que vínculos e credenciais são por espaço de trabalho
- O modelo de agentes: o que desperta quando uma menção chega
- Tickets e delegação: o que
/cc ticketabre - Arquitetura: onde a ponte fica entre servidor e clientes
- Conecte o Slack ao seu espaço de trabalho
- Configurar a integração com o Slack
- Vincular sua conta do Slack
- Personalizar o bot de chat
- Conversas de PR: as mesmas três decisões, tomadas para um host de código