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

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

ChatGPT Apps
Рівень 12 , Лекція 4
Відкрита

1. Навіщо агенту «продакшн-мислення»

Коли ви пишете звичайний бекенд, сама думка про «вихід у продакшн» автоматично вмикає режим параної: авторизація, логування, обробка помилок, ліміти, секрети в .env, а не в коді.

З агентом варто вмикати той самий режим — тільки ще жорсткіший. Причина проста: звичайний бекенд виконує рівно те, що ви написали, а агент — те, що модель сама вирішила зробити в межах наданих їй інструментів та інструкцій. Ілюзія контролю тут сильніша, ніж у класичному коді: здається, що промпт усе описує. Насправді ж ви контролюєте лише оточення й доступні дії, а не всі «думки» моделі.

Тож у цій лекції ми поступово «обкладемо» нашого агента шарами захисту:

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

Щоб було наочніше, продовжимо історію про наш GiftGenius: агента, який допомагає підібрати подарунок і трохи занурюється у світ електронної комерції (через замовлення й checkout, але поки без подробиць ACP — це буде пізніше).

2. Права: агенту не потрібні «всі кнопки світу»

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

Перше правило: агентові не потрібно вміти все. Що більше в нього інструментів, то вищий шанс, що він викличе «не ту» функцію «не в той» момент. Замість одного монструозного manageEverything(), який читає й записує будь-що, ми проєктуємо дрібні та чіткі функції. Як мінімум — розділяємо читання і запис.

Для GiftGenius це особливо очевидно: одне діло — читати список подарунків і вподобання користувача, інше — створювати чи підтверджувати замовлення (а там уже гроші). Тому зазвичай роблять так:

  • набір безпечних інструментів лише для читання (пошук подарунків, перегляд деталей),
  • окремі інструменти для запису (створення чернетки замовлення, скасування замовлення),
  • і, за потреби, ще один рівень для особливо ризикових операцій (підтвердження платежу, масові зміни).

Різні агенти для різних задач

Ще один потужний прийом — розділяти агентів за зонами відповідальності. Один агент — «підбір подарунків», інший — «керування замовленнями». Тоді навіть якщо модель у 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} заборонено для агента ${agent.id}`);
  }
  if (!canUserCallTool(userRole, tool)) {
    throw new Error(`Користувачу з роллю ${userRole} не можна викликати ${tool}`);
  }
}

У результаті навіть якщо модель раптом вирішить викликати confirm_order з неправильного агента або від імені гостя, виклик упреться в цю перевірку й перетвориться на керовану помилку, а не на незапланований платіж.

Різні конфігурації для різних оточень

У dev і staging-оточеннях ви часто хочете дати агенту більше свободи: тестові інструменти, фейкові платіжні сервіси, експериментальні функції. У production, навпаки, конфігурація максимально жорстка: частину tools вимкнено, ендпоїнти — тільки бойові, токени — лише реальні.

Найпростіша схема:

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. Ми задаємо ліміти за кроками циклу запуску, за кількістю викликів інструментів і за розміром контексту (token limit). Модель не може нескінченно «думати» й плодити виклики інструментів: у певний момент запуск завершиться з помилкою «ліміт кроків» або «ліміт часу».

Усе це складається в класичну «оборонну архітектуру», яку зручно подати схемою.

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

    subgraph Агент
      A
      B
      C
    end

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

Промпт — найслабший захист. Справжня сила починається там, де ви фізично обмежуєте, що може зробити ваш код і які API доступні.

Ліміти на цикл запуску: кроки, час, виклики інструментів

Частину sandbox можна виразити прямо в конфігурації агента: максимальну кількість кроків, загальний час виконання, ліміт викликів інструментів. Це не лише захист від runaway-циклів, а й контроль витрат.

Приклад абстрактної конфігурації run-опцій:

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

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

Такі ліміти ви потім передаєте в обгортку, яка запускає агента. Якщо раптом модель вирішила 11-й раз викликати інструмент, ви перериваєте запуск і чесно кажете користувачеві, що завдання надто складне. Це краще, ніж дозволити агентові безконтрольно «спалювати» бюджет.

Ізоляція коду та мережі

На рівні контейнера/процесу зазвичай роблять так:

Код MCP-сервера та/або агентного сервісу запускається в контейнері з файловою системою лише для читання (крім спеціально відведеного робочого каталогу) та обмеженими ресурсами (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 $».

Замість того щоб «кидати» в промпт повну історію замовлень користувача, можна зробити 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. Моніторинг і спостережуваність агента

Що саме потрібно бачити

Ми обмежили, що агент може робити, які дані він бачить і що потрапляє в логи. Наступне питання — як зрозуміти, що в усьому цьому «звіринці» агент у продакшні поводиться саме так, як ми задумали?

Звичайний моніторинг «сервіс живий / не живий» для агента майже не дає користі. Важливо не лише знати, що процес працює, а й розуміти його поведінку: які кроки він робить, які інструменти викликає, де помиляється та де зациклюється.

Мінімальний набір даних по кожному запуску:

  • 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
Логи «Історії» про конкретні запуски Детальний AgentRunLog з кроками й інструментами
Метрики Агреговані числові показники p95 тривалість запуску, середня кількість викликів інструментів, частка помилок
Трейсинг Дерево / граф запитів і підзапитів Запуск → кроки → виклики інструментів → виклики зовнішніх API (commerce, БД тощо)

Метрики потрібні, щоб зрозуміти «чи загалом усе добре?» (наприклад, частка помилок за останню годину). Логи й трейсинг — щоб розібратися, «чому погано саме тут?» і відтворити конкретний проблемний запуск.

Приклад «зачатку» метрик можна реалізувати поверх логів: періодичне завдання агрегує події 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-сервер і агентний сервіс, ізольована: контейнери з файловою системою лише для читання (крім /tmp або спеціально виділеного каталогу), обмеження CPU/RAM, мережа за allow-list доменів. Якщо агент раптом спробує викликати «щось зайве», він просто не зможе дістатися до цього фізично.

Якщо в якийсь момент ви бачите сплеск метрики «частка запусків зі статусом limits_exceeded» або «середня кількість викликів інструментів > 10», це сигнал: або промпт став надмірно балакучим, або один з інструментів глючить і змушує агента перезапускати кроки.

Це вже поведінка дорослого сервісу, а не експериментального агента «якось воно буде».

7. Типові помилки під час виведення агентів у продакшн

Усе, що ми обговорювали вище, — це «правильна» картинка продакшн-агента. На практиці найчастіше трапляються типові граблі. Зберемо їх в один список: якщо уникнути хоча б цих помилок, запуск у продакшн пройде значно спокійніше.

Помилка № 1: агенту «дозволили все».
Поширений сценарій: ви описали купу MCP-інструментів (пошук, зміна, видалення, платежі), а під час створення агента просто «згодували» йому весь список. У результаті модель може випадково викликати видалення або платіж там, де ви хотіли лише читання. Це лікується розділенням інструментів за ролями та створенням кількох вужчих агентів — у кожного з яких свій allowedTools.

Помилка № 2: перевірка прав лише в промпті.
Іноді розробники пишуть у system-інструкціях: «ніколи нічого не купуй без підтвердження користувача» — і на цьому заспокоюються. Але промпт — слабкий захист, а jailbreak-и та звичайні помилки ніхто не скасовував. Потрібні реальні перевірки на backend-рівні: «агентові цей tool дозволено» і «користувачеві цей tool дозволено». Інакше одна неакуратна генерація може призвести до дій, на які ніхто не розраховував.

Помилка № 3: секрети в промптах і логах.
Іноді хочеться «прискорити інтеграцію» й просто покласти API-ключ у system-prompt або передати його в аргументах інструмента, щоб агент сам ходив на зовнішній API. У підсумку ключ опиняється і в логах моделі, і потенційно в сторонніх системах. Це прямий шлях до витоків і блокування в Store. Секрети мають жити тільки на серверній стороні — у змінних оточення або менеджері секретів — і ніколи не потрапляти в контекст моделі.

Помилка № 4: «сирі» логи без очищення.
Під час налагодження зручно писати console.log(...) і забути про це. За кілька місяців виявляється, що в логах лежать адреси користувачів, телефони, номери замовлень із PII. Особливо неприємно у світі GDPR та інших регуляцій. Краще одразу завести центральний логер і впровадити автоматичне маскування чутливих полів — навіть якщо здається, що «ми логували лише на dev».

Помилка № 5: відсутність лімітів на поведінку агента.
Без обмежень за кроками, часом і кількістю викликів інструментів агент може зациклитися: багато разів викликати один і той самий інструмент, намагатися нескінченно виправляти одну й ту саму помилку, витрачати купу токенів і навантажувати зовнішні API. У кращому випадку ви отримаєте гігантські рахунки за моделі, у гіршому — «покладете» бекенд і розсердите всіх користувачів. Ліміти на цикл запуску й sane defaults за тайм-аутами — обовʼязкова частина конфігурації.

Помилка № 6: змішування read- і write-операцій в одному інструменті.
Іноді створюють «зручні» методи на кшталт getOrCreateOrder, які за відсутності замовлення створюють нове. Для класичного бекенду це допустимий патерн, але у світі агентів він може призвести до неочікуваних побічних ефектів: модель хотіла просто дізнатися стан, а інструмент щось створив. Значно безпечніше розділяти get_order_details і create_order_draft — тоді навіть за повторних викликів наслідки більш керовані.

Помилка № 7: ігнорування спостережуваності.
Багато хто починає з «потім прикрутимо логи та метрики, зараз головне — щоб працювало». Агенти без моніторингу — це чорна скринька: ви не знаєте, які інструменти вони викликають, скільки кроків роблять, де помиляються. Будь-яка скарга користувача перетворюється на розслідування в темній кімнаті. Набагато простіше одразу закласти структуру логів (agent_run_id, tools, статус) і базові метрики, ніж потім намагатися добудувати це поверх хаотичного коду.

1
Опитування
Агентна оркестрація, рівень 12, лекція 4
Недоступний
Агентна оркестрація
Агентна оркестрація з Agents SDK
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ