Gå til innholdet

Arkitektur

Control Center følger en funksjonsorientert Clean Architecture med et port- og adaptermønster. Kodebasen er organisert rundt forretningsdomener (funksjoner), med strenge avhengighetsregler håndhevet av en automatisert arkitekturtest.

Control Center er en tynnklientarkitektur. Ingen klient åpner databasen — en headless cc_server-prosess eier dataene (en liten global.db pluss én SQLite-fil per arbeidsområde, åpnet ved behov) og betjener dem over WebSocket-RPC. Skrivebordsappen, nettbyggingen og telefonappen er alle renderere over den samme RPC-tilkoblingen.

Skrivebordsappen velger hvordan den når serveren sin ved første oppstart: lokal (den starter en cc_server på denne maskinen og kommuniserer over loopback — standarden) eller ekstern (den ringer opp en cc_server et annet sted over en sikker WebSocket). Nettklienten er alltid ekstern. Oppstartsresolveren (bootstrap/server_backend.dart) velger banen før Riverpod eksisterer, og overstyrer deretter rpcClientProvider med den tilkoblede klienten — slik at hver funksjon leser og skriver gjennom serveren i stedet for en lokal database.

Selve RPC-laget er tilstandsløst: en økt autentiseres, men den er ikke bundet til et arbeidsområde. Hvert arbeidsområdespesifikke kall bærer sitt eget workspace_id i argumentene, og dispatcheren avviser et slikt kall uten det. Det er dette som lar to klienter på én server ha ulike aktive arbeidsområder uten et serverholdt «gjeldende arbeidsområde» å være uenige om — og det betyr at isolasjon håndheves per kall, ikke per tilkobling.

Dette er et Dart-arbeidsområde med én oppløst lockfil og tjueen medlemmer — femten pakker pluss seks apper — med skrivebords- og nettappen (control_center) som arbeidsområderot. De seks appene er den headless serverbinæren (cc_server), den headless flåteutføreren (cc_worker), telefon-PWA-en (cc_remote), WebSocket-relémegleren (cc_signaling_server), Widgetbook-galleriet (cc_gallery) og den offentlige demoverten (cc_demo_server). De femten pakkene er cc_ui (designsystem), cc_domain (ren-Dart delt kjerne: entiteter, verdiobjekter, porter, hendelser og hver funksjons domenelag), cc_harness (den innebygde agentkjernen: en ren-Dart agentløkke — meldinger, leverandørport, verktøy, komprimering, styring, hooks, underagenter — uten dart:io og uten andre cc_*-avhengigheter), cc_harness_runtime (den innebygde kjøretidsadapteren for harness: kun-VM-strømmende leverandører, OAuth-/PKCE-legitimasjonsmegling, legitimasjonslagre og det generelle verktøysettet — kjører agenter uten noen ekstern CLI installert), cc_rpc (klienttransport), cc_host (serverkjerne), cc_data (fjernrepositoryer), cc_persistence (serverbasert database), cc_server_core (app-server-komposisjon + kjøretid for identitet/tilstedeværelse/flåte/evals), cc_infra (kun-VM-adaptere), cc_mcp (MCP-verktøy), cc_mcp_client (klient for eksterne MCP-servere, som broer verktøyene deres inn i det lokale registeret), cc_markdown (markdown-motor i repoet), cc_natives (FFI-løvnode) og system_audio_capture. Se Drift og klienter for hele klientmatrisen og hva som kjører hvor.

Presentation → Application/Providers → Domain ← Infrastructure
  • Domenelaget: rene Dart-entiteter, verdiobjekter, repositorygrensesnitt, porter og domenetjenester. Null infrastrukturimporter (ingen dio, drift eller nettverksmodeller).
  • Presentasjonslaget: skjermer, widgeter, notifikatorer. Ingen direkte tilgang til drift/DAO/datalaget; alt går gjennom Riverpod-providere → repositoryer.
  • Infrastrukturlaget: konkrete implementasjoner av domeneportene og repositorygrensesnittene. Avhenger av domenet, aldri omvendt.

Den fulle formen på en funksjon er:

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

Svært få funksjoner under lib/features/ har faktisk alle fire, og hullene er informative snarere enn tilfeldige. Den delte kjernen absorberte hver funksjons domain/ inn i cc_domain, og tynnklienten inneholder ingen repositoryimplementasjoner, så de fleste funksjoner under lib/ er presentation/ pluss providers/ og ingenting annet. En data/- eller domain/-katalog som overlever i klienten, markerer noe som virkelig er klientside.

To navngitte unntak er verdt å kjenne:

  • mcp består kun av providere. Innstillings- og statusgrensesnittet ligger under settings/, og selve verktøyflaten er en pakke (cc_mcp), ikke en funksjonsmappe.
  • orchestration og plan_studio har bare presentation/ og providers/.

guardrails-funksjonen legger domenelaget sitt i cc_domain/features/guardrails/ i stedet for i rotappen, fordi serveren håndhever det: en regel kobler en ActionClass (eller et shell-commandPrefix) til en beslutning — allow, prompt eller deny — på et omfang, regler lagres per arbeidsområde, og enhver styrt agenthandling løses gjennom regelkjeden før den kjører. Oppslag går gjennom space > agent > workspace > mode preset > built-in default, og det første omfanget med en samsvarende regel bestemmer; mest restriktiv er bare et skillekriterium innenfor ett omfang og for å kombinere de ulike klassene én handling oppgir. Operatørflaten er Innstillinger → Arbeidsområde → Agenttillatelser, på /workspaces/<id>/settings/workspace/permissions. Se Tillatelsesregler for modellen.

packages/cc_domain/lib/core/domain/ inneholder entiteter og repositoryer som deles på tvers av 3+ funksjoner. Det er ren Dart uten infrastrukturavhengigheter, og det er dette som gjør at både Flutter-klienten og den Flutter-frie serverbinæren kan importere de samme definisjonene:

  • Kjerneentiteter: Agent, AgentRunLog, Workspace, Repo, ReviewSpaceAssociation
  • Identitet og flerbruker: User, Principal (sealed UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Minneentiteter: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Delte verdiobjekter: AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Delte porter: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Domenetjenester: MemoryAccessPolicy, ActivityLogger, MentionResolver, RunLivenessClassifier
  • DomainEventBus + hendelsestyper (arbeidsområde/agent, PR/gjennomgang, meldinger, saker, pipeline, orkestrering, minne, kalender/møter, identitet og medlemskap, observabilitet)

Control Center er flerbruker: mennesker og agenter er likeverdige aktører forent av en Principal. Et arbeidsområdemedlem er en User bundet til et Workspace med en WorkspaceRole (owner/admin/member/viewer/guest). Medlemskap er tilgangsgrensen — ikke en parkoblingsnøkkel — og per-repo-tildelinger hindrer at arbeidsområdemedlemskap gir flere rettigheter enn kodeverten. Den første brukeren som opprettes på en fersk server, eier installasjonen; andre blir med via invitasjon eller OIDC. Når et arbeidsområde opprettes, stemples den brukeren som owner.

Samtidig samarbeid i sanntid er autoritativ server + feltvis last-writer-wins (LWW), ikke en CRDT. Tilstedeværelse (hvem som er her, hvor de er, markører, skriveindikatorer, en agents status i sanntid og løpende kostnad) er en egen kortlivet kanal som aldri lagres; varig tilstand går på en ryggrad av optimistisk mutasjon + server-rebase + LWW. Mennesker og agenter deler én deltakerliste. Med én enkelt menneskelig operatør står tilstedeværelseskanalen i ro, og ingen deltakerlistegrafikk vises — flerbruker koster ikke enkeltbrukeren noe.

Porter er abstrakte grensesnitt i domenelaget. Adaptere er konkrete implementasjoner i infrastrukturen. Fordi den headless serveren eier dataene og må bygges uten Flutter, er nesten alle adaptere kun-VM og ligger i packages/cc_infra/lib/src/, organisert etter ansvarsområde:

Port Plassering av adapter
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/ (klientside: gjengir serverhendelser lokalt)
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 og GitHub Issues er sakssynkroniseringsadaptere i cc_infra/lib/src/tickets/sync/; lokale saker ligger i arbeidsområdedatabasen uten noen TicketProviderPort.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (en ren-Dart-domenetjeneste — den trenger ingen infrastruktur)

Appens komposisjonsrot (di/providers.dart) binder klientsideporter til implementasjoner via Riverpod-providere; serveren komponerer adapterne sine i cc_server_core.

Riverpod for all klienttilstand:

  • Notifier<T> og AsyncNotifier<T> for mutbar tilstand
  • FutureProvider<T> for asynkrone data
  • Lister og levende rader kommer inn over RPC-abonnementer som AsyncValue<List<T>> — serveren bruker Drift .watch() internt; ingen klient åpner Drift
  • MCP-verktøy mottar avhengigheter som typede konstruktørparametere, aldri Ref

Drift (SQLite), delt i to databaser, begge i cc_persistence:

  • GlobalDatabase (<dataDir>/global.db) — en kort liste over virkelig serveromfattende tabeller: arbeidsområderegisteret, brukere/innstillinger/sammenkoplede enheter, SSO-tilkoblinger, installasjonsomfattende serverinnstillinger, administrerte handlingspolicyer (en installasjonsomfattende innstramming som bare kan stramme inn arbeidsområdenes tillatelsesregler), nyhetsstrømmen per bruker, flåkøen (workere/jobber/plasseringslogg), indeksen workspace_routes før autentisering og installasjonsmetadata. Oppstart åpner bare denne filen, så oppstartskostnaden holder seg flat uansett hvor mye historikk arbeidsområdene samler opp. Å legge til en tabell her er en isolasjonsbeslutning som en ratchet-test tvinger deg til å begrunne.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) — én katalog per arbeidsområde, som inneholder én SQLite-fil pluss alt annet som bare hører til det arbeidsområdet, slik at arbeidstrærne og agentfilene til en samtale ligger ved siden av databasen og slettes sammen med den. Nesten alt ligger her: agenter, områder, saker, minne, pipelines, møter, kodegrafen, gjennomganger, repoer. Filer åpnes ved behov ved første berøring gjennom WorkspaceDatabaseManager.of(workspaceId) og betaler sin egen quick_check, FTS-/trigger-installasjon og vector_init da.

Begge halvdelene startet fra en sammenslått v1-baseline — onCreate bygger gjeldende skjema — med migrasjonstrinn lagt til bare når en utplassert database må føres videre.

Fordi ett arbeidsområde er én fil, er eksport av et arbeidsområde én enkelt VACUUM INTO-setning (workspace.export), ikke en tabell-for-tabell-dump; workspace.import tar i bruk en slik fil og bruker den innebygde workspace_meta til å skille en gjenopptatt egen fil fra en som kom fra en annen installasjon.

Begge databasene eies av cc_persistence og åpnes bare av cc_server; ingen klient åpner en database — hver klient leser/skriver over RPC. Tabeller defineres i packages/cc_persistence/lib/database/tables/, DAO-er i .../daos/; domeneentiteter er ren Dart, atskilt fra Drift-tabellklassene, med mapping i datalagene. FTS5 håndterer fulltekstsøk og sqlite_vector-innbygging, begge kun på serveren; begge databasene kjører PRAGMA foreign_keys=ON i WAL-journalmodus.

go_router, med ett faktum som styrer resten: enhver destinasjon i appen har arbeidsområdeprefiks i form av /workspaces/:workspaceId/…. Arbeidsområde-ID-en i URL-en er den eneste kilden til sannhet for det aktive arbeidsområdet — activeWorkspaceIdProvider drives fra ruten, ikke omvendt — og derfor har lesing av arbeidsområdespesifikke data utenfor en rute med prefiks ingen steder å hente ID-en fra.

  • Rute-bygger-funksjoner i router/routes.dart, som hver tar arbeidsområde-ID-en som første argument, ikke strengkonstanter
  • Fire ruter utenfor kontekst har ikke prefiks: /splash, /onboarding, /signed-out og /workspaces (velgeren)
  • ShellRoute omslutter appskallet; de fire flatene gjengis i fullskjerm utenfor det
  • Autentiseringsvakt: ufullstendig oppsett går til /onboarding; et fullført oppsett der kodevertlegitimasjonen er borte, går til /signed-out. Fullført betyr at minst én kodevert er tilkoblet for den innloggede brukeren og at minst ett arbeidsområde finnes — ikke GitHub spesifikt, og gh-CLI-en er ikke en autentiseringsmetode
  • Onboarding-port i features/auth/providers/

All ekstern HTTP ligger i serveren. Klienter ringer aldri GitHub, Linear eller Google direkte — de kaller serverens RPC-operasjoner, og til og med eksterne medier hentes gjennom serverens /proxy/media-endepunkt i stedet for fra kildeopphavet.

  • dio-klienter i packages/cc_infra/lib/src/: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphQLClient, GitLabApiClient, BitbucketApiClient, LinearGraphQlClient og GoogleCalendarApiClient
  • Injeksjon av autentiseringstokener via interceptorer, inkludert en konto-spesifikk Google OAuth-interceptor i cc_server_core som fornyer tokener ved 401
  • Alle feil mappes gjennom packages/cc_infra/lib/src/network/error_mapper.dart → typede AppException-underklasser definert i cc_domain

Serveren holder legitimasjonen som betyr noe: GitHub- og sakstokener for agentutsendinger, nøkler til LLM-leverandører og OAuth-tokener ligger alle under serverens datakatalog eller i miljøet dens. Det klientens nøkkelring fortsatt holder, er dens egen enhetslegitimasjon og parkoblingsnøkkelen for en ekstern server.

  • Hemmeligheter i flutter_secure_storage (macOS-nøkkelring, Windows-legitimasjonsbehandler, Linux libsecret) — klientens enhetslegitimasjon og eksterne parkoblingsnøkler, ikke leverandørtokener
  • shared_preferences kun for ikke-sensitive innstillinger (tema, skrift, oppsett)
  • Gjenværende leverandørhemmeligheter fra eldre installasjoner overleveres serveren én gang av features/auth/providers/credential_migration.dart og slettes deretter fra klientens nøkkelring

Arkitekturrestriksjoner valideres av test/core/architecture_constraints_test.dart, som feiler hvis avhengighetsreglene brytes.