Глава 16: Удалённое управление и облачное исполнение

Агент выходит за пределы localhost

До сих пор каждая глава предполагала, что Claude Code работает на той же машине, где лежит код. Терминал локальный. Файловая система локальная. Ответы модели возвращаются в процесс, который владеет и клавиатурой, и рабочим каталогом.

Это предположение рушится в тот момент, когда вы хотите управлять Агентом из браузера, запускать его внутри облачного контейнера или отдавать его как сервис в своей локальной сети. Агенту нужен способ принимать инструкции из веб-браузера, мобильного приложения или автоматизированного пайплайна — пересылать запросы на разрешение тому, кто не сидит у терминала, и туннелировать свой API-трафик через инфраструктуру, которая может подставлять учётные данные или завершать TLS от имени агента.

Агент решает это с помощью четырёх систем, каждая из которых отвечает за свою топологию:

У этих систем общая философия: чтение и запись асимметричны, переподключение происходит автоматически, а сбои деградируют мягко.


Bridge v1: Опрос, диспетчеризация, запуск

Bridge v1 — это система удалённого управления на основе окружения. Когда разработчик запускает claude remote-control, CLI регистрируется в API окружений, опрашивает наличие работы и порождает дочерний процесс для каждой сессии.

Перед регистрацией проходит жёсткий набор предварительных проверок: фича-гейт рантайма, проверка OAuth-токена, проверка политики организации, обнаружение мёртвого токена (кросс-процессный backoff после трёх последовательных сбоев с одним и тем же просроченным токеном), а также проактивное обновление токена, которое убирает примерно 9% регистраций, которые иначе провалились бы с первой попытки.

После регистрации bridge входит в цикл долгого опроса. Элементы работы приходят как сессии (с полем secret, содержащим session tokens, API base URL, MCP-конфиги и переменные окружения) или как healthcheck. Bridge ограничивает сообщения «нет работы» до одного раза на каждые 100 пустых опросов.

Каждая сессия порождает дочерний процесс Agent, который общается через NDJSON по stdin/stdout. Запросы на разрешение проходят через транспорт bridge во веб-интерфейс, где пользователь утверждает или отклоняет их. Этот круговой путь должен завершаться примерно за 10–14 секунд.


Bridge v2: Прямые сессии и SSE

Bridge v2 убирает весь слой API окружений — никакой регистрации, никакого опроса, никакого подтверждения, никакого heartbeat, никакой отмены регистрации. Мотивация проста: v1 требовал от сервера знать возможности машины до отправки работы. V2 сводит жизненный цикл к трём шагам:

  1. Создать сессию: POST /v1/code/sessions с OAuth-учётными данными.
  2. Подключить bridge: POST /v1/code/sessions/{id}/bridge. Возвращает worker_jwt, api_base_url и worker_epoch. Каждый вызов /bridge увеличивает epoch — это И ЕСТЬ регистрация.
  3. Открыть транспорт: SSE для чтения, ACRClient для записи.

Абстракция транспорта (ReplBridgeTransport) объединяет v1 и v2 за единым интерфейсом, так что обработке сообщений не нужно знать, с каким поколением она разговаривает.

Когда SSE-соединение обрывается из-за 401, транспорт перестраивается с новыми учётными данными из нового вызова /bridge, при этом сохраняется указатель sequence number — сообщения не теряются. Путь записи использует closures getAuthToken на уровне экземпляра вместо переменных окружения на уровне процесса, предотвращая утечку JWT между параллельными сессиями.

FlushGate

Тонкая проблема порядка: bridge нужно отправить историю беседы, одновременно принимая живые записи из веб-интерфейса. Если во время flush истории приходит живая запись, сообщения могут быть доставлены не по порядку. FlushGate ставит живые записи в очередь во время flush POST и выталкивает их по порядку после завершения.

Обновление токена и управление epoch

Bridge v2 проактивно обновляет worker JWT до истечения срока действия. Новый epoch сообщает серверу, что это тот же worker, но с новыми учётными данными. Несовпадения epoch (ответы 409) обрабатываются жёстко: оба соединения закрываются, а исключение разматывает вызывающий код, предотвращая сценарии split-brain.


Маршрутизация сообщений и дедупликация эхо

Обе версии bridge используют handleIngressMessage() как центральный маршрутизатор:

  1. Разобрать JSON, нормализовать ключи управляющих сообщений.
  2. Направить control_response в обработчик разрешений, control_request — в обработчик запросов.
  3. Проверить UUID по recentPostedUUIDs (дедупликация эха) и recentInboundUUIDs (дедупликация повторной доставки).
  4. Передать дальше проверенные пользовательские сообщения.

BoundedUUIDSet: поиск O(1), память O(capacity)

У bridge есть проблема эха — сообщения могут вернуться на поток чтения или быть доставлены дважды при переключениях транспорта. BoundedUUIDSet — это FIFO-ограниченное множество, построенное на кольцевом буфере:

class BoundedUUIDSet {
  private buffer: string[]
  private set: Set<string>
  private head = 0

  add(uuid: string): void {
    if (this.set.size >= this.capacity) {
      this.set.delete(this.buffer[this.head])
    }
    this.buffer[this.head] = uuid
    this.set.add(uuid)
    this.head = (this.head + 1) % this.capacity
  }

  has(uuid: string): boolean { return this.set.has(uuid) }
}

Пара экземпляров работает параллельно, у каждого ёмкость 2000. Поиск O(1) через Set, память O(capacity) за счёт вытеснения кольцевым буфером, никаких таймеров и TTL. Неизвестные подтипы control request получают ответ с ошибкой, а не молчание — это не даёт серверу ждать ответ, который никогда не придёт.


Асимметричная конструкция: постоянное чтение, запись через HTTP POST

Протокол Agent Runtime (ACR) использует асимметричный транспорт: чтение идёт через постоянное соединение (WebSocket или SSE), запись — через HTTP POST. Это отражает фундаментальную асимметрию в паттерне коммуникации.

Чтение — это высокая частота, низкая задержка, инициирование сервером: сотни небольших сообщений в секунду во время стриминга токенов. Постоянное соединение — единственный разумный выбор. Запись — низкая частота, инициирование клиентом, и ей нужно подтверждение: сообщения в минуту, а не в секунду. HTTP POST даёт надёжную доставку, идемпотентность через UUID и естественную интеграцию с балансировщиками нагрузки.

Попытка объединить это в один WebSocket создаёт связанность: если WebSocket падает во время записи, нужна логика повтора и нужно различать «не отправлено» и «отправлено, но подтверждение потеряно». Раздельные каналы позволяют оптимизировать каждый независимо.


Управление удалёнными сессиями

SessionsWebSocket управляет клиентской стороной WebSocket-соединения Agent Runtime. Его стратегия переподключения различает типы сбоев:

СбойСтратегия
4003 (не авторизован)Остановиться немедленно, без повторов
4001 (сессия не найдена)До 3 повторов, линейный backoff (временный сбой во время компакции)
Другой временныйЭкспоненциальный backoff, максимум 5 попыток

Type guard isSessionsMessage() принимает любой объект со строковым полем type — намеренно разрешительно. Жёстко прописанный allowlist молча отбрасывал бы новые типы сообщений до обновления клиента.


Прямое подключение: локальный сервер

Direct Connect — это самая простая топология: Agent работает как сервер, а клиенты подключаются через WebSocket. Никакого облачного посредника, никаких OAuth-токенов.

Сессии имеют пять состояний: starting, running, detached, stopping, stopped. Метаданные сохраняются в ~/.agent/server-sessions.json, чтобы можно было возобновить работу после перезапуска сервера. Схема URL agent:// даёт чистый способ адресации для локальных подключений.


Прокси upstream: инъекция учётных данных в контейнерах

Прокси upstream работает внутри контейнеров Agent Runtime и решает конкретную задачу: инъекцию корпоративных учётных данных в исходящий HTTPS-трафик из контейнера, где агент может выполнять недоверенные команды.

Последовательность настройки тщательно выверена:

  1. Прочитать токен сессии из /run/ccr/session_token.
  2. Установить prctl(PR_SET_DUMPABLE, 0) через Bun FFI — блокируя ptrace процесса по тому же UID для heap процесса. Без этого подставленный prompt-ом gdb -p $PPID мог бы вытащить токен из памяти.
  3. Скачать CA-сертификат upstream-прокси и объединить его с системным CA bundle.
  4. Запустить локальный relay CONNECT-to-WebSocket на эфемерном порту.
  5. Удалить файл токена — теперь токен существует только в heap.
  6. Экспортировать переменные окружения для всех дочерних процессов.

Каждый шаг работает по принципу fail open: ошибки отключают прокси, а не убивают сессию. Это правильный компромисс — сбой прокси означает, что некоторые интеграции не будут работать, но основная функциональность останется доступной.

Ручное кодирование Protobuf

Байты, проходящие через туннель, оборачиваются в protobuf-сообщения UpstreamProxyChunk. Схема тривиальна — message UpstreamProxyChunk { bytes data = 1; } — и Agent кодирует её вручную в десяти строках вместо подключения protobuf-runtime:

export function encodeChunk(data: Uint8Array): Uint8Array {
  const varint: number[] = []
  let n = data.length
  while (n > 0x7f) { varint.push((n & 0x7f) | 0x80); n >>>= 7 }
  varint.push(n)
  const out = new Uint8Array(1 + varint.length + data.length)
  out[0] = 0x0a  // поле 1, wire type 2
  out.set(varint, 1)
  out.set(data, 1 + varint.length)
  return out
}

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


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

Разделяйте каналы чтения и записи. Когда чтение — это высокочастотный поток, а запись — низкочастотные RPC, их объединение создаёт ненужную связанность. Пусть каждый канал падает и восстанавливается независимо.

Ограничивайте память дедупликации. Паттерн BoundedUUIDSet даёт дедупликацию с фиксированным объёмом памяти. Любой системе доставки с at-least-once нужен ограниченный буфер дедупликации, а не бесконечный Set.

Делайте стратегию переподключения пропорциональной сигналу о сбое. Постоянные сбои не должны повторяться. Временные сбои должны повторяться с backoff. Неоднозначные сбои должны повторяться с небольшим лимитом.

Храните секреты только в heap в враждебной среде. Чтение токена из файла, отключение ptrace и удаление файла убирают и файловую, и memory-inspection атаку.

Принцип fail open для вспомогательных систем. Upstream-прокси работает по fail open, потому что он даёт расширенные возможности (инъекция учётных данных), а не основную функциональность (выполнение модели).

Системы удалённого исполнения выражают более глубокий принцип: основной цикл агента (глава 5) должен быть безразличен к тому, откуда приходят инструкции и куда уходят результаты. Bridge, Direct Connect и upstream-прокси — это транспортные уровни. Обработка сообщений, выполнение инструментов и потоки разрешений поверх них одинаковы, независимо от того, сидит ли пользователь у терминала или по другую сторону WebSocket.

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