BB - это открытая программа под лицензией MIT, в которой Claude Code, Codex, Pi и ACP-агенты работают в одном окне. За 7 месяцев репозиторий собрал около 4 тысяч звёзд. Главное в BB не окно, а то, что его интерфейс достраивается плагинами и новый плагин подхватывается без перезапуска программы.

Недавно я снимал про BB ролик. Там агент за 20 минут и два промпта собрал плагин, который расшифровывает запись созвона, выделяет договорённости со сроками и пишет письмо клиенту. Ролик был про то, что это даёт пользователю, а в этой статье я разберу код и покажу, за счёт чего это работает. Всё ниже проверено по репозиторию get-bb/bb по состоянию на конец сентября 2026.

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

Сервер хранит, демон исполняет

Команда npx bb-app запускает лаунчер, а он поднимает два Node-процесса: server и host-daemon. Сервер написан на Hono, по умолчанию слушает 127.0.0.1:38886 и хранит всё состояние в SQLite, в файле ~/.bb/bb.db. Демон держит примитивы машины, то есть файлы, git, терминалы и запуск агентов. Десктопное приложение на Electron здесь только оболочка, которая запускает тот же bb-app и показывает веб-интерфейс сервера.

Примечательно в какую сторону идёт связь. Не сервер ходит к демону, а демон сам открывает к серверу WebSocket и получает по нему команды вроде turn.submit. Поэтому вторая машина подключается к BB без открытых портов: в настройках генерируется однострочник curl .../install.sh, он ставит демон как сервис launchd или systemd, и демон получает свой ключ машины. Сервер при этом это роль, а не отдельный компьютер, и на серверной машине работает такой же демон, как на остальных.

Демон и сервер не всегда обновляются одновременно, поэтому протокол между ними версионирован. Сейчас в контракте стоит HOST_DAEMON_PROTOCOL_VERSION = 220, и если версии разошлись, демон сам скачивает подходящую сборку, сверяет её SHA-256 и перезапускается. Для второй машины это важно, потому что обновлять её руками после каждого релиза сервера не нужно.

Клиентов у сервера может быть несколько: веб-интерфейс на React 19, окно Electron, CLI bb, SDK и мобильное приложение на Expo. Все они ходят в один и тот же /api/v1, поэтому тред, начатый в браузере, продолжается с телефона или из терминала. Давайте разберемся как до всех них доходят ответы агента.

Тред хранится как журнал: таблица events только дописывается, и у каждого события есть номер sequence внутри треда. Когда демон присылает новую пачку событий, сервер вставляет их и рассылает по WebSocket одно уведомление changed. Самих данных в WebSocket нет, поэтому клиент помечает свое состояние интерфейса устаревшим и дочитывает таймлайн по HTTP. Схема «уведомил, потом дочитал» проще потоковой, потому что у клиента нет своей копии треда, которая могла бы разойтись с сервером.

Свой протокол, а ACP только диалект

Чтобы события в журнале выглядели одинаково, BB приходится разговаривать с агентами, у которых совсем разные интерфейсы. Для этого у него есть собственный provider bridge protocol. Демон запускает для каждого агента отдельный процесс-мост и общается с ним по JSON-RPC 2.0 через стандартный ввод и вывод. Мост входит в плагин провайдера, демон скачивает его с сервера и перед запуском сверяет хэш.

Я ожидал, что BB построен поверх ACP, то есть Agent Client Protocol от Zed, но это не так. ACP в BB один из диалектов, которые мосты переводят в собственный протокол. Ниже в таблице указано как запускается каждый агент:

Агент

Как запускается

Как разговаривает

Claude Code

Claude Agent SDK, функция query

SDK сам поднимает CLI claude

Codex

отдельный codex app-server на каждый тред

JSON-RPC по stdio

Pi

pi --mode rpc с расширением BB

JSON по stdio, инструменты BB по fd 3 и 4

ACP-агенты

cursor-agent acp, opencode acp, hermes acp и другие

session/new, session/prompt, session/update

Нормализация идёт в два шага. Сначала мост переводит родной поток агента в небольшой набор событий: открыть ход, открыть элемент, дописать текст, закрыть элемент, сообщить расход токенов. Потом ассемблер внутри демона сам выдаёт идентификаторы ходов и элементов, следит за жизненным циклом и склеивает поток токенов окнами по 100 мс. Документация протокола описывает это разделение одной фразой: «the bridge knows the dialect, the runtime knows the timeline».

Подпись и иконку элемента присылает мост, поэтому в ядре нет таблиц с именами инструментов Claude или Codex. Нового агента можно добавить, не трогая ни ядро, ни интерфейс.

С разрешениями BB тоже не навязывает одну модель. Мост при рукопожатии сообщает поле approvalEnforcedBy, и у Claude Code там стоит provider. Это значит, что решения принимает собственный движок разрешений Claude, а BB подключается к нему через колбэк canUseTool. У Codex, Pi и ACP-агентов стоит runtime, и политику применяет сам BB. Всё, что политика не решила, приходит пользователю одинаковым запросом, у какого бы агента он ни возник.

Похоже устроены и навыки. Демон собирает все навыки в один каталог и передаёт его мосту командой skills/configure, а мост раскладывает их на язык своего агента. Для Claude Code на лету собирается Claude-плагин с симлинком на каталог навыков, Codex получает skills/extraRoots/set, Pi флаг --skill, а ACP-агенту список навыков с путями дописывается в инструкции. Поэтому навык, положенный в BB один раз, видит любой агент.

Где именно агент работает, решают environment-плагины. Встроенный git-worktree создаёт для треда отдельный worktree, а project-checkout работает прямо в чекауте проекта и не даёт сменить ветку, пока в нём есть незакоммиченные правки. Самый интересный вариант это modal-sandbox, который регистрирует не каталог, а целую машину. BB поднимает песочницу Modal, ставит туда свой демон и дальше обращается с ней как с любым другим компьютером.

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

Из чего состоит плагин

Плагин в BB это обычная папка, а его манифестом служит package.json с блоком bb. В этом блоке перечислены части плагина. Вот фрагмент package.json встроенного плагина задач:

"bb": {
  "name": "Tasks",
  "branding": { "icon": "ListTodo" },
  "server": "./server.ts",
  "app": "./app.tsx",
  "skills": ["skills"]
}

Основных частей две. Серверная часть server.ts работает внутри процесса сервера BB и через объект bb получает всё, что нужно бэкенду: хранилище, свою базу SQLite, настройки, HTTP-маршруты и фоновые задачи. Клиентская часть app.tsx встраивается прямо в окно BB, например в боковое меню или в панель треда. У мостов провайдеров есть и третья часть, которую демон запускает на машине отдельным процессом.

Провайдер агента устроен как обычный плагин, и это видно на подключении своего агента. У встроенного provider-acp есть настройка customAgents, через которую я подключал DeepSeek Harness для ролика:

bb plugin config provider-acp set customAgents '[{
  "id": "dsh",
  "displayName": "DeepSeek Harness",
  "command": "npx",
  "args": ["-y", "@deepseek-ai/dsh@latest", "--profile", "acp"]
}]'

Перезапуск после этого не нужен: плагин замечает новую настройку и регистрирует провайдера acp-dsh на лету. Учтите, что id потом не переименовать, а запись с id opencode или grok молча заменит встроенного агента. Так меняются настройки плагина, а как BB подменяет весь код плагина, разберём дальше.

Как плагин обновляется без перезапуска

Сложность здесь в самом Node. Загруженный через import модуль кэшируется, и повторный import того же файла вернёт старый код. BB обходит это хуком загрузчика registerHooks из node:module, который дописывает к адресу каждого файла внутри папки плагина метку с номером загрузки:

// apps/server/src/services/plugins/plugin-runtime.ts
const separator = resolved.url.includes("?") ? "&" : "?";
return {
  ...resolved,
  url: `${resolved.url}${separator}bbPluginLoad=${match.id}.${epoch}`,
  shortCircuit: true,
};

С новой эпохой у каждого файла новый адрес, поэтому Node загружает свежий граф модулей, а старый остаётся нетронутым. Для CommonJS-сборки то же самое делается проще: модуль грузится через require и сразу удаляется из require.cache.

Весь цикл запускает bb plugin dev. Он следит за папкой плагина, через 300 мс после правки пересобирает клиентскую часть и часть для машины и просит сервер перезагрузить плагин. Сервер собирает серверную часть в server.cjs и кэширует результат по хэшу исходников и версий SDK, BB и Node. Затем новая версия запускается на кандидатном наборе регистраций, то есть рядом со старой. Если фабрика плагина упала, сервер пишет в лог «reload failed (kept previous instance)», и старая версия продолжает работать.

Если запуск удался, старое поколение гасится в строгом порядке. Закрываются его WebSocket, останавливается процесс на машине, прерываются вызовы инструментов, а фоновые сервисы получают сигнал отмены и 5 секунд на остановку. Затем в обратном порядке выполняются хуки onDispose и закрываются базы. Старый объект bb после этого бросает PluginContextStaleError при любом обращении, поэтому забытый в старой версии таймер получит ошибку, а не запишет данные молча.

Остаётся интерфейс. Сервер рассылает по WebSocket событие plugins-changed, а адрес бандла содержит хэш содержимого, вроде /api/v1/plugin-app-assets/<hash>/app.js. Если хэш не изменился, интерфейс ничего не делает. Если изменился, он загружает модуль через import(), выгружает скрипты старого поколения и меняет регистрации, после чего React перемонтирует блоки плагина. Module federation здесь нет, хватает хэша в адресе и динамического импорта. После перезагрузки сохраняются хранилище, база, настройки и секреты, а всё, что плагин держал в памяти, пропадает.

Почему агент умеет писать плагины

Горячая перезагрузка решает только половину задачи, потому что кто-то должен ещё написать сам плагин. Эту часть закрывает встроенный плагин bb-guide. Через agents.contributeInstructions он добавляет каждому агенту введение в BB, а через agents.configure включает навык bb-plugin-authoring. Навык ведёт примерно по 19 справочникам о бэкенде, интерфейсе и жизненном цикле плагина, а главы руководства агент открывает командой bb guide.

Получается замкнутый цикл. Агент внутри BB знает SDK и правит плагин, bb plugin dev пересобирает его, и изменение появляется в том же окне, где агент работает. Так и появился плагин из ролика: агенту я описывал только экран и процесс, а устройство платформы он узнал из навыка.

Безопасность и минусы

У такой открытости есть своя цена:

  1. На публичном API /api/v1 проверки пользователя нет, есть только защита от CSRF и DNS rebinding. Документация пишет об этом прямо: «The public API is unauthenticated and permits command execution and file reads». Защищает только то, что сервер по умолчанию слушает 127.0.0.1, поэтому открывать его порт в интернет нельзя.

    Снаружи BB открывается через BB Connect или Tailscale. BB Connect устроен как исходящий туннель: плагин внутри сервера держит WebSocket к Cloudflare Worker на *.getbb.app, и через него идут все HTTP-запросы и WebSocket-соединения. Вход там через GitHub, и шлюз пускает только владельца сервера, а остальным отвечает 403. Машины подключаются с отдельным ключом, и менять список машин им запрещено.

  2. Плагины без песочницы: серверная часть работает в процессе BB, а интерфейс в том же окне. Модель доверия такая же, как у npm-пакета, и из защиты есть только запрет нативных зависимостей, установка с --ignore-scripts и аварийный режим bb plugin safe-mode. Поэтому сторонний плагин стоит читать так же внимательно, как незнакомый npm-пакет. Остальные минусы связаны с молодостью проекта. Многие точки расширения носят префикс experimental_, пользователь в BB один, ролей и совместной работы нет. Стабильная десктопная версия есть только для Мака на Apple Silicon, на других системах BB работает через сервер и браузер.

Что стоит утащить к себе

Даже если BB вам не нужен, в его коде есть решения, которые пригодятся в своих проектах:

  1. Узкая грамматика событий с ассемблером отделяет диалекты агентов от интерфейса.

  2. Схема «уведомил, потом дочитал» избавляет клиентов от собственной копии состояния.

  3. Запуск нового поколения модулей рядом со старым и откат при падении делают горячую перезагрузку безопасной для работающего сервера.

Код лежит в репозитории get-bb/bb, а как всё это выглядит в работе, я показывал в ролике. Мне кажется, самое ценное в BB не окно для агентов, а то, что программу можно менять тем же агентом, который в ней работает.