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

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

Два слоя BB: агенты снизу, одно окно сверху
Два слоя BB: агенты снизу, одно окно сверху

Идея 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 и перезапускается. Для второй машины это важно, потому что обновлять её руками после каждого релиза сервера не нужно.

Процессы BB: клиенты, сервер, демон и агенты
Процессы BB: клиенты, сервер, демон и агенты

Клиентов у сервера может быть несколько: веб-интерфейс на 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 не окно для агентов, а то, что программу можно менять тем же агентом, который в ней работает.