Přeskočit na obsah

Architektura

Control Center staví na Clean Architecture členěné podle domén, se vzorem portů a adaptérů. Kód je uspořádaný podle obchodních domén (v repozitáři features) a přísná pravidla závislostí vynucuje automatický test architektury.

Tenký klient nad bezhlavým serverem

Sekce “Tenký klient nad bezhlavým serverem”

Control Center je architektura tenkého klienta. Žádný klient databázi neotevírá. Bezhlavý proces cc_server vlastní data (malý global.db a jeden soubor SQLite na pracovní prostor, otevíraný líně) a podává je přes WebSocket RPC. Aplikace pro počítač, webové sestavení a telefonní společník jsou vykreslovače nad tím jedním RPC spojením.

Počítač si při prvním spuštění vybere, jak se k serveru dostane: místně (na tomto stroji spustí cc_server a mluví přes loopback, to je výchozí) nebo vzdáleně (připojí se k cc_server jinde přes zabezpečený WebSocket). Webový klient je vždy vzdálený. Resolver při startu (bootstrap/server_backend.dart) vybere cestu ještě předtím, než existuje Riverpod, a pak přepíše rpcClientProvider připojeným klientem. Každá doména proto čte a zapisuje přes server, ne do místní databáze.

Vrstva RPC je bezstavová: relace je ověřená, ale není vázaná na pracovní prostor. Každé volání vázané na pracovní prostor nese ve svých argumentech vlastní workspace_id a dispečer volání bez něj odmítne. Díky tomu můžou dva klienti na jednom serveru držet různé aktivní pracovní prostory a server nedrží „aktuální pracovní prostor“, o kterém by se neshodli. Izolace se vynucuje u každého volání, ne u spojení.

Je to jedno Dart workspace s vyřešeným lockfilem a jednadvaceti členy, patnáct balíčků a šest aplikací, a kořenem je aplikace pro počítač a web (control_center). Šest aplikací tvoří bezhlavá serverová binárka (cc_server), bezhlavý vykonavatel flotily (cc_worker), telefonní PWA (cc_remote), zprostředkovatel WebSocket relay (cc_signaling_server), galerie Widgetbooku (cc_gallery) a hostitel veřejného dema (cc_demo_server). Patnáct balíčků je cc_ui (design systém), cc_domain (sdílené jádro v čistém Dartu: entity, hodnotové objekty, porty, události a doménová vrstva každé domény), cc_harness (vestavěné jádro agenta: smyčka agenta v čistém Dartu, tedy zprávy, port poskytovatele, nástroje, kompakce, řízení, hooky a subagenti, bez dart:io a bez jiné závislosti cc_*), cc_harness_runtime (vestavěný adaptér běhu harnessu: streamující poskytovatelé jen pro VM, zprostředkování přihlašovacích údajů OAuth/PKCE, úložiště přihlašovacích údajů a obecná sada nástrojů; agenti běží bez nainstalovaného cizího CLI), cc_rpc (klientské transporty), cc_host (jádro serveru), cc_data (vzdálené repozitáře), cc_persistence (databáze na straně serveru), cc_server_core (kompozice aplikačního serveru a běh identity, přítomnosti, flotily a evalů), cc_infra (adaptéry jen pro VM), cc_mcp (nástroje MCP), cc_mcp_client (klient externích MCP serverů, který jejich nástroje přemosťuje do místního registru), cc_markdown (markdownový engine v repozitáři), cc_natives (listový balíček FFI) a system_audio_capture. Celou matici klientů a to, co kde běží, najdeš v Nasazení a klienti.

Presentation → Application/Providers → Domain ← Infrastructure
  • Doménová vrstva: entity v čistém Dartu, hodnotové objekty, rozhraní repozitářů, porty a doménové služby. Žádné importy infrastruktury (žádné dio, drift ani síťové modely).
  • Prezentační vrstva: obrazovky, widgety, notifiery. Žádný přímý přístup k Driftu, DAO ani datové vrstvě; všechno jde přes providery Riverpodu → repozitáře.
  • Infrastrukturní vrstva: konkrétní implementace doménových portů a rozhraní repozitářů. Závisí na doméně, a jen tímto směrem.

Úplný tvar domény je:

feature_name/
├── data/ # Repository implementations, data sources, services, DTOs, mappers
├── domain/ # Entities, repository interfaces, ports, use cases
├── presentation/ # Screens, widgets, notifiers
└── providers/ # Riverpod providers

Pod lib/features/ má všechny čtyři části opravdu jen málo domén a chybějící části jsou výmluvné: vznikly záměrně. Sdílené jádro pohltilo domain/ každé domény do cc_domain a tenký klient nedrží implementace repozitářů, takže většina domén pod lib/ je presentation/ a providers/ a nic dalšího. Adresář data/ nebo domain/, který v klientovi zůstane, označuje něco skutečně klientského.

Dvě pojmenované výjimky:

  • mcp má jen providery. Nastavení a stavové rozhraní žije pod settings/ a samotná plocha nástrojů je balíček (cc_mcp), ne složka domény.
  • orchestration a plan_studio mají jen presentation/ a providers/.

Doména guardrails má doménovou vrstvu v cc_domain/features/guardrails/, ne v kořenové aplikaci, protože ji vynucuje server: pravidlo mapuje ActionClass (nebo shellový commandPrefix) na rozhodnutí allow, prompt nebo deny v daném rozsahu. Pravidla se ukládají po pracovních prostorech a každá hlídaná akce agenta se před během vyřeší přes řetěz politik. Vyhodnocení jde po space > agent > workspace > mode preset > built-in default a rozhodne první rozsah se shodným pravidlem. Nejpřísnější pravidlo rozsekne jen remízu uvnitř jednoho rozsahu a slučování několika tříd, které jedna akce deklaruje. Plocha pro operátora je Nastavení → Pracovní prostor → Oprávnění agentů, na /workspaces/<id>/settings/workspace/permissions. Model popisují Guardraily.

packages/cc_domain/lib/core/domain/ drží entity a repozitáře sdílené napříč třemi a více doménami. Je to čistý Dart bez závislostí na infrastruktuře, takže stejné definice importuje Flutter klient i binárka serveru bez Flutteru:

  • Základní entity: Agent, AgentRunLog, Workspace, Repo, ReviewSpaceAssociation
  • Identita a multiplayer: User, Principal (sealed UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Entity paměti: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Sdílené hodnotové objekty: AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Sdílené porty: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Doménové služby: MemoryAccessPolicy, ActivityLogger, MentionResolver, RunLivenessClassifier
  • DomainEventBus a typy událostí (pracovní prostor/agent, PR/revize, zprávy, tickety, pipeline, orchestrace, paměť, kalendář/schůzky, identita a členství, pozorovatelnost)

Control Center je pro víc uživatelů: lidé a agenti jsou rovnocenní aktéři sjednocení přes Principal. Člen pracovního prostoru je User vázaný na Workspace v roli WorkspaceRole (owner/admin/member/viewer/guest). Hranicí přístupu je členství. Párovací klíč ji netvoří a oprávnění na úrovni repozitáře drží členství v pracovním prostoru v mezích práv forge. První uživatel vytvořený na čerstvém serveru vlastní instalaci; další se připojují pozvánkou nebo přes OIDC. Založení pracovního prostoru ho zapíše jako owner.

Spolupráce v reálném čase stojí na autoritativním serveru a last-writer-wins (LWW) po polích. CRDT to není. Přítomnost (kdo je tu a kde, kurzory, psaní, živý stav agenta a běžící náklady) je samostatný efemérní pruh, který se nikdy neukládá. Trvalý stav jede po páteři z optimistické mutace, rebase na serveru a LWW. Lidé a agenti sdílejí jeden seznam. Když je operátor jen jeden člověk, pruh přítomnosti stojí a rozhraní seznamu se neukáže. Multiplayer sólového uživatele nic nestojí.

Porty jsou abstraktní rozhraní v doménové vrstvě. Adaptéry jsou konkrétní implementace v infrastruktuře. Bezhlavý server vlastní data a musí se sestavit bez Flutteru, takže skoro každý adaptér je jen pro VM a žije v packages/cc_infra/lib/src/, rozdělený podle oblasti:

Port Umístění adaptéru
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/ (na straně klienta: vykresluje události serveru lokálně)
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 a GitHub Issues jsou adaptéry synchronizace ticketů v cc_infra/lib/src/tickets/sync/; místní tickety žijí v databázi pracovního prostoru a TicketProviderPort nemají.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (doménová služba v čistém Dartu, infrastrukturu nepotřebuje)

Kořen kompozice aplikace (di/providers.dart) váže klientské porty na implementace přes providery Riverpodu; server skládá své adaptéry v cc_server_core.

Riverpod pro veškerý stav klienta:

  • Notifier<T> a AsyncNotifier<T> pro měnitelný stav
  • FutureProvider<T> pro asynchronní data
  • Seznamy a živé řádky přicházejí přes odběry RPC jako AsyncValue<List<T>>. Server uvnitř používá Drift .watch(); Drift neotevírá žádný klient
  • Nástroje MCP dostávají závislosti jako typované parametry konstruktoru. Ref se jim nepředává

Drift (SQLite), rozdělený do dvou databází, obě v cc_persistence:

  • GlobalDatabase (<dataDir>/global.db) drží krátký seznam tabulek, které jsou opravdu celoserverové: registr pracovních prostorů, uživatelé, předvolby a spárovaná zařízení, připojení SSO, nastavení serveru pro celou instalaci, spravované politiky akcí (zámek na úrovni instalace, který smí guardraily pracovního prostoru jen zpřísnit), kanál novinek pro každého uživatele, fronta flotily (pracovníci, úlohy, záznam umístění), index workspace_routes před ověřením a metadata instalace. Start otevře jen tento soubor, takže cena startu zůstává stejná, ať mají pracovní prostory historie kolik chtějí. Tabulka přidaná sem je rozhodnutí o izolaci a západkový test tě nutí ho obhájit.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) je jeden adresář na pracovní prostor. Drží jeden soubor SQLite a všechno ostatní, co patří jen tomu prostoru, takže worktree rozhovoru a soubory agenta leží vedle databáze a mažou se s ní. Tady žije skoro všechno: agenti, prostory, tickety, paměť, pipelines, schůzky, graf kódu, revize, repozitáře. Soubory se otevírají líně při prvním sáhnutí přes WorkspaceDatabaseManager.of(workspaceId) a teprve tehdy zaplatí vlastní quick_check, instalaci FTS a triggerů a vector_init.

Obě poloviny vyšly ze sloučeného základu v1. onCreate staví aktuální schéma a migrační kroky se připojují, jen když je nasazenou databázi potřeba dovést dál.

Pracovní prostor je jeden soubor, takže export je jeden příkaz VACUUM INTO (workspace.export). Tabulku po tabulce se nevypisuje. workspace.import takový soubor převezme a podle vloženého workspace_meta pozná vlastní soubor převzatý znovu od souboru, který přišel z jiné instalace.

Obě databáze vlastní cc_persistence a otevírá je jen cc_server. Žádný klient DB neotevírá a každý klient čte a zapisuje přes RPC. Tabulky jsou v packages/cc_persistence/lib/database/tables/, DAO v .../daos/. Doménové entity jsou čistý Dart, oddělené od tříd tabulek Driftu, a mapování je v datových vrstvách. FTS5 řeší fulltextové hledání a embeddingy sqlite_vector, obojí jen na serveru. Obě databáze jedou s PRAGMA foreign_keys=ON v žurnálovém režimu WAL.

go_router a jedna skutečnost, která řídí zbytek: každá destinace v aplikaci má prefix pracovního prostoru ve tvaru /workspaces/:workspaceId/…. Id pracovního prostoru v URL je jediný zdroj pravdy pro aktivní pracovní prostor. activeWorkspaceIdProvider vychází z trasy, trasa z něj ne. Čtení dat vázaných na pracovní prostor mimo trasu s prefixem proto nemá odkud id vzít.

  • Funkce pro stavbu tras v router/routes.dart. Každá bere id pracovního prostoru jako první argument, ne řetězcové konstanty
  • Čtyři trasy před kontextem jsou bez prefixu: /splash, /onboarding, /signed-out a /workspaces (výběr)
  • ShellRoute obaluje shell aplikace; ty čtyři plochy se kreslí přes celou obrazovku mimo něj
  • Stráž autentizace: nedokončené nastavení jde na /onboarding; dokončené nastavení, kterému zmizel přihlašovací údaj forge, jde na /signed-out. Dokončené znamená, že přihlášený uživatel má připojenou aspoň jednu forge a že existuje aspoň jeden pracovní prostor. GitHub to být nemusí a CLI gh není způsob autentizace
  • Brána onboardingu v features/auth/providers/

Síť (na straně serveru)

Sekce “Síť (na straně serveru)”

Veškeré vnější HTTP žije na serveru. Klienti se na GitHub, Linear ani Google neobracejí přímo. Volají RPC operace serveru a i vzdálená média se berou z koncového bodu serveru /proxy/media, ne z původního zdroje.

  • Klienti dio v packages/cc_infra/lib/src/: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphQLClient, GitLabApiClient, BitbucketApiClient, LinearGraphQlClient a GoogleCalendarApiClient
  • Auth token se vkládá přes interceptory, včetně Google OAuth interceptoru po účtech v cc_server_core, který při 401 tokeny obnoví
  • Všechny chyby se mapují přes packages/cc_infra/lib/src/network/error_mapper.dart → typované podtřídy AppException definované v cc_domain

Server drží přihlašovací údaje, na kterých záleží: tokeny GitHubu a ticketovacích služeb pro spouštění agentů, klíče poskytovatelů LLM a tokeny OAuth žijí v datovém adresáři serveru nebo v jeho prostředí. V klíčence klienta zůstává jeho vlastní přihlašovací údaj zařízení a párovací klíč ke vzdálenému serveru.

  • Tajemství ve flutter_secure_storage (klíčenka macOS, Správce pověření Windows, libsecret na Linuxu): přihlašovací údaj zařízení klienta a párovací klíče ke vzdálenému serveru, ne tokeny poskytovatelů
  • shared_preferences jen pro necitlivé předvolby (motiv, písmo, rozložení)
  • Zbytky tajemství poskytovatelů ze starších instalací jednou předá serveru features/auth/providers/credential_migration.dart a potom je z klíčenky klienta smaže

Architektonická omezení ověřuje test/core/architecture_constraints_test.dart. Test selže, když se poruší pravidla závislostí.