Перейти к содержимому

Развёртывание и клиенты

Control Center — не одна программа. Это архитектура тонкого клиента: headless-сервер, которому принадлежат ваши данные, и набор клиентов, которые рисуют интерфейс поверх единственного RPC-соединения с ним. Понимание этого разделения объясняет, почему одни вещи работают там, где работают, и до чего каждый клиент может и не может дотянуться.

Процесс cc_server хранит базы данных — небольшой global.db для общесерверного состояния плюс по одному файлу SQLite на рабочее пространство, открываемые лениво, — и выполняет долгоживущую работу: движки конвейеров, службы согласования, набор инструментов MCP и слушатель оркестрации. Всё, что должно пережить закрытие окна или что два клиента должны видеть одинаково, живёт там.

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

Это сделано намеренно, и именно это позволяет двум клиентам на одном сервере находиться в разных рабочих пространствах, не имея хранимого на сервере «текущего рабочего пространства», из-за которого они могли бы разойтись. Вместо этого изоляция обеспечивается при каждом вызове, в четыре шага:

  1. Операция объявляет, относится ли она к рабочему пространству; если да и workspace_id не пришёл, вызов отклоняется.
  2. Указанный идентификатор должен принадлежать зарегистрированному рабочему пространству, и это проверяется до того, как что-либо откроет базу данных, — иначе неизвестный id создавал бы пустой призрачный workspace.db при каждом запросе. Промах означает «не найдено».
  3. Роль WorkspaceRole вызывающего должна соответствовать минимальной роли операции, которая выводится из её вида: для чтения нужна роль «Гость», для изменения — «Участник», для разрушительной операции — «Администратор».
  4. Операция, раскрывающая содержимое репозитория, дополнительно требует разрешение на уровне репозитория ровно того уровня, который объявляет, — none / read / review / write — чтобы членство в рабочем пространстве никогда не давало незаметно больше прав, чем даёт хост кода.

Главная фраза: граница доступа — не обладание ключом сопряжения, а членство.

К серверу привели три силы:

  1. Единый источник истины. Компьютер и телефон или ноутбук и компьютер должны видеть одно и то же состояние флота. Если бы у каждого была своя база данных, они бы расходились. Единый сервер означает, что все клиенты читают одни и те же живые данные.
  2. Веб не может обслуживать себя сам. Браузер не может запустить подпроцесс или открыть локальный файл, поэтому веб-сборка никогда не сможет владеть базой данных или запускать агента. Ей приходится говорить с сервером, который это умеет. Вместо того чтобы строить второй, только веб-путь к данным, приложение для компьютера перевели на тот же путь.
  3. Работа без интерфейса. Сервер, который можно запустить на маленькой машине и достучаться до него с телефона в поезде или из браузера откуда угодно, полезнее графического интерфейса, который нужно держать открытым.

Плата за это — зависимость от сервера: если он не запущен, ничего не работает. По умолчанию приложение для компьютера отвечает на это тем, что запускает сервер само.

До сервера доходят пять клиентов, и у каждого своя форма и свой профиль доверия.

Настольное приложение на Flutter (macOS, Windows, Linux) — это полный клиент. При первом запуске оно спрашивает, как ему работать, а затем запоминает выбор:

  • Локально: запустить cc_server на этой машине и следить за ним — вариант по умолчанию, самодостаточная однопользовательская конфигурация. Приложение запускает сервер, которому принадлежит база данных в каталоге поддержки приложения, и общается с ним через локальный интерфейс.
  • Удалённо: подключиться к cc_server, работающему в другом месте, по защищённому WebSocket. Данные живут на том сервере; это приложение — чистый отрисовщик.

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

Веб-сборка — то же приложение на Flutter, скомпилированное для работы в браузере и размещённое на app.usectrl.dev, так что устанавливать нечего. Браузер не может запустить подпроцесс, поэтому веб-клиент всегда удалённый: он дозванивается до cc_server по wss:// и отрисовывает полный интерфейс приложения для компьютера. После подключения он показывает те же экраны для того же флота, потому что это тот же код поверх того же RPC. Пробелы — это несколько мест, где нужна нативная возможность, которой у браузера нет; главное из них — описанный ниже слой качества записи встреч, ещё одно — уведомления ОС.

Вот почему «ничего не скачивать» и «без сервера» — разные утверждения. Веб-клиент убирает установку, но не убирает сервер. То, до чего он дозванивается, может быть cc_server на том же ноутбуке — ws://localhost:9030, которому не нужен сертификат, потому что браузеры считают локальный интерфейс доверенным, — или сервер на другой машине, до которого нужно достучаться по wss:// с сертификатом, которому браузер доверяет. cc_server отказывается привязываться к адресу, отличному от локального, без TLS, поэтому удалённый сервер — это либо его собственный сертификат, либо прокси, терминирующий TLS, либо туннель или VPN, который его предоставляет.

Сам бандл размещается отдельно от сервера, с которым он говорит. Автономный cc_server обслуживает эндпоинт RPC, набор инструментов MCP и свои прокси, но не веб-бандл: путь для статических файлов в сервере есть, однако на автономном бинарнике не настроен корень веб-файлов, поэтому обслуживать по / ему нечего. Именно опубликованный образ контейнера cc-webapp размещает бандл, а scripts/build_web.sh его собирает. Сервер принимает источники браузера только из своего списка --allowed-origins, по умолчанию — https://app.usectrl.dev, поэтому при самостоятельном размещении бандла нужно добавить его источник.

Медиа (аватары, изображения из ленты, скриншоты PR) идёт через прокси-эндпоинт сервера, а не напрямую к вышестоящим хостам, поэтому браузер никогда не обращается к произвольному источнику. Развёрнутый клиент проставляет заголовок Content-Security-Policy для каждого запроса на основе неконфиденциальной cookie, так что разрешён только источник подключённого сервера.

Приложение Remote — отдельный, более лёгкий клиент: веб-PWA на Flutter по адресу remote.usectrl.dev. Оно намеренно не является полным приложением. Оно сопрягается через WebSocket-реле с брокером и сквозным шифрованием и говорит на преимущественно читающем срезе набора инструментов: читать тикеты, сообщения и ленту новостей; отправить ответ или обновить тикет.

Телефон — субъект с меньшими привилегиями, чем локальный агент. Политика инструментов «запрещено по умолчанию», предварительно распределённый ключ, скрепляющий каждый кадр, ограничение возможностей сеанса, которое закрывает привилегированные операции (например, сопряжение) для клиента-компаньона, описанные выше проверки роли при каждом вызове и разрешений на уровне репозитория, а также ограничение частоты запросов не дают одобренному (или утёкшему) сопряжению превратиться в полный удалённый контроль. Полная модель безопасности — в разделе Удалённое управление и мобильный клиент.

Внешние инструменты — редакторы, другие агенты, CLI, — говорящие на Model Context Protocol, подключаются к набору инструментов MCP на сервере и потребляют типизированный реестр инструментов. Это путь программной интеграции, а не графический интерфейс. См. Использовать MCP-сервер и Инструменты MCP.

cc_worker — это headless-бинарник на чистом Dart, который сопрягается с cc_server, объявляет возможности своего хоста и забирает арендованные задания на выполнение. Он транслирует события процессов обратно по тому же RPC и не хранит долговременного состояния: ни базы данных, ни аутентификации, ни одобрений, ни бюджетов — всё это никогда не покидает cc_server. Воркер — это конечность, а не второй мозг. Один авторитетный сервер, N простых конечностей; ни консенсуса, ни трафика между воркерами.

Арендованное выполнение — это всё ещё обвязка аренды. Только тип agentRun запускает настоящую команду (CC_JOB_COMMAND или эхо-вывод промпта, если он не задан). Типы pipelineStep, codeIndex, goldenRender, benchmark и evalBatch сейчас проверяются командой git --version / rev-parse HEAD, так что потоковый путь задействуется, но без вида, будто эти задания действительно выполнились.

Он объявляет операционную систему, архитектуру CPU, число ядер, объём RAM, доступность Flutter SDK и доступные на хосте бэкенды песочницы (в Windows их нет). Выделенный воркер всегда объявляет также alwaysOn и acceptsParallel — именно по ним планировщик закрепления → предпочтения → переноса и распределяет задания. Проверки GPU или ML нет.

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

Аспект Сервер (cc_server) Исполнитель флота (cc_worker) Клиент для компьютера Веб-/телефонный клиент
Базы данных (Drift/SQLite) владеет ими — — —
Аутентификация, одобрения, бюджеты, идентичность владеет этим — — —
Конвейеры, службы согласования, слушатель оркестрации выполняет это — — —
Набор инструментов MCP обслуживает его — — —
Выполнение агентов выполняет (или сдаёт в аренду) выполняет арендованные задания — —
Отрисовка интерфейса — — полная полная (веб) / в основном чтение (телефон)
Запись встреч (микрофон + звук встречи) — — нативный захват только веб, через аудио демонстрации экрана
OAuth календаря (Google) хранит токены — — —

Эти два уровня изолируют выполнение по-разному. cc_server оборачивает запуски, которые выполняет сам, в песочницу ОС хоста там, где для неё есть бэкенд, — Seatbelt в macOS, bubblewrap в Linux и WSL2, — а в Windows или без bwrap и socat полагается на другие механизмы ограничения. cc_worker не изолирует выполнение вообще. Он объявляет возможность sandbox, чтобы сервер мог распределять задания по платформе, но в самом воркере её ничто не применяет: арендованное задание запускается напрямую и ограничено только окружением аренды и выданным worktree. Поэтому сдача работы в аренду выводит выполнение за пределы песочницы даже на хосте, где она есть. О том, что ограничивает запуск в каждом случае, — в разделе Песочница и безопасность.

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

Веб-клиент тоже записывает, хотя по форме таблицы этого не скажешь: он захватывает микрофон и аудиодорожку демонстрации экрана в браузере и передаёт на сервер PCM с частотой 16 kHz. Эта аудиодорожка ему обязательна, поэтому там, где браузер её не даёт, запись явно завершается ошибкой. По-настоящему только в приложении для компьютера доступен слой качества вокруг записи: подавление эха на уровне сигнала через нативный процессор, индикатор уровня входного сигнала и предупреждение о неработающем микрофоне, повторный запуск резюме, отмена обработки и действие календаря «Начать запись и привязать».

Каждый клиент подтверждает свою личность предварительно распределённым ключом, который выдаётся один раз:

  • Приложение для компьютера в локальном режиме генерирует новый ключ при каждом запуске и передаёт его запущенному серверу через переменные окружения, так что на компьютере не сохраняется ничего секретного.
  • Удалённые клиенты (приложение для компьютера в удалённом режиме, веб-клиент, телефон) используют ключ, созданный командой pair сервера и сохранённый в хранилище ключей ОС (или, на телефоне, локально).

Отзыв действует сразу, а не отложенно. Сервер следит за таблицей парных устройств и в течение секунд закрывает любой открытый сеанс, устройство которого выпало из активного набора, — и на прямом пути WebSocket, и на пути через реле. Чтобы отрезать устройство, не нужно ждать его переподключения.

Ключ никогда не проходит через URL, который видит статический хост. Когда он приходит по ссылке — на телефоне или в браузере — он едет во фрагменте URL, части после #, которую браузер никогда не отправляет серверу, отдающему страницу. Это тот инвариант, который стоит запомнить, и он действует для обоих. У веб-клиента есть ещё две точки входа: форма подключения, где вы вставляете сервер и ключ напрямую, и код приглашения, который он обменивает на учётные данные, созданные только для этого браузера.