Pontes de chat
Uma ponte de chat transforma um produto de chat externo — o Slack, hoje — numa forma de primeira classe de entrar num espaço de trabalho do Control Center. Mencione o bot e um agente desperta; responda na thread do Slack e a conversa continua; escreva um comando slash e é aberto um ticket. Por baixo, os mesmos espaços, transcrições, tickets e atribuição que a aplicação para computador usa. O chat é uma surface do espaço de trabalho, não uma cópia dele.
O Slack é o único fornecedor incluído. Não existe nenhuma implementação de Discord, Telegram, Teams ou Matrix em qualquer parte do produto. A funcionalidade está escrita sem conhecer o fornecedor, de propósito — acrescentar um segundo produto é um valor de enumeração, um adaptador e um plugin —, mas hoje «chat» significa Slack.
Duas superfícies cobrem isto e pertencem a pessoas diferentes. Ligar um espaço de trabalho ao Slack e personalizar o bot são ações de administrador, em Definições → Espaço de trabalho → Geral. Associar a sua própria conta de chat à sua identidade no Control Center é uma ação de membro, em Definições → Espaço de trabalho → Perfil e identidade.
Três decisões moldam toda a funcionalidade: em que direção corre a ligação, o que autoriza uma mensagem que chega do exterior e onde é permitido viver o conhecimento específico de cada produto.
De saída por conceção
Secção intitulada “De saída por conceção”A ponte nunca escuta o fornecedor. O Slack é contactado através de Socket Mode: o servidor pede ao Slack um URL de socket e liga para fora, recebendo depois menções, mensagens diretas e comandos slash através dessa ligação.
A consequência é o objetivo. Um portátil atrás de NAT, um servidor doméstico sem domínio, uma máquina que muda de rede — todos eles conseguem executar a ponte sem porta de entrada, sem túnel, sem certificado TLS e sem URL público para rodar quando este muda. Não há endpoint de webhook para proteger porque não existe endpoint de webhook. Quando o Control Center compõe a definição da aplicação, assume o Socket Mode e remove qualquer URL de pedido, porque uma aplicação configurada para ambos seria uma aplicação que funciona pela metade.
O custo é uma ligação que é preciso manter viva, o que é tarefa do servidor: a reconexão é um backoff exponencial com jitter limitado a 30 segundos, uma rotação de socket iniciada pelo fornecedor reconecta sem agravar esse backoff e uma credencial rejeitada é terminal — repetir indefinidamente um token revogado só renderia um limite de pedidos, por isso a ponte para e aguarda novos tokens, apresentando nas definições o erro do próprio fornecedor, à letra.
Um espaço de trabalho sem aplicação de chat não custa absolutamente nada: sem socket, sem temporizadores, sem tráfego. E tudo começa depois do banner de prontidão do servidor, porque uma aplicação de chat lenta a responder nunca deve atrasar o arranque.
É a pertença que autoriza, não a posse do token
Secção intitulada “É a pertença que autoriza, não a posse do token”A pessoa que instalou a aplicação não é a pessoa que envia a mensagem. Por isso, cada evento recebido é resolvido num Principal antes de acontecer seja o que for, e falha de forma segura com uma explicação:
- Uma conta de chat não associada recebe instruções de como associar, na thread do Slack onde escreveu.
- Uma conta associada cujo utilizador não é membro deste espaço de trabalho é instruída a pedir um convite a um administrador.
- Um membro cujo papel é só de leitura (
viewer,guest) é informado de que o seu papel não pode iniciar trabalho.
Nenhum destes casos avança. Uma mensagem de chat nunca é atribuída a alguém que não a enviou — e é isso que mantém honestos o registo de auditoria, os limites de pedidos por principal e quem reporta os tickets. Ver Multijogador — identidade, pertença e presença para o modelo de pertença em que isto se apoia.
A associação é resolução de identidade, não uma concessão de acesso. A pertença é verificada de novo em cada evento, por isso uma associação pertencente a alguém que entretanto foi removido resolve-se numa recusa e não nos seus antigos privilégios. Há dois caminhos para estabelecer uma associação: um código de uso único e curta duração que o membro escreve no chat, ou uma correspondência automática com o email verificado do fornecedor — e o caminho do email continua a exigir que o utilizador correspondido já seja membro, pelo que um desconhecido cujo endereço por acaso existe no servidor não fica associado a nada.
Os códigos são deliberadamente mantidos em memória, com um tempo de vida de 15 minutos. Persistir um deles criaria uma linha duradoura com forma de credencial para algo que deve evaporar-se, e um reinício do servidor que invalida todos os códigos pendentes é o comportamento correto, não uma regressão — o membro volta a premir «associar».
Uma recusa é respondida na thread do Slack que a mensagem abriu, como uma resposta verdadeira. Uma resposta em thread marca a mensagem a que responde, chega à única pessoa que precisa dela e mantém «não está associado» fora do próprio canal. Publicar uma mensagem é também a única capacidade que todos os fornecedores têm, por isso a explicação nunca depende de uma funcionalidade que a aplicação possa não ter.
Proveniência e o problema do eco
Secção intitulada “Proveniência e o problema do eco”Uma ponte que espelha nos dois sentidos entra em ciclo a menos que algo distinga
as duas direções. Todas as mensagens que a ponte traz para dentro são marcadas
com metadata['chat'] — fornecedor, equipa, conversa, thread, mensagem e
remetente. O espelho de saída ignora qualquer mensagem que traga essa marca.
Essa regra única é o que torna seguro o espelhamento bidirecional: uma mensagem humana escrita no Control Center é retransmitida para a thread associada (com o nome do autor como prefixo), enquanto uma mensagem que veio do chat fica onde entrou.
O que atravessa para fora é deliberadamente estreito. A resposta do agente é
retransmitida como texto de chat normal. Um pensamento curto pode aparecer na
linha Thinking… do cartão. A saída das ferramentas é uma parede de diffs e
fica no Control Center, que é também onde aponta uma resposta truncada.
Um cartão relata o trabalho; o texto continua a ser a mensagem
Secção intitulada “Um cartão relata o trabalho; o texto continua a ser a mensagem”Onde o fornecedor os apresenta, um turno transporta um cartão de tarefa, editado no local enquanto o turno decorre e colocado acima da resposta que explica.
O cartão é um plano agrupado, não um parágrafo esmagado. O Slack concatena
details e output na mesma tarefa, por isso os detalhes de uma linha são
enviados uma vez (o pensamento em Thinking…) e output nunca é transmitido em
fluxo. A preparação é uma linha cujo título é substituído (Working on it…,
Cloning …). O raciocínio acrescenta uma linha Thinking…. Cada ferramenta tem
a sua própria linha (Read lib/main.dart). A resposta é texto normal por baixo
do cartão — a vista de plano do Slack lê-a numa adição de markdown posterior, não
num fragmento misturado nas tarefas. Nada mais: nenhuma saída de ferramentas e
apenas um argumento de ferramenta de uma curta lista de autorizações, porque os
argumentos de uma ferramenta desconhecida podem ser o corpo inteiro de um
ficheiro e uma linha é uma só linha.
O cartão cobre também a espera antes do turno. Uma primeira menção cria a
conversa, e criá-la clona todos os repos em que o agente vai trabalhar — o que,
num repositório real, são minutos durante os quais ainda não foi pedido nada ao
agente e a thread parece que o bot ignorou o pedido. Por isso o cartão abre no
momento em que a menção é aceite (Working on it…) e depois essa linha de
preparação atualiza-se com as mesmas palavras que a aplicação para computador
mostra — Cloning acme/widgets…, Setting up Reviewer…,
Starting the agent… —, seguindo-se uma linha Thinking… assim que o agente
começa a raciocinar, e depois uma linha por ferramenta. Um cartão, uma mensagem,
desde
«ouvi-te» até à resposta como texto normal por baixo. A preparação só é
comunicada quando alguém no chat está à espera dela: uma conversa a ser preparada
na aplicação, ou uma conversa que ficou pendente e é retomada no arranque do
servidor, aprovisiona sem cartão nenhum.
Transporta a única coisa que o chat não pode dar ao leitor: uma ligação View
in Control Center para a conversa por trás da resposta. Os produtos de chat
aceitam http(s) numa ligação e nada mais, por isso o botão aponta para uma
pequena página que este servidor serve, e que reencaminha o navegador para a
ligação control-center:// da aplicação para computador. Essa página não lê nada
nem resolve nada — os ids no seu URL são ecoados para um deep link e para mais
lado nenhum — e um servidor sem URL público acessível produz simplesmente um
cartão sem botão, em vez de um cartão avariado.
Os cartões são uma capacidade como qualquer outra. Um fornecedor sem eles
comporta-se exatamente como antes, apenas texto, e um fornecedor que recuse um
cartão no momento da chamada mantém o seu fluxo: a resposta tem sempre
prioridade sobre o cartão que a descreve. Quando a transmissão em fluxo está
completamente desativada, o cartão concluído segue em vez disso 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 é a ligação para o
ticket acabado de criar.
As capacidades são negociadas e depois memorizadas
Secção intitulada “As capacidades são negociadas e depois memorizadas”Os produtos de chat diferem naquilo que conseguem apresentar, por isso a ponte é escrita para a superfície mais rica — uma resposta que cresce ao vivo dentro de uma thread, um estado transitório «is thinking…», uma thread com título, uma resposta efémera a um comando slash — e degrada uma capacidade de cada vez. Um fornecedor sem nada disso continua a funcionar; as respostas chegam simplesmente como uma só mensagem.
Anunciar uma capacidade não é prometê-la. As respostas em fluxo do Slack precisam de uma funcionalidade de plano pago e o Slack só recusa no momento da chamada. Uma recusa marcada como permanente é memorizada durante toda a vida da ligação: a ponte deixa de pedir e passa a publicar respostas inteiras, e as definições dizem-no claramente, porque «as minhas respostas deixaram de ser transmitidas em fluxo» deve poder ter resposta sem ler um registo. Os cartões de tarefa são descartados da mesma forma se o Slack recusar um.
Todos os limites pertencem ao fornecedor e não ao relé: o teto de mensagens, o teto de fragmentos do fluxo e se um fluxo precisa de uma âncora de thread são todos declarados pelo adaptador, e uma resposta longa é dividida num limite de parágrafo, de linha ou de palavra, nunca a meio de uma palavra.
Uma ponte, um adaptador por produto
Secção intitulada “Uma ponte, um adaptador por produto”Tudo o que tem a forma do 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 com que
isto funcione:
- O adaptador normaliza um produto: o seu transporte, as suas chamadas à API,
o seu códec de texto e as suas peculiaridades de envelope. As do Slack são
instrutivas — uma menção a um canal chega duas vezes (como
app_mentione comomessage) e a maioria dos subtipos de mensagem é decoração e não novos pedidos. Ambos são resolvidos no adaptador, por isso o núcleo nunca fica a saber que o Slack entrega duas vezes. - O descritor é a forma pública do fornecedor: que credenciais precisa, quais são os seus formatos, que URLs ajudam um utilizador a encontrá-las, o que consegue fazer e se a sua aplicação pode ser criada ou editada a partir daqui. O cartão de definições, a caixa de diálogo de ligação e a validação de argumentos são todos gerados a partir dele — o cliente devolve um fornecedor e um mapa com chaves pelos ids dos campos.
- O plugin é a composição do lado do servidor: como as credenciais se transformam numa ligação verificada, como se constrói um adaptador e como se gere a aplicação do lado do fornecedor.
Portanto, acrescentar um produto é um valor de enumeração, um adaptador e um plugin. Sem nova op RPC, sem alteração de esquema, sem alteração no cliente — e o cliente mantém-se honesto, porque um campo de que nunca ouviu falar continua a ser apresentado com a etiqueta que o servidor forneceu.
Dois detalhes impedem que a abstração transborde para cima. Os passos de gestão da aplicação para os quais um fornecedor não tem API (gerar um token, instalar a aplicação, reinstalar após uma alteração de permissões) são devolvidos como passos com nome e com as suas próprias ligações, para que o ecrã de conclusão da configuração os mostre sem saber de que fornecedor se trata. E todas as instruções que a ponte escreve nomeiam o fornecedor e o comando que a aplicação usa realmente, para que um comando renomeado ou um segundo fornecedor se leiam corretamente sem nenhuma cadeia nova.
Onde ficam as credenciais
Secçã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 de trabalho, ao lado da sua base de dados — como JSON simples em
<dataDir>/<workspaceId>/chat_credentials/<provider>.json, não na base de dados
e deliberadamente não no keychain do sistema operativo. Há duas consequências
fáceis de perder mais tarde: eliminar um espaço de trabalho desvincula o
diretório e os seus tokens vão com ele, e exportar ou fazer cópia de segurança de
um espaço de trabalho copia workspace.db, pelo que um espaço de trabalho
entregue a outra pessoa não leva tokens de bot ativos lá dentro. O ficheiro herda
a fronteira de confiança do sistema de ficheiros da máquina anfitriã — qualquer
coisa com acesso de leitura ao diretório de dados pode ler um token de bot, que é
a mesma fronteira atrás da qual estão as bases de dados.
Um ficheiro por fornecedor, escrito atomicamente e acessível apenas pelo proprietário: uma aplicação revogada é eliminada ao desvincular um ficheiro e um ficheiro corrompido custa um fornecedor em vez de todos. As credenciais entram através da operação de ligação e nunca voltam a sair — as leituras de estado respondem apenas com metadados da ligação.
O que deliberadamente não é oferecido
Secção intitulada “O que deliberadamente não é oferecido”- Definições estruturais da aplicação. O transporte, os escopos e as subscrições de eventos são impostos, não expostos. Uma aplicação que pode ser configurada para o silêncio a partir de um ecrã de definições é um pedido de assistência, não uma funcionalidade.
- Conversa mais comandos slash. A superfície do Slack é mensagens, respostas em thread e comandos slash.
- Uma segunda fonte de verdade. O chat não é um local onde o trabalho é armazenado. A thread do Slack é um espaço, a resposta é uma transcrição, o ticket é um ticket — todos eles no espaço de trabalho, acessíveis a partir de qualquer cliente.
Ver também
Secção intitulada “Ver também”- Multijogador — identidade, pertença e presença: os principals e papéis através dos quais a ponte resolve
- Espaços de trabalho e isolamento: porque é que as associações e as credenciais são por espaço de trabalho
- O modelo de agentes: o que desperta quando chega uma menção
- Tickets e delegação: o que
/cc ticketabre - Arquitetura: onde a ponte se situa entre o servidor e os clientes
- Ligue o Slack ao seu espaço de trabalho
- Configurar a integração com o Slack
- Associar a 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