Claudio Mascaro

Notas

Proteger o chat sem saber quem pergunta

Três tetos, um banimento manual e uma única exceção, cercada por escrito: como o chat deste site se protege de abuso sem guardar quem está perguntando.

O chat deste site chama um modelo cobrado por uso, fica aberto na internet e não guarda quem escreveu. Perguntado sobre proteção contra abuso, ele respondeu que o acervo não documentava nada disso. Estava certo sobre o acervo e errado sobre o chat: os tetos existiam desde o primeiro dia. A pergunta rendeu duas coisas, um buraco real no desenho e esta nota.

Os três tetos

Os três tetos nasceram junto com o chat, de propósito. Separá-los significaria existir um momento em que um endpoint de LLM sem limite estivesse público na internet. E a ordem no turno vai do mais barato ao mais caro: o contador é lido antes do acervo e do modelo, e pedido barrado não custa leitura de acervo nem token.

O primeiro é o limite por origem, contra rajada. É o mais barato e o mais fraco dos três: guarda a origem em memória, só pelo tempo da janela, e é best-effort. Aceitei assim, porque o dinheiro tem outro guardião.

O segundo é o teto de mensagens por sessão, contra a conversa infinita. Ele conta as perguntas na conversa que o cliente mandou, porque não existe sessão no servidor: não há nada que identifique o visitante para prendê-la. Um cliente que corte o próprio histórico escapa desse teto, e isso está dito em vez de escondido.

O que esse teto protege de verdade é o tamanho do contexto de cada turno, que é custo real. Contra cliente adversário quem responde é o limite por origem; contra a conta crescendo, o teto de gasto. Dar dentes a ele exigiria identificar quem pergunta, que é o que o chat recusa fazer.

O terceiro é o teto de gasto diário, e ele é fail-closed: contador ausente ou ilegível desliga o chat. O contador é do próprio provedor do modelo, remoto e durável, e é isso que torna o fail-closed verdadeiro. Um contador local nunca falharia, e um teto que nunca pode falhar não é fail-closed, é decoração. O valor do teto vem do ambiente; ausente ou fora de forma é teto ilegível, e desliga o chat. Zero é válido: significa chat desligado, sem mexer em código. E o desligamento fica visível para mim, pelo motivo por id no log, sem nada que identifique quem escreveu.

A trava final nem é do site. É o limite de crédito da própria chave no provedor, que vale com o site fora do ar, com bug no contador, com deploy errado. O site refina; a chave contém.

A razão de esse teto travar em vez de seguir já tem nota própria neste acervo. Aqui importa o lugar dele no desenho: fail-open protege trabalho, fail-closed protege dinheiro, e a rajada e a conversa infinita custam antes de o gasto do dia aparecer no contador remoto. É por isso que os outros dois existem.

A pá, não a muralha

Os três tetos deixavam um buraco, e foi a pergunta sobre abuso que o expôs: o abusador paciente. Quem fica dentro do limite de rajada e insiste queima o teto diário sozinho, e o chat escurece para o comprador real. O teto fail-closed protege o dinheiro; ninguém protegia a disponibilidade contra uma origem só. Nenhum abuso aconteceu até aqui; a decisão foi preventiva, e o tamanho dela respeita isso.

A resposta proporcional é cortar a origem, não engordar tetos globais que puniriam todo mundo: um banimento manual por origem, e cada palavra pesa. Manual porque a latência não importa e o falso positivo, sim: contra um abusador paciente, os minutos de um redeploy não mudam nada, e banir um comprador por heurística mudaria tudo. Detecção que bane sozinha aceita banir gente por engano, e o falso positivo custa exatamente o que o banimento existe para proteger. Quem lê o log e decide sou eu.

Por origem porque é a chave que a plataforma já entrega à rota, e por nenhuma outra. É pá, não muralha: endereço compartilhado pune vizinho inocente, abusador determinado troca de endereço, e as duas fraquezas estão aceitas por escrito. Recusei fingerprint e cookie: seria começar a rastrear visitante num site que não tem nem analytics, com mais obrigações de LGPD por um valor marginal.

A lista mora no ambiente, fora do git: endereço banido é dado pessoal, e dado pessoal não entra no repositório. Banir é editar a variável e fazer um novo deploy. A entrada vive até eu removê-la, e a retenção esperada é curta, porque a remoção vem quando o abuso para.

E a lista é fail-open, ao contrário do teto de gasto: ausente ou vazia, ninguém está banido; entrada torta é ignorada. É a mesma pergunta do teto, respondida pelo que está do outro lado. Atrás do teto há dinheiro, e dúvida trava. Atrás da lista há disponibilidade, e uma lista quebrada escurecendo o chat inteiro entregaria ao erro de configuração o que o abusador queria.

Origem banida degrada com motivo próprio, na mesma saída de toda degradação deste chat, que é a agenda. É o veredito mais barato de todos e vem antes do limitador: pedido banido não conta para o limitador e não custa leitura nenhuma. Esconder o banimento do banido seria teatro, porque quem troca de endereço descobre em um minuto, e ficar mudo sem motivo violaria a regra que vale para o chat inteiro: degradação tem motivo citável, sempre.

Sem identificador, com uma exceção cercada

O que o chat persiste é o texto da conversa e o idioma dela, nunca quem escreveu. O tipo do registro tem dois campos, e não há onde pôr um identificador; um teste cobra isso. A razão é uma conta simples: saber quais perguntas um comprador faz alimenta a pauta do que escrever; saber quem perguntou traz LGPD e consentimento sem valor proporcional.

O banimento precisou mexer nessa fronteira, e mexeu pelo caminho mais estreito que a torna transponível. Para eu banir uma origem, ela precisa existir em algum lugar que eu consiga ler. Então a origem pode existir em dois lugares apenas: na lista de banidos e no evento de abuso do log, o limite excedido e o pedido banido. Nenhum registro liga origem a texto de conversa. O registro da conversa continua sem identificador, com o mesmo teste de antes, e a razão original de não guardar quem escreveu, não ligar pergunta a pessoa, continua de pé. E o teto por sessão continua sem dentes: a exceção abriu o banimento, e nada além dele.

A base legal é o legítimo interesse, e o escopo mínimo faz parte da conformidade: lista curta, propósito único, retenção curta. Crescer qualquer um dos três é rever a decisão, não esticá-la.

A nota que faltava

Esta nota existe porque o chat disse que ela não existia. Quando o acervo não sustenta uma resposta, o próprio chat acusa a lacuna, e a pergunta vai para o log como pauta do que escrever; o conserto de uma lacuna é sempre uma nota nova, nunca um filtro na resposta. A pergunta sobre abuso caiu nessa lista, e o que ela pedia já estava decidido e registrado; faltava reescrever para quem chega de fora. Esta é a reescrita.