Pular para conteúdo

Guardiao: operacao e seguranca

Objetivo

Permitir que uma IA investigue dados fiscais no Qlik sem receber, no mesmo contexto, a identidade real do contribuinte.

O desenho reduz vazamento acidental e associacao indevida. Ele nao tenta proteger o computador contra um administrador local malicioso.

Fluxo de dados

Celular -> ChatGPT Remote -> Codex no PC -> MCP Guardiao(ticket)
                                             |
Celular -> Google Sheet -> ticket + CNPJ -----+
                                             |
                         VPN da prefeitura -> Qlik
  • A planilha conhece ticket + CNPJ, mas nunca recebe dados fiscais.
  • A IA conhece ticket + dados fiscais, mas nunca recebe CNPJ.
  • O Guardiao e o unico componente que associa as duas partes.
  • O CNPJ permanece na planilha durante a investigacao (o ticket sobrevive a reinicios do processo MCP); a planilha nunca e exposta a LLM.
  • encerrar_ticket apaga a associacao em memoria E o CNPJ da planilha. Para tickets esquecidos, o proprio guardiao apaga CNPJs com mais de GUARDIAO_TTL_HORAS (168h) na primeira utilizacao da fila em cada processo.

Nao escreva o CNPJ no chat que recebera o resultado da investigacao.

Planilha

Crie uma aba chamada Tickets com estes cabecalhos:

ticket_id cnpj criado_em status consumido_em consumidor comentario
caso-azul-17 valor real data pendente caso para conferir periodo curto

Somente ticket_id e cnpj sao obrigatorios. O ticket deve ter de 3 a 64 caracteres, conter letras e usar apenas letras, numeros, ., _ ou -. Use um apelido aleatorio; nunca use CNPJ, CPF, nome ou razao social como ticket. O campo cnpj aceita o CNPJ completo (com ou sem mascara) ou somente a raiz de 8 digitos. Quando recebe o CNPJ completo, o Guardiao preserva os 14 digitos somente na memoria interna para rastreamentos NFSE por estabelecimento; as ferramentas de Simples Nacional continuam usando apenas a raiz. Nenhum dos dois valores e devolvido ao modelo. comentario e opcional, fica apenas na planilha e nao e exposto ao modelo. Use-o somente para notas operacionais, sem identificadores pessoais ou dados fiscais. O arquivo docs/templates/tickets.csv pode ser importado para criar o cabecalho sem digitacao manual.

No celular, um Google Form pode alimentar essa aba. O formulario deve pedir um apelido aleatorio, o CNPJ e, opcionalmente, um comentario operacional; nao inclua identificadores pessoais adicionais, valores ou informacao fiscal.

A expiracao de tickets esquecidos e feita pelo proprio guardiao: na primeira utilizacao da fila em cada processo, CNPJs mais velhos que GUARDIAO_TTL_HORAS (padrao 168h / 7 dias, seja qual for o status) sao apagados e marcados como expirado. Linhas com CNPJ e sem data recebem criado_em na primeira varredura e passam a envelhecer a partir dali.

integrations/google_apps_script/Code.gs e opcional — so vale instalar se quiser preencher criado_em/status no envio do formulario ou ter uma limpeza que roda mesmo com o guardiao desligado por muitos dias.

Conta de servico

  1. Crie um projeto Google Cloud e habilite a Google Sheets API.
  2. Crie uma conta de servico e baixe sua chave JSON.
  3. Compartilhe apenas a planilha de tickets com o e-mail da conta de servico, como editor.
  4. Salve o JSON fora do projeto, por exemplo em %LOCALAPPDATA%\QlikGuardiao\google-service-account.json.
  5. Nao conecte essa planilha como app ou ferramenta visivel para a LLM.

O Guardiao usa somente o escopo spreadsheets. Consumir um ticket marca status=consumido mas preserva o CNPJ na celula: o mesmo ticket pode ser reconsumido apos um reinicio do processo. A celula so e limpa por encerrar_ticket, pela limpeza TTL do guardiao ou manualmente.

Configuracao local

Toda a configuracao mora em um unico arquivo fora do projeto: %LOCALAPPDATA%\QlikGuardiao\guardiao.env. Copie raw/mcp_qlik_guardiao/guardiao.env.example para la e preencha. O server.py carrega esse arquivo sozinho na inicializacao (os.environ.setdefault), entao nenhum cliente MCP precisa configurar env, nao ha setx e nao e preciso reiniciar aplicativos ao mudar valores — basta a proxima sessao MCP.

Variaveis de ambiente reais tem precedencia sobre o arquivo, com uma excecao: valores herdados contendo quebra de linha (artefato de setx mal quebrado, que ja causou HTTP 400 no Sheets) sao considerados corrompidos e o arquivo vence.

Para validar tudo de ponta a ponta (env, chave, credencial Google, fila de tickets, autenticacao Qlik) sem expor nenhum dado:

.venv\Scripts\python.exe raw\mcp_qlik_guardiao\doctor.py

Timeouts, checkpoint e orcamento

As tools NFSE consultam volumes grandes (3M+ notas/mes) e podem estourar o timeout de 600s do tools/call do MCP. As variaveis abaixo sao opcionais (o server.py tem o default embutido) e ficam comentadas em raw/mcp_qlik_guardiao/guardiao.env.example:

Variavel Default Uso
GUARDIAO_WS_TIMEOUT_S 30 timeout de connect do WebSocket do Engine
GUARDIAO_WS_RECV_TIMEOUT_S 60 timeout de recv apos conectar; cada estouro durante um reload longo vira um GetProgress (keepalive)
GUARDIAO_HTTP_TIMEOUT_S 30 timeout das chamadas REST (QRS/QPS)
GUARDIAO_RELOAD_STALL_S 300 progresso do reload parado por mais que isso (segundos) vira ErroEngineTravado em vez de hang indefinido
GUARDIAO_CHECKPOINT_DIR %LOCALAPPDATA%\QlikGuardiao\checkpoints diretorio dos checkpoints locais das tools NFSE
GUARDIAO_CHECKPOINT_TTL_HORAS 24 checkpoint mais velho que isso (horas) e descartado
GUARDIAO_BUDGET_S 480 orcamento de tempo (segundos) por chamada de tool antes de parar e devolver status: incompleto
GUARDIAO_DIAG_LOG %LOCALAPPDATA%\QlikGuardiao\diagnostico.jsonl caminho do log de diagnostico sanitizado (ver secao propria abaixo)
GUARDIAO_EXPORT_DIR %LOCALAPPDATA%\QlikGuardiao\exports diretorio dos .qvs gerados pelo fallback manual exportar_script_qvs

TLS

TLS e validado por padrao. O certificado do pmpa-qs2 e emitido pela CA interna do Qlik e so vale para o FQDN pmpa-qs2.pmpa.ad (por isso o server.py usa o FQDN). Converta a CA para PEM (ex.: openssl x509 -inform DER -in certs/<host>/root.cer -out %LOCALAPPDATA%\QlikGuardiao\qlik-ca.pem) e aponte GUARDIAO_CA_BUNDLE para ela. GUARDIAO_TLS_VERIFY=false existe apenas para diagnostico temporario e nao deve ser a configuracao permanente.

Clientes MCP

O servidor usa MCP via STDIO:

<python-do-venv> <raiz-do-projeto>\raw\mcp_qlik_guardiao\server.py

Codex

O arquivo .codex/config.toml registra o Guardiao somente para este projeto. Isso exige Codex >= 0.145 (versoes anteriores ignoravam config de projeto — a causa historica de o MCP "sumir" a cada sessao) e o repositorio marcado como confiavel. Toda a env vem do guardiao.env; nao adicione blocos env/ env_vars ao config (ha teste que garante isso). Confirme a configuracao a partir da raiz do projeto:

codex mcp list

Nao use codex mcp add para o Guardiao: esse comando grava a configuracao do usuario e deixa o servidor disponivel em outros projetos. O servidor STDIO nao deve ser iniciado com o Windows; o Codex o inicia sob demanda para a sessao e o encerra junto com ela.

As definicoes e descricoes das ferramentas ficam disponiveis para a LLM quando o MCP esta habilitado. Dados Qlik e tickets so entram no contexto quando uma ferramenta e chamada.

Para acesso pelo celular:

codex remote-control start
codex remote-control pair

O PC precisa permanecer ligado, acordado, conectado a internet e com a VPN da prefeitura ativa para consultas Qlik. Nao publique o app-server em uma porta da internet.

Claude Code

O arquivo .mcp.json na raiz registra o mesmo servidor por auto-discovery: basta abrir o Claude Code dentro do projeto e aprovar o server na primeira vez. (A instabilidade antiga do auto-discovery era o server morrendo no startup por falta de env herdada — resolvida pelo guardiao.env.)

Outra LLM

Qualquer cliente MCP com transporte STDIO pode iniciar o mesmo comando. O contrato publico esta nas anotacoes das funcoes @mcp.tool() de server.py. Clientes sem MCP podem analisar scripts e lineage, mas nao devem acessar dado real.

Uso diario

  1. No celular, crie ticket_id + CNPJ na planilha ou formulario.
  2. No chat remoto, diga: investigue o ticket caso-azul-17.
  3. Use rastrear_cnpj_simples_nacional(ticket_id=...).
  4. Continue usando o mesmo ticket para outras ferramentas — inclusive apos reiniciar a sessao ou o processo MCP (o CNPJ segue na fila ate encerrar).
  5. Ao terminar a investigacao, chame encerrar_ticket: alem de limpar a memoria, ele apaga o CNPJ da planilha (status=encerrado). Se a limpeza da planilha falhar, a resposta traz aviso pedindo remocao manual.

verificar_presenca_ecossistema(ticket_id=...) devolve somente presenca e contagem agregada nas fontes fixas usadas por Cadastro Consolidado, NFSE e dados abertos. Ela nao retorna identidade, notas ou campos cadastrais.

nfse_rastrear_ticket_periodos(ticket_id, periodo_inicial, periodo_final) conta notas por mes nas camadas de extracao, transformacao, T1, fato agregada e aplicacao final. Os periodos usam AAAAMM e ficam restritos a 202101-202412.

nfse_estatisticas_exclusao_chave(periodo_inicial, periodo_final) mede apenas agregados do universo NFSE: tipo do documento na nota, tipo da chave gerada, presenca/Encontrou no T2_Empresas_NFSE.qvd, elegibilidade da dimensao final, quantidade de contribuintes e notas. Ela nao recebe ticket nem retorna documentos ou chaves.

estatisticas_cancelamento_periodos_txt() varre os arquivos PERSIMEI e PERSIMPLES mais recentes e devolve somente contagens e percentuais agregados de cancelamento, datas inicial/final iguais, data final zerada e periodos nao cancelados com duracao de ate 30 dias (a mesma regra inversa do filtro >30 do ETL), separados entre data inicial igual ou diferente da data final.

amostrar_periodos_curtos_simples_nacional() seleciona dez contribuintes com periodos encerrados de 1 a 29 dias e datas diferentes, grava a associacao somente na fila privada e devolve tickets opacos com origem, datas e duracao.

impacto_filtro_periodos_curtos_simples_nacional() compara o T_PeriodoSN_Novo.qvd atual com uma simulacao em memoria do mesmo transformador sem o filtro >30. A resposta contem apenas totais finais de linhas e contribuintes, por regime e no conjunto; nenhum QVD e gravado.

listar_tickets_ativos mostra apenas apelidos presentes na memoria.

Consultas longas na NFSE (checkpoint e continuacao)

nfse_rastrear_ticket_periodos e nfse_estatisticas_exclusao_chave quebram o periodo pedido em chunks de 1 mes. Cada chunk concluido e salvo em um checkpoint local (GUARDIAO_CHECKPOINT_DIR) antes de seguir para o proximo - sobrevive a queda de rede, reinicio do processo MCP e ao timeout de 600s do tools/call. O checkpoint nunca guarda documento: a chave do arquivo e o conteudo so podem ter ticket_id (ja normalizado) e parametros neutros (ex. periodos AAAAMM); a funcao que monta a chave rejeita qualquer valor com 8+ digitos consecutivos.

A resposta de cada chamada traz um destes status:

  • completo: todos os chunks terminaram sem erro; o checkpoint e apagado.
  • completo_com_erros: o periodo inteiro foi percorrido, mas alguns chunks falharam de forma permanente (script/travado/auth) - vem chunks_com_erro com categoria/code/tentativas de cada um.
  • incompleto: o orcamento de tempo (GUARDIAO_BUDGET_S, default 480s) estourou antes de terminar todos os chunks. A resposta traz progresso (concluidos/total/com_erro) e proxima_acao: "chame novamente com os mesmos parametros para continuar do checkpoint". Chame de novo com os MESMOS argumentos: chunks ja concluidos sao pulados e os que falharam por motivo transitorio (rede/ocupado) sao retentados.

encerrar_ticket apaga os checkpoints do ticket junto com a memoria e o CNPJ da planilha. Checkpoints tambem expiram sozinhos por GUARDIAO_CHECKPOINT_TTL_HORAS (default 24h), limpos na primeira utilizacao da fila em cada processo.

Taxonomia de erros e retry

Toda falha de comunicacao com o Engine e classificada em uma destas categorias antes de subir para qualquer camada superior - nunca a mensagem bruta do Engine (message/qMessage), que pode ecoar trecho de script com CNPJ:

Categoria Situacao Retry automatico?
rede falha de socket/websocket sim, ate 3 tentativas, espera curta (~1.5s)
engine_ocupado code 11000, outro reload em andamento sim, espera maior (15s)
engine_script Engine recusou o script nao
engine_travado reload sem progresso por mais de GUARDIAO_RELOAD_STALL_S nao
auth autenticacao Qlik recusada nao

O retry acontece por chunk (1 mes), nunca pela consulta inteira: uma falha no mes 40 de 48 nao refaz os 39 anteriores.

diagnostico.jsonl - excecao explicita a leitura de logs

%LOCALAPPDATA%\QlikGuardiao\diagnostico.jsonl (override GUARDIAO_DIAG_LOG) registra, por whitelist estrita, uma linha JSON por falha de chunk com apenas estas chaves: ts, ferramenta, chunk, categoria, code, method, tentativas, duracao_s. Nunca tem ticket_id, documento ou mensagem do Engine.

Este arquivo especifico pode ser lido pela LLM. E o unico log de erro do Guardiao com essa permissao, porque e 100% sanitizado por construcao. A proibicao de leitura continua valendo, sem excecao, para guardiao.<pid>.log (log operacional de texto livre, para o humano) e para .mapa_pseudonimos.jsonl (desenho legado) - nunca leia esses dois.

Fallback manual: exportar_script_qvs

Quando as duas tools NFSE nao completam mesmo com checkpoint/retry (por exemplo, rede muito instavel), exportar_script_qvs(consulta, confirmado_pelo_usuario, ticket_id, periodo_inicial, periodo_final) gera um .qvs standalone para colar direto no Data Load Editor, sem depender do WebSocket nem do timeout de 600s do MCP.

  • So roda com confirmado_pelo_usuario=true. O modelo deve perguntar antes e so confirmar depois que o usuario disser explicitamente, na conversa, que esta no computador para abrir o arquivo. Sem confirmacao, a tool devolve um erro pedindo a confirmacao e nao grava nada.
  • Quando a consulta e nfse_rastrear_ticket_periodos, o CNPJ do ticket vai do processo direto para o arquivo em GUARDIAO_EXPORT_DIR (%LOCALAPPDATA%\QlikGuardiao\exports) - nunca passa pela resposta da tool nem pela conversa.
  • A resposta traz somente arquivo (caminho), instrucoes e contem_documento (bool). Nunca leia nem cole o conteudo do .qvs de volta na conversa.
  • Quando contem_documento=true, o proprio .qvs traz um aviso de sigilo no cabecalho ("NAO compartilhe este arquivo").

Diagnostico agregado da cadeia NFSE (nfse_diagnostico)

nfse_diagnostico(consulta, periodo_inicial, periodo_final) roda verificacoes agregadas da cadeia NFSE direto pela LLM, sem o operador colar script manual no Data Load Editor a cada investigacao de qualidade de dado (composicao por origem, comparativo DW vs QVD, dupla contagem na malha).

Contrato de seguranca:

  • so aceita as consultas fixas listadas em _DIAGNOSTICOS; qualquer outro valor devolve {"erro": "consulta invalida", "opcoes": [...]} sem tocar o Engine.
  • saida sempre agregada por periodo (e origem/status quando a consulta usa essas dimensoes) - nunca por contribuinte, nunca cod_cnpj/raiz. A agregacao (GROUP BY) roda no banco, nao no Qlik nem na resposta da tool.
  • nao existe, e nao deve existir, um runner generico que aceite SQL ou script arbitrario vindo da LLM: cada consulta e um script fixo definido em server.py (_script_diag_*), preso a allowlist. E essa ausencia de runner generico que impede a ferramenta virar um canal para extrair dado por contribuinte disfarcado de "diagnostico".

As 4 consultas da allowlist:

  • ping: consulta de fumaca do framework, sem dado sensivel; roda uma vez so (nao e mensal, ignora o periodo recebido).
  • composicao_origem: notas distintas por mes x flag_nacional x status, direto de vw_nfsemista_v1 no DW_Novo.
  • comparativo_dw_qvd: notas distintas do DW por mes vs QvdNoOfRecords de Nfse_<mes>.qvd e NfseNacional_<mes>.qvd, com a diferenca (faltando_apos_nacional).
  • dupla_contagem_detalhado: razao entre registros de NFSE_Detalhado_<mes>.qvd e notas distintas do DW no mes mesmo (indicador de duplicacao na malha).

Fonte dos dados: as 3 consultas reais (todas exceto ping) fazem LIB CONNECT TO 'DW_Novo' na mesma sessao do Engine que as demais tools NFSE usam, e leem QVDs via QvdNoOfRecords quando aplicavel - nunca abrem o QVD linha a linha.

Periodo: periodo_inicial/periodo_final usam AAAAMM e passam pela mesma validacao de nfse_rastrear_ticket_periodos/nfse_estatisticas_exclusao_chave. O teto agora e dinamico (ano corrente em UTC), nao mais fixo em 202412.

Continuacao: as consultas mensais (todas menos ping) rodam em chunks de 1 mes com o mesmo checkpoint e orcamento de tempo (GUARDIAO_BUDGET_S) da secao "Consultas longas na NFSE" acima, e devolvem os mesmos status:

  • completo: todos os chunks terminaram sem erro.
  • completo_com_erros: percorreu o periodo inteiro, mas alguns chunks falharam de forma permanente (chunks_com_erro).
  • incompleto: orcamento de tempo estourou antes do fim; chame de novo com os MESMOS parametros (consulta, periodo_inicial, periodo_final) para continuar do checkpoint - chunks concluidos sao pulados, os com erro transitorio sao retentados.

Como estender: uma consulta nova precisa de 3 pecas em server.py, sempre nessa ordem:

  1. entrada em _DIAGNOSTICOS (chave = nome da consulta, valor = descricao curta);
  2. um _script_diag_<nome>(mes) que devolve o script Qlik fixo, interpolando somente o mes do chunk - nada vindo do usuario;
  3. entrada em _DIAGNOSTICOS_IMPL (campos do resultado, o builder acima, qtype e se a consulta e mensal).

Builders novos passam por revisao de codigo antes de entrar em producao - e essa revisao (nao um filtro em runtime) que mantem o contrato de saida agregada e sem campo de contribuinte. Nao existe, e nao deve existir, um caminho que aceite script arbitrario fora dessas 3 pecas.

Falhas de acesso

  • Google Sheets recusou apagar o CNPJ (HTTP 403) (ao encerrar): a conta de servico consegue ler a fila, mas nao editar. Compartilhe a planilha com ela como Editor.
  • autenticacao Qlik recusada: verifique a chave JWT e o certificado do virtual proxy; esse erro ocorre depois do consumo do ticket.
  • falha TLS: configure GUARDIAO_CA_BUNDLE.
  • Qlik indisponivel: confira VPN, rede e servidor.
  • Engine ocupado (code 11000): outro reload em andamento; a tool ja retenta automaticamente apos 15s - so investigue se persistir.
  • reload sem progresso (Engine travado): sem avanco por GUARDIAO_RELOAD_STALL_S (default 5 min); verifique pressao de memoria no QMC (ver docs/GUARDIAO_QMC.md).
  • Engine recusou o script: erro de script no proprio Qlik, nao adianta retentar; consulte code/method em diagnostico.jsonl.

O Guardiao encerra explicitamente sua sessao no QPS ao terminar, evitando acumular logins entre sessoes do Codex. Depois de atualizar o codigo, reinicie o Codex uma vez para carregar o processo MCP novo.

Volume e periodos longos

Nao existe limite fixo de periodo. A ferramenta atual percorre toda a cadeia do Simples Nacional.

O volume retornado por etapa usa:

  • GUARDIAO_DEFAULT_ROW_LIMIT=500
  • GUARDIAO_MAX_ROW_LIMIT=50000
  • GUARDIAO_DEFAULT_SAMPLE_LIMIT=20
  • GUARDIAO_MAX_SAMPLE_LIMIT=500

Esses numeros sao configuracao operacional. Para uma investigacao maior:

  1. passe limite_linhas_por_etapa na ferramenta;
  2. confira truncado em cada etapa;
  3. se necessario, aumente o teto por variavel de ambiente e reinicie o MCP.

Para centenas de milhares de registros, uma nova ferramenta deve agregar e filtrar localmente antes de retornar o resultado. Isso reduz tokens sem impedir que o Qlik examine todo o periodo ou todas as linhas.

Uso por colegas

O recomendado e cada desenvolvedor autorizado ter:

  • clone proprio do repositorio privado;
  • ambiente virtual proprio;
  • identidade Qlik propria, para auditoria individual;
  • chave Qlik e credencial Google fora do clone;
  • GUARDIAO_OPERATOR_ID proprio;
  • aba ou planilha de tickets propria.

Nao compartilhe a sessao Remote do seu PC como forma normal de colaboracao. Isso mistura credenciais, historico, aprovacoes e autoria. Compartilhe codigo e documentacao pelo repositorio, e cada operador conecta sua propria instalacao.

Se ambos usarem a mesma planilha, use tickets diferentes e, preferencialmente, abas separadas em GUARDIAO_SHEETS_RANGE.

Migracao do desenho antigo

  • raw/trace_cnpj.py agora encerra imediatamente e nao consulta o Qlik.
  • .mapa_pseudonimos.jsonl nao e mais usado.
  • Chaves JWT novas nao devem ficar em certs/ ou raw/mcp_qlik_guardiao/.
  • Logs antigos e mapas legados nao sao apagados automaticamente. Revise e remova esses arquivos manualmente quando nao forem mais necessarios.
  • Rotacione a chave de API encontrada nos scripts e as chaves Qlik que permaneceram acessiveis dentro do projeto.

Testes

Os testes nao acessam Google ou Qlik:

.\.venv\Scripts\python.exe -m unittest discover -s tests -v

Eles verificam consumo e limpeza da celula, cache em memoria, ausencia de parametro CNPJ nas ferramentas publicas, limites configuraveis e truncamento.