RTP DesportoMax Verstappen regressa às vitórias na Fórmula 1 e Antonelli reforça liderançaESPN DeportesNFL arranca su gira europea con duelo entre Colts y Commanders en LondresThe Jerusalem PostFlydubai hijacker's years in Australia under scrutiny as authorities probe link to country - reportPunchJUST IN: WAEC appoints new head of Nigeria officeInquirerDriver killed, 2 others injured in Quezon highway crashESPNTransfer rumors, news: Arsenal among clubs chasing winger YildizSportstarMohun Bagan vs Punjab FC LIVE score, IFA Shield semifinal: MBSG 0-0 PFC; Subhasish Bose's goal ruled outZDF heuteAktuelle Pressemitteilungen des ZDFPremium TimesNDLEA intercepts N5bn cannabis, codeine consignments at Lagos, Onne portsWirtualna PolskaNiespokojnie na wschodzie Polski. Alert RCB dla mieszkańcówStraits Times SportRallying-Evans ends years of heartache to take first world titleLa TerceraEl caos bajo la lluvia de Malasia: Max Verstappen obtiene su primer triunfo del año en un alocado Gran Premio de Bahrein
The Daily Newsstand · Free, Always
Sunday, October 4, 2026

Как мы пускаем ИИ на боевые сайты, или Что происходит после команды «исправляй»

Translate

В первой части я рассказывал, как мы дошли от обычного копирования кода между MODX и ChatGPT до нормального предметного интерфейса к CMS. Агент перестал видеть сайт как россыпь файлов и строк в базе и получил возможность работать с ресурсами, чанками, TV и зависимостями между ними. В какой-то момент этого стало достаточно, чтобы ставить задачу примерно так же, как я поставил бы её разработчику, без подробной инструкции, куда именно залезть и какую строку там поменять.

Следующий шаг напрашивался сам собой. Если агент уже разобрался в проблеме и нашёл решение, какого чёрта я должен брать его код, подключаться по SSH, делать резервную копию, заливать исправление, запускать проверки и потом ещё ходить по сайту смотреть, не отвалилось ли что-нибудь рядом. Хотелось просто написать «Посмотри, почему форма перестала отправляться, исправь и проверь» и получить в ответ не инструкцию из десяти пунктов, которые мне потом самому предстоит выполнить, а уже исправленный сайт.

Вот на этом месте и началась вторая часть нашего проекта. Сам доступ на запись дать оказалось совсем несложно. К MODX MCP добавились SSH, физические файлы, логи, браузер и несколько служебных инструментов. Гораздо больше времени ушло на то, чтобы после команды «исправляй» не сидеть у агента за спиной и не следить, что именно он там сейчас собирается натворить.

Первым неожиданно сдался SSH

Вот уж от кого никто не ожидал. Технологии сто лет в обед, всё давно придумано, работает везде. Агенту от неё тоже требуется немного: прочитать файл, посмотреть журнал ошибок, выполнить php -l, запустить сборку или какую-нибудь диагностическую команду.

Оказалось, что человек и агент пользуются SSH несколько по-разному. Человек обычно подключился к серверу и какое-то время там живёт. Что-то посмотрел, подумал, покурил, выполнил следующую команду. Агент ту же задачу дробит на много коротких операций: прочитал один файл, полез во второй, поискал строку по проекту, посмотрел лог, уточнил версию PHP, запустил проверку, снова перечитал файл. Когда таких задач несколько, а несколько сайтов лежат на одном аккаунте хостинга, количество подключений растёт очень бодро.

На одном из хостингов это довольно быстро закончилось блокировками. Пришлось много общаться с поддержкой и отдельно объяснять, что такое AI-агент, почему он вообще лезет по SSH и откуда берётся такое количество коротких соединений. Слава богу, нас поняли, но сами блокировки от этого никуда не делись. За трое суток мы получили их три раза, каждый раз примерно на шесть часов, и оставалось только ждать, пока доступ снова откроют. Добавить наш IP в белый список у хостера не получилось, такой возможности там просто нет.

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

effective_config="$("$REAL_SSH" -G "$@" 2>/dev/null || true)"

resolved_host="$(
  printf '%s\n' "$effective_config" |
  awk '$1 == "hostname" {print $2; exit}'
)"

resolved_user="$(
  printf '%s\n' "$effective_config" |
  awk '$1 == "user" {print $2; exit}'
)"

resolved_port="$(
  printf '%s\n' "$effective_config" |
  awk '$1 == "port" {print $2; exit}'
)"

Привязывать ограничения к сайту оказалось неправильно. У нас могут быть три разных домена, три записи в SSH-конфигурации и один реальный пользователь на одном сервере. Провайдеру, понятно, совершенно всё равно, как красиво мы назвали эти проекты у себя.

Поэтому пул соединений строится по фактическому адресу, а получившийся ключ заодно очищается перед использованием в имени каталога:

pool_key="$(
  printf '%s@%s:%s' \
    "$resolved_user" \
    "$resolved_host" \
    "$resolved_port" |
  tr -c 'A-Za-z0-9._@:-' '_'
)"

Дальше используется обычный ControlMaster, чтобы десять команд агента не превращались в десять новых авторизаций:

SSH_OPTS=(
  -o ControlMaster=auto
  -o ControlPath="$CONTROL_PATH_TEMPLATE"
  -o ControlPersist=1h
  -o ServerAliveInterval=60
  -o ServerAliveCountMax=3
)

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

if ! master_alive; then
  exec {bootstrap_fd}>"$pool_root/master.lock"
  /usr/bin/flock "$bootstrap_fd"

  if ! master_alive; then
    acquire_slot
    "$REAL_SSH" "${SSH_OPTS[@]}" "$@" &
    ssh_pid=$!

    for _ in $(seq 1 100); do
      if master_alive; then
        /usr/bin/flock -u "$bootstrap_fd"
        exec {bootstrap_fd}>&-
        wait "$ssh_pid"
        exit $?
      fi

      if ! kill -0 "$ssh_pid" 2>/dev/null; then
        wait "$ssh_pid"
        status=$?
        /usr/bin/flock -u "$bootstrap_fd"
        exec {bootstrap_fd}>&-
        exit "$status"
      fi

      sleep 0.05
    done
  fi
fi

Параллельные команды тоже пришлось ограничить. По умолчанию сейчас допускается восемь одновременно выполняющихся команд на один фактический SSH-аккаунт, а для этого хостинга мы поставили ограничение в две.

После очередной переделки мы специально проверили самый неприятный случай, когда готового SSH-соединения ещё нет, и одновременно запустили 12 команд. В результате получили одну SSH-авторизацию и не больше двух одновременно выполнявшихся команд.

Ничего нового мы здесь, конечно, не изобрели. ControlMaster, flock и ограничение параллельности были задолго до ChatGPT. Забавно другое: ограничение хостинга, которого я при обычной работе мог не увидеть годами, агент нашёл почти сразу.

SSH оставили для диагностики, а запись файлов вынесли отдельно

Когда SSH перестал отваливаться, возник вопрос посерьёзнее. Если агенту доступна обычная командная оболочка, изменить файл он технически может чем угодно: sed, cat, Python, Perl, да хоть PHP-скриптом на лету.

Наша SSH-обёртка сама по себе не является песочницей и не разбирает каждую выполняемую команду. Она занимается транспортом, соединениями и параллельностью. Боевые файлы по нашим правилам меняются через site-file, а не через ssh, scp, rsync или случайно подвернувшийся sed.

Можно было оставить всю последовательность действий на совести модели и записать в инструкции, что сначала надо прочитать свежий файл, показать разницу, сохранить копию, после записи перечитать его и для PHP обязательно выполнить php -l. Но мне хотелось, чтобы этот порядок не зависел от того, насколько внимательно агент в конкретный момент прочитал собственную инструкцию.

Поэтому для физических файлов появился отдельный инструмент site-file. Через штатный файловый канал содержимое части вещей агенту вообще не отдаётся даже на чтение. Например:

HARD_BLOCK_FILES = {
  "config.core.php",
  ".env",
  "auth.json",
  "credentials.json",
  "composer-auth.json",
  "core/config/config.inc.php",
}

HARD_BLOCK_GLOBS = (
  ".env.*",
  "*.pem",
  "*.key",
  "*.p12",
  "*.pfx",
)

Закрыты также .git/, .ssh/, .codex/, .agents/, .site-manager/, node_modules/, служебные каталоги MODX с конфигурацией, логами, сессиями, пакетами и ещё несколько мест, где для обычной работы с сайтом агенту делать совершенно нечего.

Важно, что речь именно про штатный файловый канал. Сам SSH технически остаётся обычным SSH и имеет те права, которые есть у системного пользователя. Поэтому правило «физические файлы production читаем и меняем через site-file» закреплено ещё и в общих инструкциях агента. Иначе вся эта конструкция довольно быстро превратилась бы в очень дорогой способ вызвать cat.

Через что-нибудь вроде:

../../.ssh/id_rsa

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

def normalize_rel(value: str, *, allow_root: bool = False) -> str:
  value = (value or "").strip().replace("\\", "/")
  
  if value in ("", "."):
    if allow_root:
      return ""
    fail("Путь не указан")
    
  if value.startswith("/"):
    value = value.lstrip("/")
    
  p = PurePosixPath(value)
  
  if p.is_absolute() or ".." in p.parts:
    fail(f"Небезопасный путь: {value}", 5)
    
  rel = p.as_posix()
  
  while rel.startswith("./"):
    rel = rel[2:]
    
  if not rel and not allow_root:
    fail("Пустой путь")    
  return rel

Начальный / здесь трактуется как путь относительно корня сайта, а попытка пройти наружу через .. блокируется.

Есть и промежуточная категория. .htaccess, index.php, php.ini, PHP-файлы, core/, connectors/ и некоторые другие чувствительные места иногда действительно приходится менять, поэтому полностью закрывать их бессмысленно. Но обычная запись туда уже не проходит и требует отдельного --allow-sensitive.

В итоге у агента есть SSH, но из этого совершенно не следует, что SSH автоматически превращается в штатный редактор всего сервера.

Сначала смотрим, потом пишем

По нашим правилам обычная работа с site-file put начинается без --yes. Инструмент ещё раз читает текущую версию непосредственно с боевого сервера, считает контрольные суммы и показывает разницу:

print(f"PATH: {rel}")
print(
  f"BEFORE: {'exists' if before_exists else 'new'} "
  f"size={len(before) if before_exists else 0} "
  f"sha256={sha256(before) if before_exists else '-'}"
)
print(f"AFTER: size={len(after)} sha256={sha256(after)}")
print_diff(rel, before, after, before_exists)

Без --yes на этом всё заканчивается:

if not args.yes:
  print("PREVIEW ONLY. Для применения повторите с --yes.")
  return

Технически никто не мешает сразу вызвать команду с --yes, поэтому порядок «сначала посмотреть, потом применить» задаётся правилами агента. Сам инструмент при этом всё равно перечитывает свежую версию и показывает разницу непосредственно перед записью. То есть меняется тот файл, который сейчас лежит на боевом сайте, а не копия, которую агент видел неизвестно когда.

Если изменение применяется, сначала сохраняется предыдущая версия файла вместе с его параметрами:

manifest = {
  "format": 1,
  "site_id": site_id,
  "created_at": datetime.now().astimezone().isoformat(timespec="seconds"),    
  "operation": operation,    
  "path": rel,    
  "before_exists": before_exists,    
  "before_mode": mode or None,    
  "before_size": len(before) if before_exists else 0,    
  "before_sha256": sha256(before) if before_exists else None,    
  "before_file": str(before_file.relative_to(bundle)) if before_file else None,
}

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

def write_remote(cfg: dict, rel: str, data: bytes, mode: str) -> None:
  target = remote_path(cfg, rel)
  parent = str(PurePosixPath(target).parent)    
  tmp = parent + f"/.mcp-write-{uuid.uuid4().hex}"
  
  cmd = (
    "set -e; "
    f"mkdir -p -- {q(parent)}; "
    f"cat > {q(tmp)}; "
    f"chmod {q(mode)} -- {q(tmp)}; "
    f"mv -f -- {q(tmp)} {q(target)}"
  )
  
  run_ssh(cfg, cmd, input_bytes=data, check=True)

После записи файл снова читается с сервера. Контрольная сумма должна совпасть с той версией, которую собирались записать. Для PHP дополнительно запускается php -l.

Можно сразу добавить проверку нужной страницы через --check-path, но здесь у нас два разных уровня проверки. Из site-file вызывается короткий site-check с --no-baseline. Он нужен, чтобы быстро убедиться, что сама страница после записи по-прежнему открывается без HTTP-ошибки. Консольные и сетевые проблемы в отчёт при этом тоже собираются, но с сохранённым эталоном не сравниваются. Полноценную проверку новых ошибок мы запускаем отдельно, про неё чуть ниже.

Если после записи контрольная сумма не совпала, PHP не прошёл проверку или обязательная проверка страницы завершилась ошибкой, предыдущая версия восстанавливается:

try:
  write_remote(cfg, rel, after, write_mode)
  
  exists2, verify, _ = fetch_bytes(cfg, rel)
  
  if not exists2 or sha256(verify) != sha256(after):
    raise SiteFileError("Проверка sha256 после записи не прошла")
    
  if PurePosixPath(rel).suffix.lower() == ".php":
    php_lint(cfg, rel)    
    
  if args.check_path:
    site_check(args.site_id, args.check_path)
    
except Exception as exc:
  print(f"ERROR AFTER WRITE: {exc}", file=sys.stderr)
  print("ROLLBACK: восстанавливаю предыдущую версию.", file=sys.stderr)
  restore_previous(cfg, rel, before_exists, before, mode)
  raise
Жизненный цикл изменения файла: так агент безопасно вносит изменения на боевом сайте

Жизненный цикл изменения файла: так агент безопасно вносит изменения на боевом сайте

После успешной операции остаётся резервная копия, разница между версиями и запись в истории изменений. Собственно, вот эту часть работы мне и хотелось убрать с себя. Разобраться, почему форма падает, бывает интересно. В двадцать пятый раз скачать PHP-файл, сохранить копию, залить обратно, выполнить php -l и открыть страницу уже сильно меньше.

А потом мы начали разворачивать агентурную сеть

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

Блокировка внутри site-file здесь ничего не решает. Второй процесс может в то же самое время менять чанк через MODX MCP. Блокировать только MCP тоже бессмысленно по той же причине, только наоборот. Поэтому блокировать пришлось проект целиком.

У каждого сайта появился общий project.lock, а в owner.json сохраняется, кто именно сейчас его держит. Реальная структура выглядит примерно так:

{  
  "format": 1,
  "site_id": "example-site",
  "token": "...",
  "pid": 18427,
  "boot_id": "...",
  "started_at": "...",
  "source": "chatgpt",
  "actor_id": "...",
  "actor_name": "...",
  "model": "...",
  "command": "..."
}

Теперь второй изменяющий процесс не слышит крика из туалета «занято!», а получает вполне нормальный ответ:

PROJECT BUSY: example-site
Владелец: source=chatgpt actor_id=...
С 2026-10-03T...
Команда: ...

Чтению это при этом не мешает. Если кто-то уже меняет сайт, другой агент всё ещё может посмотреть файлы, разобраться в структуре или подготовить следующую работу. Нельзя только начать второе изменение поверх первого.

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

Я всё-таки разработчик, а не художник, поэтому картинки тоже пришлось делегировать ИИ. Всё, глазки отдохнули? Возвращаемся к тому, как не угробить боевой сайт.

Я всё-таки разработчик, а не художник, поэтому картинки тоже пришлось делегировать ИИ. Всё, глазки отдохнули? Возвращаемся к тому, как не угробить боевой сайт.

А вариантов для этого хватает и без отключения света. Процесс можно прибить, соединение может оборваться, что-нибудь может аварийно завершиться, наконец, может перезагрузиться уже сам сервер. Процесс исчез, а project.lock остался лежать и продолжает честно защищать сайт. Даже немного чересчур честно.

Одного PID здесь недостаточно, потому что после перезагрузки тот же номер когда-нибудь может достаться уже совершенно другому процессу. Поэтому вместе с PID сохраняется Linux boot_id:

def stale(lock_dir: Path, owner: dict) -> bool:
  current_boot = boot_id()
  owner_boot = str(owner.get("boot_id") or "")
  
  if current_boot and owner_boot and current_boot != owner_boot:
    return True
    
  pid = int(owner.get("pid") or 0)
  
  if pid and not pid_alive(pid):
    return True
    
  if not owner:
    try:
      return time.time() - lock_dir.stat().st_mtime > 30
    except Exception:
      return True
  return False

Если сервер перезагружался, старая блокировка автоматически считается протухшей. Если процесс умер без перезагрузки, это определяется по PID. Даже оставшийся без нормального owner.json каталог блокировки не живёт вечно, через 30 секунд его уже можно признать протухшим.

Живую блокировку штатная команда снимать не позволяет, иначе довольно скоро любой достаточно настойчивый агент мог бы обнаружить универсальный способ борьбы с PROJECT BUSY: удалить то, что мешает, и продолжить.

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

Не все изменения одинаково страшные, одни страшнее других

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

Поэтому деление «чтение безопасно, запись опасна» довольно быстро отправилось в корзину. Обычное локальное и легко обратимое изменение агент в рабочем режиме может сделать сам, если понятна область влияния и сохранено исходное состояние. А удаление, массовое изменение, пакет, критичная настройка или общий элемент, используемый в куче мест, уже требуют другого отношения.

Здесь надо уточнить ещё одну вещь. Правила у нас лежат не в одном огромном файле. Есть глобальный AGENTS.md, который действует для всех проектов. В нём находятся общие правила работы: когда требуется подтверждение, как обращаться с боевыми файлами, что делать при PROJECT BUSY, когда обязательна резервная копия и какие обходные пути использовать нельзя.

А у каждого сайта есть свой проектный AGENTS.md. Там уже хранится то, что относится именно к этому проекту: CMS, структура, дополнения, принятые соглашения, известные особенности и всё то, что агенту имеет смысл помнить именно здесь.

Ниже настоящий проектный AGENTS.md одного из наших production-сайтов. Я убрал домен, название клиента, внутренние имена, SSH alias и серверные пути. Само содержание специально не причёсывал под статью. Спойлер получился здоровенный, но я специально оставил файл почти целиком. Иначе опять получился бы аккуратный демонстрационный пример, из которого не очень понятно, зачем вся эта затея вообще нужна.

Реальный AGENTS.md сайта (обезличено)
# Правила проекта «Production-сайт клиента»

Это production-сайт на MODX 3. Рабочий канал MODX — MCP-сервер `modx` через `modx3mcp-site example-site`.

## Состояние проекта

Инвентаризация выполнена 29.09.2026 по данным production через MODX MCP и безопасный канал `site-file`. Это коммерческий сайт компании, работающей в сфере производства и услуг. Он работает на MODX Revolution 3.2.4-pl, PHP 8.3.31 и MySQL. Основной публичный контекст — `web`; второй контекст — `mgr`.

В MODX зарегистрировано 128 ресурсов, 15 шаблонов, 45 чанков, 36 сниппетов, 13 плагинов и 34 TV. Страницы собираются шаблонами из общих чанков; услуги, отзывы, вопросы и команда хранятся ресурсами, а характеристики и блоки данных — преимущественно в TV.

Формы используют FetchIt, собственный сниппет обработки заявок и FormIt; код содержит условную отправку заявок в Bitrix24 через отдельную настройку, но её фактическая конфигурация не подтверждена.

Интерфейс использует Bootstrap 5.3.0, собственные CSS/SCSS и несколько небольших JS-библиотек.

Workspace не является копией production; изменения физических файлов возможны только через `site-file`.

## Проверенное состояние подключения

- Домен: `https://example-site.ru/`.
- MODX Revolution: `3.2.4-pl`.
- MODX3 MCP: `1.0.0`.
- Веб-PHP по данным MCP: `8.3.31`.
- База данных: MySQL.
- Окружение: `production`; режим: `work`.
- Workspace: `/home/agent/sites/example-site`.
- Workspace не является локальной копией production.
- Корень production: `<production-root>`.
- MODX endpoint: `https://example-site.ru/assets/components/.../api.php`.
- Физические файлы доступны только через штатный безопасный канал `site-file` по зарегистрированному SSH alias.
- `site-file doctor` пройден: ROOT/WRITE/SHA256/PHP — OK.
- Браузерная проверка главной страницы пройдена: HTTP 200.
- Для обработки изображений включён `site-image` с `engine = "auto"`: на этом хостинге автоматически выбран ImageMagick CLI, потому что PHP Imagick отсутствует. WebP поддерживается.

## Архитектура и структура MODX

- Счётчики MCP на дату инвентаризации: 128 ресурсов, 28 опубликованных, 0 удалённых; 15 шаблонов, 45 чанков, 36 сниппетов, 13 плагинов, 34 TV, 15 категорий, 2 контекста и 3 файловых media source.
- Контексты: `web` — основной сайт и `mgr` — админка.
- Основные настройки: `site_status=1`, `default_template=1`, `friendly_urls=1`, `friendly_urls_strict=1`, `use_alias_path=1`, `container_suffix=/`, `cache_resource=1`. `mail_use_smtp=1`.
- Значения системных настроек не заменяют проверку настроек контекста и ClientConfig.
- Зарегистрировано 19 component-директорий: `ace`, `cacheclear`, `ckeditor`, `clientconfig`, `controlerrorlog`, `fetchit`, `formit`, `gallery`, `guzzle7`, `lastmodified`, `minifyx`, `modxmcp`, `pdotools`, `phpthumbon`, `scss`, `tagcanonical`, `translit`, `tvtable`, `upgrademodx`. MCP не вернул версии этих компонентов.
- Пространства имён, найденные в списке системных настроек: `core`, `ace`, `ckeditor`, `clientconfig`, `controlerrorlog`, `fetchit`, `formit`, `gallery`, `lastmodified`, `minifyx`, `modxmcp`, `pdotools`, `phpthumbon`, `tagcanonical`, `tvtable`, `upgrademodx`.
- Проверка интеграций MCP: `miniShop2`, `MIGX`, `VersionX` и `VirtualPage` не установлены. Каталог товаров на miniShop2 и таблицы MIGX не обнаружены.
- Версии Extras и полный список установленных transport packages доступными read-only wrappers не получены.

### Карта ресурсов и шаблонов

Основные корневые ресурсы: главная, услуги, кейсы, цены, отзывы, информационные страницы и контакты. Отдельно существует системная папка.

Часть технических ресурсов использует шаблон `404`; не выводить назначение шаблона только из его названия.

| Шаблон | Использование по MCP |
|---|---|
| Главная | главная и ещё один ресурс |
| Услуга | 13 ресурсов; 11 опубликованы |
| Специализированная услуга | отдельная посадочная страница |
| Прайс лист | страница цен |
| Отзывы | страница отзывов |
| Кейс | страницы кейсов |
| Отзыв для набора | 23 ресурсные записи отзывов |
| Член команды | 4 записи команды |
| Контакты | страница контактов |
| О нас / Производство | информационная страница |
| Оплата и доставка | информационная страница |
| Статика | две служебные/текстовые страницы |
| UI | служебная страница |
| 404 | технические ресурсы |
| Вопрос ответ | по MCP ресурсов с этим шаблоном нет |

Ресурсы с шаблоном `Отзыв для набора`, FAQ и командные записи не опубликованы как самостоятельные страницы; чанки используют `pdoResources` с `showUnpublished=1`.

Главная включает общие чанки `head`, `header`, `banner`, `services`, `price`, `feedback`, `about`, `video`, `faq`, `reviews`, `footer`, `modal`.

Несколько дополнительных блоков главной сейчас закомментированы.

Одна из страниц кейсов опубликована под неопубликованным контейнером; прямой URL на момент проверки отвечал HTTP 200.

### Шаблоны, чанки и сниппеты

- Общие чанки: `head` — метаданные, canonical, SCSS/CSS и согласие на cookies; `header` — контакты и меню `pdoMenu`; `footer` — подвал и меню; `modal` — модальные формы.
- Их зависимости широкие: `head`, `header`, `footer` подключены примерно к 12 шаблонам, `modal` — примерно к 10.
- Основные секционные чанки строят перечни услуг, FAQ, отзывы, таблицы цен, видеоблоки и формы.
- Важные собственные сниппеты: обработчик заявок, сниппеты Schema.org, `tagCanonical`, `TVTable`, `GetIP`.
- Несколько существующих сниппетов граф зависимостей и поиск не нашли во внешних активных вызовах.
- `pdoTools` / `pdoResources` / `pdoMenu` используются для списков ресурсов, меню, отзывов и FAQ.
- Категории элементов связывают vendor-элементы с `pdoTools`, `FormIt`, `FetchIt`, `Gallery`, `MinifyX`, `SCSS`, `TVTable` и служебными Extras.
- Все 13 зарегистрированных плагинов включены.
- Среди используемых событий: `OnManagerPageBeforeRender`, `OnRichTextEditorRegister`, `OnHandleRequest`, `OnMODXInit`, `OnWebPagePrerender`, `OnFileManagerUpload`, `OnDocFormSave`, `OnSiteRefresh`, `OnBeforeCacheUpdate` и другие.

### TV, динамические блоки и формы

- Страницы услуг используют набор TV для фотографий, описания, цены, характеристик, сроков, гарантий и дополнительного содержимого.
- Цены хранятся в TV табличного типа; вступление и заключение — в отдельных TV.
- Кейсы используют TV для описания, цены, срока, фотографий, размеров, таблиц и идентификаторов галерей.
- FAQ и отзывы связываются на страницах через отдельные TV.
- Отзывные записи имеют рейтинг и дату.
- Команда хранится ресурсами с TV фотографии.
- Отдельная TV привязана к ряду шаблонов для schema-разметки.
- Формы в нескольких чанках используют `FetchIt` с собственным сниппетом обработки заявок; он запускает FormIt с хуками сохранения заявки и email.
- В коде есть условная отправка обычных заявок в Bitrix24 через отдельную настройку.
- Реальные значения email, webhook, аналитики и части доменных настроек на момент инвентаризации не подтверждены.
- Плагин `ClientConfig` переносит контекстные настройки в placeholders и `$modx->getOption()`, но его наборы MCP прочитать не смог.
- Компонент `Gallery` установлен; шаблоны кейсов используют его через идентификаторы галерей.
- Несколько файловых media source зарегистрированы, но MCP отказал в просмотре их содержимого из-за выключенного `modxmcp.allow_root_filesystem_read`.

### Физические файлы, верстка и SEO

- Workspace не зеркалирует production.
- Основные публичные файлы находятся в `assets/css/`, `assets/scss/`, `assets/js/`, `assets/img/`, `assets/images/`, `assets/gallery/`, `assets/video/`.
- Основные CSS-файлы находятся в `assets/css/`, JS — в `assets/js/`.
- Фотографии разложены по тематическим каталогам, видео — в `assets/video/`.
- Bootstrap — 5.3.0 по заголовку `bootstrap.bundle.min.js`; собственные SCSS-исходники лежат в `assets/scss/`.
- Новые CSS-библиотеки и версии Bootstrap не подключать без явного разрешения.
- Чанки `head` / `header` содержат подключение SCSS, canonical, cookie-consent, настройки аналитики и мета-поля верификации.
- Часть placeholders может приходить из ClientConfig и контекстов, которые во время инвентаризации не были полностью доступны для чтения.
- Friendly URLs и вложенные alias включены, используется завершающий слеш `/`.
- Есть отдельные ресурсы `robots.txt`, `sitemap.xml` и `llms.txt`.
- В `.htaccess` обнаружены 301-редиректы, нормализация URL и стандартный MODX rewrite на `index.php`.
- Static Elements практически не используются. Не менять способ хранения элементов без отдельной причины.

## Технические наблюдения и неясности

- Граф зависимостей на 29.09.2026: 143 узла, 195 связей, отсутствующих ссылок на элементы — 0.
- Отмечено 38 проверенных кандидатов в orphan, включая несколько неиспользуемых по графу чанков, сниппетов и шаблонов; это не доказательство ненужности.
- Перед любым удалением отдельно проверять использование и динамические вызовы.
- Один из опубликованных служебных ресурсов содержит вызов сниппета очистки кеша. Его публичную доступность намеренно не проверяли, чтобы не запускать действие очистки.
- Перед изменениями этого ресурса или сниппета сначала выяснять область воздействия.
- Проверка главной на дату инвентаризации: HTTP 200, без горизонтального overflow; браузерный отчёт отметил два прерванных запроса к MP4.
- Несколько других проверенных страниц вернули HTTP 200.
- Один маршрут в одной проверке вернул 404, затем HTTP 200 примерно через минуту; прямой доступ по ID также вернул HTTP 200. Считать маршрут нестабильным и повторно проверять перед SEO- или маршрутными изменениями.
- Полные настройки контекстов и ClientConfig, зарегистрированные property sets, версии transport packages, содержимое некоторых media source и точные корни этих sources через доступные read-only wrappers проверить не удалось.
- MCP сообщает, что просмотр файловой media source отключён защитой `modxmcp.allow_root_filesystem_read`; не обходить это через другой канал.
- В базе есть ресурсные записи отзывов и команды; самостоятельными опубликованными страницами они не являются.
- Отдельный раздел статей или новостей в карте не обнаружен, но это не исключает произвольный текст внутри ресурсов.

## Как работать

- Тексты страниц менять в ресурсах; структурированные данные услуг, кейсов, цен и отзывов — в соответствующих TV.
- Для услуги сначала проверять шаблон и смысл TV: одни и те же TV могут использоваться несколькими типами страниц.
- Верстку общих областей искать в чанках `head`, `header`, `footer`, `modal`; блоки страниц — в соответствующих секционных чанках.
- PHP-логика находится в сниппетах и плагинах.
- MODX-ресурсы, шаблоны, чанки, сниппеты, плагины, TV и поддерживаемые настройки менять через MODX3 MCP.
- Физические CSS/JS/PHP/изображения и другие файлы читать и точечно менять через `site-file`; не использовать произвольный `scp` / `rsync` для рабочих изменений.
- Перед изменением общих элементов `head`, `header`, `footer`, `modal`, `feedback` и исполняемых PHP-элементов анализировать полный граф зависимостей и затронутые страницы.
- Не менять ядро MODX.
- Не создавать дублирующие элементы без проверки существующих.
- Перед записью физического файла получить свежую production-версию и проверить diff.
- После изменений проверять фактический результат через `site-check example-site <путь>`.
- Для PageSpeed использовать `site-pagespeed example-site <путь>`.
- Для изображений использовать штатный `site-image`.
- Перед изменениями изображений при необходимости проверять `site-image doctor example-site`; движок выбирается автоматически.
- Preview без `--yes` не меняет production.
- Опасные, удаляющие, массовые и инфраструктурные операции подчиняются общим правилам глобального `AGENTS.md`.
- Не открывать служебный маршрут очистки кеша в браузерной проверке: его GET может иметь побочный эффект.

## Служебные файлы

- `HISTORY.md` — история пользовательских изменений проекта.
- `.codex/config.toml` — MCP-конфигурация этого workspace.

Например, перед изменением существующего элемента MODX мы отдельно смотрим его использование через modx-impact:

modx-impact example-site chunk product_card

Он показывает прямые упоминания элемента и граф зависимостей. Если чанк используется в половине каталога, это уже не совсем «поменять одну строчку», даже если сама правка занимает ровно одну строку. По той же причине массовые изменения, удаления и общие элементы требуют отдельного подтверждения и более широкой проверки.

Хороший пример получился с очисткой корзины MODX. Перед окончательным удалением ресурсов мы проверяем плагины на события OnBeforeEmptyTrash и OnEmptyTrash. Если обработчиков нет, сценарий ограничивается резервной копией базы. Если они есть и могут во время очистки затронуть что-то ещё, делается уже полная копия файлов и базы.

В итоге риск мы оцениваем не по названию команды и не по количеству строк, а по реальной области последствий. Одна строчка в конфигурации иногда опаснее сотни строк обычного текста.

Успешная команда ещё не означает, что сайт работает

Файл может записаться без единой ошибки, контрольная сумма совпадёт, PHP бодро пройдёт php -l, а сайт после этого всё равно будет выглядеть как произведение современного искусства.

На фронтенде это особенно легко получить. CSS полностью корректен, но на телефоне появился горизонтальный скролл. JavaScript синтаксически нормальный, но падает уже после загрузки. В разметке появился правильный путь к картинке, которая в ответ отдаёт 404.

Для этого появился site-check:

site-check example-site \
  /catalog/product/ \  
  --mobile

Он открывает страницу через Puppeteer и собирает HTTP-код, console.error, исключения JavaScript, неудачные сетевые запросы, ответы 4xx/5xx у ресурсов и горизонтальную прокрутку.

Можно одновременно проверить нужный текст:

site-check example-site \  
  /contacts/ \
  --contains "Отправить"

или наличие конкретного элемента:

site-check example-site \  
  /catalog/ \  
  --selector ".product-card"

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

Поэтому у site-check появился эталон. Его можно отдельно записать для конкретного пути и ширины экрана. При следующих проверках система сравнивает текущий результат с этим состоянием и отдельно выделяет только новые ошибки:

known = issue_set(
  baseline_entry.get(field) or []
)

current = issue_set(current_items)

new_items = sorted(current - known)

if new_items:
  new_issue_count += len(new_items)

То же самое делается с горизонтальной прокруткой. Если она была до нашей правки, это плохо, но это другая задача. Если стала больше именно после изменения, текущая проверка не проходит.

Это отдельная проверка от той короткой, которую можно вызвать прямо из site-file. Там нам нужно быстро убедиться, что после записи сама страница продолжает нормально открываться. Полноценный запуск site-check с сохранённым эталоном уже позволяет ловить именно новые ошибки консоли, сетевых запросов и верстки.

Вот эта штука оказалась неожиданно полезной. Мне не надо перед каждой маленькой правкой сначала доводить старый сайт до идеального состояния. Достаточно хотя бы не сделать его хуже.

MODX тоже успел напомнить, что он здесь не просто так

В первой статье я подробно рассказывал случай с VersionX, когда содержимое элемента менялось правильно, но само изменение проходило мимо штатного механизма сохранения MODX, поэтому новая версия не создавалась. Повторять эту историю здесь смысла нет.

Для второй части важен только вывод. После появления SSH предметный MCP никуда не делся. Физический файл разумно менять как файл, а сущность MODX лучше менять через MODX, потому что вокруг неё могут быть события, плагины, версии и прочая жизнь, про которую прямой SQL или произвольная запись ничего не знают.

В итоге вокруг MCP выросла ещё одна система

В какой-то момент стало заметно, что сам MODX MCP уже занимает только часть всей конструкции. Рядом появились SSH-транспорт, файловые операции, резервные копии, project.lock, история изменений, браузерная проверка и отдельные безопасные сценарии для разных операций.

Если сильно упростить, получается примерно так:

Общий контур работы агента с боевым сайтом: серверные инструменты и MODX MCP остаются разными каналами.

Общий контур работы агента с боевым сайтом: серверные инструменты и MODX MCP остаются разными каналами.

И вот здесь проект окончательно перестал быть только историей про MODX.

Предметный интерфейс CMS по-прежнему важен, но SSH, файловые операции, блокировки, резервные копии, история и проверка страниц сами по себе от MODX уже почти не зависят. Если завтра рядом появится другая CMS, большая часть этой обвязки ей тоже понадобится. Как только окончательно обкатаем всё это на MODX, следующим, скорее всего, будет WordPress.

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

И вот после этого команда «исправляй» начинает означать примерно то, что я от неё и хотел. Не «вот тебе сервер, удачи», а «разберись с задачей, внеси изменение, проверь результат и оставь мне возможность вернуть всё назад».

Собственно, ради этого всё и затевалось. Код ИИ и раньше писал вполне прилично. Больше всего меня раздражала вся ручная возня между «понятно, что надо сделать» и «готово, проверено». Вот эту часть работы мы постепенно и пытаемся отдать ему целиком.

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

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

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.