Tovább a tartalomhoz

Architektúra

A Control Center a funkcióközpontú Clean Architecture-t követi, portok és adapterek mintával. A kódbázis üzleti domainek (funkciók) köré szerveződik, a szigorú függőségi szabályokat pedig egy automatizált architektúrateszt kényszeríti ki.

Vékony kliens egy headless szerver fölött

Szekció neve “Vékony kliens egy headless szerver fölött”

A Control Center vékonykliens-architektúra. Egyetlen kliens sem nyitja meg az adatbázist — az adatok egy headless cc_server folyamat birtokában vannak (egy kis global.db, valamint munkaterületenként egy SQLite-fájl, amelyek lustán nyílnak meg), és WebSocket RPC-n keresztül szolgálja ki azokat. Az asztali alkalmazás, a webes build és a telefonos társalkalmazás egyaránt ezen az egyetlen RPC-kapcsolaton keresztül jelenít meg mindent.

Az asztali alkalmazás az első indításnál eldönti, hogyan éri el a szerverét: helyi módon (a gépen elindít egy cc_server folyamatot, és loopbacken keresztül beszél vele — ez az alapértelmezett) vagy távoli módon (egy máshol futó cc_server-hez csatlakozik biztonságos WebSocketen keresztül). A webes kliens mindig távoli. Az indítási feloldó (bootstrap/server_backend.dart) még a Riverpod létezése előtt kiválasztja az utat, majd a csatlakozott klienssel felülírja az rpcClientProvider-t — így minden funkció a szerveren keresztül olvas és ír, nem pedig egy helyi adatbázisból.

Maga az RPC-réteg állapotmentes: a munkamenet hitelesített, de nincs egy munkaterülethez kötve. Minden munkaterület-hatókörű hívás a saját workspace_id azonosítóját hordozza az argumentumaiban, és a diszpécser visszautasítja az ilyen hívást, ha az hiányzik. Ez teszi lehetővé, hogy egy szerveren két kliens különböző aktív munkaterületet tartson anélkül, hogy lenne egy szerveren tárolt „aktuális munkaterület”, amiben egyet kellene érteniük — és azt is jelenti, hogy az elszigetelés hívásonként érvényesül, nem kapcsolatonként.

Ez egyetlen, feloldott lockfile-lal rendelkező Dart-munkaterület huszonegy taggal — tizenöt csomag plusz hat alkalmazás —, amelynek gyökere az asztali és webes alkalmazás (control_center). A hat alkalmazás: a headless szerver bináris (cc_server), a headless flottavégrehajtó (cc_worker), a telefonos PWA (cc_remote), a WebSocket relay broker (cc_signaling_server), a Widgetbook-galéria (cc_gallery) és a nyilvános demóhoszt (cc_demo_server). A tizenöt csomag: cc_ui (dizájnrendszer), cc_domain (pure-Dart megosztott kernel: entitások, értékobjektumok, portok, események és minden funkció domainrétege), cc_harness (a beépített ügynökkernel: pure-Dart ügynökciklus — üzenetek, szolgáltatói port, eszközök, tömörítés, irányítás, hookok, alügynökök — dart:io nélkül, és semmilyen más cc_* függőség nélkül), cc_harness_runtime (a beépített harness futásidejű adaptere: VM-only streamingszolgáltatók, OAuth/PKCE hitelesítőadat-brókerelés, hitelesítőadat-tárak és az általános eszközkészlet — külső CLI telepítése nélkül futtat ügynököket), cc_rpc (kliensoldali transzportok), cc_host (szerverkernel), cc_data (távoli tárolók), cc_persistence (szerveroldali adatbázis), cc_server_core (alkalmazás-szerver összeállítása + identitás/jelenlét/flotta/kiértékelések futásideje), cc_infra (VM-only adapterek), cc_mcp (MCP-eszközök), cc_mcp_client (kliens külső MCP-szerverekhez, amelyek eszközeit a helyi regiszterbe hidalja át), cc_markdown (tárolón belüli markdown-motor), cc_natives (FFI-levélmodul) és system_audio_capture. A teljes kliensmátrixot és azt, hogy mi hol fut, lásd: Telepítés és kliensek.

Presentation → Application/Providers → Domain ← Infrastructure
  • Domainréteg: pure Dart entitások, értékobjektumok, tárolófelületek, portok és domain-szolgáltatások. Nulla infrastruktúra-import (nincs dio, drift vagy hálózati modell).
  • Megjelenítési réteg: képernyők, widgetek, notifierek. Nincs közvetlen drift/DAO/adatréteg-hozzáférés; minden a Riverpod-szolgáltatókon → tárolókon keresztül megy.
  • Infrastruktúraréteg: a domainportok és tárolófelületek konkrét implementációi. A domaintől függ, sosem fordítva.

A teljes funkciófelépítés:

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

Nagyon kevés funkció a lib/features/ alatt hordozza ténylegesen mind a négyet, és a hiányok inkább tanulságosak, mint véletlenek. A megosztott kernel minden funkció domain/ könyvtárát a cc_domain-be olvasztotta, a vékony kliens pedig nem tartalmaz tárolóimplementációkat, ezért a lib/ alatti funkciók többsége csak presentation/ és providers/, semmi más. Az ügyfélben megmaradó data/ vagy domain/ könyvtár valami valóban kliensoldalit jelez.

Két nevesített kivételt érdemes ismerni:

  • mcp csak szolgáltatókból áll. A beállításai és az állapotkezelő felülete a settings/ alatt található, maga az eszközfelület pedig egy csomag (cc_mcp), nem funkciómappa.
  • orchestration és plan_studio csak presentation/ és providers/ könyvtárat tartalmaz.

A guardrails funkció a domainrétegét a cc_domain/features/guardrails/ alá teszi a gyökér alkalmazás helyett, mert a szerver kényszeríti ki: egy szabály egy ActionClass-t (vagy egy shell commandPrefix-et) képez le egy döntésre — allow, prompt vagy deny — egy hatókörön belül, a szabályok munkaterületenként tárolódnak, és minden szabályozott ügynökművelet a szabályláncon keresztül oldódik fel, mielőtt lefutna. A feloldás a space > agent > workspace > mode preset > built-in default sorrendet járja be, és az első egyező szabállyal rendelkező hatókör dönt; a legszigorúbb csak egy hatókörön belül döntetlen eldöntésére szolgál, valamint az egy művelet által deklarált több hatásfajta kombinálásakor. A kezelői felület a Beállítások → Munkaterület → Ügynökjogosultságok, a /workspaces/<id>/settings/workspace/permissions útvonalon. A modellről lásd: Korlátok.

A packages/cc_domain/lib/core/domain/ azokat az entitásokat és tárolókat tartalmazza, amelyeket 3+ funkció oszt meg. Pure Dart, nulla infrastruktúra-függőséggel, épp ez teszi lehetővé, hogy a Flutter-kliens és a Flutter nélküli szerver bináris ugyanazokat a definíciókat importálja:

  • Alapentitások: Agent, AgentRunLog, Workspace, Repo, ReviewSpaceAssociation
  • Identitás és többszereplős mód: User, Principal (sealed UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Memóriaentitások: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Megosztott értékobjektumok: AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Megosztott portok: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Domain-szolgáltatások: MemoryAccessPolicy, ActivityLogger, MentionResolver, RunLivenessClassifier
  • DomainEventBus + eseménytípusok (munkaterület/ügynök, PR/átnézés, üzenetküldés, jegykezelés, pipeline, orchestráció, memória, naptár/megbeszélések, identitás és tagság, megfigyelhetőség)

A Control Center többfelhasználós: az emberek és az ügynökök egyenrangú szereplők, amelyeket egy Principal fog össze. Egy munkaterülettag egy User, amely egy Workspace-hez van kötve egy WorkspaceRole-lal (owner/admin/member/viewer/guest). A tagság a hozzáférési határ — nem egy párosítási kulcs —, a tárolónkénti jogosultságok pedig megakadályozzák, hogy a munkaterület-tagság a kódhoszt fölé helyezze magát. A friss szerveren létrehozott első felhasználó birtokolja a telepítést; a többiek meghívóval vagy OIDC-vel csatlakoznak. Egy munkaterület létrehozása ezt a felhasználót owner-ként rögzíti.

A valós idejű együttműködés autoritatív szerver + mezőnkénti last-writer-wins (LWW), nem CRDT. A jelenlét (ki van itt, hol van, kurzorok, gépelés, egy ügynök élő állapota és futási költsége) külön, soha nem perzisztált efemer sáv; a tartós állapot optimalista módosítás + szerveroldali rebase + LWW gerincen halad. Az emberek és az ügynökök egy közös névsoron osztoznak. Egyetlen emberi kezelő esetén a jelenléti sáv tétlen, és nem jelenik meg a névsorhoz tartozó felület — a többszereplős mód semmibe sem kerül az egyedül dolgozó felhasználónak.

A portok absztrakt felületek a domainrétegben. Az adapterek konkrét implementációk az infrastruktúrában. Mivel a headless szerver birtokolja az adatokat, és Flutter nélkül kell buildelődnie, szinte minden adapter VM-only, és a packages/cc_infra/lib/src/ alatt található, témák szerint szervezve:

Port Az adapter helye
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/ (kliensoldali: a szervereseményeket helyben jeleníti meg)
AgentBackend cc_infra/lib/src/dispatch/backends/ (acp_backend.dart, harness_backend.dart, cli_backends.dart)
TicketProviderPort cc_infra/lib/src/tickets/linear/ (LinearTicketAdapter). A Jira, a ClickUp és a GitHub Issues jegyszinkronizáló adapterek a cc_infra/lib/src/tickets/sync/ alatt; a helyi jegyek a munkaterület adatbázisában élnek, TicketProviderPort nélkül.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (pure-Dart domain-szolgáltatás — nincs szüksége infrastruktúrára)

Az alkalmazás összeállítási gyökere (di/providers.dart) a kliensoldali portokat Riverpod-szolgáltatókon keresztül köti implementációkhoz; a szerver a cc_server_core-ban állítja össze az adaptereit.

Riverpod minden kliensállapothoz:

  • Notifier<T> és AsyncNotifier<T> a módosítható állapothoz
  • FutureProvider<T> aszinkron adatokhoz
  • A listák és az élő sorok RPC-előfizetéseken keresztül AsyncValue<List<T>> formában érkeznek — a szerver belül a Drift .watch() metódusát használja; egyetlen kliens sem nyitja meg a Driftet
  • Az MCP-eszközök a függőségeiket típusos konstruktorparaméterként kapják, sosem Ref-ként

Drift (SQLite), két adatbázisra bontva, mindkettő a cc_persistence-ben:

  • GlobalDatabase (<dataDir>/global.db) — valóban szerver szintű táblák rövid listája: a munkaterület-nyilvántartás, a felhasználók/beállítások/párosított eszközök, az SSO-kapcsolatok, a telepítés szintű szerverbeállítások, a kezelt műveletszabályzatok (egy telepítés szintű korlát, amely csak szigoríthatja a munkaterület korlátait), a felhasználónkénti hírfolyam, a flottasor (workerek/jobok/elhelyezési napló), a hitelesítés előtti workspace_routes index és a telepítési metaadatok. Az indítás csak ezt a fájlt nyitja meg, így az indulási költség lapos marad, akármennyi előzményt halmoznak fel a munkaterületek. Egy tábla ide való hozzáadása elszigetelési döntés, amelyet egy racsnis teszt miatt meg kell indokolni.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) — munkaterületenként egy könyvtár, amely egy SQLite-fájlt és minden mást tartalmaz, ami csak ahhoz a munkaterülethez tartozik, így egy beszélgetés worktree-jei és ügynökfájljai az adatbázis mellett helyezkednek el, és vele együtt törlődnek. Szinte minden itt él: ügynökök, terek, jegyek, memória, pipeline-ok, megbeszélések, a kódgráf, átnézések, repók. A fájlok az első érintéskor lustán nyílnak meg a WorkspaceDatabaseManager.of(workspaceId)-n keresztül, és ilyenkor saját maguk fizetik meg a quick_check, az FTS/trigger telepítés és a vector_init költségét.

Mindkét fél egy összelapított v1 bázisvonalról indult — az onCreate építi fel a jelenlegi sémát —, a migrációs lépések csak akkor kerülnek mögé, ha egy telepített adatbázist tovább kell vinni.

Mivel egy munkaterület egy fájl, a munkaterület exportálása egyetlen VACUUM INTO utasítás (workspace.export), nem táblánkénti dump; a workspace.import átvesz egy ilyen fájlt, és a beágyazott workspace_meta segítségével különbözteti meg a saját, újra átvett fájlját egy másik telepítésből származótól.

Mindkét adatbázis a cc_persistence tulajdona, és csak a cc_server nyitja meg; egyetlen kliens sem nyit adatbázist — minden kliens RPC-n keresztül olvas/ír. A táblák a packages/cc_persistence/lib/database/tables/ alatt vannak definiálva, a DAO-k a .../daos/ alatt; a domainentitások pure Dartok, különválnak a Drift táblaosztályaitól, a leképezés az adatrétegekben történik. Az FTS5 végzi a teljes szöveges keresést, a sqlite_vector a beágyazásokat, mindkettő csak szerveroldalon; mindkét adatbázis PRAGMA foreign_keys=ON beállítással fut WAL naplózási módban.

go_router, egyetlen ténnyel, amely az egészet meghatározza: minden alkalmazáson belüli cél útvonal munkaterület-előtagot kap, /workspaces/:workspaceId/… formában. Az URL-ben szereplő munkaterület-azonosító az aktív munkaterület egyetlen igazságforrása — az activeWorkspaceIdProvider az útvonalból vezetődik le, nem fordítva —, ezért a munkaterület-hatókörű adatok beolvasásának egy előtagolt útvonalon kívül nincs honnan megkapnia az azonosítóját.

  • Útvonalépítő függvények a router/routes.dart-ban, mindegyik az első argumentumként kapja a munkaterület-azonosítót, nem sztringkonstansként
  • Négy, kontextus előtti útvonal előtag nélküli: /splash, /onboarding, /signed-out és /workspaces (a választó)
  • A ShellRoute az alkalmazás héját csomagolja; ez a négy felület teljes képernyőn jelenik meg azon kívül
  • Hitelesítési őr: a befejezetlen beállítás a /onboarding-ra megy; a befejezett beállítás, amelynek a kódhoszt-hitelesítő adata eltűnt, a /signed-out-ra. A „befejezett” legalább egy, a bejelentkezett felhasználóhoz csatlakoztatott kódhosztot és legalább egy létező munkaterületet jelent — nem kifejezetten a GitHubot, és a gh CLI nem hitelesítési mód
  • Bevezetési kapu a features/auth/providers/-ben

A teljes külső HTTP a szerverben él. A kliensek soha nem kapcsolódnak közvetlenül a GitHubhoz, a Linearhoz vagy a Google-hoz — szerveres RPC-műveleteket hívnak, és még a távoli média is a szerver /proxy/media végpontján keresztül érkezik, nem a felsőbb szintű eredet helyéről.

  • dio kliensek a packages/cc_infra/lib/src/ alatt: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphQLClient, GitLabApiClient, BitbucketApiClient, LinearGraphQlClient és GoogleCalendarApiClient
  • Hitelesítési token befecskendezése interceptorekon keresztül, beleértve egy fiókonkénti Google OAuth interceptort a cc_server_core-ban, amely 401 esetén megújítja a tokeneket
  • Minden hiba a packages/cc_infra/lib/src/network/error_mapper.dart-on keresztül képeződik le → a cc_domain-ben definiált típusos AppException leszármazottakra

A szerver birtokolja az igazán fontos hitelesítő adatokat: az ügynökindításokhoz szükséges GitHub- és jegykezelési tokenek, az LLM-szolgáltatói kulcsok és az OAuth-tokenek mind a szerver adatkönyvtára vagy annak környezete alatt élnek. Amit a kliens kulcstárolója még őriz, az a saját eszközhitelesítő adata és egy távoli szerver párosítási kulcsa.

  • Titkok a flutter_secure_storage-ban (macOS kulcstároló, Windows hitelesítőadat-kezelő, Linux libsecret) — a kliens eszközhitelesítő adata és a távoli párosítási kulcsok, nem a szolgáltatói tokenek
  • shared_preferences csak nem érzékeny beállításokhoz (téma, betűtípus, elrendezés)
  • A régebbi telepítésekből visszamaradt szolgáltatói titkokat a features/auth/providers/credential_migration.dart egyszer átadja a szervernek, majd törli a kliens kulcstárolójából

Az architektúra-megkötéseket a test/core/architecture_constraints_test.dart ellenőrzi, amely elbukik, ha a függőségi szabályok sérülnek.