vek1-api — gotchas
Gotchas e armadilhas
1. .env.docker no histórico com keys vazadas
Commit bf11231 (2025-10-14) adicionou OPENAI_API_KEY, SUPABASE_ANON_KEY, TOGETHER_API_KEY no .env.docker. PR #1 deletou o arquivo mas histórico Git ainda contém. Repo é público. Keys nunca rotacionadas (pelo menos não documentado).
Mitigação: rotate as 3 keys + git filter-repo ou git filter-branch pra remover do histórico.
2. DATABASE_URL plaintext entre Vercel e VPS
vek1-postgres exposto em 0.0.0.0:5434 com sslmode=disable. Vercel se conecta direto via TCP. Senha trafega em texto claro pela internet em cada query.
Mitigação pendente: TLS no Postgres direto (cert + key + pg_hba) OU PgBouncer/nginx stream TLS termination.
3. Better Auth manda camelCase mas Pydantic exige snake_case
Adapter (vek1/src/lib/auth-http-adapter.ts) converte com snakeKeys() na ida e camelKeys() na volta. Se adicionar campo novo num modelo Better Auth e esquecer de mapear, 422 silencioso. Sempre testar fluxo end-to-end ao alterar shape de user/session/account/verification.
4. findOne(verification) vs findMany(verification) no reset-password
Better Auth chama findMany (não findOne) pra lookup de verification token no reset-password (e6c6ff0). Mapeamento findManyRows(verification) cobre identifier E value. Se Better Auth adicionar outro use case, validar.
5. Race PKCE em consumeOne(verification)
Find + delete tradicional tem janela onde 2 tabs paralelas consomem o mesmo token. Resolvido via DELETE..RETURNING atomic single round-trip:
def consume_verification(identifier: str, value: str) -> dict | None:
return db.fetch_one("""
DELETE FROM verification
WHERE identifier = %s AND value = %s
RETURNING *
""", (identifier, value))
Não voltar pro padrão find+delete sem entender isso.
6. findAccountsByUser precisa retornar password hash
Better Auth updatePassword (e login) chama findAccountsByUser e espera password field no row. Pydantic response schema deve incluir password (resolvido em PR #23 94e80b1). Se filtrar password por "segurança", quebra login silenciosamente.
7. updateMany(account) precisa de filtro manual no adapter
vek1-api recebe update por id; Better Auth chama com where=[userId, providerId='credential']. Adapter no vek1 resolve via findManyRows (que retorna TODOS accounts por userId) + filtro manual por providerId (30e1afa). Se adicionar outro provider de account (Google OAuth, etc.), validar.
8. Auth cache compara expires_at como naive UTC
974fafc — sessão expirava cedo demais porque expires_at da row vinha com timezone (datetime aware), e comparação contra datetime.utcnow() (naive) lançava TypeError. Resolvido convertendo ambos pra naive UTC. Cuidado se adicionar campo de timestamp novo no cache.
9. LLM hallucina product_id como slug
sales_assistant às vezes inventa product_id baseado em slug do produto. PR #4 f077988 valida items contra products table no create_order e DROPA fake product_ids (FK error 23503 antes desse fix). Order pode terminar sem nenhum item se LLM hallucinou tudo. Prompt foi reforçado (b04300d) mas não 100%.
10. LLM pula create_order mesmo após confirmação do cliente
Tendência LLM declarar "pedido criado" sem executar tool. Prompt tem "🚨 LEI CRÍTICA" explícita (config/agents_config.yaml):
"Você NUNCA pode escrever uma mensagem do tipo 'pedido criado', 'pedido registrado', ... SEM ter chamado a ferramenta
create_orderna MESMA resposta."
tests/test_function_calling.py valida isso (integration real DeepSeek). Reduzido a ocorrência raro mas pode acontecer em prompts ambíguos.
11. Function calling loop pode atingir limite 5 iterações
LLM pode encadear search → details → filter → search again → ... sem decidir. max_iterations=5 corta; resposta truncada chega no user. Logs mostram function_calls: [...] no metadata pra diagnose.
12. Skip persistência em messages_history se lead_id present
vek1 já grava — dd2ac9f removeu dupla escrita. Mas se chamar /chat direto sem passar lead_id (ex: webhook teste, integration test), vek1-api grava. Não esquecer dessa diferença ao debugar.
13. Cache auth não invalida cross-process
auth_service cache é singleton dict no processo. Se escalar horizontal (multi-replica vek1-api), cache fica inconsistente — login em replica A não vê session criada em replica B até TTL expirar. Hoje single replica, ok.
14. Lookup by-evolution-instance precisa de company_id
PR #17 4bbb465 — webhook handler do vek1 chama /webhooks/agents/by-evolution-instance/{instance_id} e precisa do company_id no retorno pra escrever em audit_log com actor_user_id correto. Antes não retornava. Cuidado se refatorar o endpoint.
15. Slowapi rate limit é per-IP in-memory
- Não distribuído (single replica)
- Reset em restart do container
- Atrás de nginx, IP é extraído via
X-Forwarded-For(get_remote_address). Garantir nginx envia o header correto.
16. GZip pode interferir em streaming
GZipMiddleware buffera response inteira antes de comprimir. Não usa Streaming Response (SSE etc.) — não temos hoje no vek1-api (SSE vive no vek1 com Postgres LISTEN/NOTIFY direto).
17. large_file_middleware só pra /process-file
Aumenta limit de body pra 50MB nesse endpoint. Outros endpoints usam default FastAPI (~1MB). Se adicionar endpoint que aceita upload, lembrar de adicionar exception.
18. slowapi import opcional pode mascarar erro real
try: import slowapi ... except ImportError: _HAS_SLOWAPI = False — se slowapi instalou mas falhou por outro motivo (versão errada, dep missing), log fala "não instalado" misleading. Verificar deps com pip list | grep slowapi.
19. agents_config.yaml volume mount no compose
Path no compose: ./config:/app/config. Mudanças no arquivo são vistas pelo container sem rebuild. Mas precisa POST /agents/reload pra recarregar (ou restart). Não esquecer.
20. Networks compose precisam de vault-site_default external
Pra alcançar vault-ollama, vek1-api precisa estar na rede vault-site_default (criada pelo compose do vault site). Se rebuild full sem essa rede existir, container starta mas Ollama unreachable → /embed retorna 500. Checar docker network ls.