Ga naar inhoud

Architectuur

Control Center volgt feature-first Clean Architecture met een poorten-en-adapterspatroon. De codebase is georganiseerd rond bedrijfsdomeinen (features), met strikte afhankelijkheidsregels die door een geautomatiseerde architectuurtest worden gehandhaafd.

Control Center is een thin-clientarchitectuur. Geen enkele client opent de database — een headless cc_server-proces bezit de data (een kleine global.db plus één SQLite-bestand per werkruimte, lazy geopend) en serveert die via WebSocket RPC. De desktopapp, de webbuild en de telefoonpartner zijn allemaal renderers bovenop die ene RPC-verbinding.

De desktop kiest bij de eerste start hoe hij zijn server bereikt: lokaal (hij start een cc_server op deze machine en praat via loopback — de standaard) of extern (hij kiest een cc_server elders via een beveiligde WebSocket). De webclient is altijd extern. De bootresolver (bootstrap/server_backend.dart) kiest het pad voordat Riverpod bestaat en overschrijft daarna rpcClientProvider met de verbonden client — zo leest en schrijft elke feature via de server in plaats van via een lokale database.

De RPC-laag zelf is stateless: een sessie is geauthenticeerd, maar niet gebonden aan een werkruimte. Elke call die aan een werkruimte is gebonden, draagt zijn eigen workspace_id in zijn argumenten mee, en de dispatcher weigert zo’n call zonder dat id. Daardoor kunnen twee clients op één server verschillende actieve werkruimtes hebben zonder dat er een door de server bijgehouden “huidige werkruimte” is waarover ze het oneens kunnen zijn — en het betekent dat isolatie per call wordt gehandhaafd, niet per verbinding.

Dit is één Dart-workspace met één opgelost lockfile, van eenentwintig leden — vijftien pakketten plus zes apps — met de desktop- en webapp (control_center) als workspace-root. De zes apps zijn de headless serverbinary (cc_server), de headless fleet-uitvoerder (cc_worker), de telefoon-PWA (cc_remote), de WebSocket-relaybroker (cc_signaling_server), de Widgetbook-galerij (cc_gallery) en de publieke demohost (cc_demo_server). De vijftien pakketten zijn cc_ui (designsysteem), cc_domain (pure-Dart gedeelde kernel: entiteiten, value objects, poorten, gebeurtenissen en de domeinlaag van elke feature), cc_harness (de ingebouwde agentkernel: een pure-Dart agentloop — berichten, providerpoort, tools, compactie, sturing, hooks, subagents — zonder dart:io en zonder andere cc_*-afhankelijkheid), cc_harness_runtime (de ingebouwde harness-runtimeadapter: alleen-VM-streamingproviders, het brokeren van OAuth/PKCE-referenties, referentieopslag en de generieke toolset — draait agents zonder dat er een externe CLI is geïnstalleerd), cc_rpc (clienttransports), cc_host (serverkernel), cc_data (externe repositories), cc_persistence (serverzijdige database), cc_server_core (compositie van de app-server + runtime voor identiteit/aanwezigheid/fleet/evals), cc_infra (alleen-VM-adapters), cc_mcp (MCP-tools), cc_mcp_client (client voor externe MCP-servers die hun tools in het lokale register inbrengt), cc_markdown (markdown-engine in de repo), cc_natives (FFI-bladpakket) en system_audio_capture. Zie Deployment en clients voor de volledige clientmatrix en wat waar draait.

Presentation → Application/Providers → Domain ← Infrastructure
  • Domeinlaag: pure Dart-entiteiten, value objects, repository-interfaces, poorten en domeinservices. Nul infrastructuur-imports (geen dio, drift of netwerkmodellen).
  • Presentatielaag: schermen, widgets, notifiers. Geen directe toegang tot drift/DAO/datalaag; alles loopt via Riverpod-providers → repositories.
  • Infrastructuurlaag: concrete implementaties van domeinpoorten en repository-interfaces. Is afhankelijk van het domein, nooit andersom.

De volledige vorm van een feature is:

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

Heel weinig features onder lib/features/ hebben echt alle vier mappen, en die gaten zijn informatief, niet toevallig. De gedeelde kernel heeft de domain/ van elke feature opgenomen in cc_domain, en de thin client bevat geen repository-implementaties, dus de meeste features onder lib/ bestaan alleen uit presentation/ plus providers/. Een data/- of domain/-map die in de client overblijft, markeert iets dat echt aan de clientkant zit.

Twee met naam genoemde uitzonderingen zijn het kennen waard:

  • mcp bestaat alleen uit providers. De instellingen- en status-UI staat onder settings/ en het tooloppervlak zelf is een pakket (cc_mcp), geen featuremap.
  • orchestration en plan_studio bevatten alleen presentation/ en providers/.

De feature guardrails legt zijn domeinlaag in cc_domain/features/guardrails/ in plaats van in de rootapp, omdat de server hem handhaaft: een regel koppelt een ActionClass (of een shell-commandPrefix) aan een beslissing — allow, prompt of deny — binnen een scope, regels worden per werkruimte opgeslagen en elke bewaakte agentactie loopt vóór uitvoering door de beleidsketen. De resolutie doorloopt space > agent > workspace > mode preset > built-in default en de eerste scope met een passende regel beslist; meest beperkend is alleen een tiebreak binnen één scope en bij het combineren van de verschillende klassen die één actie opgeeft. Het bedieningsoppervlak voor de operator is Instellingen → Werkruimte → Agentmachtigingen, op /workspaces/<id>/settings/workspace/permissions. Zie Beveiligingen voor het model.

packages/cc_domain/lib/core/domain/ bevat entiteiten en repositories die door 3+ features worden gedeeld. Het is pure Dart met nul infrastructuurafhankelijkheden, waardoor zowel de Flutter-client als de Flutter-vrije serverbinary dezelfde definities kan importeren:

  • Kernentiteiten: Agent, AgentRunLog, Workspace, Repo, ReviewSpaceAssociation
  • Identiteit en multiplayer: User, Principal (sealed UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Geheugenentiteiten: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Gedeelde value objects: AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Gedeelde poorten: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Domeinservices: MemoryAccessPolicy, ActivityLogger, MentionResolver, RunLivenessClassifier
  • DomainEventBus + gebeurtenistypen (werkruimte/agent, PR/review, berichten, ticketing, pipeline, orkestratie, geheugen, agenda/vergaderingen, identiteit en lidmaatschap, observability)

Control Center is multiuser: mensen en agents zijn gelijkwaardige actoren, verenigd onder één Principal. Een lid van een werkruimte is een User die aan een Workspace is gebonden met een WorkspaceRole (owner/admin/member/viewer/guest). Lidmaatschap is de toegangsgrens — geen koppelingssleutel — en machtigingen per repo voorkomen dat lidmaatschap van een werkruimte meer privileges geeft dan de forge. De eerste gebruiker die op een verse server wordt aangemaakt, bezit de installatie; anderen komen erbij via een uitnodiging of OIDC. Bij het aanmaken van een werkruimte wordt die gebruiker als owner vastgelegd.

Realtime samenwerking is een autoritatieve server + last-writer-wins (LWW) per veld, geen CRDT. Aanwezigheid (wie er is, waar ze zijn, cursors, typen, de livestatus en lopende kosten van een agent) is een apart vluchtig kanaal dat nooit wordt opgeslagen; duurzame state loopt over een ruggengraat van optimistische mutatie + server-rebase + LWW. Mensen en agents delen één ledenlijst. Bij één menselijke operator staat het aanwezigheidskanaal stil en verschijnt er geen UI voor de ledenlijst — multiplayer kost de sologebruiker niets.

Poorten zijn abstracte interfaces in de domeinlaag. Adapters zijn concrete implementaties in de infrastructuur. Omdat de headless server de data bezit en Flutter-vrij moet bouwen, is bijna elke adapter alleen voor de VM en staat hij in packages/cc_infra/lib/src/, geordend per aandachtsgebied:

Poort Locatie van de 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/ (aan de clientkant: rendert servergebeurtenissen lokaal)
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 en GitHub Issues zijn ticket-sync-adapters in cc_infra/lib/src/tickets/sync/; lokale tickets staan in de database van de werkruimte, zonder TicketProviderPort.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (een pure-Dart-domeinservice — heeft geen infrastructuur nodig)

De compositieroot van de app (di/providers.dart) koppelt poorten aan de clientkant aan implementaties via Riverpod-providers; de server stelt zijn adapters samen in cc_server_core.

Riverpod voor alle clientstate:

  • Notifier<T> en AsyncNotifier<T> voor muteerbare state
  • FutureProvider<T> voor async data
  • Lijsten en liverijen komen binnen via RPC-subscriptions als AsyncValue<List<T>> — de server gebruikt intern Drift .watch(); geen enkele client opent Drift
  • MCP-tools krijgen afhankelijkheden als getypeerde constructorparameters, nooit Ref

Drift (SQLite), opgesplitst in twee databases, beide in cc_persistence:

  • GlobalDatabase (<dataDir>/global.db) — een korte lijst met tabellen die echt serverbreed zijn: het werkruimteregister, gebruikers/voorkeuren/gekoppelde apparaten, SSO-verbindingen, installatiebrede serverinstellingen, beheerde actiebeleidsregels (een installatiebrede begrenzing die beveiligingen van werkruimtes alleen maar strenger kan maken), de nieuwsfeed per gebruiker, de fleetwachtrij (workers/jobs/plaatsingslog), de pre-auth-index workspace_routes en installatiemetadata. Bij het opstarten wordt alleen dit bestand geopend, dus de opstartkosten blijven vlak, hoeveel geschiedenis de werkruimtes ook opbouwen. Een tabel hier toevoegen is een isolatiebeslissing waarvoor een ratchet-test je dwingt te argumenteren.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) — één map per werkruimte, met daarin één SQLite-bestand plus alles wat alleen bij die werkruimte hoort, zodat de worktrees en agentbestanden van een gesprek naast de database staan en ermee worden verwijderd. Bijna alles staat hier: agents, ruimtes, tickets, geheugen, pipelines, vergaderingen, de codegraph, reviews, repos. Bestanden worden lazy geopend bij het eerste gebruik via WorkspaceDatabaseManager.of(workspaceId) en betalen dan hun eigen quick_check, FTS/trigger-installatie en vector_init.

Beide helften begonnen met een samengevoegde v1-basislijn — onCreate bouwt het huidige schema — waarbij migratiestappen alleen worden toegevoegd wanneer een gedeployde database moet worden meegenomen.

Omdat één werkruimte één bestand is, is het exporteren van een werkruimte één VACUUM INTO-statement (workspace.export), geen dump tabel voor tabel; workspace.import neemt zo’n bestand over en gebruikt de ingebedde workspace_meta om te bepalen of het zijn eigen, opnieuw overgenomen bestand betreft of een bestand van een andere installatie.

Beide databases zijn eigendom van cc_persistence en worden alleen door cc_server geopend; geen enkele client opent een DB — elke client leest en schrijft via RPC. Tabellen worden gedefinieerd in packages/cc_persistence/lib/database/tables/, DAO’s in .../daos/; domeinentiteiten zijn pure Dart, los van de Drift-tabelklassen, met mapping in de datalagen. FTS5 verzorgt zoeken in volledige tekst en sqlite_vector-embeddings, beide alleen op de server; beide databases draaien met PRAGMA foreign_keys=ON in WAL-journalmodus.

go_router, met één gegeven dat de rest bepaalt: elke bestemming in de app krijgt een werkruimteprefix als /workspaces/:workspaceId/…. Het werkruimte-id in de URL is de enige bron van waarheid voor de actieve werkruimte — activeWorkspaceIdProvider wordt vanuit de route aangestuurd, niet andersom — en daarom heeft het lezen van gegevens met werkruimtescope buiten een geprefixte route nergens om zijn id vandaan te halen.

  • Routebuilder-functies in router/routes.dart, elk met het werkruimte-id als eerste argument, geen stringconstanten
  • Vier routes vóór de context hebben geen prefix: /splash, /onboarding, /signed-out en /workspaces (de kiezer)
  • ShellRoute omvat de app-shell; die vier oppervlakken renderen schermvullend daarbuiten
  • Auth-guard: een onvolledige installatie leidt naar /onboarding; een voltooide installatie waarvan de forge-referentie verdwenen is, leidt naar /signed-out. Voltooid betekent dat er voor de aangemelde gebruiker minstens één forge is gekoppeld en dat er minstens één werkruimte bestaat — niet specifiek GitHub, en de gh-CLI is geen authenticatiemethode
  • Onboarding-gate in features/auth/providers/

Alle externe HTTP zit in de server. Clients bellen nooit rechtstreeks naar GitHub, Linear of Google — ze roepen RPC-operaties op de server aan, en zelfs externe media worden via het /proxy/media-endpoint van de server opgehaald in plaats van bij de oorspronkelijke bron.

  • dio-clients in packages/cc_infra/lib/src/: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphQLClient, GitLabApiClient, BitbucketApiClient, LinearGraphQlClient en GoogleCalendarApiClient
  • Injectie van auth-tokens via interceptors, waaronder een Google OAuth-interceptor per account in cc_server_core die tokens vernieuwt bij een 401
  • Alle fouten worden gemapt via packages/cc_infra/lib/src/network/error_mapper.dart → getypeerde subklassen van AppException die in cc_domain zijn gedefinieerd

De server bezit de referenties die ertoe doen: GitHub- en ticketingtokens voor het starten van agents, LLM-providersleutels en OAuth-tokens staan allemaal onder de datadir van de server of in zijn omgeving. Wat de keychain van de client nog bevat, is zijn eigen apparaatreferentie en de koppelingssleutel voor een externe server.

  • Secrets in flutter_secure_storage (macOS-keychain, Windows Credential Manager, Linux libsecret) — de apparaatreferentie van de client en koppelingssleutels voor een externe server, geen providertokens
  • shared_preferences alleen voor niet-gevoelige voorkeuren (thema, lettertype, layout)
  • Overgebleven providersecrets uit oudere installaties worden één keer aan de server overhandigd door features/auth/providers/credential_migration.dart en daarna uit de keychain van de client verwijderd

Architectuurbeperkingen worden gevalideerd door test/core/architecture_constraints_test.dart, dat faalt als afhankelijkheidsregels worden geschonden.