CodeGym /Cursos /ChatGPT Apps /Produção e segurança: permissões, sandbox, segredos, moni...

Produção e segurança: permissões, sandbox, segredos, monitoramento

ChatGPT Apps
Nível 12 , Lição 4
Disponível

1. Por que um agente precisa de “mentalidade de produção”

Quando você escreve um backend comum, a própria ideia de “ir para produção” automaticamente ativa o modo paranoia: autorização, logging, tratamento de erros, limites, segredos em .env, e não no código.

Com um agente, você deve adotar o mesmo modo, só que ainda mais rígido. A razão é simples: um backend comum executa exatamente o que você escreveu, enquanto um agente executa o que o modelo decide por si fazer dentro dos instrumentos e instruções que lhe foram dados. A ilusão de controle aqui é mais forte do que no código clássico: parece que o prompt descreve tudo, mas na prática você controla apenas o ambiente e as ações disponíveis, não todos os “pensamentos” do modelo.

Por isso, nesta aula vamos cercar nosso agente com “camadas de proteção” gradualmente:

  • primeiro, limitamos o que exatamente ele pode fazer (permissões de ferramentas e separação de agentes),
  • depois isolamos o ambiente de execução (sandbox e limites),
  • colocamos ordem nos segredos e PII,
  • e, por fim, ativamos observabilidade: logs, métricas e tracing básico.

Para ser concreto, seguiremos com a nossa história do GiftGenius: um agente que ajuda a escolher presentes e toca um pouco o mundo de commerce (via pedido e checkout, mas ainda sem detalhes de ACP — isso virá depois).

2. Permissões: o agente não precisa de “todos os botões do mundo”

Princípio do mínimo privilégio (Least Privilege)

Primeira regra: o agente não precisa saber fazer tudo. Quanto mais ferramentas ele tiver, maior a chance de chamar a função “errada” no “momento errado”. Em vez de um monstro manageEverything() que lê e escreve qualquer coisa, projetamos funções pequenas e claras, pelo menos separadas entre leitura e escrita.

Para o GiftGenius isso fica especialmente claro: uma coisa é ler a lista de presentes e preferências do usuário, outra é criar ou confirmar um pedido (aí já envolve dinheiro). Por isso, normalmente fazemos:

  • um conjunto de ferramentas seguras “somente leitura” (busca de presentes, visualização de detalhes),
  • ferramentas “write” separadas (criar rascunho de pedido, cancelar pedido),
  • e, se necessário, mais um nível para operações especialmente perigosas (confirmação de pagamento, alterações em massa).

Agentes diferentes para tarefas diferentes

Outro recurso poderoso é separar agentes por áreas de responsabilidade. Um agente para “escolha de presentes”, outro para “gestão de pedidos”. Assim, mesmo que o modelo no agente de presentes “saia um pouco da linha”, ele fisicamente não conseguirá chamar a ferramenta de pagamento, porque ela simplesmente não existe na sua configuração.

Vamos imaginar um tipo minimalista de configuração de agente e ferramentas:


// Tipos simplificados para explicar as ideias
type ToolName = 'suggest_gifts' | 'get_gift_details' |
  'create_order_draft' | 'confirm_order';

type AgentConfig = {
  id: string;
  allowedTools: ToolName[];
  maxSteps: number;
};

Agora descrevemos dois agentes do GiftGenius:

export const giftPlannerAgent: AgentConfig = {
  id: 'gift-planner',
  allowedTools: ['suggest_gifts', 'get_gift_details'],
  maxSteps: 6,
};

export const orderAgent: AgentConfig = {
  id: 'order-manager',
  allowedTools: ['create_order_draft', 'confirm_order'],
  maxSteps: 4,
};

Sim, isso ainda é uma abstração, mas a essência é simples: mesmo que o código tenha todas as quatro ferramentas, um agente específico recebe apenas o subconjunto necessário.

Vinculando permissões a usuário e papéis

É importante lembrar que temos duas entidades diferentes:

  • o usuário e suas permissões (se esse user_id pode comprar algo, cancelar, ver histórico),
  • o agente e suas ferramentas permitidas.

O ideal é que cada chamada de ferramenta passe por duas verificações: “isso é permitido para o agente?” e “isso também é permitido para o usuário?”.

De forma simplificada:

type UserRole = 'guest' | 'customer' | 'admin';

function canUserCallTool(role: UserRole, tool: ToolName): boolean {
  if (tool === 'confirm_order') {
    return role === 'customer' || role === 'admin';
  }
  if (tool === 'create_order_draft') {
    return role !== 'guest';
  }
  return true; // leitura permitida a todos
}

No lado do MCP/backend, ao processar a chamada de uma ferramenta, podemos fazer a verificação dupla:

function assertToolAllowed(
  agent: AgentConfig,
  userRole: UserRole,
  tool: ToolName,
) {
  if (!agent.allowedTools.includes(tool)) {
    throw new Error(`A ferramenta ${tool} é proibida para o agente ${agent.id}`);
  }
  if (!canUserCallTool(userRole, tool)) {
    throw new Error(`O usuário com papel ${userRole} não pode chamar ${tool}`);
  }
}

No fim, mesmo que o modelo de repente decida chamar confirm_order a partir do agente errado ou em nome de um convidado — a chamada vai bater nessa verificação e virar um erro controlado, em vez de um pagamento não planejado.

Configurações diferentes por ambiente

Nos ambientes dev e staging, você frequentemente quer dar mais liberdade ao agente: ferramentas de teste, serviços de pagamento falsos, funcionalidades experimentais. Em produção, ao contrário, a configuração é o mais rígida possível: parte das ferramentas desativada, endpoints apenas de produção, tokens somente reais.

Esquema bem simples:

type Env = 'dev' | 'staging' | 'production';

const env = (process.env.APP_ENV as Env) ?? 'dev';

const orderAgentByEnv: Record<Env, AgentConfig> = {
  dev: {
    id: 'order-manager-dev',
    allowedTools: ['create_order_draft', 'confirm_order'],
    maxSteps: 8,
  },
  staging: {
    id: 'order-manager-staging',
    allowedTools: ['create_order_draft', 'confirm_order'],
    maxSteps: 6,
  },
  production: {
    id: 'order-manager-prod',
    allowedTools: ['create_order_draft'], // confirm apenas por um caminho separado
    maxSteps: 4,
  },
};

export const currentOrderAgent = orderAgentByEnv[env];

Em produção, confirm_order pode ser totalmente movido para um agente “perigoso” separado, que você chama apenas após o clique explícito em “Confirmar pedido” no widget e verificações adicionais.

3. Sandbox: o agente não precisa de acesso root ao seu universo

Níveis de isolamento

Depois de definirmos permissões para agentes e usuários, passamos ao próximo nível de proteção — sandbox e isolamento do ambiente de execução.

A sandbox para o agente e suas ferramentas pode ser dividida, grosso modo, em alguns níveis:

  1. Nível do código das ferramentas. Limitamos o acesso ao sistema de arquivos, rede e recursos do processo: não permitimos escrever em qualquer lugar, acessar domínios arbitrários, rodar infinitamente em CPU ou consumir gigabytes de memória.
  2. Nível do Agents SDK. Definimos limites de passos do ciclo de execução (run), quantidade de tool-calls e tamanho do contexto (token limit). O modelo não pode “pensar” indefinidamente e proliferar tool-calls — em algum momento, a execução termina com erro de “limite de passos” ou “limite de tempo”.

Tudo isso compõe uma “arquitetura defensiva” clássica, que é fácil representar com um esquema.

graph TD
    A[Prompt / instruções de sistema] --> B[JSON Schema das ferramentas]
    B --> C[Permissões do agente e do usuário]
    C --> D[Sandbox da infraestrutura]
    D --> E[Serviços externos / banco de dados]

    subgraph Agente
      A
      B
      C
    end

    subgraph Infraestrutura
      D
    end

O prompt é a proteção mais fraca; a força real começa quando você limita fisicamente o que seu código pode fazer e quais APIs estão disponíveis.

Limites do ciclo de execução (run): passos, tempo, tool-calls

Parte da sandbox pode ser expressa diretamente na configuração do agente: quantidade máxima de passos, tempo total de execução, limite de tool-calls. Isso não é só proteção contra ciclos sem fim, mas também controle de custo.

Exemplo de configuração abstrata de opções de execução:

type RunLimits = {
  maxSteps: number;
  maxToolCalls: number;
  timeoutMs: number;
};

const defaultLimits: RunLimits = {
  maxSteps: 8,
  maxToolCalls: 10,
  timeoutMs: 30_000,
};

Você então passa esses limites para o wrapper que inicia o agente. Se o modelo decidir fazer a 11ª chamada de ferramenta, você interrompe a execução do agente e informa honestamente ao usuário que a tarefa é complexa demais, em vez de deixar o agente queimar o orçamento sem controle.

Isolamento de código e rede

No nível de contêiner/processo, as práticas comuns são:

O código do servidor MCP e/ou do serviço do agente roda em um contêiner com sistema de arquivos read-only (exceto um diretório de trabalho dedicado) e recursos limitados (CPU, RAM). A rede é configurada por allow-list: é possível acessar apenas os serviços externos necessários (seu backend de commerce, o provedor de pagamentos, alguns APIs externos), e não a internet arbitrária.

Para cenários com agentes isso é especialmente crítico: o modelo pode tentar acessar algum API “aleatório” ou ler arquivos inesperados, e é ótimo se, mesmo nessas tentativas, ele fisicamente não tiver permissões para alcançar recursos desnecessários.

No código, isso geralmente não aparece como uma “linha mágica de TypeScript”, mas como configurações do seu orquestrador (Docker Compose, Kubernetes, Vercel, Fly.io etc.). Porém, é útil pensar nisso já na fase de projeto:

  • a ferramenta que executa código de terceiros (por exemplo, geração de relatório com comandos de shell) deve rodar em um ambiente separado e rigidamente isolado;
  • as ferramentas não devem ter possibilidade de ler arquivos alheios, segredos, configs;
  • o acesso de rede é melhor ser explicitamente restrito por domínios ou IPs.

4. Segredos e dados confidenciais: o que o agente não precisa saber

Onde os segredos devem viver — e onde não

Regra básica: nenhum segredo — chaves de API, senhas, tokens de acesso — deve ir para o prompt do modelo, para o widget, para os logs ou para o repositório. Eles vivem:

  • em variáveis de ambiente (process.env.SOMETHING),
  • em um gerenciador de segredos (AWS Secrets Manager, GCP Secret Manager, Vault etc.),
  • em stores separados e criptografados, cujo acesso é estritamente controlado.

No nosso GiftGenius, por exemplo, existe a chave do commerce API da loja. Precisamos que o agente possa criar um rascunho de pedido via ferramenta do MCP, mas o modelo não deve ver a chave.

// mcp/tools/createOrderDraft.ts
const COMMERCE_API_KEY = process.env.COMMERCE_API_KEY!;

export async function createOrderDraft(args: {
  userId: string;
  giftId: string;
  quantity: number;
}) {
  // O modelo nunca verá COMMERCE_API_KEY — ele está apenas aqui, no servidor
  const res = await fetch(`${process.env.COMMERCE_API_URL}/orders/draft`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${COMMERCE_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(args),
  });

  if (!res.ok) {
    throw new Error(`Commerce API returned ${res.status}`);
  }

  return res.json(); // Na resposta, retornaremos ao agente um objeto já seguro
}

Importante: na resposta da ferramenta você não deve “vazar” chaves ou outros detalhes sensíveis. Ao agente basta saber o draftOrderId, a lista de itens e, possivelmente, o status.

PII e minimização de dados no contexto

Além de segredos, existe a categoria PII (dados pessoais dos usuários): nomes, telefones, endereços de entrega, email etc. O agente muitas vezes não precisa de todo esse texto “cru”. É suficiente um perfil estruturado: “gosta de jogos de tabuleiro”, “idade 30–35”, “orçamento aproximado 50–70$”.

Em vez de colocar no prompt o histórico completo de pedidos do usuário, você pode fazer uma ferramenta get_user_profile_summary, que retorna um perfil já agregado e anonimizado.

type ProfileSummary = {
  ageRange: '18-25' | '26-35' | '36-50' | '50+';
  interests: string[];
  preferredBudget: { min: number; max: number };
};

export async function getUserProfileSummary(userId: string): Promise<ProfileSummary> {
  // Aqui você consulta o banco de dados, mas expõe apenas informação agregada
  return {
    ageRange: '26-35',
    interests: ['jogos de tabuleiro', 'gadgets'],
    preferredBudget: { min: 30, max: 80 },
  };
}

O modelo vê exatamente o necessário para escolher um presente — e nada além disso.

Scrubbing de logs

Logs são um lugar natural onde segredos e PII aparecem por acidente. Especialmente se você escrever um logger “conveniente” do tipo console.log(...) e imprimir “qualquer coisa”.

Uma boa abordagem é ter um logger central que, antes de imprimir, percorre a carga útil e mascara campos sensíveis.

type LogPayload = Record<string, unknown>;

const SENSITIVE_KEYS = ['email', 'phone', 'cardNumber', 'token'];

function scrub(payload: LogPayload): LogPayload {
  const result: LogPayload = {};
  for (const [key, value] of Object.entries(payload)) {
    if (SENSITIVE_KEYS.includes(key)) {
      result[key] = '***redacted***';
    } else {
      result[key] = value;
    }
  }
  return result;
}

export function logEvent(event: string, payload: LogPayload) {
  const safe = scrub(payload);
  console.log(JSON.stringify({ event, ...safe }));
}

Agora, em vez de um dia lidar com um incidente de produção “estamos logando telefones e tokens de clientes há meio ano”, você constrói o sistema desde o início para que isso seja simplesmente impossível. Isso não é apenas uma questão de cuidado, mas também de requisitos futuros de compliance (GDPR e leis locais): quanto menos PII nos logs, mais simples é a vida do produto.

5. Monitoramento e observabilidade do agente

O que exatamente precisamos enxergar

Limitamos o que o agente pode fazer, quais dados ele vê e o que entra nos logs. A próxima pergunta é — como entender se, nesse “zoológico”, o agente em produção se comporta como planejado?

Monitoramento comum “serviço vivo / não vivo” é quase inútil para um agente. Não basta saber que o processo está de pé; precisamos entender o comportamento: quais passos ele executa, quais ferramentas chama, onde erra, onde entra em loop.

Conjunto mínimo de dados por execução (run):

  • agent_run_id — identificador único da execução;
  • user_id anônimo ou ID de sessão;
  • nome do agente e ambiente;
  • lista de ferramentas chamadas: nome, quantidade, tempo total;
  • etapas do workflow e em qual etapa paramos;
  • status final: success, partial_success, failed, canceled, timeout, limits_exceeded.

Isso pode ser modelado como uma estrutura:

type RunStatus =
  | 'success'
  | 'partial_success'
  | 'failed'
  | 'canceled'
  | 'timeout'
  | 'limits_exceeded';

type ToolCallLog = {
  name: ToolName;
  durationMs: number;
  success: boolean;
};

type AgentRunLog = {
  runId: string;
  agentId: string;
  userId: string;
  env: Env;
  startedAt: string;
  finishedAt: string;
  status: RunStatus;
  toolCalls: ToolCallLog[];
  errorMessage?: string;
};

Exemplo de “wrapper” em torno da execução do agente

Suponha que você tenha uma função runAgent que encapsula a chamada real do Agents SDK. Vamos envolvê-la com monitoramento:

async function runAgentWithLogging(
  agent: AgentConfig,
  input: string,
  userId: string,
): Promise<string> {
  const runId = crypto.randomUUID();
  const startedAt = new Date();

  const toolCalls: ToolCallLog[] = [];

  try {
    const result = await runAgent(agent, input, {
      userId,
      limits: defaultLimits,
      onToolCall: (name, durationMs, success) => {
        toolCalls.push({ name, durationMs, success });
      },
    });

    const finishedAt = new Date();

    const log: AgentRunLog = {
      runId,
      agentId: agent.id,
      userId,
      env,
      startedAt: startedAt.toISOString(),
      finishedAt: finishedAt.toISOString(),
      status: 'success',
      toolCalls,
    };

    logEvent('agent_run', log);
    return result;
  } catch (err) {
    const finishedAt = new Date();
    const log: AgentRunLog = {
      runId,
      agentId: agent.id,
      userId,
      env,
      startedAt: startedAt.toISOString(),
      finishedAt: finishedAt.toISOString(),
      status: 'failed',
      toolCalls,
      errorMessage: (err as Error).message,
    };
    logEvent('agent_run', log);
    throw err;
  }
}

Aqui, runAgent é uma caixa-preta que pode ser implementada via um Agents SDK real; mostramos como adicionar observabilidade sem se prender a uma API específica.

Logs vs métricas vs tracing

É útil distinguir três níveis de observabilidade:

Nível O que é Exemplo para o agente GiftGenius
Logs “Histórias” sobre execuções (runs) específicas AgentRunLog detalhado com etapas e ferramentas
Métricas Indicadores numéricos agregados duração p95 do run, número médio de tool-calls, taxa de erro
Tracing Árvore / grafo de requisições e subrequisições Run → etapas → tool-calls → chamadas a APIs externas (commerce, banco de dados etc.)

Métricas servem para responder “está tudo bem no geral?” (por exemplo, taxa de erro na última hora). Logs e tracing servem para entender “por que está ruim aqui?” e reproduzir uma execução problemática específica.

Um germe de métricas pode ser implementado sobre os logs: uma tarefa periódica agrega eventos agent_run e calcula p95 de duração, quantidade de erros etc.

6. Como isso fica no GiftGenius de ponta a ponta

Para que tudo não pareça um conjunto de abstrações, vamos juntar a figura para o nosso app didático.

O agente gift-planner, no ambiente de produção, tem apenas ferramentas seguras: escolha de presentes e obtenção de detalhes. Ele não vê pagamentos nem gestão de pedidos. Suas instruções de sistema dizem que ele não deve prometer ao usuário “eu pago tudo por você”, mas no máximo preparar recomendações e, talvez, um rascunho de lista de presentes.

O agente order-manager existe separadamente e sabe apenas trabalhar com pedidos. Em produção ele pode criar apenas um rascunho de pedido (create_order_draft), e a confirmação do pedido (confirm_order) ou é feita por uma pessoa via um gatilho explícito de UI no widget, ou está disponível apenas em dev/staging. Suas ferramentas usam segredos (chaves da loja) exclusivamente no backend, e retornam apenas os campos necessários.

Ambos os agentes são executados por um wrapper runAgentWithLogging, que aplica limites e grava logs com agent_run_id, userId, ambiente e lista de ferramentas. Não há email nem telefones nos logs; esses campos são limpos previamente pelo scrubber. O perfil do usuário é usado de forma anonimizada: faixa etária, interesses, orçamento — e não o texto completo do histórico de compras.

A infraestrutura onde vivem o servidor MCP e o serviço do agente é isolada: contêineres com sistema de arquivos read-only (exceto /tmp ou um diretório dedicado), limites de CPU/RAM e rede por allow-list de domínios. Se o agente tentar acessar “algo aleatório”, ele simplesmente não conseguirá chegar até lá fisicamente.

Se em algum momento você vir um pico na métrica “fração de runs com status limits_exceeded” ou “número médio de tool-calls > 10”, você entende que ou o prompt ficou verborrágico demais ou alguma ferramenta está com bug e força o agente a reiniciar etapas.

Isso já é o comportamento de um serviço maduro, e não de um agente experimental “vai que dá”.

7. Erros típicos ao levar agentes para produção

Tudo o que discutimos acima é a “visão correta” de um agente de produção. Na prática, porém, aparecem armadilhas comuns. Vamos juntá-las em uma lista: se você evitar ao menos estes erros, o deploy para produção será bem mais tranquilo.

Erro nº 1: você “permitiu tudo” ao agente.
Cenário comum: você descreveu um monte de ferramentas MCP (busca, alteração, exclusão, pagamentos) e, ao criar o agente, simplesmente forneceu toda a lista. Como resultado, o modelo pode chamar exclusão ou pagamento por engano onde você queria apenas leitura. Resolve-se separando ferramentas por papéis e criando vários agentes mais focados, cada um com seu allowedTools.

Erro nº 2: verificar permissões apenas no prompt.
Às vezes os desenvolvedores escrevem nas instruções de sistema: “nunca compre nada sem confirmação do usuário” e se dão por satisfeitos. Mas o prompt é uma proteção fraca — jailbreaks e erros acontecem. São necessárias verificações reais no backend: “essa ferramenta é permitida para o agente” e “essa ferramenta é permitida para o usuário”, caso contrário, uma geração descuidada pode levar a ações imprevistas.

Erro nº 3: segredos em prompts e logs.
Às vezes dá vontade de “acelerar a integração” e colocar a chave de API no system prompt ou passá-la nos argumentos da ferramenta, para o agente chamar diretamente um API externo. No fim, a chave aparece nos logs do modelo e potencialmente em sistemas de terceiros. É caminho direto para vazamentos e banimento na Store. Segredos devem viver apenas no lado do servidor, em variáveis de ambiente ou gerenciadores de segredos, e nunca entrar no contexto do modelo.

Erro nº 4: logs “crus” sem scrubbing.
Durante a depuração é conveniente escrever console.log(...) e esquecer. Depois de alguns meses, descobre-se que há endereços de usuários, telefones e números de pedidos com PII nos logs. Especialmente ruim no mundo de GDPR e outras regulações. É melhor adotar desde cedo um logger central e mascaramento automático de campos sensíveis, mesmo que pareça que “logamos apenas no dev”.

Erro nº 5: ausência de limites no comportamento do agente.
Sem limites de passos, tempo e quantidade de tool-calls, o agente pode entrar em loop: chamar repetidamente a mesma ferramenta, tentar corrigir infinitamente o mesmo erro, gastar um monte de tokens e sobrecarregar APIs externas. No melhor caso, você terá contas gigantes com modelos; no pior, vai derrubar o backend e irritar todos os usuários. Limites no ciclo de execução e padrões razoáveis de timeouts são parte obrigatória da configuração.

Erro nº 6: misturar operações de leitura e escrita na mesma ferramenta.
Às vezes criam métodos “práticos”, como getOrCreateOrder, que, na ausência do pedido, cria um novo. Para um backend clássico isso pode ser aceitável, mas no mundo de agentes pode gerar efeitos colaterais inesperados: o modelo queria apenas saber o estado, e a ferramenta criou algo. É muito mais seguro separar get_order_details e create_order_draft; assim, mesmo com chamadas repetidas, as consequências são mais controláveis.

Erro nº 7: ignorar observabilidade.
Muitos começam com “depois colocamos logs e métricas, agora o importante é funcionar”. Agentes sem monitoramento são uma caixa-preta: você não sabe que ferramentas eles chamam, quantos passos executam, onde erram. Qualquer reclamação do usuário vira uma investigação no escuro. É muito mais fácil projetar desde já a estrutura de logs (agent_run_id, ferramentas, status) e métricas básicas do que tentar construir isso em cima de um código caótico depois.

1
Pesquisa/teste
Orquestração de agentes, nível 12, lição 4
Indisponível
Orquestração de agentes
Orquestração de agentes com o Agents SDK
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION