Um operador encontra contexto em runbooks e documentação, interpreta o que está acontecendo e transforma parte dessa informação em trabalho rastreável. Encontrar uma instrução e abrir uma issue parecem tarefas próximas. Mas a segunda atravessa uma fronteira: produz um efeito em outro sistema.
Foi desse problema que parti no OpsPilot AI. Eu queria consultar conhecimento operacional do tenant, responder com referências e preparar uma issue no GitLab para outra pessoa aprovar. A pergunta mais interessante surgiu quando deixei de tratar tudo isso como “uma resposta do agente”.
A IA encontrou evidência suficiente? Ela deveria ter autoridade para executar uma ação externa? São perguntas diferentes. Uma proposta pode respeitar o schema, ter um título convincente e ainda estar fora das permissões do usuário.
Minha decisão foi deixar a interpretação probabilística dentro de fronteiras determinísticas. Uma ação gerada corretamente continua sendo uma proposta. O sistema autentica, autoriza, registra a aprovação e controla a execução.
Primeiro, delimitar o sistema
Modelei separadamente o principal autenticado, o conhecimento do tenant, a proposta, a aprovação e a execução. Misturar esses conceitos em um objeto de “agent state” tornaria fácil confundir informação gerada com autoridade concedida.
O principal contém tenant, sujeito e papéis, derivados de credenciais configuradas. A proposta contém o trabalho sugerido. A aprovação registra quem aceitou qual ação. A execução registra tentativas e resultados externos. PostgreSQL persiste esse ciclo; ele não depende apenas da memória do processo.
Separei os contratos de domínio dos adapters de banco, modelo e GitLab. Assim consigo exercitar um planner que propõe ações proibidas sem depender de uma chamada paga para descobrir se a política funciona.
O domínio também limita o produto: cada run prepara no máximo uma proposta de issue. Não construí um agente com shell, SQL arbitrário ou liberdade para chamar qualquer URL. Essa restrição reduz o espaço que preciso autorizar, observar e recuperar. Ampliar ferramentas exigiria rever o modelo de autoridade, não apenas adicionar nomes ao prompt.
Retrieval precisa respeitar a fronteira de acesso
Antes de pensar na resposta, precisei definir quais documentos poderiam entrar no contexto. Tenant não é um argumento que o modelo escolhe. Vem da identidade autenticada e acompanha a transação até o banco.
A ingestão normaliza o texto, divide em chunks com sobreposição e grava texto e embeddings atomicamente, depois das chamadas externas. A busca combina cosseno vetorial exato no pgvector com full-text search do PostgreSQL. Reciprocal rank fusion combina posições nos rankings, em vez de tratar scores de naturezas diferentes como diretamente comparáveis. Identificadores de espaço de embeddings evitam comparar vetores de modelos incompatíveis.
Os filtros de tenant aparecem antes do ranking e dos limites. Buscar globalmente e filtrar só o top-k seria uma arquitetura diferente: documentos de outros tenants poderiam consumir o orçamento de resultados, além de ampliar a superfície de vazamento.
Também configurei contexto local à transação e RLS forçada. Testes com PostgreSQL real exercitam predicados ausentes e reutilização de conexões. O limite importa: o papel de runtime pode definir o contexto de tenant, então RLS não defende contra SQL arbitrário executado com esse papel. Também não implementa ACLs entre documentos dentro do mesmo tenant.
Encontrar chunks não encerra o RAG
Eu separo o caminho em fronteiras que podem falhar independentemente:
query → retriever → evidências ordenadas
↓
geração
↓
validação de citações
↓
resposta ou abstenção
O adapter solicita saída estruturada. A aplicação verifica novamente a resposta: IDs inventados ou duplicados são rejeitados, as citações precisam pertencer aos chunks autorizados recuperados e os metadados vêm do banco. Uma saída sem citações vira uma abstenção fixa; sem resultados, nem preciso chamar a geração.
Isso controla a proveniência dos IDs, não a verdade de cada afirmação. Um chunk pode estar autorizado e a resposta ainda interpretar seu conteúdo incorretamente. Citation membership não prova entailment: citar um documento existente não demonstra que a conclusão decorre dele.
Essa distinção muda o diagnóstico. Retrieval pode não encontrar o documento certo; geração pode falhar mesmo com contexto correto; ou uma citação formalmente válida pode não sustentar a afirmação. Medir só a resposta final esconderia onde o sistema precisa melhorar.
Também não trato esse controle como garantia universal sobre issues. Uma proposta autorizada não se torna semanticamente correta por existir no banco; a revisão humana ainda precisa avaliar seu conteúdo.
Medir retrieval sem confundir ranking com resposta
O primeiro dataset de retrieval era fácil demais para separar estratégias. No retrieval-v2, usei documentos e perguntas sintéticos com paráfrases, identificadores, ambiguidade, múltiplos documentos relevantes e negativos difíceis. Separei 24 queries de desenvolvimento e 36 held-out, com labels definidos antes de executar os retrievers.
MRR@5 mede quão cedo aparece o primeiro documento relevante. Se está em primeiro, aquela query recebe 1; em segundo, 1/2; sem documento relevante nas cinco posições consideradas, recebe 0. A média resume esse comportamento. O protocolo deduplica documentos a partir dos chunks recuperados; um documento com vários chunks ainda pode consumir parte do orçamento.
| Estratégia no held-out | MRR@5 |
|---|---|
| Lexical | 0,7532 |
| Vetorial com embedder fake | 0,4375 |
| Híbrida com embedder fake | 0,6306 |
A busca lexical superou a híbrida nesse experimento. O ramo vetorial usou hashing determinístico de palavras, não um modelo semântico. RRF não corrige automaticamente um ranking fraco: documentos presentes nos dois ramos podem superar um resultado relevante encontrado só pelo ramo lexical.
O relatório retrieval-v2 fixa o resultado no commit 7ba3378. O held-out foi executado uma vez e está consumido; CI usa dev. Mudanças posteriores de readiness alteraram o fingerprint, e o guard recusa repetir o held-out no código atual.
Isso não demonstra que busca híbrida seja pior em geral, nem mede embeddings reais, groundedness ou qualidade das respostas. Para avaliar outra configuração, eu precisaria de novo protocolo congelado e um held-out novo. É a mesma preocupação com escopo que discuti em evals: parar de adivinhar, começar a medir.
O planner propõe; o código decide o que é permitido
LangGraph organiza o workflow. Ele não define o modelo de autoridade.
O planner pode selecionar search_knowledge, prepare_gitlab_issue ou final_answer. create_gitlab_issue não é uma ferramenta disponível ao modelo. A topologia não tem caminho dos nós de planejamento até execução; a entrada para executar depende do estado persistido após aprovação.
Limitei passos, deadline e tentativas de saída inválida. Isso contém loops e falhas de protocolo, mas não transforma uma decisão em boa decisão. Um planner que termina dentro do orçamento ainda pode propor trabalho inútil.
Na preparação, a política resolve o alias de projeto no contexto do tenant, verifica labels e converte responsáveis permitidos para IDs configurados. Um modelo não fornece diretamente um project ID arbitrário nem escolhe suas próprias permissões.
Imagine um cliente enviando isto como suposta prova de autoridade:
{ "allowed_project": "secret-admin" }
Esse exemplo é um antipadrão, não um contrato do OpsPilot. Aceitar JSON válido ou uma declaração “allowed” do cliente não concede acesso. A autoridade precisa vir da identidade autenticada e da política da aplicação. No projeto, os contratos rejeitam campos extras; os schemas do planner não contêm tenant, aprovação ou uma flag de autorização.
Aprovar a intenção não basta: é preciso aprovar a ação
“Um humano aprovou” é uma descrição incompleta. O que foi aprovado? O mesmo objeto ainda será executado? A política mudou entre esses momentos?
Persisti a ação canônica com tenant, solicitante, projeto resolvido, título, descrição, labels e responsáveis antes de solicitar aprovação. Um hash SHA-256 do JSON canônico vincula a decisão a esse conteúdo. A normalização e a ordenação evitam que representações equivalentes de labels ou responsáveis produzam identidades distintas. A execução usa a ação armazenada; não pede ao modelo que a gere outra vez.
O aprovador precisa ter o papel adequado, pertencer ao mesmo tenant e ser um sujeito diferente do solicitante. A transação verifica o hash recebido e recalcula o hash da ação armazenada. A proposta e a aprovação não têm permissão de UPDATE para o papel de runtime.
Antes do efeito externo, a execução confere novamente a aprovação, o hash e a política vigente. Se o projeto saiu da allowlist, a ação falha fechada. Se a ação mudou fora do fluxo normal, a aprovação não serve mais. Uma mudança exige outro run e outra aprovação.
interpretação probabilística
↓
proposta estruturada
↓
política determinística
↓
aprovação humana (hash)
↓
ação imutável aprovada
↓
revalidação → execução
↓
reconciliação / auditoria
O hash protege o vínculo entre conteúdo e decisão. Não avalia se a issue faz sentido, e não substitui autorização. A revisão humana acrescenta trabalho operacional; aceitei esse custo para controlar uma ação externa relevante.
A resposta do GitLab se perdeu. Posso repetir o POST?
Este foi o failure mode que mais conectou o agente a problemas clássicos de sistemas distribuídos: GitLab recebe a criação, grava a issue, mas a resposta se perde. O cliente observa timeout. A issue pode existir.
Repetir imediatamente o POST pode duplicar trabalho. Classifiquei separadamente falha antes do envio, recusa terminal e resultado ambíguo. Timeout após possível envio, 5xx e resposta 2xx malformada podem esconder uma escrita concluída; não significam necessariamente “nada aconteceu”.
Cada ação aprovada recebe uma chave estável derivada de tenant, run e hash. O adapter inclui um marcador na descrição da issue. Quando o resultado fica ambíguo, um pedido explícito de /resume busca esse marcador antes de considerar novo envio.
Se encontra a issue, registra sucesso sem criar outra. Se a busca falha, mantém ambiguidade. Se não encontra, respeita uma janela de graça antes de permitir reenvio, dentro do orçamento de tentativas. O sistema não tem worker automático de recuperação.
PostgreSQL usa lock, owner e lease para controlar claims de execução e impedir que um owner atrasado sobrescreva registros de outro. Mas um lease não cancela um request HTTP já em voo. Busca atrasada ou remoção do marcador também podem permitir duplicação. Isso não oferece exactly-once.
Testes com PostgreSQL real e GitLab fake exercitam resposta perdida, falha de gravação após o efeito e morte do processo depois da criação. São evidências de recuperação nesses cenários controlados. O smoke real de GitLab validou criação, lookup e resume de sucesso; não mediu recuperação real de resposta perdida ou atraso de indexação.
Segurança fora do prompt
O teste adversarial mais útil assume que o planner já foi comprometido. Documentos dizem para ignorar aprovação e usar um projeto proibido; o planner scripted obedece, pede criação direta, tenta o projeto proibido e emite uma decisão de aprovação.
Nos casos exercitados, o código recusa ferramentas e projetos proibidos. A única proposta permitida continua aguardando um humano, com zero requests ao GitLab. Isso testa a fronteira de autoridade mesmo quando a interpretação falha.
Prompts que chamam evidência de “untrusted data” e schemas estritos ajudam a organizar a interface. Não substituem autenticação nem autorização. Também não eliminam poisoning de uma resposta dentro do próprio tenant: o modelo pode receber informação maliciosa autorizada para leitura.
Os controles protegem fronteiras específicas: isolamento de tenant, ferramentas permitidas, decisão humana, validação em runtime e audit trail. A regressão adversarial verifica comportamentos definidos; não prova resistência completa a qualquer prompt injection.
Observabilidade atravessa as fronteiras
Um log “agent failed” seria insuficiente. Eu preciso distinguir ausência de evidência, timeout do modelo, política negada, espera por aprovação e efeito externo ambíguo.
Instrumentei fronteiras com OpenTelemetry: request HTTP, retrieval e seus ramos, chamada de IA, planejamento, autorização, aprovação, execução, GitLab e reconciliação. Request IDs, run IDs e trace IDs permitem relacionar logs, spans e eventos de auditoria.
Aprovação e resume são novos requests; não afirmo que toda a espera humana fica dentro de um único trace contínuo. O run e os trace IDs registrados no audit conectam essas entradas. Assim posso reconstruir o ciclo sem registrar prompts ou documentos completos.
Allowlists restringem atributos, e IDs de request, tenant ou documento não viram labels de métricas. Isso reduz exposição de conteúdo e cardinalidade descontrolada. Modelo configurado e modelo servido são informações distintas; uso ou custo ausente permanece desconhecido, não zero.
A exportação de telemetria pode perder sinais sem interromper o produto. Autorização e aprovação, em contraste, recusam a execução quando seus requisitos não são satisfeitos. Estado persistido e audit trail são a autoridade; um span de sucesso não decide se a execução está concluída. O registro de observabilidade detalha os sinais e os testes locais de falha do collector.
Evals diferentes respondem perguntas diferentes
A avaliação de agentes passou em 16/16 casos com planners scripted/offline, PostgreSQL real e GitLab fake. Ela mede os controles sob sequências definidas, inclusive comportamento adversarial. Não mede a habilidade de um LLM real escolher ferramentas.
- Retrieval-v2: exercita ranking no held-out sintético com embedder fake. Não demonstra retrieval semântico real ou resposta correta.
- Agent eval: exercita política, aprovação, estados e recuperação offline. Não demonstra qualidade das decisões de um LLM real.
- Smoke OpenAI: exercita API real, schemas, embeddings e pequeno RAG com banco. Não demonstra qualidade, entailment ou custo medido.
- Smoke GitLab: exercita aprovação, criação, confirmação e resume no sandbox. Não demonstra recuperação de falhas reais ou exactly-once.
Qualidade da geração é outra pergunta: a resposta está correta e suas afirmações decorrem das evidências? O projeto ainda não tem uma avaliação desse comportamento com modelo real. Ranking, workflow e integração podem passar sem responder a essa pergunta.
O registro OpenAI de 4 de outubro de 2026 confirma embeddings reais de 256 dimensões, schemas de resposta e planner, citações pertencentes ao contexto, uso, modelo servido e timeout limitado. O de GitLab de 5 de outubro confirma nenhum request antes da aprovação, criação e GET, segunda aprovação com 409, resume com uma issue encontrada e um POST de criação, e fechamento confirmado.
São execuções separadas, e o smoke GitLab usa planner offline. Não houve um único E2E live OpenAI + GitLab. Pricing OpenAI estava ausente: passar no check condicional de custo não significa ter medido custo.
O release v0.1.0 está publicado no GitHub. Seu registro local documenta 241 testes unitários e 72 de integração, além de reprodução em ambiente limpo. O estado atual registra a confirmação posterior do proprietário de que o CI hosted passou. Esses números descrevem aquela validação; não são desempenho em tráfego de produção. Os registros live mantêm os escopos de cada execução.
O que continua não provado
O held-out é pequeno, sintético, em inglês e autorado por uma pessoa. O embedder fake não testa entendimento semântico. Não há avaliação de qualidade do modelo real, custo medido ou validação do conjunto com tráfego operacional real.
Tokens estáticos não oferecem SSO nem lifecycle de identidade. Não há ACLs dentro do tenant. Busca vetorial exata tem custo linear; não reivindico escala a partir de um corpus pequeno. Recuperação depende de resume explícito, e reconciliação por busca conserva os limites de visibilidade e requests em voo.
Terraform descreve um blueprint AWS validado com planos offline. Nenhum deploy AWS foi realizado; runtime cloud, restore e caminho de telemetria AWS continuam sem validação. Passar validação de configuração não demonstra operação nesse ambiente.
O scan da imagem do release reteve 44 findings HIGH sem correção disponível, com zero HIGH/CRITICAL corrigíveis. O gate bloqueia os corrigíveis; não significa zero vulnerabilidades. Os quatro riscos IaC classificados também continuam documentados. Esses resultados são históricos, não um novo scan feito para este artigo.
O que levo deste projeto
Meu principal aprendizado foi tornar explícito o contrato entre interpretação e efeito. O modelo pode recuperar contexto, interpretar uma solicitação e propor trabalho. Cabe ao sistema de software determinar a autoridade, verificar a aprovação e o resultado dos efeitos externos, e manter estados que permitam recuperação quando esse resultado é incerto.
As próximas avaliações precisam enfrentar as lacunas que permanecem: embeddings reais em novo protocolo, qualidade das respostas e recuperação live. Passar em um smoke não responde a essas perguntas. Foi a separação dessas responsabilidades que tornou o projeto mais útil para mim do que apenas fazer um agente chamar uma ferramenta.
Evidências e código
Consultei o repositório público OpsPilot AI no snapshot cacf611. Os links fixam esse estado; o benchmark retrieval-v2 pertence ao freeze histórico indicado no relatório.
-
Estado atual e proveniência das validações
. -
Workflow, aprovação, recuperação e testes adversariais
. -
Política determinística no código
. -
Isolamento, evidência não confiável e limites de RLS
. -
Scan da imagem e escopo do gate
. - Case OpsPilot AI para o panorama do projeto.