JavaRush /Курсы /ChatGPT Apps /Продакшен и безопасность: права, sandbox, секреты, монито...

Продакшен и безопасность: права, sandbox, секреты, мониторинг

ChatGPT Apps
12 уровень , 4 лекция
Открыта

1. Зачем агенту «продакшен‑мышление»

Когда вы пишете обычный backend, сама идея «выйти в прод» автоматически включает режим паранойи: авторизация, логирование, обработка ошибок, лимиты, секреты в .env, а не в коде.

С агентом нужно включить тот же режим, только ещё более жёсткий. Причина простая: обычный backend выполняет ровно то, что вы написали, а агент — то, что модель сама решила сделать в рамках заданных ей инструментов и инструкций. Иллюзия контроля здесь сильнее, чем в классическом коде: кажется, что промпт всё описывает, но на самом деле вы контролируете только окружение и доступные действия, а не все мысли модели.

Поэтому в этой лекции мы будем постепенно обкладывать нашего агента «слоями защиты»:

  • сначала ограничим, что именно он может делать (права инструментов и разделение агентов),
  • потом изолируем среду выполнения (sandbox и лимиты),
  • наведём порядок с секретами и PII,
  • и наконец, включим наблюдаемость: логи, метрики и базовый трейсинг.

Чтобы было предметно, будем продолжать историю с нашим GiftGenius: агент, который помогает подобрать подарок и чуть-чуть залезает в commerce‑мир (через заказ и checkout, но пока без подробностей ACP — это будет позже).

2. Права: агенту не нужны «все кнопки мира»

Принцип наименьших привилегий (Least Privilege)

Первое правило: агенту не нужно уметь всё. Чем больше у него инструментов, тем выше шанс, что он вызовет «не ту» функцию в «не тот» момент. Вместо одного монструозного manageEverything(), который читает и пишет что угодно, мы проектируем мелкие, чёткие функции, разделённые хотя бы на чтение и запись.

Для GiftGenius это особенно ясно: одно дело — читать список подарков и предпочтения пользователя, другое — создавать или подтверждать заказ (там уже деньги). Поэтому мы обычно делаем:

  • набор безопасных «read‑only» инструментов (поиск подарков, просмотр деталей),
  • отдельные «write» инструменты (создание черновика заказа, отмена заказа),
  • и, если нужно, ещё один уровень для особо опасных операций (подтверждение платежа, массовые изменения).

Разные агенты под разные задачи

Ещё один мощный приём — разделять агентов по зонам ответственности. Один агент — «подбор подарков», другой — «управление заказами». Тогда даже если модель в gift‑агенте немного «съедет с катушек», она физически не сможет вызвать платежный инструмент, потому что его просто нет в её конфигурации.

Представим минималистичный тип конфигурации агента и инструментов:


// Упрощённые типы для объяснения идей
type ToolName = 'suggest_gifts' | 'get_gift_details' |
  'create_order_draft' | 'confirm_order';

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

Теперь опишем два агента 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,
};

Да, это пока абстракция, но суть проста: даже если в коде есть все четыре инструмента, конкретный агент получает только нужное подмножество.

Привязка прав к пользователю и ролям

Важно помнить, что у нас есть две разные сущности:

  • пользователь и его права (можно ли этому user_id вообще что-то покупать, отменять, видеть историю),
  • агент и его разрешённые инструменты.

В идеале каждый вызов инструмента должен проходить обе проверки: «агенту это разрешено?» и «пользователю это тоже разрешено?».

Условно:

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; // чтение разрешаем всем
}

На стороне MCP/бэкенда при обработке tool‑вызова мы можем делать двойную проверку:

function assertToolAllowed(
  agent: AgentConfig,
  userRole: UserRole,
  tool: ToolName,
) {
  if (!agent.allowedTools.includes(tool)) {
    throw new Error(`Tool ${tool} запрещён для агента ${agent.id}`);
  }
  if (!canUserCallTool(userRole, tool)) {
    throw new Error(`Пользователь с ролью ${userRole} не может вызывать ${tool}`);
  }
}

В итоге даже если модель вдруг решит вызвать confirm_order из неправильного агента или от имени гостя — вызов упрётся в эту проверку и превратится в управляемую ошибку, а не в незапланированный платёж.

Разные конфигурации по окружениям

В dev и staging окружениях вы часто хотите дать агенту больше свободы: тестовые инструменты, фейковые платёжные сервисы, экспериментальные функции. В production, наоборот, конфигурация максимально жёсткая: часть tools отключена, endpoints — только боевые, токены — только реальные.

Простейшая схема:

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 только через отдельный путь
    maxSteps: 4,
  },
};

export const currentOrderAgent = orderAgentByEnv[env];

В проде confirm_order может быть вообще вынесен в отдельный «опасный» агент, который вы вызываете только после явного клика «Подтвердить заказ» в виджете и дополнительных проверок.

3. Sandbox: агенту не нужен root‑доступ к вашей вселенной

Уровни изоляции

После того как мы задали права для агентов и пользователей, переходим к следующему уровню защиты — sandbox и изоляции среды выполнения.

Sandbox для агента и его инструментов можно условно поделить на несколько уровней:

  1. Уровень кода инструментов. Мы ограничиваем доступ к файловой системе, сети и ресурсам процесса: не даём писать куда попало, ходить в произвольные домены, бесконечно крутиться в CPU или кушать гигабайты памяти.
  2. Уровень Agents SDK. Мы задаём лимиты по шагам run‑цикла, по количеству tool‑вызовов и по размеру контекста (token limit). Модель не может бесконечно «думать» и плодить tool‑calls — в какой-то момент run завершится с ошибкой «лимит шагов» или «лимит времени».

Всё это складывается в классическую «оборонительную архитектуру», которую удобно представить схемой.

graph TD
    A[Промпт / system‑инструкции] --> B[JSON Schema инструментов]
    B --> C[Пермиссии агента и пользователя]
    C --> D[Sandbox инфраструктуры]
    D --> E[Внешние сервисы / БД]

    subgraph Агент
      A
      B
      C
    end

    subgraph Инфраструктура
      D
    end

Промпт — самая слабая защита; настоящая сила начинается там, где вы физически ограничиваете, что может сделать ваш код и какие API доступны.

Лимиты на run‑цикл: шаги, время, tool‑calls

Часть sandbox можно выразить прямо в конфигурации агента: максимальное количество шагов, общее время выполнения, лимит tool‑вызовов. Это не только защита от runaway‑циклов, но и контроль затрат.

Пример абстрактной конфигурации run‑опций:

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

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

Такие лимиты вы потом передаёте в обёртку, которая запускает агента. Если вдруг модель решила 11‑й раз дернуть инструмент — вы прерываете запуск агента и честно говорите пользователю, что задача слишком сложная, а не даёте агенту бесконтрольно жечь бюджет.

Изоляция кода и сети

На уровне контейнера/процесса обычные практики такие:

Код MCP‑сервера и/или агентного сервиса запускается в контейнере с read‑only файловой системой (кроме специально отведённого рабочего каталога) и ограниченными ресурсами (CPU, RAM). Сеть настроена по allow‑list: можно ходить только к нужным внешним сервисам (ваш commerce‑backend, платежка, пара внешних API), а не в произвольный интернет.

Для агентных сценариев это особенно критично: модель может попытаться выйти в какой‑нибудь «левый» API или прочитать неожиданные файлы, и хорошо, если даже при таких попытках у неё физически не будет прав добраться до лишних ресурсов.

В коде это обычно выглядит не как «магическая строчка TypeScript», а как настройки вашего оркестратора (Docker Compose, Kubernetes, Vercel, Fly.io и т.д.). Но в перспективе полезно думать об этом уже на этапе проектирования:

  • инструмент, который запускает сторонний код (например, генерацию отчёта с shell‑командами), должен работать в отдельном жёстко изолированном окружении;
  • инструменты не должны иметь возможность читать чужие файлы, секреты, конфиги;
  • сетевой доступ лучше явно ограничивать по доменам или IP.

4. Секреты и конфиденциальные данные: что агенту знать не нужно

Где секретам жить, а где не жить

Базовое правило: никакие секреты — API‑ключи, пароли, access‑токены — не должны попадать в промпт модели, в виджет, в логи и в репозиторий. Они живут:

  • в переменных окружения (process.env.SOMETHING),
  • в менеджере секретов (AWS Secrets Manager, GCP Secret Manager, Vault и т.п.),
  • в отдельных зашифрованных сторах, доступ к которым строго контролируется.

В нашем GiftGenius, например, есть ключ к commerce‑API магазина. Нам нужно, чтобы агент мог создавать черновик заказа через MCP‑инструмент, но сам ключ модель видеть не должна.

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

export async function createOrderDraft(args: {
  userId: string;
  giftId: string;
  quantity: number;
}) {
  // Модель никогда не увидит COMMERCE_API_KEY — он только здесь, на сервере
  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(); // В ответ агенту отдадим уже безопасный объект
}

Важно: в ответе инструмента вы не должны «протаскивать» ключи или другие чувствительные детали. Агенту достаточно знать draftOrderId, список позиций и, возможно, статус.

PII и минимизация данных в контексте

Кроме секретов есть ещё категория PII (персональные данные пользователей): имена, телефоны, адреса доставки, email и т.п. Агенту часто не нужен весь этот «сырой» текст. Достаточно структурированного профиля: «любит настольные игры», «возраст 30–35», «примерный бюджет 50–70$».

Вместо того чтобы кидать в prompt полную историю заказов пользователя, можно сделать tool get_user_profile_summary, который вернёт уже агрегированный и обезличенный профиль.

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> {
  // Здесь вы лезете в БД, но наружу отдаёте только агрегированную информацию
  return {
    ageRange: '26-35',
    interests: ['настолки', 'гаджеты'],
    preferredBudget: { min: 30, max: 80 },
  };
}

Модель видит ровно столько, сколько нужно для подбора подарка, и не больше.

Scrubbing логов

Логи — естественное место, где случайно всплывают секреты и PII. Особенно если написать «удобный» логгер вида console.log(...) и печатать «всё подряд».

Хороший подход — иметь центральный логгер, который перед печатью проходит по полезной нагрузке и маскирует чувствительные поля.

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

Теперь вместо того, чтобы когда‑нибудь ловить production‑инцидент «мы логируем телефоны и токены клиентов уже полгода», вы изначально строите систему так, что это просто невозможно. Это не только вопрос аккуратности, но и будущих требований комплаенса (GDPR и локальные законы): чем меньше PII в логах, тем проще жить продукту.

5. Мониторинг и наблюдаемость агента

Что именно нужно видеть

Мы ограничили, что агент может делать, какие данные он видит и что попадает в логи. Следующий вопрос — как понять, что во всём этом «зоопарке» агент в проде ведёт себя так, как мы задумали?

Обычный мониторинг «сервис жив / не жив» для агента почти бесполезен. Нам важно не только знать, что процесс жив, но и понимать его поведение: какие шаги он делает, какие инструменты вызывает, где ошибается, где зацикливается.

Минимальный набор данных по каждому run:

  • agent_run_id — уникальный идентификатор запуска;
  • анонимный user_id или сессионный ID;
  • имя агента и окружение;
  • список вызванных tools: имя, количество, суммарное время;
  • шаги workflow и на каком шаге мы остановились;
  • итоговый статус: success, partial_success, failed, canceled, timeout, limits_exceeded.

Можно оформить это как структуру:

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

Пример «обёртки» вокруг запуска агента

Предположим, у вас есть функция runAgent, которая инкапсулирует реальный вызов Agents SDK. Обернём её мониторингом:

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

Здесь runAgent — это чёрный ящик, который может быть реализован через реальный Agents SDK; мы же показываем, как добавить наблюдаемость, не завязываясь на конкретный API.

Логи vs метрики vs трейсинг

Удобно различать три уровня наблюдаемости:

Уровень Что это Пример для агента GiftGenius
Логи «Истории» про конкретные run‑ы Детальный AgentRunLog c шагами и инструментами
Метрики Агрегированные числовые показатели p95 длительность run, среднее число tool‑calls, error‑rate
Трейсинг Дерево / граф запросов и подзапросов Run → шаги → tool‑calls → вызовы внешних API (commerce, БД и т.п.)

Метрики нужны, чтобы понять «а вообще всё хорошо?» (например, error‑rate за последний час). Логи и трейсинг — чтобы разобраться «почему плохо именно здесь?» и воспроизвести конкретный проблемный run.

Пример зачатка метрик можно реализовать поверх логов: периодическая задача агрегирует agent_run события и считает p95 длительности, количество ошибок и т.п.

6. Как это выглядит в GiftGenius целиком

Чтобы всё не казалось набором абстракций, соберём картинку для нашего учебного приложения.

Агент gift-planner в production окружении имеет только безопасные инструменты: подбор подарков и получение деталей. Он не видит ни платежей, ни управления заказами. Его system‑инструкции говорят о том, что он не должен обещать пользователю «я всё оплачу за вас», а максимум — подготовить рекомендации и, возможно, черновик списка подарков.

Агент order-manager существует отдельно и умеет только работать с заказами. В продакшене он может создавать лишь черновик заказа (create_order_draft), а подтверждение заказа (confirm_order) либо выполняется человеком через явный UI‑триггер в виджете, либо доступно только в dev/staging. Его инструменты используют секреты (API‑ключи магазина) исключительно на backend‑стороне, а в ответ проксируют только нужные поля.

Оба агента запускаются через обёртку runAgentWithLogging, которая навешивает лимиты и записывает логи с agent_run_id, userId, окружением и списком инструментов. В логах нет email и телефонов; эти поля заранее вычищаются скруббером. Профиль пользователя используется в обезличенном виде: возрастной диапазон, интересы, бюджет, но не полный текст истории покупок.

Инфраструктура, в которой живёт MCP‑сервер и агентный сервис, изолирована: контейнеры с read‑only файловой системой (кроме /tmp или специально выделенного каталога), ограничением CPU/RAM, сетью по allow‑list доменов. Если агент вдруг попытается дернуть «что‑то левое», он просто не сможет до этого добраться физически.

Если в какой‑то момент вы видите всплеск метрики «доля run‑ов со статусом limits_exceeded» или «среднее число tool‑calls > 10», вы понимаете, что либо промпт стал излишне болтливым, либо один из инструментов глючит и заставляет агента перезапускать шаги.

Это уже поведение взрослого сервиса, а не экспериментального агента «как-нибудь сойдёт».

7. Типичные ошибки при выводе агентов в продакшен

Всё, что мы обсуждали выше, — это «правильная» картинка продакшен‑агента. На практике же чаще всего встречаются типовые грабли. Соберём их в один список: если избежать хотя бы этих ошибок, запуск в прод пройдёт гораздо спокойнее.

Ошибка №1: агенту «разрешили всё».
Распространённый сценарий: вы описали кучу MCP‑инструментов (поиск, изменение, удаление, платежи), а при создании агента просто скормили ему весь список. В результате модель может случайно вызвать удаление или платеж там, где хотели только чтение. Лечится разделением инструментов по ролям и созданием нескольких более узких агентов, у каждого из которых свой allowedTools.

Ошибка №2: проверка прав только в промпте.
Иногда разработчики пишут в system‑инструкциях: «никогда не покупай ничего без подтверждения пользователя» и на этом успокаиваются. Но промпт — слабая защита, а jail‑breakи и просто ошибки никто не отменял. Нужны реальные проверки на backend‑уровне: «агенту этот tool разрешён» и «пользователю этот tool разрешён», иначе одна неаккуратная генерация может привести к действиям, на которые никто не рассчитывал.

Ошибка №3: секреты в промптах и логах.
Иногда хочется «ускорить интеграцию» и просто положить API‑ключ в system‑prompt или передать его в tool‑аргументах, чтобы агент сам ходил на внешний API. В итоге ключ оказывается и в логах модели, и потенциально в сторонних системах. Это прямой путь к утечкам и бану в Store. Секреты должны жить только на серверной стороне, в переменных окружения или менеджере секретов, и никогда не попадать в контекст модели.

Ошибка №4: «сырые» логи без scrub‑инга.
При отладке удобно писать console.log(...) и забыть про это. Через пару месяцев оказывается, что в логах лежат адреса пользователей, телефоны, номера заказов с PII. Особенно неприятно в мире GDPR и других регуляций. Лучше сразу завести центральный логгер и внедрить автоматическое маскирование чувствительных полей, даже если кажется, что «мы логируем только на dev».

Ошибка №5: отсутствие лимитов на поведение агента.
Без ограничений по шагам, времени и количеству tool‑вызовов агент может зациклиться: многократно вызывать один и тот же инструмент, пытаться бесконечно исправлять одну и ту же ошибку, тратить кучу токенов и грузить внешние API. В лучшем случае вы получите гигантские счета за модели, в худшем — положите backend и разозлите всех пользователей. Лимиты на run‑цикл и sane defaults по таймаутам — обязательная часть конфигурации.

Ошибка №6: смешивание read и write‑операций в одном инструменте.
Иногда создают «удобные» методы вроде getOrCreateOrder, которые при отсутствии заказа создают новый. Для классического backend это допустимый паттерн, но в мире агентов это может привести к неожиданным побочным эффектам: модель хотела просто узнать состояние, а инструмент что‑то создал. Гораздо безопаснее разделять get_order_details и create_order_draft, тогда даже при повторных вызовах последствия более контролируемы.

Ошибка №7: игнорирование наблюдаемости.
Многие начинают с «потом прикрутим логи и метрики, сейчас главное — чтобы работало». Агенты без мониторинга — это чёрный ящик: вы не знаете, какие инструменты они вызывают, сколько шагов делают, где ошибаются. Любая жалоба пользователя превращается в расследование в тёмной комнате. Гораздо проще сразу заложить структуру логов (agent_run_id, tools, статус) и базовые метрики, чем потом пытаться достроить это поверх хаотичного кода.

1
Задача
ChatGPT Apps, 12 уровень, 4 лекция
Недоступна
Scrubbed logger для серверных событий (PII/Secrets-safe)
Scrubbed logger для серверных событий (PII/Secrets-safe)
1
Задача
ChatGPT Apps, 12 уровень, 4 лекция
Недоступна
Двойная проверка прав (agent permissions + user role) для tool-call
Двойная проверка прав (agent permissions + user role) для tool-call
1
Опрос
Агентная оркестрация, 12 уровень, 4 лекция
Недоступен
Агентная оркестрация
Агентная оркестрация с Agents SDK
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ