CodeGym /Cursos /ChatGPT Apps /Producción y seguridad: permisos, sandbox, secretos, moni...

Producción y seguridad: permisos, sandbox, secretos, monitorización

ChatGPT Apps
Nivel 12 , Lección 4
Disponible

1. Por qué un agente necesita «mentalidad de producción»

Cuando escribes un backend normal, la propia idea de «salir a producción» activa automáticamente el modo paranoia: autorización, registro de logs, manejo de errores, límites, secretos en .env y no en el código.

Con un agente hay que activar el mismo modo, solo que aún más estricto. La razón es sencilla: un backend normal hace exactamente lo que escribiste, mientras que un agente hace lo que el modelo decide por sí mismo dentro de las herramientas e instrucciones disponibles. La ilusión de control es más fuerte que en el código clásico: parece que el prompt lo describe todo, pero en realidad solo controlas el entorno y las acciones disponibles, no todos los “pensamientos” del modelo.

Por eso, en esta lección iremos rodeando a nuestro agente con «capas de defensa» paso a paso:

  • primero limitaremos lo que puede hacer exactamente (permisos de herramientas y separación de agentes),
  • luego aislaremos el entorno de ejecución (sandbox y límites),
  • ordenaremos el manejo de secretos y PII,
  • y por último, activaremos la observabilidad: logs, métricas y trazado básico.

Para que sea concreto, continuaremos con la historia de nuestro GiftGenius: un agente que ayuda a elegir un regalo y entra un poco en el mundo del comercio (mediante pedido y checkout, pero aún sin detalles de ACP — eso vendrá más tarde).

2. Permisos: el agente no necesita «todos los botones del mundo»

Principio de mínimo privilegio (Least Privilege)

Primera regla: el agente no necesita saber hacerlo todo. Cuantas más herramientas tenga, mayor es la probabilidad de que invoque «la función equivocada» en «el momento equivocado». En lugar de un monstruoso manageEverything() que lee y escribe cualquier cosa, diseñamos funciones pequeñas y claras, separadas al menos en lectura y escritura.

Para GiftGenius esto es especialmente evidente: una cosa es leer la lista de regalos y las preferencias del usuario; otra, crear o confirmar un pedido (ahí ya hay dinero). Por ello, normalmente hacemos:

  • un conjunto de herramientas «read‑only» seguras (búsqueda de regalos, ver detalles),
  • herramientas «write» separadas (crear borrador de pedido, cancelar pedido),
  • y, si hace falta, otro nivel para operaciones especialmente peligrosas (confirmación de pago, cambios masivos).

Agentes distintos para tareas distintas

Otro enfoque potente es separar los agentes por áreas de responsabilidad. Un agente para «selección de regalos», otro para «gestión de pedidos». Así, incluso si el modelo del agente de regalos «se descarrila» un poco, físicamente no podrá invocar la herramienta de pago, porque simplemente no está en su configuración.

Imaginemos un tipo minimalista de configuración de agente e instrumentos:


// Tipos simplificados para explicar las ideas
type ToolName = 'suggest_gifts' | 'get_gift_details' |
  'create_order_draft' | 'confirm_order';

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

Ahora definamos dos agentes de 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,
};

Sí, esto es todavía una abstracción, pero la idea es simple: aunque el código tenga las cuatro herramientas, cada agente concreto recibe solo el subconjunto necesario.

Vincular permisos a usuario y roles

Es importante recordar que tenemos dos entidades distintas:

  • el usuario y sus permisos (si este user_id puede comprar algo, cancelar, ver el historial),
  • el agente y sus herramientas permitidas.

En el ideal, cada invocación de herramienta debería pasar dos comprobaciones: «¿el agente tiene permiso?» y «¿el usuario también tiene permiso?».

De forma aproximada:

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; // permitimos lectura a todos
}

En el lado del MCP/backend, al procesar una invocación de herramienta podemos hacer la doble comprobación:

function assertToolAllowed(
  agent: AgentConfig,
  userRole: UserRole,
  tool: ToolName,
) {
  if (!agent.allowedTools.includes(tool)) {
    throw new Error(`La herramienta ${tool} no está permitida para el agente ${agent.id}`);
  }
  if (!canUserCallTool(userRole, tool)) {
    throw new Error(`El usuario con el rol ${userRole} no puede invocar ${tool}`);
  }
}

Como resultado, incluso si el modelo decide invocar confirm_order desde el agente equivocado o en nombre de un invitado, la llamada chocará con esta comprobación y se convertirá en un error controlado, no en un pago no planificado.

Configuraciones distintas por entorno

En los entornos de dev y staging a menudo quieres dar más libertad al agente: herramientas de prueba, servicios de pago falsos, funciones experimentales. En producción, al contrario, la configuración es lo más estricta posible: parte de las herramientas desactivadas, endpoints solo de producción, tokens reales.

Esquema sencillo:

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 solo mediante una ruta separada
    maxSteps: 4,
  },
};

export const currentOrderAgent = orderAgentByEnv[env];

En producción, confirm_order puede estar sacado a un agente «peligroso» separado, al que llamas solo tras el clic explícito de «Confirmar pedido» en el widget y comprobaciones adicionales.

3. Sandbox: el agente no necesita acceso root a tu universo

Niveles de aislamiento

Después de establecer permisos para agentes y usuarios, pasamos al siguiente nivel de defensa: sandbox y aislamiento del entorno de ejecución.

El sandbox para el agente y sus herramientas puede dividirse, de forma aproximada, en varios niveles:

  1. Nivel del código de las herramientas. Limitamos el acceso al sistema de archivos, la red y los recursos del proceso: no permitimos escribir en cualquier parte, acceder a dominios arbitrarios, girar indefinidamente en CPU o consumir gigabytes de memoria.
  2. Nivel del Agents SDK. Establecemos límites por pasos del ciclo de ejecución, por número de invocaciones de herramientas y por tamaño del contexto (límite de tokens). El modelo no puede «pensar» indefinidamente ni generar tool‑calls sin fin: en algún momento la ejecución termina con «límite de pasos» o «límite de tiempo».

Todo esto se combina en una «arquitectura defensiva» clásica, que es útil representar con un esquema.

graph TD
    A[Prompt / instrucciones de sistema] --> B[JSON Schema de herramientas]
    B --> C[Permisos del agente y del usuario]
    C --> D[Sandbox de la infraestructura]
    D --> E[Servicios externos / BD]

    subgraph Agente
      A
      B
      C
    end

    subgraph Infraestructura
      D
    end

El prompt es la defensa más débil; la verdadera fuerza empieza donde limitas físicamente lo que puede hacer tu código y a qué API puede acceder.

Límites del ciclo de ejecución: pasos, tiempo, tool‑calls

Parte del sandbox puede expresarse directamente en la configuración del agente: número máximo de pasos, tiempo total de ejecución, límite de invocaciones de herramientas. Esto no solo protege de ciclos desbocados, sino que también controla costes.

Ejemplo de configuración abstracta de opciones de ejecución:

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

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

Luego pasas estos límites a la envoltura que lanza al agente. Si el modelo decide invocar una herramienta por 11.ª vez, interrumpes la ejecución y dices al usuario que la tarea es demasiado compleja, en lugar de dejar que el agente queme el presupuesto sin control.

Aislamiento del código y de la red

Al nivel del contenedor/proceso, las prácticas habituales son:

El código del servidor MCP y/o del servicio del agente se ejecuta en un contenedor con sistema de archivos de solo lectura (salvo un directorio de trabajo designado) y recursos limitados (CPU, RAM). La red está configurada con lista de permitidos: solo se puede acceder a los servicios externos necesarios (tu backend de comercio, el procesador de pagos, un par de API externas), no a Internet arbitrario.

Para escenarios con agentes esto es especialmente crítico: el modelo puede intentar salir a alguna API «extraña» o leer archivos inesperados; y es deseable que, incluso si lo intenta, físicamente no tenga permisos para alcanzar recursos de más.

En el código esto normalmente no es «una línea mágica de TypeScript», sino la configuración de tu orquestador (Docker Compose, Kubernetes, Vercel, Fly.io, etc.). Pero a futuro es útil pensar en ello ya en la fase de diseño:

  • la herramienta que ejecuta código de terceros (por ejemplo, generación de informes con comandos de shell) debe funcionar en un entorno separado y fuertemente aislado;
  • las herramientas no deben poder leer archivos ajenos, secretos, configuraciones;
  • el acceso de red es mejor limitarlo explícitamente por dominios o IP.

4. Secretos y datos confidenciales: lo que el agente no necesita saber

Dónde deben vivir los secretos y dónde no

Regla básica: ningún secreto — claves de API, contraseñas, tokens de acceso — debe llegar al prompt del modelo, al widget, a los logs ni al repositorio. Viven:

  • en variables de entorno (process.env.SOMETHING),
  • en un gestor de secretos (AWS Secrets Manager, GCP Secret Manager, Vault, etc.),
  • en stores cifrados separados con acceso estrictamente controlado.

En nuestro GiftGenius, por ejemplo, hay una clave para el API de comercio de la tienda. Necesitamos que el agente pueda crear un borrador de pedido mediante una herramienta MCP, pero el modelo no debe ver la clave.

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

export async function createOrderDraft(args: {
  userId: string;
  giftId: string;
  quantity: number;
}) {
  // El modelo nunca verá COMMERCE_API_KEY: solo existe aquí, en el 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(); // Al agente le devolveremos ya un objeto seguro
}

Importante: en la respuesta de la herramienta no debes «arrastrar» claves u otros detalles sensibles. Al agente le basta con saber el draftOrderId, la lista de posiciones y, quizá, el estado.

PII y minimización de datos en el contexto

Además de los secretos, existe la categoría PII (datos personales de usuarios): nombres, teléfonos, direcciones de envío, emails, etc. El agente a menudo no necesita todo ese texto «en crudo». Basta con un perfil estructurado: «le gustan los juegos de mesa», «edad 30–35», «presupuesto aproximado 50–70 $».

En lugar de meter en el prompt el historial completo de pedidos del usuario, puedes crear la herramienta get_user_profile_summary, que devolverá un perfil ya agregado y 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> {
  // Aquí consultas la BD, pero hacia fuera solo expones información agregada
  return {
    ageRange: '26-35',
    interests: ['juegos de mesa', 'gadgets'],
    preferredBudget: { min: 30, max: 80 },
  };
}

El modelo ve exactamente lo necesario para elegir un regalo, y nada más.

Scrubbing de logs

Los logs son el lugar natural donde los secretos y la PII pueden aflorar por accidente. Especialmente si escribes un logger «cómodo» tipo console.log(...) e imprimes «todo».

Un buen enfoque es tener un logger central que, antes de imprimir, recorra la carga útil y enmascare los campos sensibles.

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 }));
}

Así, en lugar de acabar con un incidente en producción tipo «llevamos medio año registrando teléfonos y tokens de clientes», construyes el sistema desde el principio para que eso sea simplemente imposible. No es solo una cuestión de cuidado, sino de futuros requisitos de compliance (GDPR y leyes locales): cuanta menos PII en los logs, más fácil la vida del producto.

5. Monitorización y observabilidad del agente

Qué exactamente hay que ver

Hemos limitado lo que el agente puede hacer, qué datos ve y qué llega a los logs. La siguiente pregunta es: ¿cómo saber que, en todo este «zoológico», el agente en producción se comporta como pensamos?

El monitor estándar «servicio vivo/no vivo» es casi inútil para un agente. No solo importa saber que el proceso está vivo, sino entender su comportamiento: qué pasos hace, qué herramientas invoca, dónde falla, dónde se entra en bucle.

Conjunto mínimo de datos por cada ejecución (run):

  • agent_run_id: identificador único de la ejecución;
  • user_id anónimo o ID de sesión;
  • nombre del agente y entorno;
  • lista de tools invocadas: nombre, cantidad, tiempo total;
  • pasos del workflow y en qué paso nos detuvimos;
  • estado final: success, partial_success, failed, canceled, timeout, limits_exceeded.

Se puede modelar como estructura:

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;
};

Ejemplo de «envoltura» alrededor del lanzamiento del agente

Supongamos que tienes una función runAgent que encapsula la llamada real al Agents SDK. La envolvemos con monitorización:

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;
  }
}

Aquí, runAgent es una caja negra que puede implementarse mediante un Agents SDK real; nosotros mostramos cómo añadir observabilidad sin atarse a una API concreta.

Logs vs métricas vs trazado

Es útil distinguir tres niveles de observabilidad:

Nivel Qué es Ejemplo para el agente GiftGenius
Logs «Historias» sobre ejecuciones concretas AgentRunLog detallado con pasos y herramientas
Métricas Indicadores numéricos agregados p95 de duración de la ejecución, número medio de tool‑calls, error‑rate
Trazado Árbol/grafo de solicitudes y subsolicitudes Run → pasos → tool‑calls → llamadas a APIs externas (comercio, BD, etc.)

Las métricas sirven para responder «¿en general todo va bien?» (por ejemplo, error‑rate de la última hora). Los logs y el trazado permiten entender «¿por qué está mal aquí?» y reproducir una ejecución problemática concreta.

Un embrión de métricas puede implementarse sobre los logs: una tarea periódica agrega los eventos agent_run y calcula p95 de duraciones, número de errores, etc.

6. Cómo se ve todo esto en GiftGenius

Para que no parezca un conjunto de abstracciones, armemos el panorama para nuestra aplicación didáctica.

El agente gift-planner en el entorno de producción solo tiene herramientas seguras: selección de regalos y obtención de detalles. No ve ni pagos ni gestión de pedidos. Sus instrucciones de sistema indican que no debe prometer al usuario «yo lo pago por ti», sino como máximo preparar recomendaciones y, quizá, un borrador de lista de regalos.

El agente order-manager existe por separado y solo sabe trabajar con pedidos. En producción puede crear únicamente el borrador del pedido (create_order_draft), y la confirmación del pedido (confirm_order) la realiza una persona mediante un disparador explícito en el UI del widget, o solo está disponible en dev/staging. Sus herramientas usan secretos (claves del API de la tienda) exclusivamente en el backend, y en la respuesta solo exponen los campos necesarios.

Ambos agentes se lanzan mediante la envoltura runAgentWithLogging, que aplica límites y escribe logs con agent_run_id, userId, entorno y lista de herramientas. En los logs no hay emails ni teléfonos; estos campos se limpian previamente con el scrubber. El perfil del usuario se utiliza en forma anonimizada: rango de edad, intereses, presupuesto, pero no el texto completo del historial de compras.

La infraestructura en la que viven el servidor MCP y el servicio del agente está aislada: contenedores con sistema de archivos de solo lectura (salvo /tmp o un directorio asignado), límites de CPU/RAM, red con lista de dominios permitidos. Si el agente intenta invocar «algo extraño», simplemente no podrá llegar físicamente.

Si en algún momento ves un pico en la métrica «porcentaje de ejecuciones con estado limits_exceeded» o «número medio de tool‑calls > 10», entiendes que o el prompt se ha vuelto demasiado locuaz o alguna herramienta está fallando y obliga al agente a reiniciar pasos.

Esto ya es el comportamiento de un servicio maduro, no de un agente experimental de «que sea lo que sea».

7. Errores típicos al llevar agentes a producción

Todo lo que discutimos arriba es la «imagen correcta» de un agente en producción. En la práctica, lo que más aparece son tropiezos típicos. Reunamos una lista: si evitas al menos estos errores, el lanzamiento a producción será mucho más tranquilo.

Error n.º 1: al agente «se le permitió todo».
Escenario común: describiste un montón de herramientas MCP (búsqueda, modificación, borrado, pagos) y, al crear el agente, simplemente le diste toda la lista. Como resultado, el modelo puede invocar por accidente un borrado o un pago donde solo querías lectura. Se soluciona separando herramientas por roles y creando varios agentes más acotados, cada uno con su propio allowedTools.

Error n.º 2: comprobar permisos solo en el prompt.
A veces los desarrolladores escriben en las instrucciones de sistema: «nunca compres nada sin la confirmación del usuario» y con eso se quedan tranquilos. Pero el prompt es una protección débil, y los jail‑breaks y errores no desaparecen. Hacen falta comprobaciones reales en el backend: «el agente tiene esta herramienta permitida» y «el usuario tiene esta herramienta permitida», de lo contrario una generación descuidada puede llevar a acciones imprevistas.

Error n.º 3: secretos en prompts y logs.
A veces apetece «acelerar la integración» y meter la clave de API en el system‑prompt o pasarla en los argumentos de la herramienta, para que el propio agente llame a la API externa. Al final, la clave aparece en los logs del modelo y potencialmente en sistemas de terceros. Es un camino directo a fugas y a un baneo en la Store. Los secretos deben vivir solo en el lado del servidor, en variables de entorno o gestores de secretos, y nunca llegar al contexto del modelo.

Error n.º 4: logs «en crudo» sin scrubbing.
Durante la depuración es cómodo escribir console.log(...) y olvidarse. A los pocos meses resulta que en los logs hay direcciones de usuarios, teléfonos, números de pedidos con PII. Especialmente desagradable en el mundo del GDPR y otras regulaciones. Mejor crear un logger central desde el principio e implantar enmascaramiento automático de campos sensibles, incluso si crees que «solo registramos en dev».

Error n.º 5: ausencia de límites al comportamiento del agente.
Sin límites de pasos, tiempo y número de invocaciones de herramientas, el agente puede entrar en bucle: invocar una y otra vez la misma herramienta, intentar corregir infinitamente el mismo error, gastar muchos tokens y cargar APIs externas. En el mejor de los casos pagarás facturas gigantes por modelos; en el peor, tumbarás el backend y enfadarás a todos los usuarios. Los límites del ciclo de ejecución y valores razonables por defecto de timeouts son parte obligatoria de la configuración.

Error n.º 6: mezclar operaciones de lectura y escritura en una misma herramienta.
A veces se crean métodos «cómodos» como getOrCreateOrder, que crean un nuevo pedido si no existe. Para un backend clásico es un patrón aceptable, pero en el mundo de los agentes puede producir efectos secundarios inesperados: el modelo solo quería conocer el estado, y la herramienta creó algo. Es más seguro separar get_order_details y create_order_draft; así, incluso con invocaciones repetidas, las consecuencias son más controlables.

Error n.º 7: ignorar la observabilidad.
Muchos empiezan con «ya añadiremos logs y métricas después; ahora lo importante es que funcione». Los agentes sin monitorización son una caja negra: no sabes qué herramientas invocan, cuántos pasos hacen, dónde fallan. Cualquier queja de usuario se convierte en una investigación a oscuras. Es mucho más fácil definir desde el principio la estructura de logs (agent_run_id, tools, estado) y métricas básicas que intentar construirlo después sobre código caótico.

1
Cuestionario/control
Orquestación de agentes, nivel 12, lección 4
No disponible
Orquestación de agentes
Orquestación de agentes con Agents SDK
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION