Architektura
Przegląd
Dział zatytułowany „Przegląd”Control Center stosuje Clean Architecture w podejściu feature-first ze wzorcem portów i adapterów. Kod jest zorganizowany wokół domen biznesowych (funkcji), a ścisłe reguły zależności wymusza zautomatyzowany test architektury.
Cienki klient nad serwerem headless
Dział zatytułowany „Cienki klient nad serwerem headless”Control Center to architektura cienkiego klienta. Żaden klient nie otwiera bazy danych — dane należą do procesu headless cc_server (mały plik global.db plus po jednym pliku SQLite na obszar roboczy, otwieranym leniwie), który udostępnia je przez RPC po WebSocket. Aplikacja desktopowa, wersja webowa i towarzysz na telefonie to wyłącznie renderery działające na tym jednym połączeniu RPC.
Aplikacja desktopowa przy pierwszym uruchomieniu wybiera, jak łączy się ze swoim serwerem: lokalnie (uruchamia cc_server na tej maszynie i rozmawia przez interfejs loopback — domyślnie) albo zdalnie (łączy się z cc_server gdzie indziej przez bezpieczny WebSocket). Klient webowy jest zawsze zdalny. Resolver startowy (bootstrap/server_backend.dart) wybiera ścieżkę, zanim istnieje Riverpod, a następnie nadpisuje rpcClientProvider podłączonym klientem — dzięki temu każda funkcja czyta i zapisuje przez serwer, a nie przez lokalną bazę danych.
Sama warstwa RPC jest bezstanowa: sesja jest uwierzytelniona, ale nie jest powiązana z obszarem roboczym. Każde wywołanie w zakresie obszaru roboczego niesie własny workspace_id w argumentach, a dyspozytor odrzuca takie wywołanie bez niego. Właśnie dzięki temu dwa klienty na jednym serwerze mogą mieć różne aktywne obszary robocze, bez trzymanego po stronie serwera „bieżącego obszaru roboczego”, o który mogłyby się spierać — a izolacja jest wymuszana przy każdym wywołaniu, nie przy połączeniu.
To pojedynczy Dart workspace współdzielący jeden rozwiązany plik lockfile, liczący dwadzieścia jeden członów — piętnaście pakietów plus sześć aplikacji — z aplikacją desktopową i webową (control_center) jako korzeniem workspace. Sześć aplikacji to binarka serwera headless (cc_server), headless wykonawca floty (cc_worker), PWA na telefon (cc_remote), broker przekaźnika WebSocket (cc_signaling_server), galeria Widgetbook (cc_gallery) oraz publiczny host demo (cc_demo_server). Piętnaście pakietów to cc_ui (system projektowy), cc_domain (współdzielony kernel w czystym Darcie: encje, obiekty wartości, porty, zdarzenia i warstwa domenowa każdej funkcji), cc_harness (wbudowany kernel agenta: pętla agenta w czystym Darcie — wiadomości, port dostawcy, narzędzia, kompaktowanie, sterowanie, hooki, subagenci — bez dart:io i bez żadnej innej zależności cc_*), cc_harness_runtime (wbudowany adapter środowiska wykonawczego: dostawcy strumieniowania działający tylko na VM, broker poświadczeń OAuth/PKCE, magazyny poświadczeń i ogólny zestaw narzędzi — uruchamia agentów bez zainstalowanego zewnętrznego CLI), cc_rpc (transporty klienta), cc_host (kernel serwera), cc_data (zdalne repozytoria), cc_persistence (baza danych po stronie serwera), cc_server_core (kompozycja serwera aplikacji + środowisko wykonawcze tożsamości/obecności/floty/ewaluacji), cc_infra (adaptery działające tylko na VM), cc_mcp (narzędzia MCP), cc_mcp_client (klient zewnętrznych serwerów MCP, włączający ich narzędzia do lokalnego rejestru), cc_markdown (silnik markdown wbudowany w repozytorium), cc_natives (liść FFI) oraz system_audio_capture. Pełną matrycę klientów i to, co działa gdzie, znajdziesz w Wdrażanie i klienci.
Reguła zależności
Dział zatytułowany „Reguła zależności”Presentation → Application/Providers → Domain ← Infrastructure- Warstwa domenowa: encje, obiekty wartości, interfejsy repozytoriów, porty i usługi domenowe w czystym Darcie. Zero importów infrastruktury (żadnego dio, drift ani modeli sieciowych).
- Warstwa prezentacji: ekrany, widżety, notifiery. Brak bezpośredniego dostępu do drift/DAO/warstwy danych; wszystko przechodzi przez providery Riverpod → repozytoria.
- Warstwa infrastruktury: konkretne implementacje portów domenowych i interfejsów repozytoriów. Zależy od domeny, nigdy odwrotnie.
Struktura funkcji
Dział zatytułowany „Struktura funkcji”Pełny kształt funkcji to:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersNiewiele funkcji w lib/features/ faktycznie ma wszystkie cztery katalogi, a te luki są informacyjne, nie przypadkowe. Współdzielony kernel wchłonął domain/ każdej funkcji do cc_domain, a cienki klient nie zawiera żadnych implementacji repozytoriów, więc większość funkcji w lib/ to wyłącznie presentation/ plus providers/ i nic więcej. Katalog data/ albo domain/, który przetrwał w kliencie, oznacza coś rzeczywiście działającego po stronie klienta.
Warto znać dwa nazwane wyjątki:
mcpto tylko providery. Jego interfejs ustawień i statusu mieszka wsettings/, a same narzędzia są pakietem (cc_mcp), nie katalogiem funkcji.orchestrationiplan_studiomają tylkopresentation/iproviders/.
Funkcja guardrails umieszcza swoją warstwę domenową w cc_domain/features/guardrails/, a nie w głównej aplikacji, ponieważ wymusza ją serwer: reguła mapuje ActionClass (albo powłokowy commandPrefix) na decyzję — allow, prompt lub deny — w danym zasięgu, reguły są przechowywane dla każdego obszaru roboczego, a każde objęte nimi działanie agenta przechodzi przez łańcuch polityk, zanim zostanie wykonane. Rozstrzyganie idzie po space > agent > workspace > mode preset > built-in default, a decyduje pierwszy zasięg z pasującą regułą; najbardziej restrykcyjna reguła jest jedynie rozstrzygnięciem przy remisie w obrębie jednego zasięgu oraz przy łączeniu kilku klas, które deklaruje jedno działanie. Powierzchnią dla operatora są Ustawienia → Obszar roboczy → Uprawnienia agentów, pod /workspaces/<id>/settings/workspace/permissions. Model opisują Guardraile.
Współdzielony kernel
Dział zatytułowany „Współdzielony kernel”packages/cc_domain/lib/core/domain/ zawiera encje i repozytoria współdzielone przez 3 lub więcej funkcji. To czysty Dart bez żadnych zależności infrastrukturalnych, dzięki czemu zarówno klient Flutter, jak i niezależna od Fluttera binarka serwera mogą importować te same definicje:
- Encje podstawowe:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Tożsamość i wieloosobowość:
User,Principal(sealedUserPrincipal|AgentPrincipal),WorkspaceMember,WorkspaceRole - Encje pamięci:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Współdzielone obiekty wartości:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Współdzielone porty:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Usługi domenowe:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ typy zdarzeń (obszar roboczy/agent, PR/recenzja, wiadomości, zgłoszenia, potok, orkiestracja, pamięć, kalendarz/spotkania, tożsamość i członkostwo, obserwowalność)
Tożsamość i wieloosobowość
Dział zatytułowany „Tożsamość i wieloosobowość”Control Center jest wieloużytkownikowy: ludzie i agenci to równorzędni aktorzy zjednoczeni przez Principal. Członek obszaru roboczego to User powiązany z Workspace na poziomie WorkspaceRole (owner/admin/member/viewer/guest). Członkostwo jest granicą dostępu — nie klucz parowania — a uprawnienia na poziomie repozytoriów nie pozwalają, by członkostwo w obszarze roboczym dawało więcej uprawnień niż sam host kodu. Pierwszy użytkownik utworzony na świeżym serwerze jest właścicielem instalacji; pozostali dołączają przez zaproszenie lub OIDC. Utworzenie obszaru roboczego oznacza tego użytkownika jako owner.
Współpraca w czasie rzeczywistym opiera się na serwerze autorytatywnym i regule last-writer-wins (LWW) na poziomie poszczególnych pól, a nie na CRDT. Obecność (kto tu jest, gdzie się znajduje, kursory, pisanie, bieżący status agenta i koszt uruchomienia) to osobny ulotny kanał, który nigdy nie jest utrwalany; stan trwały jedzie po szkielecie optymistycznych mutacji + przebudowy po stronie serwera + LWW. Ludzie i agenci dzielą jeden skład. Przy jednym operatorze-człowieku kanał obecności pozostaje bezczynny i nie pojawiają się żadne elementy interfejsu związane ze składem — wieloosobowość nic nie kosztuje użytkownika pracującego solo.
Porty i adaptery
Dział zatytułowany „Porty i adaptery”Porty to abstrakcyjne interfejsy w warstwie domenowej. Adaptery to konkretne implementacje w infrastrukturze. Ponieważ serwer headless jest właścicielem danych i musi budować się bez Fluttera, niemal każdy adapter działa tylko na VM i mieszka w packages/cc_infra/lib/src/, uporządkowany według zagadnień:
| Port | Lokalizacja adaptera |
|---|---|
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/ (po stronie klienta: renderuje zdarzenia z serwera lokalnie) |
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 i GitHub Issues to adaptery synchronizacji zgłoszeń w cc_infra/lib/src/tickets/sync/; lokalne zgłoszenia mieszkają w bazie danych obszaru roboczego bez TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (usługa domenowa w czystym Darcie — nie potrzebuje infrastruktury) |
Korzeń kompozycji aplikacji (di/providers.dart) wiąże porty po stronie klienta z implementacjami przez providery Riverpod; serwer komponuje swoje adaptery w cc_server_core.
Zarządzanie stanem
Dział zatytułowany „Zarządzanie stanem”Riverpod dla całego stanu klienta:
Notifier<T>iAsyncNotifier<T>do stanu zmiennegoFutureProvider<T>do danych asynchronicznych- Listy i wiersze na żywo przychodzą przez subskrypcje RPC jako
AsyncValue<List<T>>— serwer wewnętrznie używa Drift.watch(); żaden klient nie otwiera Drift - Narzędzia MCP otrzymują zależności jako typowane parametry konstruktora, nigdy przez
Ref
Baza danych
Dział zatytułowany „Baza danych”Drift (SQLite), podzielony na dwie bazy danych, obie w cc_persistence:
GlobalDatabase(<dataDir>/global.db) — krótka lista tabel naprawdę wspólnych dla całego serwera: rejestr obszarów roboczych, użytkownicy/preferencje/sparowane urządzenia, połączenia SSO, ustawienia serwera dla całej instalacji, zarządzane polityki działań (ograniczenie dla całej instalacji, które może tylko zaostrzać guardraile obszarów roboczych), newsfeed dla każdego użytkownika, kolejka floty (log workerów/zadań/przydziałów), indeksworkspace_routesużywany przed uwierzytelnieniem oraz metadane instalacji. Rozruch otwiera tylko ten plik, więc koszt startu pozostaje stały niezależnie od tego, ile historii zgromadzą obszary robocze. Dodanie tutaj tabeli to decyzja o izolacji, którą test zapadkowy zmusza cię uzasadnić.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — jeden katalog na obszar roboczy, zawierający jeden plik SQLite oraz wszystko inne, co należy wyłącznie do tego obszaru roboczego, dzięki czemu worktree rozmowy i pliki agentów leżą obok bazy danych i są usuwane razem z nią. Mieszka tu niemal wszystko: agenci, przestrzenie, zgłoszenia, pamięć, potoki, spotkania, graf kodu, recenzje, repozytoria. Pliki otwierają się leniwie przy pierwszym użyciu przezWorkspaceDatabaseManager.of(workspaceId)i dopiero wtedy płacą za własnyquick_check, instalację FTS/wyzwalaczy orazvector_init.
Obie połowy zaczęły od spłaszczonej bazy wyjściowej v1 — onCreate buduje bieżący schemat — a kroki migracji dopisuje się tylko wtedy, gdy wdrożoną bazę danych trzeba przenieść dalej.
Ponieważ jeden obszar roboczy to jeden plik, wyeksportowanie obszaru roboczego to pojedyncze polecenie VACUUM INTO (workspace.export), a nie zrzut tabela po tabeli; workspace.import przyjmuje taki plik i korzysta z osadzonego workspace_meta, aby odróżnić ponowne przyjęcie własnego pliku od pliku pochodzącego z innej instalacji.
Obie bazy danych należą do cc_persistence i otwiera je wyłącznie cc_server; żaden klient nie otwiera bazy danych — każdy klient czyta i zapisuje przez RPC. Tabele są zdefiniowane w packages/cc_persistence/lib/database/tables/, DAO w .../daos/; encje domenowe to czysty Dart, oddzielony od klas tabel Drift, z mapowaniem w warstwach danych. FTS5 obsługuje wyszukiwanie pełnotekstowe, a sqlite_vector osadzenia — oba tylko po stronie serwera; obie bazy działają z PRAGMA foreign_keys=ON w trybie dziennika WAL.
Trasowanie
Dział zatytułowany „Trasowanie”go_router, z jednym faktem, który rządzi resztą: każde miejsce docelowe w aplikacji ma prefiks obszaru roboczego w postaci /workspaces/:workspaceId/…. Identyfikator obszaru roboczego w adresie URL jest jedynym źródłem prawdy o aktywnym obszarze roboczym — activeWorkspaceIdProvider jest sterowany trasą, a nie odwrotnie — dlatego odczyt danych w zakresie obszaru roboczego poza trasą z prefiksem nie ma skąd wziąć swojego identyfikatora.
- Funkcje budujące trasy w
router/routes.dart, każda przyjmuje identyfikator obszaru roboczego jako pierwszy argument, a nie stałe tekstowe - Cztery trasy sprzed kontekstu nie mają prefiksu:
/splash,/onboarding,/signed-outi/workspaces(wybór obszaru roboczego) ShellRouteopakowuje powłokę aplikacji; te cztery widoki renderują się na pełnym ekranie poza nią- Strażnik uwierzytelniania: nieukończona konfiguracja trafia do
/onboarding; ukończona konfiguracja, której poświadczenie hosta kodu zniknęło, trafia do/signed-out. Ukończona oznacza, że dla zalogowanego użytkownika podłączono co najmniej jeden host kodu i istnieje co najmniej jeden obszar roboczy — niekoniecznie GitHub, a CLIghnie jest metodą uwierzytelniania - Brama onboardingu w
features/auth/providers/
Sieć (po stronie serwera)
Dział zatytułowany „Sieć (po stronie serwera)”Cały zewnętrzny ruch HTTP mieszka w serwerze. Klienci nigdy nie łączą się bezpośrednio z GitHubem, Linear ani Google — wywołują operacje RPC serwera, a nawet zdalne multimedia są pobierane przez punkt końcowy /proxy/media serwera, a nie z pierwotnego źródła.
- Klienty dio w
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClientiGoogleCalendarApiClient - Wstrzykiwanie tokenów uwierzytelniania przez interceptory, w tym interceptor Google OAuth dla poszczególnych kont w
cc_server_core, który odświeża tokeny przy kodzie 401 - Wszystkie błędy mapowane przez
packages/cc_infra/lib/src/network/error_mapper.dart→ typowane podklasyAppExceptionzdefiniowane wcc_domain
Bezpieczeństwo
Dział zatytułowany „Bezpieczeństwo”Serwer przechowuje poświadczenia, które mają znaczenie: tokeny GitHub i systemów zgłoszeń do uruchamiania agentów, klucze dostawców LLM oraz tokeny OAuth — wszystko to mieszka w katalogu danych serwera lub w jego środowisku. Pęk kluczy klienta wciąż przechowuje tylko jego własne poświadczenie urządzenia i klucz parowania ze zdalnym serwerem.
- Sekrety w
flutter_secure_storage(pęk kluczy macOS, menedżer poświadczeń Windows, libsecret w Linuksie) — poświadczenie urządzenia klienta i klucze parowania ze zdalnym serwerem, a nie tokeny dostawców shared_preferenceswyłącznie do niewrażliwych preferencji (motyw, czcionka, układ)- Pozostałe sekrety dostawców ze starszych instalacji są jednorazowo przekazywane serwerowi przez
features/auth/providers/credential_migration.dart, a następnie usuwane z pęku kluczy klienta
Egzekwowanie architektury
Dział zatytułowany „Egzekwowanie architektury”Ograniczenia architektury weryfikuje test/core/architecture_constraints_test.dart, który kończy się niepowodzeniem, gdy reguły zależności zostaną naruszone.
Powiązane koncepcje
Dział zatytułowany „Powiązane koncepcje”- Obszary robocze i izolacja: jak wymuszana jest izolacja
- Cykl życia wysyłania agentów: przepływ wysyłania
- Zdarzenia domenowe: komunikacja między funkcjami
- Wdrażanie i klienci: model serwera z cienkim klientem i matryca klientów