Hoppa till innehåll

Arkitektur

Control Center följer en funktionsorienterad Clean Architecture med mönstret portar och adaptrar. Kodbasen är organiserad kring affärsdomäner (funktioner), med strikta beroenderegler som upprätthålls av ett automatiskt arkitekturtest.

Control Center har en tunn klientarkitektur. Ingen klient öppnar databasen — en headless cc_server-process äger data (en liten global.db plus en SQLite-fil per arbetsyta, som öppnas först vid behov) och exponerar den över WebSocket-RPC. Skrivbordsappen, webbversionen och telefonkompanjonen är alla renderare ovanpå den enda RPC-anslutningen.

Skrivbordsappen väljer vid första starten hur den når sin server: lokalt (den startar en cc_server på den här maskinen och kommunicerar över loopback — standard) eller fjärr (den ansluter till en cc_server någon annanstans över en säker WebSocket). Webbklienten är alltid fjärr. Startupplösaren (bootstrap/server_backend.dart) väljer väg innan Riverpod finns och skriver sedan över rpcClientProvider med den anslutna klienten — så varje funktion läser och skriver via servern i stället för en lokal databas.

Själva RPC-lagret är tillståndslöst: en session autentiseras, men den är inte bunden till en arbetsyta. Varje anrop som gäller en arbetsyta bär sitt eget workspace_id i argumenten, och dispatcharen avvisar ett sådant anrop utan det. Det är det som gör att två klienter på en och samma server kan ha olika aktiva arbetsytor utan en serverhållen “aktuell arbetsyta” att vara oense om — och det innebär att isoleringen upprätthålls per anrop, inte per anslutning.

Det här är en Dart-arbetsyta med en enda resolverad lockfil och tjugoen medlemmar — femton paket plus sex appar — med skrivbords- och webbappen (control_center) som arbetsytans rot. De sex apparna är den headless serverbinären (cc_server), den headless fleet-exekutorn (cc_worker), telefon-PWA:n (cc_remote), WebSocket-reläbrokern (cc_signaling_server), Widgetbook-galleriet (cc_gallery) och den publika demovärden (cc_demo_server). De femton paketen är cc_ui (designsystem), cc_domain (delad kärna i ren Dart: entiteter, värdeobjekt, portar, händelser och varje funktions domänlager), cc_harness (den inbyggda agentkärnan: en agentloop i ren Dart — meddelanden, leverantörsport, verktyg, kompaktering, styrning, hooks, subagenter — utan dart:io och utan något annat cc_*-beroende), cc_harness_runtime (den inbyggda harness-runtime-adaptern: strömmande leverantörer som bara finns på Dart-VM:en, förmedling av autentiseringsuppgifter via OAuth/PKCE, förråd för autentiseringsuppgifter och den generiska verktygsuppsättningen — kör agenter utan att någon extern CLI är installerad), cc_rpc (klienttransport), cc_host (serverkärna), cc_data (fjärrarkiv), cc_persistence (serverdatabas), cc_server_core (app-server-sammansättning + körtid för identitet/närvaro/fleet/utvärderingar), cc_infra (adaptrar som bara finns på Dart-VM:en), cc_mcp (MCP-verktyg), cc_mcp_client (klient för externa MCP-servrar, som bryggar deras verktyg in i det lokala registret), cc_markdown (markdownmotor i repot), cc_natives (FFI-löv) och system_audio_capture. Se Driftsättning och klienter för hela klientmatrisen och vad som körs var.

Presentation → Application/Providers → Domain ← Infrastructure
  • Domänlagret: entiteter, värdeobjekt, repository-gränssnitt, portar och domäntjänster i ren Dart. Noll importer från infrastrukturen (ingen dio, drift eller nätverksmodeller).
  • Presentationslagret: skärmar, widgets, notifiers. Ingen direkt åtkomst till drift/DAO/datalagret; allt går via Riverpod-providers → repositories.
  • Infrastrukturlagret: konkreta implementationer av domänportar och repository-gränssnitt. Är beroende av domänen, aldrig tvärtom.

Den fullständiga formen för en funktion är:

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

Väldigt få funktioner under lib/features/ har faktiskt alla fyra, och luckorna är informativa snarare än oavsiktliga. Den delade kärnan har införlivat varje funktions domain/ i cc_domain, och den tunna klienten innehåller inga repository-implementationer, så de flesta funktioner under lib/ är presentation/ plus providers/ och inget annat. En data/- eller domain/-katalog som blir kvar i klienten markerar något som verkligen hör till klientsidan.

Två namngivna undantag är värda att känna till:

  • mcp har bara providers. Dess inställnings- och statusgränssnitt ligger under settings/, och själva verktygsytan är ett paket (cc_mcp), inte en funktionsmapp.
  • orchestration och plan_studio har bara presentation/ och providers/.

Funktionen guardrails lägger sitt domänlager i cc_domain/features/guardrails/ i stället för i rotappen, eftersom servern upprätthåller den: en regel mappar en ActionClass (eller ett commandPrefix i skalet) till ett beslut — allow, prompt eller deny — i en omfattning, regler lagras per arbetsyta och varje styrd agentåtgärd löses genom policykedjan innan den körs. Upplösningen går space > agent > workspace > mode preset > built-in default och den första omfattningen med en matchande regel avgör; mest restriktiv fungerar bara som utslagsregel inom en omfattning och för att kombinera de klasser en åtgärd deklarerar. Ytan som operatören möter är Inställningar → Arbetsyta → Agentbehörigheter, på /workspaces/<id>/settings/workspace/permissions. Se Räcken för modellen.

packages/cc_domain/lib/core/domain/ innehåller entiteter och repositories som delas av tre eller fler funktioner. Det är ren Dart utan några infrastruktursberoenden, vilket är det som gör att både Flutter-klienten och den Flutter-fria serverbinären kan importera samma definitioner:

  • Kärnentiteter: Agent, AgentRunLog, Workspace, Repo, ReviewSpaceAssociation
  • Identitet och multiplayer: User, Principal (sealed UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Minneentiteter: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Delade värdeobjekt: AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Delade portar: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Domäntjänster: MemoryAccessPolicy, ActivityLogger, MentionResolver, RunLivenessClassifier
  • DomainEventBus + händelsetyper (arbetsyta/agent, PR/granskning, meddelanden, ärenden, pipeline, orkestrering, minne, kalender/möten, identitet och medlemskap, observerbarhet)

Control Center är byggt för flera användare: människor och agenter är likställda aktörer som förenas av en Principal. En arbetsytas medlem är en User som är bunden till en Workspace med en WorkspaceRole (owner/admin/member/viewer/guest). Medlemskapet är åtkomstgränsen — inte en parkopplingsnyckel — och beviljanden per repo hindrar arbetsytans medlemskap från att ge fler privilegier än kodvärden. Den första användaren som skapas på en ny server äger installationen; andra ansluter via inbjudan eller OIDC. När en arbetsyta skapas stämplas användaren som owner.

Samarbete i realtid bygger på auktoritativ server + last-writer-wins (LWW) per fält, inte på en CRDT. Närvaro (vilka som är här, var de är, markörer, skrivande, en agents live-status och pågående kostnad) är en separat efemär kanal som aldrig sparas; beständigt tillstånd färdas på en ryggrad av optimistiska mutationer + server-rebase + LWW. Människor och agenter delar en och samma deltagarlista. Med en enda mänsklig operatör står närvarokanalen stilla och ingen deltagarlista visas i gränssnittet — multiplayer kostar ingenting för den som kör själv.

Portar är abstrakta gränssnitt i domänlagret. Adaptrar är konkreta implementationer i infrastrukturen. Eftersom headless-servern äger data och måste kunna byggas utan Flutter finns nästan varje adapter bara för Dart-VM:en och ligger i packages/cc_infra/lib/src/, ordnade efter ansvarsområde:

Port Adapterplats
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/ (klientsida: återger serverhändelser 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 och GitHub Issues är ärendesynkroniserande adaptrar i cc_infra/lib/src/tickets/sync/; lokala ärenden ligger i arbetsytans databas utan någon TicketProviderPort.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (en domäntjänst i ren Dart — den behöver ingen infrastruktur)

Appens sammansättningsrot (di/providers.dart) binder portar på klientsidan till implementationer via Riverpod-providers; servern sätter samman sina adaptrar i cc_server_core.

Riverpod för allt klienttillstånd:

  • Notifier<T> och AsyncNotifier<T> för föränderligt tillstånd
  • FutureProvider<T> för asynkrona data
  • Listor och liverader kommer in via RPC-prenumerationer som AsyncValue<List<T>> — servern använder Drift .watch() internt; ingen klient öppnar Drift
  • MCP-verktyg tar emot beroenden som typade konstruktorparametrar, aldrig Ref

Drift (SQLite), uppdelat i två databaser, båda i cc_persistence:

  • GlobalDatabase (<dataDir>/global.db) — en kort lista med tabeller som verkligen gäller hela servern: arbetsyteregistret, användare/inställningar/parkopplade enheter, SSO-anslutningar, serverinställningar för hela installationen, hanterade åtgärdspolicyer (en installationstäckande spärr som bara kan skärpa arbetsytornas räcken), nyhetsflödet per användare, fleet-kön (fleet-workers, jobb, placeringslogg), workspace_routes-indexet före autentisering och installationsmetadata. Vid start öppnas bara den här filen, så startkostnaden förblir konstant hur mycket historik arbetsytorna än samlar på sig. Att lägga till en tabell här är ett isoleringsbeslut som ett spärrtest tvingar dig att argumentera för.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) — en katalog per arbetsyta som innehåller en SQLite-fil plus allt annat som bara hör till den arbetsytan, så att ett samtals worktrees och agentfiler ligger bredvid databasen och raderas med den. Nästan allt bor här: agenter, ytor, ärenden, minne, pipelines, möten, kodgrafen, granskningar, repos. Filerna öppnas först vid första åtkomst via WorkspaceDatabaseManager.of(workspaceId) och betalar då sin egen quick_check, installation av FTS/triggers och vector_init.

Båda halvorna utgick från en sammanslagen v1-baslinje — onCreate bygger det aktuella schemat — och migreringssteg läggs bara till när en driftsatt databas måste föras framåt.

Eftersom en arbetsyta är en fil är export av en arbetsyta en enda VACUUM INTO-sats (workspace.export), inte en dump tabell för tabell; workspace.import tar över en sådan fil och använder den inbäddade workspace_meta för att avgöra om det är en egen fil som tagits över igen eller en fil som kommer från en annan installation.

Båda databaserna ägs av cc_persistence och öppnas bara av cc_server; ingen klient öppnar en databas — varje klient läser och skriver över RPC. Tabeller definieras i packages/cc_persistence/lib/database/tables/, DAO:er i .../daos/; domänentiteter är ren Dart, åtskilda från Drift-tabellklasser, med mappning i datalagren. FTS5 hanterar fritextsökning och sqlite_vector-inbäddningar, båda endast på servern; båda databaserna kör PRAGMA foreign_keys=ON i WAL-journalläge.

go_router, med ett faktum som styr resten: varje destination i appen har arbetsyteprefix i form av /workspaces/:workspaceId/…. Arbetsytans id i URL:en är den enda källan till sanning för den aktiva arbetsytan — activeWorkspaceIdProvider drivs från routen, inte tvärtom — vilket är anledningen till att läsning av arbetsytespecifika data utanför en prefixad route inte har någonstans att hämta sitt id ifrån.

  • Routebyggar-funktioner i router/routes.dart, som var och en tar arbetsytans id som sitt första argument, inte strängkonstanter
  • Fyra routes före kontexten saknar prefix: /splash, /onboarding, /signed-out och /workspaces (väljaren)
  • ShellRoute omsluter appskalet; de fyra ytorna återges i fullskärm utanför det
  • Autentiseringsvakt: ofullständig konfiguration går till /onboarding; en slutförd konfiguration vars kodvärdsuppgifter har försvunnit går till /signed-out. Slutförd betyder att minst en kodvärd är ansluten för den inloggade användaren och att minst en arbetsyta finns — inte GitHub specifikt, och gh-CLI:t är inte en autentiseringsmetod
  • Onboardinggrind i features/auth/providers/

All extern HTTP finns i servern. Klienter ansluter aldrig direkt till GitHub, Linear eller Google — de anropar serverns RPC-operationer, och till och med fjärrmedia hämtas via serverns /proxy/media-ändpunkt i stället för från den ursprungliga källan.

  • dio-klienter i packages/cc_infra/lib/src/: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphQLClient, GitLabApiClient, BitbucketApiClient, LinearGraphQlClient och GoogleCalendarApiClient
  • Injektion av autentiseringstoken via interceptors, inklusive en kontospecifik Google OAuth-interceptor i cc_server_core som förnyar tokens vid 401
  • Alla fel mappas via packages/cc_infra/lib/src/network/error_mapper.dart → typade AppException-subklasser som definieras i cc_domain

Servern håller de autentiseringsuppgifter som spelar roll: GitHub- och ärendetokens för agentstarter, nycklar till LLM-leverantörer och OAuth-tokens finns alla under serverns datakatalog eller i dess miljö. Det som klientens nyckelring fortfarande håller är dess egen enhetsautentiseringsuppgift och parkopplingsnyckeln för en fjärrserver.

  • Hemligheter i flutter_secure_storage (macOS-nyckelring, Windows-autentiseringshanteraren, Linux libsecret) — klientens enhetsautentiseringsuppgift och fjärrparkopplingsnycklar, inte leverantörstokens
  • shared_preferences endast för icke-känsliga inställningar (tema, typsnitt, layout)
  • Kvarblivna leverantörshemligheter från äldre installationer lämnas en gång över till servern av features/auth/providers/credential_migration.dart och raderas sedan från klientens nyckelring

Arkitekturbegränsningar valideras av test/core/architecture_constraints_test.dart, som misslyckas om beroendereglerna bryts.