Pular para o conteúdo
Voltar para o blog

Dando a um agente de IA acesso limitado ao ERP

Publicado em por raimonesbarros

kthdatabridge

Passei os últimos dias construindo uma ponte entre agentes de IA e a base de dados do ERP Sankhya. O nome dela é KTHDataBridge, e ela responde uma pergunta que hoje quase todo time de desenvolvimento está fazendo em voz baixa: dá para deixar um agente ler o banco sem se arrepender depois?

A resposta curta é que dá — mas o trabalho não está em conectar. Conectar é fácil. O trabalho está em decidir, antes, o que exatamente o agente pode alcançar, e em fazer com que essa decisão viva no servidor, e não na boa vontade de quem escreve o prompt.

Por que eu precisei disso

Todo dia eu fazia ao agente a mesma família de perguntas: qual é o fonte desta View, o que essa Package faz, quem usa esta tabela, o que quebra se eu mexer neste Trigger. E todo dia acontecia a mesma coisa: ele não tinha como saber, então ele inventava um Sankhya plausível. Nome de tabela quase certo, coluna que não existe, dependência imaginada.

Um agente sem acesso ao dado real não fica em silêncio. Ele chuta com confiança — e chute com confiança é pior que resposta nenhuma, porque custa tempo de revisão.

Do outro lado, a alternativa era eu mesmo abrir o banco, copiar o fonte, colar no chat, e repetir isso a cada pergunta. Funciona, e é exatamente o tipo de trabalho que a máquina deveria estar fazendo.

O que a ponte é, na prática

Duas metades. Um módulo Java implantado dentro do próprio Sankhya, que expõe serviços HTTP; e um servidor MCP em contêiner, que roda na rede local e atende o time inteiro. Quem consome não instala nada: aponta o cliente para a URL, informa o token da equipe, e pronto.

São doze ferramentas. Cinco de catálogo e consulta — fonte de View, Package, Trigger e Function, DDL de índice, busca por nome, mapa de dependências, e o SELECT propriamente dito. E sete do acervo de BI: os gadgets e painéis do Construtor de Telas, com listagem, definição em XML, análise de impacto, prévia do dado, validação, histórico com diff semântico e gravação.

Dessas doze, duas escrevem. E o desenho inteiro do projeto gira em torno de manter essas duas separadas de todo o resto.

O desafio de ceder acesso a um agente

Aqui está a parte que eu não esperava. O problema não é técnico — é de modelo mental.

Quando você dá acesso a uma pessoa, existe um contrato implícito: ela sabe o que é teste e produção, ela hesita antes de rodar algo estranho, e ela responde pelo que fez. Um agente não tem nada disso. Ele não hesita. Ele é uma máquina de produzir a próxima ação plausível, e "plausível" e "seguro" são coisas diferentes.

Três consequências disso:

  1. Escrita disfarçada de leitura. Pedir "somente SELECT" parece trivial até você perceber quantas formas existem de um comando parecer leitura e não ser.
  2. Vazamento por acidente, não por má-fé. O agente pede todas as colunas da tabela de parceiros porque é a coisa mais natural do mundo — e pode levar junto CPF, endereço, telefone e salário de muita gente, para dentro de um contexto que ninguém auditou.
  3. A requisição é montada por quem a trava deveria conter. Este é o ponto que muda tudo: qualquer proteção que dependa de um parâmetro enviado pelo cliente é uma proteção que o próprio agente pode desligar. Não por malícia — por autonomia.

O terceiro item é o que eu levaria para qualquer outro projeto do gênero. A defesa tem que morar no servidor, e não pode ter interruptor.

Os cuidados que ficaram no código

Cinco defesas em série no SELECT. Cada uma numa classe própria, e a ordem entre elas importa. Primeiro o SQL vira um "esqueleto": literais, comentários e identificadores entre aspas são apagados, com as posições preservadas. Sem esse passo, um filtro que compare um nome com a palavra DELETE seria recusado por engano, e um DROP escondido dentro de um comentário passaria por descuido. Depois vem a lista negra por token — palavra inteira, nunca pedaço, porque DTINSERT não é INSERT.

A terceira é a que eu mais gosto, porque não depende de lista nenhuma: a consulta inteira vira subconsulta de SELECT * FROM (…) WHERE ROWNUM <= ?. Não existe DML que sobreviva a ser posto dentro de um FROM — morre no parser do Oracle, antes de executar. Lista negra é enumeração de coisas ruins, e enumeração sempre envelhece; o envelope estrutural é uma propriedade.

A quarta barra DB LINK, e a quinta é a máscara de dado pessoal. Somam-se a elas três tetos: um corte de linhas no banco, outro no driver, e um tempo limite — o único que salva de uma consulta que varre uma tabela enorme sem devolver nada.

Dado pessoal sai mascarado, e a máscara é do lado do servidor. Entre o ResultSet e o JSON, no Java — não no cliente MCP. A distinção é o ponto inteiro: o serviço HTTP responde a quem o chamar, e máscara feita no cliente protegeria apenas o cliente que a implementa. São doze categorias, e cada uma decide o que fica visível com um motivo escrito ao lado.

Alguns exemplos do raciocínio: de um CPF ficam os três dígitos antes dos verificadores, porque mostrar os verificadores daria conferência de graça a quem tentasse adivinhar. De um nome fica a inicial de cada palavra, senão a coluna vira uma fila de asteriscos idênticos e você não distingue uma linha da outra. De um salário não fica nem o tamanho — num valor, a quantidade de dígitos é a informação. E de dado sensível não fica nada, porque ali o vazamento não é constrangimento, é discriminação.

E não existe parâmetro que desligue a máscara. Parâmetro viaja na requisição, e a requisição é montada pelo agente de quem a máscara protege o cadastro. Quem precisa do dado em claro abre a tela do Sankhya.

O SQL não pode disfarçar a coluna. Essa foi a segunda rodada de aprendizado. A máscara é decidida pelo nome da coluna no resultado — e nome de coluna é justamente o que quem escreve o SELECT controla. Sem uma segunda trava, ela cairia com quatro letras: bastava dar um apelido curto à coluna de documento. Então coluna sensível só passa de duas formas: nua, ou comparada por igualdade. Quem filtra por um CPF já tem aquele CPF; a consulta não entrega o que o chamador não trouxe.

Todo o resto é recusado com a explicação. Apelido apaga a máscara. Expressão apaga também, porque o rótulo de uma expressão é o texto dela cortado em trinta caracteres, e uma expressão comprida empurra o nome da coluna para fora do corte. E LIKE, > e BETWEEN sobre coluna sensível são recusados por um motivo mais sutil: eles são um oráculo. "Começa com" e "maior que" respondem sim ou não sobre um valor escondido, e uma sequência dessas respostas reconstrói o valor inteiro. ORDER BY é o mesmo oráculo por outro caminho — quem ordena por uma coluna que não vê a descobre comparando resultado com resultado.

Credencial não é mascarada, é recusada. Senha, token, chave, certificado: qualquer coluna dessa família no texto do SQL derruba a consulta antes de ela rodar, projetada ou em filtro — inclusive numa comparação por igualdade, que nas outras categorias passaria. Ali a frase "quem filtra por um valor já o tem" se inverte: a igualdade responde sobre um palpite, e uma sequência de palpites é força bruta com o servidor fazendo o trabalho.

O que escreve fica separado, e sob ordem expressa. As duas ferramentas de escrita exigem uma palavra de confirmação conferida no servidor Java, e a descrição da ferramenta manda enviá-la apenas quando o usuário pediu aquilo na conversa. A palavra não prova que ele pediu — nada prova. O que ela garante é que a chamada não saiu de uma escolha automática, que é o caso realista. O mesmo vale para a consulta por DB LINK, a única que sai desta instalação: ela não escreve, mas responde com as credenciais gravadas no link, num banco que não é este, então também exige confirmação.

As descrições das ferramentas são longas de propósito. Elas são o único contexto que o agente tem na hora de escolher, e o catálogo do Oracle tem armadilhas que levam à conclusão errada quando não são ditas antes: uma PACKAGE sem BODY não tem código, INVALID não significa erro, e uma lista de dependentes vazia não significa "ninguém usa". Documentação, aqui, é parte do mecanismo — não enfeite dele.

O que a trava não garante

Isso está escrito na documentação do projeto, e eu acho que deveria estar em todo projeto parecido.

Um SELECT pode chamar uma function PL/SQL, e uma function declarada como transação autônoma pode gravar. Nenhuma análise de texto pega isso: a escrita não está no SQL, está do outro lado de uma chamada que parece leitura.

E o perímetro real não é o meu validador — são os GRANTs do usuário com que o Sankhya conecta ao banco, que é o dono do schema. O módulo impede o acidente e o comando malicioso óbvio; ele não substitui permissão de banco. O caminho correto seria um usuário Oracle separado, com GRANT SELECT no que o agente pode ler e nada mais. Isso é decisão de infraestrutura, não de código, e ainda não está feito.

Há mais: o token da equipe é único, então ele não distingue quem chamou nem permite revogar o acesso de uma pessoa sem trocar o de todas. E, na rede interna, ele trafega em claro.

Escrever essas três coisas custou menos que qualquer uma das travas, e provavelmente vale mais. Segurança que você não sabe onde termina não é segurança — é sorte com nome bonito.

Os benefícios, que são o motivo de tudo isso existir

Com a ponte no ar, a conversa mudou de natureza. O agente não descreve mais o que ele acha que a View faz: ele traz o fonte. Antes de eu mexer num objeto, ele me diz o que usa e o que é usado por aquilo. Uma pergunta sobre o dicionário de dados vira uma consulta, e a consulta volta com dado real, mascarado onde precisa.

Três ganhos concretos:

  • O chute acabou. A diferença entre um agente que consulta e um que supõe não é de qualidade, é de categoria. Um é ferramenta; o outro é aposta.
  • A análise de impacto ficou barata. "O que quebra se eu alterar isso" era meia hora de garimpo antes de qualquer mudança. Hoje é uma pergunta. E no acervo de BI a mesma ideia responde que gadgets citam uma tabela, que painéis usam um gadget, e o que sobrou sem uso.
  • Uma instalação, o time inteiro. O contêiner sobe numa máquina e atende todo mundo pela rede. Duas linhas de configuração no cliente, sem Node e sem clonar repositório.

E há um benefício que eu não tinha previsto: o exercício de escrever as travas me obrigou a documentar o banco melhor do que ele estava documentado antes. As armadilhas do catálogo, as categorias de dado pessoal, o que cada máscara preserva e por quê — nada disso existia por escrito. Existe agora, porque uma máquina precisava ler.

O que eu levo disso

Ceder acesso a um agente não é um problema de permissão, é um problema de desenho. A pergunta útil não é "Se eu confio", é "O acesso tá dentro do limite de segurança?".

Três regras ficaram comigo:

  1. A trava mora no servidor, e não tem interruptor. Se um parâmetro desliga, ela não é uma trava — é uma sugestão.
  2. Prefira propriedades a listas. O envelope de ROWNUM protege contra comandos que ninguém enumerou; a lista negra só protege contra os que alguém lembrou de escrever.
  3. Documente o limite com o mesmo cuidado com que você escreve a defesa. Quem usa a ferramenta precisa saber onde a proteção termina, e essa frase precisa estar escrita antes de alguém descobrir do jeito difícil.

O resto — a conveniência, a velocidade, o agente que finalmente sabe do que está falando — vem depois. E vem justamente porque veio depois.

Código fechado. O projeto roda em ambiente corporativo da Kothe SA.
Dando a um agente de IA acesso limitado ao ERP | oradev