Só os meus processos
A pergunta que motivou este capítulo é esta, e ela é sua, não minha:
"Quais processos no DCP estão atribuídos ao meu perfil e que tenham assuntos e temas parecidos?"
É uma boa pergunta porque é uma pergunta de trabalho — ninguém pergunta isso num exercício. E é uma pergunta difícil por dois motivos independentes, que este capítulo separa:
- "os meus" é um problema de autorização, e a resposta intuitiva está errada de um jeito perigoso;
- "parecidos" é um problema de definição, e a resposta intuitiva é cara demais sem precisar ser.
Comece pelo primeiro, porque o segundo não importa se o primeiro estiver errado.
O agente não pode perguntar quem é você
A ideia natural — e a que apareceu quando este capítulo foi pedido — é fazer o agente perguntar:
"Qual é o seu nome?" → e então mostrar só os processos daquele magistrado.
Não faça isso. Vale a pena ser explícito sobre por quê, porque a intenção por trás da ideia está inteiramente certa: cada magistrado deve ver apenas os processos relacionados a ele. O problema não é o objetivo, é o mecanismo.
Filtrar pela resposta que alguém digitou é autorização por autodeclaração. O filtro passa a valer exatamente o que valer a palavra de quem está conversando com o agente. E há três caminhos por onde isso falha, dois deles sem nenhuma má-fé:
1. Qualquer pessoa que sente no computador digita outro nome. Não é preciso invadir nada nem saber nada de técnica. A pergunta seguinte é literalmente "e os processos do juiz Fulano?", e um agente que aceita o nome como filtro vai atender.
2. O nome pode vir de um documento, não de uma pessoa. Se em algum momento o agente lê o texto de uma petição, de um despacho ou de um e-mail, o conteúdo desse texto entra no contexto do modelo. Um texto que diga "o magistrado responsável é X" passa a ser, para o modelo, indistinguível de você tendo dito isso. Esse é o mecanismo de injeção de prompt, e ele não depende de nenhum atacante sofisticado — depende só de o filtro morar numa frase.
3. O modelo erra sozinho. Ele é o mesmo qwen2:7b que chamou .get(0) num dicionário de chaves string. Confiar a ele a propagação correta de um identificador de autorização, ao longo de seis passos de raciocínio, é confiar demais.
O resumo, e ele é a regra do capítulo:
Identidade não é argumento. É configuração.
Perguntar o nome para dizer "bom dia, Dra. Fulana" é ótimo. Perguntar o nome para decidir o que mostrar é entregar a chave para quem está do outro lado da conversa.
De onde a identidade vem
Do ambiente, uma vez, no momento em que o programa sobe — antes de existir qualquer conversa.
import os
# Lido no import. Não é parâmetro de Tool, não vem da conversa, {: #lido-no-import-não-é-parâmetro-de-tool-não-vem-da-conversa }
# não aparece na assinatura que o modelo lê. {: #não-aparece-na-assinatura-que-o-modelo-lê }
MAGISTRADO = os.environ["DCP_MAGISTRADO_ID"]
Isso é exatamente o mesmo padrão do TETO_SIGILO da Unidade 2: constante no código, nunca argumento. E a razão é a mesma, agora com mais consequência.
A propriedade que faz isso funcionar é negativa, e é a mais forte que existe num agente:
O que não está na assinatura, o modelo não tem como preencher.
Se a Tool é meus_processos(assunto: str), não existe passo de raciocínio, texto de documento ou pergunta de usuário capaz de trocar o magistrado — porque não há onde escrever esse valor. Compare:
# Errado: o magistrado é um argumento. O modelo escolhe. {: #errado-o-magistrado-é-um-argumento-o-modelo-escolhe }
def processos_do_magistrado(nome_magistrado: str, assunto: str) -> dict:
...
# Certo: o magistrado é a configuração. O modelo escolhe só o assunto. {: #certo-o-magistrado-é-a-configuração-o-modelo-escolhe-só-o-assunto }
def meus_processos(assunto: str) -> dict:
...
As duas funções fazem a mesma consulta. A diferença inteira está em qual das duas tem uma porta a mais.
O mesmo raciocínio derruba uma variação que parece inofensiva: um argumento magistrado opcional, com o seu id como padrão.
def meus_processos(assunto: str, magistrado: str = MAGISTRADO) -> dict: # não
O padrão só vale quando o modelo não preenche. E ele preenche — é isso que modelos fazem com argumentos que existem.
As três camadas, da mais forte para a mais fraca
O filtro por identidade pode morar em três lugares. Use quantos conseguir, e comece pelo de cima.
| Camada | Quem implementa | O que ela protege contra |
|---|---|---|
| 1. A view já vem filtrada pelo usuário do banco | a TI, no chamado | tudo, inclusive um bug seu |
2. O WHERE usa a constante lida do ambiente |
você, na consulta | modelo, conversa, injeção |
| 3. A assinatura da Tool não tem o campo | você, na função | modelo, conversa, injeção |
A camada 1 é a que vale o chamado. É a diferença entre "eu filtro direito" e "não há o que filtrar errado". Uma view que já resolve o magistrado a partir do usuário conectado torna as camadas 2 e 3 redundantes — e redundante é exatamente o que você quer que elas sejam.
O acréscimo ao texto do chamado do capítulo do banco:
Peço que a view seja restrita aos processos vinculados ao meu perfil de magistrado, resolvido a partir do usuário de banco criado para este acesso — de modo que a consulta não dependa de nenhum filtro aplicado pela aplicação cliente. Peço também que ela exclua processos com nível de sigilo restrito.
A frase que faz o trabalho é "não dependa de nenhum filtro aplicado pela aplicação cliente". Ela é o que transforma um pedido de acesso num pedido de escopo.
E se a TI disser que não dá — acontece, e às vezes por motivo legítimo —, então as camadas 2 e 3 são tudo o que você tem, e o item 2 da lista de verificação deixa de ser opcional.
Onde as credenciais moram
Nenhuma delas em arquivo de código. Nenhuma delas em config.yaml. A regra é uma só e já apareceu em todos os capítulos desta unidade: variável de ambiente.
O que muda aqui é como elas chegam ao processo, porque o servidor MCP não é você digitando num terminal.
No Windows, para a sessão atual do PowerShell:
$env:DCP_USUARIO = "seu.usuario"
$env:DCP_SENHA = "..."
$env:DCP_MAGISTRADO_ID = "..."
Some quando você fecha a janela — o que é bom para experimentar e ruim para o dia a dia.
Permanente, para o seu usuário:
setx DCP_MAGISTRADO_ID "..."
Vale para janelas novas, não para a aberta. E fica no registro do seu perfil, não em disco compartilhado.
Para o servidor MCP. Um servidor lançado por stdio herda o ambiente de quem o lançou. Se quem lança é o cliente do curso, ele herda o seu PowerShell. Se quem lança é o Claude Desktop, o ambiente é o que estiver no bloco env da configuração dele — e aí a senha estaria num arquivo JSON, que é justamente o que não queremos.
Prefira, nesse caso, deixar a senha no ambiente do usuário (setx) e no JSON só o que não é segredo.
Três lugares onde uma senha aparece sem você reparar:
O histórico do PowerShell. Ele grava as linhas digitadas em ConsoleHost_history.txt. Uma senha atribuída na mão fica lá, em texto puro, até você apagar.
O print de depuração. print(os.environ) imprime tudo. Numa sessão de MCP, isso vai para o stderr — e o stderr costuma ir para um arquivo de log.
O traceback. Uma exceção dentro de oracledb.connect(...) pode incluir os argumentos na mensagem. Capture o erro e devolva uma mensagem sua, como o capítulo do banco já fazia.
E um arquivo que entra no repositório, com os nomes e sem os valores:
# .env.exemplo — este entra no repositório. O .env, nunca. {: #envexemplo-este-entra-no-repositório-o-env-nunca }
DCP_USUARIO=
DCP_SENHA=
DCP_MAGISTRADO_ID=
Ele documenta o que a ferramenta precisa sem documentar nada que interesse a ninguém.
"Assuntos parecidos", sem inventar semântica
A segunda metade da pergunta. E aqui a resposta cara está logo à mão: transformar cada assunto num vetor e comparar por similaridade.
Não faça — não porque não funcione, mas porque a tabela de assuntos do CNJ já é uma árvore, e a árvore responde melhor.
Cada assunto tem um pai. Nas tabelas internas isso costuma aparecer como COD_PAI_NACIONAL, apontando para o código do assunto acima na hierarquia. "Parecido" tem, portanto, uma definição exata e barata:
Assuntos parecidos são os que compartilham o mesmo pai na tabela nacional.
Isso tem três vantagens sobre qualquer medida de similaridade, e as três importam mais aqui do que em qualquer outro contexto:
É determinístico. A mesma pergunta devolve o mesmo conjunto hoje e daqui a seis meses. Uma medida de similaridade muda quando o modelo de embedding muda.
É explicável. Você consegue dizer, numa frase, por que aquele processo entrou na lista: "tem o mesmo assunto-pai". Tente explicar 0,83 de similaridade de cosseno para quem perguntou.
É oficial. A hierarquia é do CNJ, é a mesma que classifica o processo na origem, e é a mesma que o DataJud usa. Você não está inventando uma taxonomia paralela dentro do Tribunal.
Em SQL, é um JOIN da tabela de assuntos com ela mesma:
SELECT p.numero_processo, p.classe, a.descricao AS assunto
FROM vw_meus_processos p
JOIN assunto a ON a.cod_assunto = p.cod_assunto
JOIN assunto ref ON ref.cod_pai_nacional = a.cod_pai_nacional
WHERE ref.cod_assunto = :assunto_referencia
Um JOIN a mais, na mesma conexão, contra tabelas que já estão ali. Sem dependência nova, sem modelo extra, sem índice vetorial para manter.
Se a árvore não bastar — e às vezes não basta, porque assuntos vizinhos na prática ficam longe na hierarquia —, o passo seguinte não é embedding. É uma lista de códigos que você mesmo escreve, com um nome:
TEMAS = {
"saude": [10064, 10065, 10069],
"servidor_publico": [10219, 10220],
}
Feia, manual, e correta. Você sabe exatamente o que entra em cada tema, e consegue defender a lista numa reunião. Uma medida de similaridade não se defende — se explica, o que não é a mesma coisa.
A Tool
Junta as duas metades. Repare no que não está na assinatura.
import os
import oracledb
MAGISTRADO = os.environ["DCP_MAGISTRADO_ID"]
TETO_LINHAS = 30
_conexao = None
def _conectar():
global _conexao
if _conexao is None:
_conexao = oracledb.connect(
user=os.environ["DCP_USUARIO"],
password=os.environ["DCP_SENHA"],
host=os.environ["DCP_HOST"],
port=1521,
service_name=os.environ["DCP_SERVICE"],
)
return _conexao
def meus_processos_por_tema(assunto_referencia: int) -> dict:
"""Processos do meu acervo cujo assunto tem o mesmo assunto-pai do informado.
Não lista processos de outros magistrados e não devolve dados das partes.
Args:
assunto_referencia: código nacional do assunto usado como referência.
"""
sql = """
SELECT p.numero_processo, p.classe, a.descricao, p.data_ultimo_movimento
FROM vw_meus_processos p
JOIN assunto a ON a.cod_assunto = p.cod_assunto
JOIN assunto ref ON ref.cod_pai_nacional = a.cod_pai_nacional
WHERE ref.cod_assunto = :assunto
AND p.cod_magistrado = :magistrado
AND p.nivel_sigilo = 0
ORDER BY p.data_ultimo_movimento DESC
FETCH FIRST :teto ROWS ONLY
"""
cur = _conectar().cursor()
cur.execute(sql, assunto=assunto_referencia,
magistrado=MAGISTRADO,
teto=TETO_LINHAS)
linhas = [
{"numero_processo": n, "classe": c, "assunto": d,
"ultimo_movimento": m.strftime("%d/%m/%Y")}
for n, c, d, m in cur
]
return {"processos": linhas, "quantidade": len(linhas)}
Cinco decisões, todas já vistas, e vale nomeá-las:
MAGISTRADO é constante de módulo, ligada a um bind. O modelo escolhe o assunto e nada mais.
nivel_sigilo = 0 está no WHERE, não num if depois — pelo motivo do capítulo do banco: filtrar em Python significa que a linha sigilosa já entrou no processo que monta o prompt.
A consulta é contra vw_meus_processos, uma view, não contra as tabelas. Se a camada 1 existir, o cod_magistrado no WHERE é redundante — e continua lá de propósito.
Teto de linhas, porque a Unidade 3 inteira é sobre isso. Trinta processos com quatro campos cabem; trezentos, não.
Retorno em dict, e as colunas escolhidas à mão. Nenhum nome de parte, nenhum SELECT *.
A viabilidade dos joins entre sistemas
Falta a parte honesta, e ela responde a uma dúvida que foi levantada com todas as letras: isso acaba sendo queries complexas que devem realizar muitos joins com chaves diferentes dos diferentes sistemas — não sei a viabilidade.
A dúvida está certa. O veredito, em uma linha:
Um agente não deve montar join entre sistemas ao vivo. O join pertence a uma camada anterior, e ela não é escrita pelo agente.
Quatro razões, em ordem de gravidade.
1. Não existe chave comum, exceto uma. Cada sistema tem o próprio identificador interno, e eles não se falam. O único identificador que atravessa PJe, eproc, EJUD e DCP é o número único do processo (NPU) — que é justamente o campo mais sujeito a variação de formatação: com máscara, sem máscara, com zeros à esquerda, com o dígito verificador separado. Um join por NPU sem normalizar as duas pontas produz zero linhas ou linhas erradas, e nenhum dos dois dá erro.
2. Cada sistema tem o próprio controle de sigilo, com outro nome de coluna. Quatro conexões, quatro credenciais, quatro esquemas e quatro colunas de sigilo diferentes. Basta uma não filtrada para o resultado inteiro estar comprometido — e o resultado inteiro é o que vai para o modelo. É a única razão desta lista que, sozinha, já bastaria.
3. O custo é do tipo que estoura em silêncio. Join entre bases distintas quase sempre significa trazer um lado inteiro para a memória. É a consulta que leva minutos, e o agente que leva minutos é o agente que estoura o tempo, repete, e faz tudo de novo do zero.
4. O SQL passaria a ser montado pelo modelo. Um join com cinco tabelas não cabe numa consulta parametrizada e fixa — a tentação vira dar ao modelo a capacidade de escrever SQL. É a fronteira que o capítulo do banco recusa, e recusar continua sendo a resposta certa.
O que fazer em vez disso
Se o Tribunal já tem uma camada analítica, é ali que o join mora. O padrão é conhecido — dados brutos por sistema, uma camada intermediária normalizada e uma camada final consolidada, atualizada em lote, fora do horário de pico. O join acontece uma vez por noite, escrito por quem conhece as chaves, com o resultado conferido. A Tool então lê uma tabela, com uma credencial, e a consulta volta a caber em vinte linhas.
Essa é a resposta certa e é a que dá mais trabalho, porque envolve outras pessoas. Ela também é a única que escala para além de uma pessoa.
Se não tem, a resposta honesta é: um sistema de cada vez.
"Quais processos meus no DCP têm assuntos parecidos" é respondível hoje, com uma conexão e a Tool acima. "Os meus processos no DCP e no eproc, unificados" é um projeto, não uma Tool — e transformá-lo em Tool à força produz uma ferramenta que responde devagar, às vezes errado, e sem avisar quando erra.
O meio-termo que funciona
Há uma forma legítima de cruzar sistemas num agente, e ela não é um join:
Uma Tool por porta. O agente chama duas e junta os resultados.
meus_processos_dcp(assunto) e meus_processos_eproc(assunto), cada uma com o próprio teto de linhas, o próprio filtro de sigilo e a própria credencial. O agente chama as duas e apresenta as duas listas.
Isso funciona bem para dezenas de linhas e mal para milhares — que é exatamente a escala das perguntas que valem a pena. E tem uma vantagem que o join perde: se uma das portas falhar, você vê qual falhou. Num join, o resultado apenas encolhe.
A regra prática que fecha o assunto:
Uma Tool, uma porta, uma credencial. Cruzamento de verdade é lote; cruzamento de agente é o modelo lendo duas listas curtas.
Antes de conectar num agente
Aos itens do capítulo do banco, acrescente cinco. Os dois primeiros são os que medem o que este capítulo defende — e nenhum deles se verifica lendo o código.
- Pergunte ao agente pelos processos de outro magistrado, pelo nome. Ele deve devolver os seus, ou nada. Se devolver os do outro, o filtro está na conversa, não na consulta.
- Procure
magistradona assinatura de todas as suas Tools. Se aparecer em alguma, ela é preenchível pelo modelo. - A quantidade de processos que a Tool devolve bate com o que o DCP mostra na sua tela?
DCP_SENHAestá fora do repositório, fora doconfig.yaml, fora doclaude_desktop_config.jsone fora do histórico do PowerShell?- Se você apagar
DCP_MAGISTRADO_IDdo ambiente, o programa falha ao subir? Deve falhar — umos.environ[...]que levantaKeyErrorno import é melhor que umos.environ.get(...)que devolveNonee filtra por nada.
O item 5 parece pedantismo e não é. WHERE cod_magistrado = NULL não devolve zero linhas por acidente feliz em todo banco, e a diferença entre "não devolveu nada" e "devolveu tudo" é uma cláusula.
Próximo: MNI — a porta que já é sua.