Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

Зашитая конфигурация.

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

##Признаки и симптомы

ИИ-ассистент склонен выдавать правдоподобный литерал ровно там, где должна быть ссылка. Код работает в демонстрации, поэтому запах переживает ревью, если его не искать специально.

Характерные признаки:

  • Встроенные эндпоинты, порты, пути и тайм-ауты, разбросанные по бизнес-логике вместо модуля конфигурации: http://localhost:3000, /tmp/cache, 5432, setTimeout(..., 30000).
  • Магические числа/строки без именованной константы — счётчики повторов, размеры страниц, ограничения частоты, пороги функций.
  • Один и тот же литерал, повторяющийся в разных файлах, потому что модель сгенерировала его заново, а не импортировала существующую константу (тот же паттерн GitClear «копипаст вверх, рефакторинг вниз» в миниатюре).
  • Зашитые учётные данные / ключи — API-токены, ключи шифрования или строки Basic-авторизации, вставленные из обучающих данных.
  • Существующий config.ts / .env / объект настроек, который новый код полностью игнорирует.
  • Значения, вшитые под окружение: URL dev/стейджинга или ID тестового аккаунта, жёстко прописанные в пути, который уезжает в продакшен.
// 🚩 Сгенерировано ИИ: каждая настройка — литерал, dev-значения вшиты
export async function syncOrders() {
  const res = await fetch("https://api.staging.acme.dev/v1/orders", {
    headers: { Authorization: "Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc" },
    signal: AbortSignal.timeout(30000),
  });
  const orders = (await res.json()).slice(0, 50); // почему 50?
  for (let i = 0; i < 3; i++) { /* повтор 3 раза... почему 3? */ }
}

Строка Authorization — это срабатывание gitleaks/Semgrep; URL, 30000, 50 и 3 — запахи магических значений; а api.staging.acme.dev — это dev-значение, которое вот-вот достигнет продакшена.

##Причины проблемы

Почему модели это порождают

  • Литерал — самый вероятный следующий токен. Имея fetch(, самым дешёвым продолжением, удовлетворяющим непосредственному запросу, является конкретная строка URL, а не config.apiBaseUrl. Разрешение ссылки требует знания о том, что где-то существует символ; выдача значения не требует ничего. Модели оптимизируют локально правдоподобный, запускаемый фрагмент.
  • Обучающий корпус состоит из самодостаточных фрагментов. Учебники, примеры из README и ответы на Stack Overflow подставляют значения прямо в код, чтобы они работали автономно. Модель усвоила, что «хороший пример кода» зашивает значения — противоположность гигиене продакшена.
  • Нет контекста репозитория / ограниченное окно. Модель обычно не читала ваш config/, схему переменных окружения или файл констант, поэтому не может их переиспользовать. В пересказе DevClass исследования GitClear это связывается с ростом дублирования: ассистенты реже предлагают переиспользовать похожую функцию — отчасти из-за ограниченного размера контекста. Собственные данные GitClear показывают, что число блоков из пяти и более дублирующихся строк за 2024 год выросло примерно в восемь раз, а доля строк, связанных с рефакторингом («перемещённых»), упала с 25% изменённых строк в 2021 году до менее чем 10% в 2024-м.
  • Угодничество / склонность отвечать буквально на запрос. Если попросить «добавить синхронизацию заказов», модель выдаёт что-то, что работает сейчас; вынесение конфигурации — это лишний обвес, который она не предложит по своей инициативе, если не сказать.
  • Устаревание из-за даты отсечения обучения. Зашитые литералы часто оказываются устаревшими литералами — старые эндпоинты, устаревшие версии API, порты по умолчанию или слабые криптографические константы. В статье OX Security о безопасности вайб-кодинга устаревшие практики вроде хеширования MD5 объясняются «шаблонами, усвоенными из legacy-кодовых баз, где такие практики когда-то были отраслевым стандартом», и отмечается, что «модель совершенно не знает, что стандарты безопасности изменились».

Чем это вредит

  • Сопровождаемость — Стрельба дробью. Изменение одного тайм-аута или базового URL означает охоту за каждым дублированным литералом по всей кодовой базе; пропустите один — и поведение незаметно разойдётся в зависимости от места вызова.
  • Корректность в разных окружениях. Зашитый в логику URL стейджинга или ID dev-аккаунта уезжает в продакшен; магические пороги без имени рассинхронизируются со значениями, которые должны были бы отражать.
  • Безопасность. Зашитые токены и ключи — это утечка учётных данных: попав в коммит, они навсегда остаются в истории git. Эмпирическое исследование сгенерированного ИИ кода сборки (arXiv 2601.16839) нашло 364 запаха в коде сборки из пул-реквестов, написанных агентами, в том числе 25 зашитых путей/URL и 5 зашитых учётных данных с высокой степенью серьёзности; более 61% этих агентских пул-реквестов были влиты.
  • Нагрузка на ревью и техдолг. Ревьюерам приходится проверять происхождение каждого литерала, а значения, которые управляют поведением через вспомогательные функции и пороги, вручную проверять труднее всего. В интервью Stack Overflow с Эно Рейесом из Factory он замечает: «чем качественнее кодовая база, тем сильнее ИИ ускорял организацию», — а разрастание зашитых значений подтачивает именно этот базовый уровень.

##Лечение

Тактики промптинга / ревью

  • Направьте модель на вашу конфигурационную поверхность: «Прочитай src/config.ts и .env.example; используй config.* / process.env для каждого URL, порта, тайм-аута и учётных данных. Не вводи литералы.» Модели подставляют значения, потому что не знают о существовании символа, — назовите его.
  • Явно запретите магические значения: «Никаких магических чисел или строк — выноси именованные константы.» Затем заставьте прогнать eslint --rule no-magic-numbers и сканер секретов (gitleaks/Semgrep) и исправить то, что они отметят. Согласно рекомендациям Factory, встройте линтеры/сканеры в цикл, чтобы агент исправлялся сам, а не опирался на ручное ревью.
  • Дайте схему окружения (zod/envalid/.env.example), чтобы модели было куда класть значения, а не угадывать их.
  • Прогрепайте новые диффы на http, localhost, IP-литералы, Bearer и голые цифры в аргументах вызовов перед слиянием.

Рефакторинг — приёмы из каталога и одна практика работы с конфигурацией:

  • Замена магического числа символьной константой — для порогов, счётчиков, размеров.
  • Извлечение класса — чтобы собрать все настройки в один типизированный и валидируемый модуль конфигурации.
  • Вынесите значения, зависящие от окружения, и секреты в переменные окружения (конфигурация по 12-factor) и читайте их только через этот модуль; никогда не коммитьте секреты.
// config.ts — единый, валидируемый источник истины
import { z } from "zod";
const env = z.object({
  ORDERS_API_BASE_URL: z.string().url(),
  ORDERS_API_TOKEN: z.string().min(1),
  ORDERS_TIMEOUT_MS: z.coerce.number().default(30_000),
  ORDERS_PAGE_SIZE: z.coerce.number().default(50),
  ORDERS_MAX_RETRIES: z.coerce.number().default(3),
}).parse(process.env);
export const config = env;
// ✅ после: ссылки, а не литералы — одно место для изменений, секрет вне исходников
import { config } from "./config";
export async function syncOrders() {
  const res = await fetch(`${config.ORDERS_API_BASE_URL}/v1/orders`, {
    headers: { Authorization: `Bearer ${config.ORDERS_API_TOKEN}` },
    signal: AbortSignal.timeout(config.ORDERS_TIMEOUT_MS),
  });
  const orders = (await res.json()).slice(0, config.ORDERS_PAGE_SIZE);
  for (let i = 0; i < config.ORDERS_MAX_RETRIES; i++) { /* ... */ }
}

Если секрет уже попал в коммит, смените его — удаление строки не очищает историю git.

##Обнаружение анализаторами