Arhitectură
Prezentare generală
Secțiune intitulată „Prezentare generală”Control Center urmează Clean Architecture orientată pe funcționalități, cu modelul porturi și adaptoare. Baza de cod este organizată în jurul domeniilor de business (funcționalități), cu reguli stricte de dependență impuse de un test automat de arhitectură.
Un client subțire peste un server headless
Secțiune intitulată „Un client subțire peste un server headless”Control Center este o arhitectură cu client subțire. Niciun client nu deschide baza de date — un proces headless cc_server deține datele (un global.db mic plus un fișier SQLite pentru fiecare spațiu de lucru, deschis la cerere) și le servește prin RPC peste WebSocket. Aplicația desktop, versiunea web și companionul de telefon sunt toate simple interfețe de randare peste acea unică conexiune RPC.
Desktopul alege cum ajunge la serverul său la prima lansare: local (pornește un cc_server pe această mașină și comunică prin loopback — varianta implicită) sau remote (se conectează la un cc_server aflat în altă parte, printr-un WebSocket securizat). Clientul web este întotdeauna remote. Rezolvatorul de pornire (bootstrap/server_backend.dart) alege calea înainte ca Riverpod să existe, apoi suprascrie rpcClientProvider cu clientul conectat — astfel încât fiecare funcționalitate citește și scrie prin server, nu printr-o bază de date locală.
Stratul RPC în sine este fără stare: o sesiune este autentificată, dar nu este legată de un spațiu de lucru. Fiecare apel care ține de un spațiu de lucru își cară propriul workspace_id în argumente, iar dispecerul refuză un astfel de apel fără el. Asta permite ca doi clienți de pe același server să aibă spații de lucru active diferite fără un „spațiu de lucru curent” ținut de server cu care să intre în contradicție — și înseamnă că izolarea este impusă la fiecare apel, nu la fiecare conexiune.
Acesta este un singur spațiu de lucru Dart cu lockfile rezolvat, de douăzeci și unu de membri — cincisprezece pachete plus șase aplicații — cu aplicația desktop+web (control_center) ca rădăcină a spațiului de lucru. Cele șase aplicații sunt binarul de server headless (cc_server), executantul headless de flotă (cc_worker), PWA-ul de telefon (cc_remote), brokerul releu WebSocket (cc_signaling_server), galeria Widgetbook (cc_gallery) și gazda publică de demo (cc_demo_server). Cele cincisprezece pachete sunt cc_ui (sistem de design), cc_domain (kernel partajat în Dart pur: entități, obiecte-valoare, porturi, evenimente și stratul de domeniu al fiecărei funcționalități), cc_harness (kernelul de agent integrat: o buclă de agent în Dart pur — mesaje, port de furnizor, instrumente, compactare, direcționare, hook-uri, subagenți — fără dart:io și fără nicio altă dependență cc_*), cc_harness_runtime (adaptorul de runtime al harness-ului integrat: furnizori de streaming doar pentru VM, brokeraj de acreditări OAuth/PKCE, depozite de acreditări și setul generic de instrumente — rulează agenți fără niciun CLI extern instalat), cc_rpc (transporturi de client), cc_host (kernel de server), cc_data (repository-uri remote), cc_persistence (bază de date pe server), cc_server_core (compoziție aplicație-server + runtime de identitate/prezență/flotă/eval-uri), cc_infra (adaptoare doar pentru VM), cc_mcp (instrumente MCP), cc_mcp_client (client pentru servere MCP externe, care le aduce instrumentele în registrul local), cc_markdown (motor markdown din depozit), cc_natives (nod frunză FFI) și system_audio_capture. Vezi Implementare și clienți pentru matricea completă de clienți și ce rulează unde.
Regula dependențelor
Secțiune intitulată „Regula dependențelor”Presentation → Application/Providers → Domain ← Infrastructure- Stratul de domeniu: entități, obiecte-valoare, interfețe de repository, porturi și servicii de domeniu în Dart pur. Zero importuri de infrastructură (fără dio, drift sau modele de rețea).
- Stratul de prezentare: ecrane, widgeturi, notifiers. Fără acces direct la drift/DAO/stratul de date; totul trece prin furnizorii Riverpod → repository-uri.
- Stratul de infrastructură: implementări concrete ale porturilor de domeniu și ale interfețelor de repository. Depinde de domeniu, niciodată invers.
Structura funcționalităților
Secțiune intitulată „Structura funcționalităților”Structura completă a unei funcționalități este:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersFoarte puține funcționalități din lib/features/ chiar au toate cele patru, iar lipsurile sunt informative, nu accidentale. Kernelul partajat a absorbit domain/ al fiecărei funcționalități în cc_domain, iar clientul subțire nu ține nicio implementare de repository, așa că majoritatea funcționalităților din lib/ sunt presentation/ plus providers/ și nimic altceva. Un director data/ sau domain/ care supraviețuiește în client marchează ceva cu adevărat de partea clientului.
Merită cunoscute două excepții numite:
mcpconține doar providers. Interfața sa de setări și de stare se află subsettings/, iar suprafața de instrumente în sine este un pachet (cc_mcp), nu un director de funcționalitate.orchestrationșiplan_studioconțin doarpresentation/șiproviders/.
Funcționalitatea guardrails își pune stratul de domeniu în cc_domain/features/guardrails/, nu în aplicația rădăcină, pentru că serverul o impune: o regulă asociază un ActionClass (sau un commandPrefix de shell) unei decizii — allow, prompt sau deny — la un domeniu de aplicare, regulile sunt stocate pentru fiecare spațiu de lucru, iar fiecare acțiune guvernată a unui agent se rezolvă prin lanțul de politici înainte de a rula. Rezolvarea parcurge space > agent > workspace > mode preset > built-in default și primul domeniu de aplicare cu o regulă potrivită decide; varianta cea mai restrictivă este doar un criteriu de departajare în interiorul unui singur domeniu de aplicare și la combinarea mai multor clase declarate de o acțiune. Suprafața pentru operator este Setări → Spațiu de lucru → Permisiuni agent, la /workspaces/<id>/settings/workspace/permissions. Vezi Guardrail-uri pentru model.
Kernel partajat
Secțiune intitulată „Kernel partajat”packages/cc_domain/lib/core/domain/ conține entități și repository-uri partajate de 3 sau mai multe funcționalități. Este Dart pur, cu zero dependențe de infrastructură, ceea ce permite atât clientului Flutter, cât și binarului de server fără Flutter să importe aceleași definiții:
- Entități de bază:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Identitate și multiplayer:
User,Principal(sealedUserPrincipal|AgentPrincipal),WorkspaceMember,WorkspaceRole - Entități de memorie:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Obiecte-valoare partajate:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Porturi partajate:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Servicii de domeniu:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ tipuri de evenimente (spațiu de lucru/agent, PR/revizuire, mesagerie, tichete, pipeline, orchestrare, memorie, calendar/întâlniri, identitate și apartenență, observabilitate)
Identitate și multiplayer
Secțiune intitulată „Identitate și multiplayer”Control Center este multi-utilizator: oamenii și agenții sunt actori de rang egal, unificați de un Principal. Un membru al unui spațiu de lucru este un User legat de un Workspace printr-un WorkspaceRole (owner/admin/member/viewer/guest). Apartenența este granița de acces — nu o cheie de asociere — iar acordurile per repo împiedică apartenența la spațiul de lucru să acorde mai multe privilegii decât forge-ul. Primul utilizator creat pe un server nou deține instalarea; ceilalți se alătură prin invitație sau OIDC. Crearea unui spațiu de lucru îl marchează pe acel utilizator ca owner.
Colaborarea în timp real folosește modelul server autoritativ + last-writer-wins (LWW) pe fiecare câmp, nu un CRDT. Prezența (cine e aici, unde se află, cursoare, tastare, starea live a unui agent și costul rulării) este o cale efemeră separată, care nu este niciodată persistată; starea durabilă se bazează pe o coloană vertebrală de mutații optimiste + rebazare pe server + LWW. Oamenii și agenții împart aceeași listă de participanți. Cu un singur operator uman, calea de prezență rămâne inactivă și nu apare nicio interfață de listă — multiplayer nu costă nimic utilizatorul care lucrează singur.
Porturi și adaptoare
Secțiune intitulată „Porturi și adaptoare”Porturile sunt interfețe abstracte din stratul de domeniu. Adaptoarele sunt implementări concrete în infrastructură. Deoarece serverul headless deține datele și trebuie să se compileze fără Flutter, aproape fiecare adaptor este doar pentru VM și se află în packages/cc_infra/lib/src/, organizat pe preocupări:
| Port | Locația adaptorului |
|---|---|
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/ (de partea clientului: randează local evenimentele serverului) |
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 și GitHub Issues sunt adaptoare de sincronizare a tichetelor în cc_infra/lib/src/tickets/sync/; tichetele locale se află în baza de date a spațiului de lucru, fără TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (un serviciu de domeniu în Dart pur — nu are nevoie de infrastructură) |
Rădăcina de compoziție a aplicației (di/providers.dart) leagă porturile de pe client de implementări prin furnizorii Riverpod; serverul își compune adaptoarele în cc_server_core.
Gestionarea stării
Secțiune intitulată „Gestionarea stării”Riverpod pentru toată starea clientului:
Notifier<T>șiAsyncNotifier<T>pentru starea mutabilăFutureProvider<T>pentru date asincrone- Listele și rândurile live sosesc prin abonamente RPC ca
AsyncValue<List<T>>— serverul folosește intern Drift.watch(); niciun client nu deschide Drift - Instrumentele MCP primesc dependențele ca parametri de constructor tipizați, niciodată
Ref
Baza de date
Secțiune intitulată „Baza de date”Drift (SQLite), împărțit în două baze de date, ambele în cc_persistence:
GlobalDatabase(<dataDir>/global.db) — o listă scurtă de tabele cu adevărat globale pentru server: registrul spațiilor de lucru, utilizatori/preferințe/dispozitive asociate, conexiuni SSO, setări de server la nivel de instalare, politici de acțiuni administrate (o limită la nivel de instalare care poate doar înăspri guardrail-urile spațiilor de lucru), fluxul de știri per utilizator, coada flotei (workeri/sarcini/jurnal de plasare), indexulworkspace_routesdinainte de autentificare și metadatele instalării. Procesul de pornire deschide doar acest fișier, așa că costul pornirii rămâne constant indiferent cât de mult istoric acumulează spațiile de lucru. Adăugarea unui tabel aici este o decizie de izolare pe care un test de tip ratchet te obligă să o argumentezi.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — un director pentru fiecare spațiu de lucru, care conține un fișier SQLite plus tot ce aparține doar acelui spațiu de lucru, astfel încât worktree-urile unei conversații și fișierele agenților stau lângă baza de date și sunt șterse odată cu ea. Aproape totul se află aici: agenți, spații, tichete, memorie, pipeline-uri, întâlniri, graful de cod, revizuiri, repo-uri. Fișierele se deschid la cerere, la prima atingere, prinWorkspaceDatabaseManager.of(workspaceId), și își plătesc atunci propriulquick_check, instalarea FTS/trigger șivector_init.
Ambele jumătăți au pornit de la o bază de referință v1 aplatizată — onCreate construiește schema actuală — cu pași de migrare adăugați doar atunci când o bază de date deja implementată trebuie dusă mai departe.
Deoarece un spațiu de lucru este un singur fișier, exportul unui spațiu de lucru este o singură instrucțiune VACUUM INTO (workspace.export), nu un dump tabel cu tabel; workspace.import adoptă un astfel de fișier și folosește workspace_meta încorporat pentru a distinge propriul fișier readoptat de unul provenit din altă instalare.
Ambele baze de date sunt deținute de cc_persistence și deschise doar de cc_server; niciun client nu deschide o bază de date — fiecare client citește/scrie prin RPC. Tabelele sunt definite în packages/cc_persistence/lib/database/tables/, DAO-urile în .../daos/; entitățile de domeniu sunt în Dart pur, separate de clasele de tabel Drift, cu maparea în straturile de date. FTS5 se ocupă de căutarea full-text, iar sqlite_vector de embeddinguri, ambele doar pe server; ambele baze de date rulează PRAGMA foreign_keys=ON în modul de jurnal WAL.
go_router, cu un singur fapt care guvernează restul: fiecare destinație din aplicație are prefixul spațiului de lucru sub forma /workspaces/:workspaceId/…. Id-ul spațiului de lucru din URL este singura sursă de adevăr pentru spațiul de lucru activ — activeWorkspaceIdProvider este condus de rută, nu invers — motiv pentru care citirea datelor care țin de un spațiu de lucru în afara unei rute cu prefix nu are de unde să ia id-ul.
- Funcții de construire a rutelor în
router/routes.dart, fiecare primind id-ul spațiului de lucru ca prim argument, nu constante de tip string - Patru rute dinainte de context nu au prefix:
/splash,/onboarding,/signed-outși/workspaces(selectorul de spații de lucru) ShellRouteînvelește shell-ul aplicației; acele patru suprafețe se afișează pe tot ecranul, în afara lui- Gardă de autentificare: o configurare incompletă duce la
/onboarding; o configurare completă a cărei acreditare de forge a dispărut duce la/signed-out. Complet înseamnă că cel puțin un forge este conectat pentru utilizatorul autentificat și că există cel puțin un spațiu de lucru — nu neapărat GitHub, iar CLI-ulghnu este o metodă de autentificare - Poarta de onboarding în
features/auth/providers/
Rețelistică (pe server)
Secțiune intitulată „Rețelistică (pe server)”Tot HTTP-ul extern se află pe server. Clienții nu apelează niciodată direct GitHub, Linear sau Google — ei apelează operațiuni RPC de pe server, iar chiar și conținutul media remote este adus prin endpointul /proxy/media al serverului, nu de la originea din amonte.
- Clienți dio în
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClientșiGoogleCalendarApiClient - Injectarea tokenului de autentificare prin interceptori, inclusiv un interceptor Google OAuth per cont în
cc_server_core, care reîmprospătează tokenurile la 401 - Toate erorile sunt mapate prin
packages/cc_infra/lib/src/network/error_mapper.dart→ subclase tipizateAppExceptiondefinite încc_domain
Securitate
Secțiune intitulată „Securitate”Serverul deține acreditările care contează: tokenurile GitHub și de ticketing pentru lansarea agenților, cheile furnizorilor LLM și tokenurile OAuth se află toate în directorul de date al serverului sau în mediul acestuia. Ce mai păstrează keychainul clientului este propria acreditare de dispozitiv și cheia de asociere pentru un server remote.
- Secrete în
flutter_secure_storage(keychain macOS, credential manager Windows, libsecret Linux) — acreditarea de dispozitiv a clientului și cheile de asociere remote, nu tokenurile furnizorilor shared_preferencesdoar pentru preferințe nesensibile (temă, font, aspect)- Secretele rămase de la furnizori din instalări mai vechi sunt predate serverului o singură dată de
features/auth/providers/credential_migration.dart, apoi sunt șterse din keychainul clientului
Aplicarea arhitecturii
Secțiune intitulată „Aplicarea arhitecturii”Constrângerile de arhitectură sunt validate de test/core/architecture_constraints_test.dart, care eșuează dacă regulile de dependență sunt încălcate.
Concepte conexe
Secțiune intitulată „Concepte conexe”- Spații de lucru și izolare: cum este impusă izolarea
- Ciclul de viață al trimiterii agenților: fluxul de trimitere
- Evenimente de domeniu: comunicarea între funcționalități
- Implementare și clienți: modelul de server cu client subțire și matricea de clienți