Web-шеллы для AI-агентов: как я дал Codex и Claude Code веб-интерфейс без открытых портов

Web-шеллы для AI-агентов: как я дал Codex и Claude Code веб-интерфейс без открытых портов

AI-агенты живут на сервере. Это удобно, пока вы сидите за тем же компьютером и открываете терминал. Но что делать, когда сервер в другом городе, а вы с iPad на диване? Или когда агенту нужен срочный ручной ввод, а SSH-ключи остались на рабочем ноутбуке?

Telegram-боты для управления агентами я пробовал. Вывел из эксплуатации через месяц: задержка, неудобный ввод, риск утечки токенов. Нужен был настоящий терминал — но такой, чтобы не открывать порты наружу и не зависеть от SSH.

Решение: ttyd + tmux + Cloudflare Tunnel + Cloudflare Access. Ниже — схема, конфиги и выводы после месяца эксплуатации.

Почему не SSH и не Telegram-боты

До веб-шеллов я прошёл два подхода. Оба не выдержали реальной эксплуатации.

SSH

SSH — золотой стандарт, но в моём сценарии он ломался в самый неподходящий момент:

  • Ключи не синхронизируются между устройствами. Утром я за ноутбуком, вечером с iPad. На iPad нет моего приватного ключа — терминал недоступен.
  • NAT и файрволы. Сервер за NAT’ом провайдера. Чтобы SSH работал извне, нужен либо проброс портов, либо jump host, либо Tailscale. Каждый слой — потенциальная точка отказа.
  • Сессия рвётся при смене сети. Переключился с Wi-Fi на мобильный интернет — сессия умерла. Агент продолжал работать, а я терял контекст.

Telegram-боты

Сделал двух ботов: @Codex_rolex_bot и @Claude_rolex_bot. Каждый принимал команды и возвращал вывод агента. Идея казалась элегантной — весь интерфейс в мессенджере.

Реальность:

  • Задержка 2–5 секунд на каждый round-trip. Для «почини конфиг» — терпимо. Для интерактивной работы — ад.
  • Нет цветового вывода. Claude Code и Codex используют ANSI-цвета для подсветки синтаксиса и диффов. Через Telegram бот это превращалось в Escape-кашу: [32m+добавлено[0m.
  • Ограничение длины сообщения. Telegram режет сообщения на ~4096 символов. Вывод агента с диффом на 200 строк резался на 4–5 сообщений, перемешанных с уведомлениями.
  • Риск утечки токенов. Токен бота лежит в переменных окружения и конфигах. При любом баге агент мог «случайно» отправить его в чат.

Через месяц я выключил обоих ботов, удалил systemd-юниты и поставил крест на Telegram как на операторский интерфейс.

Веб-шелл: как это работает

Идея простая: запустить терминал агента в браузере. Реализация — цепочка из четырёх компонентов.

1. ttyd — терминал в браузере

ttyd — это микро-веб-сервер, который шарит командную оболочку через WebSocket. Браузер подключается к ttyd и получает полноценный терминал — с цветами, скроллом и размером окна.

В отличие от эмуляторов типа xterm.js, которые требуют свой бэкенд, ttyd делает всё сам: слушает порт, принимает WebSocket-соединения и транслирует их в pty. Минимум движущихся частей.

2. tmux — чтобы сессия не рвалась

Без tmux каждая перезагрузка страницы запускала бы новую сессию агента. Неудобно и небезопасно: два параллельных Codex в одной директории — гарантированный конфликт.

Решение — persistent tmux сессия с фиксированным именем:

  • Codex Fast — сессия bronevik-codex-fast
  • Claude Fast — сессия bronevik-claude-fast

ttyd при запуске подключается к существующей tmux-сессии. Если сессии нет — создаёт. Если браузер переподключился (обрыв WebSocket, смена сети) — ttyd возвращается в ту же самую tmux-сессию. Агент продолжает работать, контекст не теряется.

Несколько браузеров могут смотреть одну и ту же сессию одновременно. Удобно для парного debugging’а: открыл на ноутбуке и iPad — видишь одно и то же.

3. Cloudflare Tunnel — без открытых портов

ttyd слушает на 127.0.0.1:7681 (Codex) и 127.0.0.1:7683 (Claude). Эти порты недоступны извне — и не должны быть доступны.

Для внешнего доступа используется Cloudflare Tunnel (cloudflared). Он создаёт исходящее соединение с Cloudflare edge и проксирует трафик внутрь. Никакого проброса портов на роутере, никаких changing IP-адресов.

Конфиг /etc/cloudflared/config.yml:

ingress:
  - hostname: hermes.vdsservers.org
    path: /codex-fast-shell*
    service: http://127.0.0.1:7681
  - hostname: hermes.vdsservers.org
    path: /claude-fast-shell*
    service: http://127.0.0.1:7683
  - hostname: hermes.vdsservers.org
    service: http://127.0.0.1:3000
  - service: http_status:404

Порядок важен: сначала проверяются префиксы путей, потом catch-all на основной сайт, потом 404. Устаревшие URL’ы (/codex-shell, /claude-shell) намеренно роутятся на 404 — чтобы никто не стучался по старым ссылкам.

4. Cloudflare Access — аутентификация перед терминалом

Это критический слой. Без него любой, кто знает URL, получает root-терминал в браузере.

Cloudflare Access вставляется между пользователем и туннелем. При первом заходе браузер редиректится на страницу логина Cloudflare — email, OTP, можно прикрутить GitHub OAuth или Google Workspace. Только после успешной аутентификации трафик доходит до ttyd.

Неаутентифицированный запрос получает 302 redirect на страницу логина. Это работает на уровне Cloudflare edge — трафик вообще не доходит до сервера, пока пользователь не доказал, кто он.

Что под капотом: systemd и скрипты запуска

Всё управляется через systemd — два юнита, каждый отвечает за своего агента.

# ttyd-codex-fast.service
[Service]
ExecStart=/usr/bin/ttyd \
  --port 7681 \
  --interface 127.0.0.1 \
  /opt/bronevik-maintenance/scripts/codex-fast-web-shell.sh

Скрипт codex-fast-web-shell.sh делает три вещи:

  1. Проверяет, существует ли tmux-сессия bronevik-codex-fast
  2. Если нет — создаёт и запускает в ней /usr/local/bin/codex-maintain-fast
  3. Если есть — подключается к ней (tmux attach -t bronevik-codex-fast)

Флаг --interface 127.0.0.1 принципиален. Забудете его — ttyd слушает на 0.0.0.0 и доступен всем в локальной сети. Это критическая ошибка: даже если порт не проброшен наружу, сосед по WiFi или другой контейнер на хосте получают доступ к терминалу.

Отдельный важный момент: пакетный ttyd.service (тот, что идёт с Ubuntu) должен быть отключён. Он слушает дефолтный порт и запускает стандартный shell — не то, что нам нужно. Проверяется это просто:

systemctl status ttyd.service  # должен быть disabled/inactive

Быстрые шеллы: режим low effort

Codex и Claude Code — мощные инструменты, но в полном режиме они тратят много токенов и времени на «размышления». Для оперативного обслуживания это избыточно.

Режим Fast решает эту проблему:

  • Codex Fast поднимается с model_reasoning_effort=low — агент меньше думает, быстрее отвечает. Для команд типа «посмотри статус бэкапа», «перезапусти юнит», «покажи логи за последний час» этого более чем достаточно.
  • Claude Fast запускается с --effort low — аналогично, сокращённый режим reasoning.

Полный режим (medium/high effort) остаётся для тяжёлых задач — рефакторинг, расследование инцидентов, проектирование. Но на каждый день быстрые шеллы экономят 30–50% токенов без потери полезности.

Проверка работоспособности: чек-лист

После настройки или перезагрузки сервера я проверяю состояние одной командой:

bronevik-status

В выводе ищу секцию knowledge-vault и agent-shells. Ожидаемый результат:

  • ttyd_default_active=inactive — пакетный ttyd не перехватил порты
  • codex_fast_shell_active=active — наш шелл на 7681 жив
  • claude_fast_shell_active=active — наш шелл на 7683 жив
  • ss -tlnp показывает 127.0.0.1:7681 и 127.0.0.1:7683, нет порта 7682

Для ручной проверки:

# Локально
curl -s http://127.0.0.1:7681/ | head -5   # должен вернуть HTML ttyd
curl -s http://127.0.0.1:7683/ | head -5

# Публично — должен вернуть 302 (редирект на Cloudflare Access)
curl -sI https://hermes.vdsservers.org/codex-fast-shell/ | grep HTTP

tmux-сессии смотрю через:

sudo -u rolex tmux ls
# Ожидаю: bronevik-codex-fast или bronevik-claude-fast

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

Чего не случилось: ошибки, которые я обошёл

За месяц эксплуатации веб-шеллов не случилось того, чего я боялся.

Shell не убежал наружу. Не было случая, чтобы ttyd оказался доступен без Cloudflare Access. Проверяется это двумя уровнями: локальный curl на 127.0.0.1 (должен работать) и публичный curl на домен (должен получить 302). Если публичный возвращает HTML ttyd без редиректа на логин — что-то сломалось в настройках Access.

Сессии не плодятся. Была гипотеза, что при плохом интернете WebSocket будет рваться и ttyd начнёт создавать новые сессии. Не подтвердилось — tmux с фиксированным именем решил проблему. Даже при обрыве на 30 секунд переподключение возвращается в ту же сессию.

Несколько браузеров не конфликтуют. Открыл Codex на ноутбуке и iPad одновременно — оба видят одно и то же окно tmux. Ввод с любого устройства работает, вывод дублируется. Это даже удобнее, чем SSH: не нужно делать tmux attach при смене устройства.

Производительность терминала не страдает. ttyd + WebSocket добавляют ~50ms задержки по сравнению с прямым SSH. Для работы с AI-агентом это незаметно — основная задержка всё равно на стороне LLM-провайдера (API call до OpenRouter идёт 1–5 секунд).

Что дальше: быстрые шеллы как часть лестницы автономии

Веб-шеллы не заменяют автоматизацию. Это не канал для рутинной работы агентов — для этого есть MCP-мост с allowlist’ом действий. Веб-шелл — интерфейс для человека: когда нужно быстро глянуть, что делает агент, поправить промпт в реальном времени или вмешаться в нестандартной ситуации.

В текущей архитектуре у агентов есть три канала взаимодействия с оператором:

  1. Асинхронные задачи через Hermes в Telegram — постановка задачи, получение результата
  2. MCP-мост обслуживания — узкий набор именованных действий: статус, бэкап, перезапуск
  3. Веб-шеллы — полный интерактивный доступ к агенту, когда нужны руки

Эта трёхуровневая модель закрывает весь спектр: от полностью автоматического до полностью ручного режима. И ни один из каналов не требует открытых портов, SSH-ключей или проброса NAT.

Если вы тоже держите AI-агентов на своём сервере и устали от SSH-ключей на всех устройствах — попробуйте связку ttyd + tmux + Cloudflare Tunnel. Настройка занимает час, окупается за неделю.