Architektur
Überblick
Abschnitt betitelt „Überblick“Control Center folgt einer Clean Architecture nach Features, mit dem Muster Ports und Adapter. Der Code ist nach Fachdomänen (Features) gegliedert. Strikte Abhängigkeitsregeln setzt ein automatischer Architekturtest durch.
Ein Thin Client über einem Headless-Server
Abschnitt betitelt „Ein Thin Client über einem Headless-Server“Control Center ist eine Thin-Client-Architektur. Kein Client öffnet die Datenbank. Ein Headless-Prozess cc_server besitzt die Daten (eine kleine global.db plus eine SQLite-Datei pro Arbeitsbereich, die erst bei Bedarf geöffnet wird) und stellt sie über WebSocket-RPC bereit. Die Desktop-App, der Web-Build und die Begleit-App fürs Telefon sind Renderer über genau dieser einen RPC-Verbindung.
Beim ersten Start wählt der Desktop, wie er seinen Server erreicht: lokal (er startet auf diesem Rechner einen cc_server und spricht über Loopback, das ist die Vorgabe) oder entfernt (er verbindet sich mit einem cc_server an einem anderen Ort über eine gesicherte WebSocket-Verbindung). Der Web-Client ist immer entfernt. Der Boot-Resolver (bootstrap/server_backend.dart) wählt den Weg, bevor es Riverpod gibt, und überschreibt dann rpcClientProvider mit dem verbundenen Client. Jedes Feature liest und schreibt deshalb über den Server, nicht in eine lokale Datenbank.
Die RPC-Schicht selbst ist zustandslos. Eine Sitzung ist authentifiziert, aber an keinen Arbeitsbereich gebunden. Jeder Aufruf mit Arbeitsbereich trägt in seinen Argumenten ein eigenes workspace_id, und der Dispatcher lehnt einen solchen Aufruf ohne dieses Argument ab. So können zwei Clients auf einem Server verschiedene aktive Arbeitsbereiche halten, ohne dass der Server einen „aktuellen Arbeitsbereich“ vorhält, über den sie sich uneins wären. Die Isolation gilt pro Aufruf, nicht pro Verbindung.
Das ist ein einzelnes Dart-Workspace mit aufgelöstem Lockfile und 21 Mitgliedern: 15 Pakete plus 6 Apps. Die Desktop- und Web-App (control_center) ist die Workspace-Wurzel. Die sechs Apps sind die Headless-Server-Binärdatei (cc_server), der Headless-Fleet-Executor (cc_worker), die Telefon-PWA (cc_remote), der WebSocket-Relay-Broker (cc_signaling_server), die Widgetbook-Galerie (cc_gallery) und der öffentliche Demo-Host (cc_demo_server). Die 15 Pakete sind cc_ui (Designsystem), cc_domain (gemeinsamer Kern in reinem Dart: Entitäten, Wertobjekte, Ports, Ereignisse und die Domänenschicht jedes Features), cc_harness (der eingebaute Agenten-Kernel: eine Agentenschleife in reinem Dart, mit Nachrichten, Provider-Port, Tools, Kompaktierung, Steuerung, Hooks und Subagenten, ohne dart:io und ohne andere cc_*-Abhängigkeit), cc_harness_runtime (der eingebaute Laufzeitadapter des Harness: Streaming-Provider nur für die VM, Vermittlung von OAuth/PKCE-Zugangsdaten, Zugangsdatenspeicher und der generische Tool-Satz; Agenten laufen ohne installiertes externes CLI), cc_rpc (Client-Transporte), cc_host (Server-Kernel), cc_data (entfernte Repositories), cc_persistence (Datenbank auf der Serverseite), cc_server_core (Komposition des App-Servers plus Laufzeit für Identität, Präsenz, Fleet und Evals), cc_infra (Adapter nur für die VM), cc_mcp (MCP-Tools), cc_mcp_client (Client für externe MCP-Server, der deren Tools in die lokale Registry überführt), cc_markdown (eigene Markdown-Engine im Repo), cc_natives (FFI-Blattpaket) und system_audio_capture. Die vollständige Client-Matrix und was wo läuft, steht unter Bereitstellung und Clients.
Abhängigkeitsregel
Abschnitt betitelt „Abhängigkeitsregel“Presentation → Application/Providers → Domain ← Infrastructure- Domänenschicht: Entitäten in reinem Dart, Wertobjekte, Repository-Schnittstellen, Ports und Domänendienste. Keine Infrastruktur-Imports (kein dio, kein Drift, keine Netzwerkmodelle).
- Präsentationsschicht: Screens, Widgets, Notifier. Kein direkter Zugriff auf Drift, DAOs oder die Datenschicht. Alles läuft über Riverpod-Provider → Repositories.
- Infrastrukturschicht: konkrete Implementierungen der Domänenports und Repository-Schnittstellen. Sie hängt von der Domäne ab, nur in dieser Richtung.
Aufbau eines Features
Abschnitt betitelt „Aufbau eines Features“Die vollständige Form eines Features ist:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersUnter lib/features/ tragen nur sehr wenige Features wirklich alle vier Teile, und die Lücken sagen etwas aus. Sie sind kein Versehen. Der gemeinsame Kern hat das domain/ jedes Features nach cc_domain übernommen, und der Thin Client hält keine Repository-Implementierungen. Die meisten Features unter lib/ bestehen deshalb aus presentation/ plus providers/ und sonst nichts. Ein Verzeichnis data/ oder domain/, das im Client übrig bleibt, markiert etwas, das wirklich auf der Clientseite liegt.
Zwei benannte Ausnahmen:
mcphat nur Provider. Einstellungen und Statusoberfläche liegen untersettings/, und die Tool-Fläche selbst ist ein Paket (cc_mcp), kein Feature-Ordner.orchestrationundplan_studiotragen nurpresentation/undproviders/.
Das Feature guardrails legt seine Domänenschicht in cc_domain/features/guardrails/ ab, nicht in der Root-App, weil der Server sie durchsetzt. Eine Regel bildet eine ActionClass (oder ein Shell-commandPrefix) in einem Geltungsbereich auf eine Entscheidung ab: allow, prompt oder deny. Regeln liegen pro Arbeitsbereich, und jede geregelte Agentenaktion wird vor dem Lauf über die Policy-Kette aufgelöst. Die Auflösung geht space > agent > workspace > mode preset > built-in default entlang, und der erste Geltungsbereich mit einer passenden Regel entscheidet. Die strengste Regel gilt nur als Stichentscheid innerhalb eines Geltungsbereichs und beim Zusammenführen der mehreren Klassen, die eine Aktion deklariert. Die Oberfläche für den Betreiber ist Einstellungen → Arbeitsbereich → Agent-Berechtigungen, unter /workspaces/<id>/settings/workspace/permissions. Das Modell steht unter Schutzregeln.
Gemeinsamer Kern
Abschnitt betitelt „Gemeinsamer Kern“packages/cc_domain/lib/core/domain/ hält Entitäten und Repositories, die in mindestens drei Features vorkommen. Es ist reines Dart ohne Infrastrukturabhängigkeiten. Deshalb importieren der Flutter-Client und die Server-Binärdatei ohne Flutter dieselben Definitionen:
- Kernentitäten:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Identität und Multiplayer:
User,Principal(sealedUserPrincipal|AgentPrincipal),WorkspaceMember,WorkspaceRole - Gedächtnis-Entitäten:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Gemeinsame Wertobjekte:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Gemeinsame Ports:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Domänendienste:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBusund Ereignistypen (Arbeitsbereich/Agent, PR/Review, Nachrichten, Tickets, Pipeline, Orchestrierung, Gedächtnis, Kalender/Besprechungen, Identität und Mitgliedschaft, Observability)
Identität und Multiplayer
Abschnitt betitelt „Identität und Multiplayer“Control Center ist mehrbenutzerfähig. Menschen und Agenten sind gleichrangige Akteure, zusammengefasst durch einen Principal. Ein Mitglied eines Arbeitsbereichs ist ein User, gebunden an einen Workspace in einer WorkspaceRole (owner/admin/member/viewer/guest). Die Zugriffsgrenze ist die Mitgliedschaft. Ein Kopplungsschlüssel bildet sie nicht, und Grants pro Repo halten die Mitgliedschaft im Arbeitsbereich innerhalb der Rechte des Code-Hosts. Der erste User, der auf einem frischen Server angelegt wird, besitzt die Installation. Weitere treten per Einladung oder über OIDC bei. Das Anlegen eines Arbeitsbereichs trägt diesen User als owner ein.
Die Zusammenarbeit in Echtzeit läuft über einen autoritativen Server und Last-Writer-Wins (LWW) pro Feld. Ein CRDT ist das nicht. Präsenz (wer anwesend ist und wo, Cursor, Tippen, der Live-Status eines Agenten und die laufenden Kosten) ist eine eigene flüchtige Spur, die nie gespeichert wird. Dauerhafter Zustand läuft über optimistische Mutation, Rebase auf dem Server und LWW. Menschen und Agenten teilen eine Liste. Bei einem einzelnen menschlichen Operator ruht die Präsenzspur, und die Oberfläche der Anwesenheitsliste bleibt aus. Für eine einzelne Person kostet Multiplayer nichts.
Ports und Adapter
Abschnitt betitelt „Ports und Adapter“Ports sind abstrakte Schnittstellen in der Domänenschicht. Adapter sind konkrete Implementierungen in der Infrastruktur. Weil der Headless-Server die Daten besitzt und ohne Flutter gebaut werden muss, ist fast jeder Adapter nur für die VM und liegt in packages/cc_infra/lib/src/, nach Zuständigkeit geordnet:
| Port | Adapter-Ort |
|---|---|
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/ (clientseitig: rendert Serverereignisse lokal) |
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 und GitHub Issues sind Ticket-Sync-Adapter in cc_infra/lib/src/tickets/sync/. Lokale Tickets liegen in der Datenbank des Arbeitsbereichs, ohne TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (ein Domänendienst in reinem Dart, er braucht keine Infrastruktur) |
Die Kompositionswurzel der App (di/providers.dart) bindet clientseitige Ports über Riverpod-Provider an Implementierungen. Der Server setzt seine Adapter in cc_server_core zusammen.
Zustandsverwaltung
Abschnitt betitelt „Zustandsverwaltung“Riverpod für den gesamten Client-Zustand:
Notifier<T>undAsyncNotifier<T>für veränderlichen ZustandFutureProvider<T>für asynchrone Daten- Listen und Live-Zeilen kommen über RPC-Abonnements als
AsyncValue<List<T>>. Der Server nutzt intern Drift.watch(). Kein Client öffnet Drift - MCP-Tools bekommen ihre Abhängigkeiten als typisierte Konstruktorparameter, nie als
Ref
Datenbank
Abschnitt betitelt „Datenbank“Drift (SQLite), aufgeteilt in zwei Datenbanken, beide in cc_persistence:
GlobalDatabase(<dataDir>/global.db) hält eine kurze Liste wirklich serverweiter Tabellen: die Registry der Arbeitsbereiche, User, Präferenzen und gekoppelte Geräte, SSO-Verbindungen, serverweite Einstellungen der Installation, verwaltete Aktionsrichtlinien (eine installationsweite Klammer, die Schutzregeln eines Arbeitsbereichs nur verschärfen kann), den Newsfeed pro User, die Fleet-Warteschlange (Worker, Jobs, Placement-Log), den Indexworkspace_routesvor der Authentifizierung und die Installationsmetadaten. Der Start öffnet nur diese Datei, deshalb bleibt der Startaufwand gleich, egal wie viel Verlauf die Arbeitsbereiche ansammeln. Eine Tabelle hier anzulegen ist eine Isolationsentscheidung, und ein Ratchet-Test zwingt dich, sie zu begründen.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) ist ein Verzeichnis pro Arbeitsbereich. Darin liegen eine SQLite-Datei und alles andere, das nur zu diesem Arbeitsbereich gehört, sodass die Worktrees einer Unterhaltung und die Agentendateien neben der Datenbank liegen und mit ihr gelöscht werden. Hier liegt fast alles: Agenten, Bereiche, Tickets, Gedächtnis, Pipelines, Besprechungen, der Code-Graph, Reviews und Repos. Dateien öffnen sich erst beim ersten Zugriff überWorkspaceDatabaseManager.of(workspaceId)und führen dann ihr eigenesquick_check, die Installation von FTS und Triggern sowievector_initaus.
Beide Hälften gehen von einer zusammengefassten v1-Basis aus. onCreate baut das aktuelle Schema. Migrationsschritte kommen nur dazu, wenn eine ausgerollte Datenbank weitergetragen werden muss.
Ein Arbeitsbereich ist eine Datei. Der Export ist deshalb ein einzelnes Statement VACUUM INTO (workspace.export), kein Dump Tabelle für Tabelle. workspace.import übernimmt eine solche Datei und unterscheidet anhand des eingebetteten workspace_meta die eigene, erneut übernommene Datei von einer Datei, die von einer anderen Installation stammt.
Beide Datenbanken gehören cc_persistence, und nur cc_server öffnet sie. Kein Client öffnet eine Datenbank. Jeder Client liest und schreibt über RPC. Tabellen stehen in packages/cc_persistence/lib/database/tables/, DAOs in .../daos/. Domänenentitäten sind reines Dart, getrennt von den Drift-Tabellenklassen. Das Mapping liegt in den Datenschichten. FTS5 übernimmt die Volltextsuche, sqlite_vector die Embeddings. Beides läuft nur auf dem Server. Beide Datenbanken laufen mit PRAGMA foreign_keys=ON im WAL-Journalmodus.
Routing
Abschnitt betitelt „Routing“go_router, mit einer Tatsache, die den Rest bestimmt: jedes Ziel in der App trägt den Arbeitsbereich als Präfix, in der Form /workspaces/:workspaceId/…. Die Arbeitsbereichs-ID in der URL ist die einzige Quelle der Wahrheit für den aktiven Arbeitsbereich. activeWorkspaceIdProvider folgt der Route, die Route folgt ihm nicht. Außerhalb einer Route mit diesem Präfix gibt es keine Quelle für die Arbeitsbereichs-ID, und arbeitsbereichsbezogene Daten lassen sich dort nicht lesen.
- Route-Builder-Funktionen in
router/routes.dart. Jede nimmt die Arbeitsbereichs-ID als erstes Argument, keine String-Konstanten - Vier Routen vor dem Kontext haben kein Präfix:
/splash,/onboarding,/signed-outund/workspaces(die Auswahl) ShellRouteumschließt die App-Shell. Diese vier Flächen rendern außerhalb davon im Vollbild- Auth-Guard: Ein unvollständiges Setup führt nach
/onboarding. Ein vollständiges Setup, dessen Zugangsdaten zum Code-Host fehlen, führt nach/signed-out. Vollständig heißt, dass für den angemeldeten User mindestens ein Code-Host verbunden ist und mindestens ein Arbeitsbereich existiert. GitHub ist dafür nicht vorgeschrieben, und diegh-CLI ist kein Authentifizierungsverfahren - Das Onboarding-Gate liegt in
features/auth/providers/
Netzwerk (serverseitig)
Abschnitt betitelt „Netzwerk (serverseitig)“Sämtliches externes HTTP liegt auf dem Server. Clients sprechen GitHub, Linear und Google nicht direkt an. Sie rufen RPC-Operationen des Servers auf, und auch entfernte Medien kommen über den Endpunkt /proxy/media des Servers, nicht vom ursprünglichen Host.
- dio-Clients in
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClientundGoogleCalendarApiClient - Interceptoren setzen die Auth-Tokens ein, einschließlich eines Google-OAuth-Interceptors pro Konto in
cc_server_core, der Tokens bei einem 401 erneuert - Alle Fehler laufen über
packages/cc_infra/lib/src/network/error_mapper.dart→ typisierte Unterklassen vonAppException, definiert incc_domain
Sicherheit
Abschnitt betitelt „Sicherheit“Der Server hält die Zugangsdaten, auf die es ankommt: GitHub- und Ticket-Tokens für Agentenstarts, Schlüssel der LLM-Anbieter und OAuth-Tokens liegen im Datenverzeichnis des Servers oder in seiner Umgebung. In der Schlüsselkette des Clients bleiben sein eigenes Geräte-Credential und der Kopplungsschlüssel für einen Remote-Server.
- Geheimnisse in
flutter_secure_storage(Schlüsselbund unter macOS, Anmeldeinformationsverwaltung unter Windows, libsecret unter Linux): das Geräte-Credential des Clients und die Kopplungsschlüssel für einen Remote-Server. Tokens der Anbieter liegen hier nicht shared_preferencesnur für nicht sensible Präferenzen (Thema, Schrift, Layout)- Anbieter-Geheimnisse aus älteren Installationen übergibt
features/auth/providers/credential_migration.darteinmal an den Server und löscht sie danach aus der Schlüsselkette des Clients
Durchsetzung der Architektur
Abschnitt betitelt „Durchsetzung der Architektur“Die Architekturvorgaben prüft test/core/architecture_constraints_test.dart. Der Test schlägt fehl, wenn Abhängigkeitsregeln verletzt sind.
Verwandte Konzepte
Abschnitt betitelt „Verwandte Konzepte“- Arbeitsbereiche und Isolation: wie die Isolation durchgesetzt wird
- Lebenszyklus der Agenten-Ausführung: der Ablauf einer Ausführung
- Domain-Ereignisse: Kommunikation zwischen Features
- Bereitstellung und Clients: das Thin-Client-Modell mit Server und die Client-Matrix