Архітектура
Control Center дотримується Clean Architecture з первинністю фіч (feature-first) і патерном «порти та адаптери». Кодову базу організовано навколо бізнес-доменів (фіч), а суворі правила залежностей перевіряє автоматизований тест архітектури.
Тонкий клієнт поверх headless-сервера
Розділ із назвою «Тонкий клієнт поверх headless-сервера»Control Center має архітектуру тонкого клієнта. Жоден клієнт не відкриває базу даних — дані належать headless-процесу cc_server (невелика база global.db плюс один файл SQLite на робочий простір, що відкривається ліниво), і він віддає їх через WebSocket RPC. Десктопний застосунок, вебзбірка та застосунок-компаньйон для телефона — усі вони лише рендерери поверх цього єдиного RPC-з’єднання.
Під час першого запуску десктоп обирає, як він з’єднується зі своїм сервером: локально (він запускає cc_server на цій машині та спілкується через loopback — типовий варіант) або віддалено (він під’єднується до cc_server деінде через захищене WebSocket-з’єднання). Вебклієнт завжди працює віддалено. Розв’язувач завантаження (bootstrap/server_backend.dart) обирає шлях ще до появи Riverpod, а потім перевизначає rpcClientProvider під’єднаним клієнтом — тож кожна фіча читає й записує через сервер, а не через локальну базу даних.
Сам шар RPC є безстановим: сесія проходить автентифікацію, але не прив’язана до робочого простору. Кожен виклик у межах робочого простору передає власний workspace_id у своїх аргументах, і диспетчер відхиляє такий виклик без нього. Саме це дає змогу двом клієнтам на одному сервері тримати різні активні робочі простори без серверного «поточного робочого простору», щодо якого вони могли б розійтися, — і означає, що ізоляція забезпечується на рівні виклику, а не з’єднання.
Це Dart-воркспейс з єдиним зафіксованим lockfile, що налічує двадцять одного члена — п’ятнадцять пакетів і шість застосунків, — а коренем воркспейсу є застосунок для десктопа й вебу (control_center). Шість застосунків — це бінарник headless-сервера (cc_server), headless-виконавець флоту (cc_worker), PWA для телефона (cc_remote), брокер-ретранслятор WebSocket (cc_signaling_server), галерея Widgetbook (cc_gallery) і хост публічної демонстрації (cc_demo_server). П’ятнадцять пакетів — це cc_ui (система дизайну), cc_domain (спільне ядро на чистому Dart: сутності, об’єкти-значення, порти, події та доменний шар кожної фічі), cc_harness (вбудоване ядро агента: цикл агента на чистому Dart — повідомлення, порт провайдера, інструменти, компакція, керування, хуки, субагенти — без dart:io і без жодної іншої залежності cc_*), cc_harness_runtime (адаптер середовища виконання вбудованого harness: потокові провайдери лише для VM, брокеринг облікових даних OAuth/PKCE, сховища облікових даних і загальний набір інструментів — запускає агентів без установленого зовнішнього CLI), cc_rpc (транспорти клієнта), cc_host (ядро сервера), cc_data (віддалені репозиторії), cc_persistence (серверна БД), cc_server_core (композиція застосунку й сервера + середовище виконання ідентичності/присутності/флоту/eval), cc_infra (адаптери лише для VM), cc_mcp (інструменти MCP), cc_mcp_client (клієнт для зовнішніх MCP-серверів, який приєднує їхні інструменти до локального реєстру), cc_markdown (рушій markdown у репозиторії), cc_natives (листовий пакет FFI) і system_audio_capture. Про повну матрицю клієнтів і те, що де працює, див. Розгортання та клієнти.
Правило залежностей
Розділ із назвою «Правило залежностей»Presentation → Application/Providers → Domain ← Infrastructure- Доменний шар: сутності, об’єкти-значення, інтерфейси репозиторіїв, порти й доменні сервіси на чистому Dart. Жодних імпортів з інфраструктури (ані dio, ані drift, ані мережевих моделей).
- Шар представлення: екрани, віджети, нотифікатори. Без прямого доступу до drift, DAO чи шару даних; усе проходить через провайдери Riverpod → репозиторії.
- Інфраструктурний шар: конкретні реалізації доменних портів та інтерфейсів репозиторіїв. Залежить від домену й ніколи навпаки.
Структура фічі
Розділ із назвою «Структура фічі»Повна структура фічі така:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersДуже мало фіч під lib/features/ справді мають усі чотири каталоги, і ці прогалини радше інформативні, ніж випадкові. Спільне ядро поглинуло domain/ кожної фічі в cc_domain, а тонкий клієнт не містить реалізацій репозиторіїв, тож більшість фіч під lib/ — це presentation/ плюс providers/ і більше нічого. Каталог data/ або domain/, що залишився в клієнті, позначає щось справді клієнтське.
Варто знати про два винятки з назвами:
mcpмістить лише провайдери. Його інтерфейс налаштувань і статусу живе підsettings/, а сам набір інструментів — це пакет (cc_mcp), а не тека фічі.orchestrationіplan_studioмістять лишеpresentation/іproviders/.
Фіча guardrails виносить свій доменний шар у cc_domain/features/guardrails/, а не в кореневий застосунок, бо його забезпечує сервер: правило зіставляє ActionClass (або shell-commandPrefix) із рішенням — allow, prompt чи deny — у певній області застосування, правила зберігаються для кожного робочого простору, і кожна підконтрольна дія агента перед запуском розв’язується через ланцюжок політик. Розв’язання проходить шлях space > agent > workspace > mode preset > built-in default, і вирішує перша область застосування з відповідним правилом; найсуворіше правило лише розв’язує нічию в межах однієї області та потрібне для об’єднання кількох класів, які оголошує одна дія. Інтерфейс для оператора — Налаштування → Робочий простір → Дозволи агента, за шляхом /workspaces/<id>/settings/workspace/permissions. Модель описано в розділі Обмеження.
Спільне ядро
Розділ із назвою «Спільне ядро»packages/cc_domain/lib/core/domain/ містить сутності та репозиторії, спільні для трьох і більше фіч. Це чистий Dart без жодних інфраструктурних залежностей — саме завдяки цьому і Flutter-клієнт, і серверний бінарник без Flutter можуть імпортувати ті самі визначення:
- Основні сутності:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Ідентичність і спільна робота:
User,Principal(sealedUserPrincipal|AgentPrincipal),WorkspaceMember,WorkspaceRole - Сутності пам’яті:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Спільні об’єкти-значення:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Спільні порти:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Доменні сервіси:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ типи подій (робочий простір/агент, PR/перевірка, повідомлення, тікети, конвеєр, оркестрація, пам’ять, календар/зустрічі, ідентичність і членство, спостережуваність)
Ідентичність і спільна робота
Розділ із назвою «Ідентичність і спільна робота»Control Center — багатокористувацька система: люди й агенти є рівноправними дійовими особами, об’єднаними через Principal. Учасник робочого простору — це User, прив’язаний до Workspace у межах WorkspaceRole (owner/admin/member/viewer/guest). Межа доступу — це членство, а не ключ прив’язки, і дозволи на рівні репозиторію не дають членству в робочому просторі перевищити права хоста коду. Перший користувач, створений на чистому сервері, володіє інсталяцією; інші приєднуються за запрошенням або через OIDC. Створення робочого простору позначає цього користувача як owner.
Спільна робота в реальному часі — це авторитетний сервер + last-writer-wins (LWW) для кожного поля, а не CRDT. Присутність (хто тут, де саме, курсори, набір тексту, поточний статус агента й поточні витрати) — це окрема ефемерна смуга, яка ніколи не зберігається; довготривалий стан тримається на основі оптимістичних змін + повторного узгодження на сервері + LWW. Люди й агенти поділяють один реєстр. Коли оператор лише один, смуга присутності простоює і жодних елементів інтерфейсу реєстру не з’являється — спільна робота нічого не коштує користувачу-одинаку.
Порти та адаптери
Розділ із назвою «Порти та адаптери»Порти — це абстрактні інтерфейси в доменному шарі. Адаптери — конкретні реалізації в інфраструктурі. Оскільки headless-сервер володіє даними й має збиратися без Flutter, майже кожен адаптер призначений лише для VM і лежить у packages/cc_infra/lib/src/, згрупований за призначенням:
| Порт | Розташування адаптера |
|---|---|
SandboxPort |
cc_infra/lib/src/sandboxing/ (native_sandbox_adapter.dart) |
CredentialBrokerPort |
cc_infra/lib/src/sandboxing/ (env_credential_broker.dart, github_fine_grained_broker.dart) |
GitRepoInspectorPort |
cc_infra/lib/src/git/ (git_repo_inspector.dart) |
EmbeddingPort |
cc_infra/lib/src/embedding/ (embedding_service.dart) |
NotificationPort |
lib/core/notifications/ (на боці клієнта: локально рендерить серверні події) |
AgentBackend |
cc_infra/lib/src/dispatch/backends/ (acp_backend.dart, harness_backend.dart, cli_backends.dart) |
TicketProviderPort |
cc_infra/lib/src/tickets/linear/ (LinearTicketAdapter). Jira, ClickUp і GitHub Issues — це адаптери синхронізації тікетів у cc_infra/lib/src/tickets/sync/; локальні тікети живуть у базі даних робочого простору без TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (доменний сервіс на чистому Dart — інфраструктура йому не потрібна) |
Корінь композиції застосунку (di/providers.dart) зв’язує клієнтські порти з реалізаціями через провайдери Riverpod; сервер збирає свої адаптери в cc_server_core.
Керування станом
Розділ із назвою «Керування станом»Riverpod для всього стану клієнта:
Notifier<T>іAsyncNotifier<T>для змінюваного стануFutureProvider<T>для асинхронних даних- Списки та живі рядки надходять через RPC-підписки як
AsyncValue<List<T>>— усередині сервер використовує Drift.watch(); жоден клієнт не відкриває Drift - Інструменти MCP отримують залежності як типізовані параметри конструктора, ніколи не
Ref
База даних
Розділ із назвою «База даних»Drift (SQLite), розділений на дві бази даних, обидві в cc_persistence:
GlobalDatabase(<dataDir>/global.db) — короткий список таблиць, справді спільних для всього сервера: реєстр робочих просторів, користувачі/налаштування/прив’язані пристрої, підключення SSO, серверні налаштування всієї інсталяції, керовані політики дій (затискач на рівні інсталяції, який може лише посилювати обмеження робочих просторів), стрічка новин для кожного користувача, черга флоту (воркери/завдання/журнал розміщення), індексworkspace_routesдо автентифікації та метадані інсталяції. Завантаження відкриває лише цей файл, тож витрати на запуск залишаються незмінними, скільки б історії не накопичили робочі простори. Додавання таблиці сюди — це рішення про ізоляцію, яке ratchet-тест змушує вас обґрунтувати.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — один каталог на робочий простір, що містить один файл SQLite та все інше, що належить лише цьому робочому простору, тож worktree розмови й файли агентів лежать поряд із базою даних і видаляються разом із нею. Тут живе майже все: агенти, простори, тікети, пам’ять, конвеєри, зустрічі, граф коду, перевірки, репозиторії. Файли відкриваються ліниво при першому зверненні черезWorkspaceDatabaseManager.of(workspaceId)і тоді ж виконують власніquick_check, встановлення FTS/тригерів таvector_init.
Обидві половини починалися зі зведеної базової версії v1 — onCreate будує поточну схему — а кроки міграції додаються лише тоді, коли розгорнуту базу даних треба перенести далі.
Оскільки один робочий простір — це один файл, експорт робочого простору є одним виразом VACUUM INTO (workspace.export), а не дампом таблиця за таблицею; workspace.import приймає такий файл і за вбудованим workspace_meta розрізняє повторно прийнятий власний файл від файла з іншої інсталяції.
Обидві бази даних належать cc_persistence, і відкриває їх лише cc_server; жоден клієнт не відкриває БД — кожен клієнт читає й записує через RPC. Таблиці визначено в packages/cc_persistence/lib/database/tables/, DAO — у .../daos/; доменні сутності — це чистий Dart, окремо від класів таблиць Drift, а зіставлення відбувається в шарах даних. FTS5 відповідає за повнотекстовий пошук, а sqlite_vector — за ембеддинги; і те, й інше працює лише на сервері. Обидві бази даних працюють із PRAGMA foreign_keys=ON у режимі журналу WAL.
Маршрутизація
Розділ із назвою «Маршрутизація»go_router, і одна особливість визначає все інше: кожен внутрішній маршрут застосунку має префікс робочого простору у вигляді /workspaces/:workspaceId/…. Ідентифікатор робочого простору в URL — єдине джерело істини для активного робочого простору: activeWorkspaceIdProvider керується маршрутом, а не навпаки, — саме тому читання даних у межах робочого простору поза маршрутом із префіксом не має звідки взяти його ідентифікатор.
- Функції-білдери маршрутів у
router/routes.dart, кожна бере ідентифікатор робочого простору як перший аргумент, а не рядкові константи - Чотири маршрути до контексту не мають префікса:
/splash,/onboarding,/signed-outі/workspaces(засіб вибору) ShellRouteобгортає оболонку застосунку; ці чотири екрани рендеряться на весь екран поза нею- Вартовий автентифікації: незавершене налаштування веде на
/onboarding; завершене налаштування, у якого зникли облікові дані хоста коду, веде на/signed-out. Завершене означає, що для користувача, який увійшов, підключено щонайменше один хост коду і існує щонайменше один робочий простір — не обов’язково саме GitHub, і CLIghне є способом автентифікації - Шлюз онбордингу в
features/auth/providers/
Мережа (на боці сервера)
Розділ із назвою «Мережа (на боці сервера)»Увесь зовнішній HTTP живе на сервері. Клієнти ніколи не звертаються до GitHub, Linear чи Google напряму — вони викликають RPC-операції сервера, і навіть віддалені медіа завантажуються через серверну кінцеву точку /proxy/media, а не з початкового джерела.
- Клієнти dio в
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClientіGoogleCalendarApiClient - Вставлення токенів автентифікації через перехоплювачі, зокрема перехоплювач Google OAuth для кожного облікового запису в
cc_server_core, який оновлює токени при 401 - Усі помилки проходять через
packages/cc_infra/lib/src/network/error_mapper.dart→ типізовані підкласиAppException, визначені вcc_domain
Безпека
Розділ із назвою «Безпека»Сервер зберігає облікові дані, які мають значення: токени GitHub і тікет-систем для запусків агентів, ключі провайдерів LLM і токени OAuth живуть у каталозі даних сервера або в його середовищі. У ланцюжку ключів клієнта залишаються лише його власні облікові дані пристрою та ключ прив’язки до віддаленого сервера.
- Секрети у
flutter_secure_storage(ланцюжок ключів macOS, диспетчер облікових даних Windows, libsecret у Linux) — облікові дані пристрою клієнта та ключі прив’язки до віддаленого сервера, а не токени провайдерів shared_preferencesлише для нечутливих налаштувань (тема, шрифт, компонування)- Секрети провайдерів, що залишилися від старіших інсталяцій,
features/auth/providers/credential_migration.dartодин раз передає серверу, а потім видаляє з ланцюжка ключів клієнта
Дотримання архітектури
Розділ із назвою «Дотримання архітектури»Обмеження архітектури перевіряє test/core/architecture_constraints_test.dart; він падає, якщо правила залежностей порушено.
Пов’язані поняття
Розділ із назвою «Пов’язані поняття»- Робочі простори та ізоляція: як забезпечується ізоляція
- Життєвий цикл запуску агента: перебіг запуску
- Доменні події: комунікація між фічами
- Розгортання та клієнти: модель тонкого клієнта із сервером і матриця клієнтів