Arsitektur
Ringkasan
Bagian berjudul “Ringkasan”Control Center menganut Clean Architecture yang mengutamakan fitur dengan pola port dan adapter. Basis kode disusun berdasarkan domain bisnis (fitur), dengan aturan dependensi ketat yang ditegakkan oleh pengujian arsitektur otomatis.
Klien tipis di atas server tanpa antarmuka
Bagian berjudul “Klien tipis di atas server tanpa antarmuka”Control Center adalah arsitektur klien tipis. Tidak ada klien yang membuka basis data — sebuah proses cc_server tanpa antarmuka memiliki datanya (satu global.db kecil plus satu berkas SQLite per ruang kerja, dibuka secara lazy) dan menyajikannya melalui RPC WebSocket. Aplikasi desktop, build web, dan pendamping ponsel semuanya merupakan perender di atas satu koneksi RPC tersebut.
Desktop memilih cara menjangkau servernya saat pertama kali diluncurkan: lokal (ia menjalankan cc_server di mesin ini dan berkomunikasi melalui loopback — perilaku default) atau jarak jauh (ia menghubungi cc_server di tempat lain melalui WebSocket yang aman). Klien web selalu jarak jauh. Resolver saat boot (bootstrap/server_backend.dart) memilih jalur tersebut sebelum Riverpod ada, lalu menimpa rpcClientProvider dengan klien yang terhubung — sehingga setiap fitur membaca dan menulis melalui server, bukan basis data lokal.
Lapisan RPC itu sendiri bersifat stateless: sebuah sesi diautentikasi, tetapi tidak terikat pada satu ruang kerja. Setiap panggilan yang bercakupan ruang kerja membawa workspace_id-nya sendiri di argumennya dan dispatcher menolak panggilan semacam itu tanpa id tersebut. Itulah yang memungkinkan dua klien pada satu server memegang ruang kerja aktif yang berbeda tanpa ada “ruang kerja saat ini” yang dipegang server untuk diperdebatkan — dan artinya isolasi ditegakkan per panggilan, bukan per koneksi.
Ini adalah workspace Dart dengan satu lockfile yang sudah diresolusi berisi dua puluh satu anggota — lima belas paket plus enam aplikasi — dengan aplikasi desktop+web (control_center) sebagai akar workspace. Keenam aplikasi tersebut adalah biner server tanpa antarmuka (cc_server), eksekutor fleet tanpa antarmuka (cc_worker), PWA ponsel (cc_remote), broker relai WebSocket (cc_signaling_server), galeri Widgetbook (cc_gallery), dan host demo publik (cc_demo_server). Kelima belas paket tersebut adalah cc_ui (sistem desain), cc_domain (kernel bersama Dart murni: entitas, objek nilai, port, peristiwa, dan lapisan domain setiap fitur), cc_harness (kernel agen bawaan: loop agen Dart murni — pesan, port penyedia, alat, pemadatan, pengarahan, hook, subagen — tanpa dart:io dan tanpa dependensi cc_* lain), cc_harness_runtime (adapter runtime harness bawaan: penyedia streaming khusus VM, perantara kredensial OAuth/PKCE, penyimpanan kredensial, dan kumpulan alat generik — menjalankan agen tanpa CLI eksternal yang terpasang), cc_rpc (transport klien), cc_host (kernel server), cc_data (repositori jarak jauh), cc_persistence (basis data sisi server), cc_server_core (komposisi app-server + runtime identitas/kehadiran/fleet/evaluasi), cc_infra (adapter khusus VM), cc_mcp (alat MCP), cc_mcp_client (klien untuk server MCP eksternal, yang menjembatani alat mereka ke registri lokal), cc_markdown (mesin markdown dalam repo), cc_natives (daun FFI), dan system_audio_capture. Lihat Deployment dan klien untuk matriks klien lengkap dan apa yang berjalan di mana.
Aturan dependensi
Bagian berjudul “Aturan dependensi”Presentation → Application/Providers → Domain ← Infrastructure- Lapisan domain: entitas Dart murni, objek nilai, antarmuka repositori, port, dan layanan domain. Tanpa impor infrastruktur sama sekali (tanpa dio, drift, atau model jaringan).
- Lapisan presentasi: layar, widget, notifier. Tidak ada akses langsung ke drift/DAO/lapisan data; semuanya melewati provider Riverpod → repositori.
- Lapisan infrastruktur: implementasi konkret port domain dan antarmuka repositori. Bergantung pada domain, tidak pernah sebaliknya.
Struktur fitur
Bagian berjudul “Struktur fitur”Bentuk lengkap sebuah fitur adalah:
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providersSangat sedikit fitur di bawah lib/features/ yang benar-benar membawa keempat direktori itu, dan celah tersebut informatif, bukan kebetulan. Kernel bersama menyerap domain/ setiap fitur ke dalam cc_domain dan klien tipis tidak memuat implementasi repositori, sehingga sebagian besar fitur di bawah lib/ hanya berisi presentation/ plus providers/ dan tidak ada yang lain. Direktori data/ atau domain/ yang bertahan di klien menandai sesuatu yang memang milik sisi klien.
Ada dua pengecualian bernama yang perlu diketahui:
mcphanya berisi provider. UI pengaturan dan statusnya berada di bawahsettings/dan permukaan alatnya sendiri berupa paket (cc_mcp), bukan folder fitur.orchestrationdanplan_studiohanya membawapresentation/danproviders/.
Fitur guardrails menempatkan lapisan domainnya di cc_domain/features/guardrails/ alih-alih di aplikasi akar, karena server yang menegakkannya: sebuah aturan memetakan ActionClass (atau commandPrefix shell) ke keputusan — allow, prompt, atau deny — pada suatu cakupan, aturan disimpan per ruang kerja, dan setiap tindakan agen yang diatur diselesaikan melalui rantai kebijakan sebelum dijalankan. Resolusi menelusuri space > agent > workspace > mode preset > built-in default dan cakupan pertama yang memiliki aturan cocok yang memutuskan; paling restriktif hanya menjadi penentu seri dalam satu cakupan dan untuk menggabungkan beberapa kelas yang dideklarasikan satu tindakan. Permukaan yang dihadapi operator adalah Pengaturan → Ruang kerja → Izin agen, di /workspaces/<id>/settings/workspace/permissions. Lihat Pagar pengaman untuk modelnya.
Kernel bersama
Bagian berjudul “Kernel bersama”packages/cc_domain/lib/core/domain/ memuat entitas dan repositori yang dibagi ke 3+ fitur. Ini Dart murni tanpa dependensi infrastruktur, yang memungkinkan klien Flutter maupun biner server tanpa Flutter mengimpor definisi yang sama:
- Entitas inti:
Agent,AgentRunLog,Workspace,Repo,ReviewSpaceAssociation - Identitas dan multiplayer:
User,Principal(sealedUserPrincipal|AgentPrincipal),WorkspaceMember,WorkspaceRole - Entitas memori:
MemoryFact,MemoryPolicy,AgentWorkingMemory,MemoryAccessGrant - Objek nilai bersama:
AgentSkills,AgentRole,Mode,SandboxBackend,SandboxSpec,RunCost - Port bersama:
SandboxPort,CredentialBrokerPort,WorkspaceFilesystemPort,GitRepoInspectorPort,EmbeddingPort - Layanan domain:
MemoryAccessPolicy,ActivityLogger,MentionResolver,RunLivenessClassifier DomainEventBus+ jenis peristiwa (ruang kerja/agen, PR/tinjauan, perpesanan, tiket, pipeline, orkestrasi, memori, kalender/rapat, identitas & keanggotaan, observabilitas)
Identitas dan multiplayer
Bagian berjudul “Identitas dan multiplayer”Control Center bersifat multi-pengguna: manusia dan agen adalah aktor setara yang disatukan oleh Principal. Anggota ruang kerja adalah User yang terikat pada Workspace dengan WorkspaceRole (owner/admin/member/viewer/guest). Keanggotaan adalah batas akses — bukan kunci pemasangan — dan hibah per repo menjaga agar keanggotaan ruang kerja tidak memberi hak istimewa melebihi forge. Pengguna pertama yang dibuat di server baru memiliki instalasi tersebut; pengguna lain bergabung melalui undangan atau OIDC. Membuat ruang kerja menandai pengguna itu sebagai owner.
Kolaborasi real-time adalah server otoritatif + last-writer-wins (LWW) per bidang, bukan CRDT. Kehadiran (siapa yang ada di sini, di mana mereka berada, kursor, sedang mengetik, status langsung agen dan biaya yang sedang berjalan) adalah jalur sementara terpisah yang tidak pernah dipersistenkan; state yang tahan lama ditopang oleh tulang punggung mutasi optimistis + rebase server + LWW. Manusia dan agen berbagi satu daftar. Dengan satu operator manusia saja, jalur kehadiran menganggur dan tidak ada elemen antarmuka daftar yang muncul — multiplayer tidak membebani pengguna tunggal sama sekali.
Port dan adapter
Bagian berjudul “Port dan adapter”Port adalah antarmuka abstrak di lapisan domain. Adapter adalah implementasi konkret di infrastruktur. Karena server tanpa antarmuka memiliki datanya dan harus dapat dibangun tanpa Flutter, hampir semua adapter khusus VM dan berada di packages/cc_infra/lib/src/, diatur berdasarkan area tanggung jawabnya:
| Port | Lokasi 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/ (sisi klien: merender peristiwa server secara 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, dan GitHub Issues adalah adapter sinkronisasi tiket di cc_infra/lib/src/tickets/sync/; tiket lokal berada di basis data ruang kerja tanpa TicketProviderPort. |
PipelineEnginePort |
cc_domain features/pipelines/domain/services/ (layanan domain Dart murni — tidak memerlukan infrastruktur) |
Akar komposisi aplikasi (di/providers.dart) mengikat port sisi klien ke implementasinya melalui provider Riverpod; server menyusun adapter-nya di cc_server_core.
Manajemen state
Bagian berjudul “Manajemen state”Riverpod untuk seluruh state klien:
Notifier<T>danAsyncNotifier<T>untuk state yang dapat berubahFutureProvider<T>untuk data asinkron- Daftar dan baris langsung tiba melalui langganan RPC sebagai
AsyncValue<List<T>>— server menggunakan Drift.watch()secara internal; tidak ada klien yang membuka Drift - Alat MCP menerima dependensi sebagai parameter konstruktor bertipe, tidak pernah
Ref
Basis data
Bagian berjudul “Basis data”Drift (SQLite), terbagi menjadi dua basis data, keduanya di cc_persistence:
GlobalDatabase(<dataDir>/global.db) — daftar singkat tabel yang benar-benar berskala server: registri ruang kerja, pengguna/preferensi/perangkat yang dipasangkan, koneksi SSO, pengaturan server selevel instalasi, kebijakan tindakan terkelola (pembatas selevel instalasi yang hanya dapat memperketat pagar pengaman ruang kerja), umpan berita per pengguna, antrean fleet (pekerja/tugas/log penempatan), indeksworkspace_routespra-autentikasi, dan metadata instalasi. Boot hanya membuka berkas ini, sehingga biaya startup tetap datar tidak peduli seberapa banyak riwayat yang dikumpulkan ruang kerja. Menambahkan tabel di sini adalah keputusan isolasi yang harus Anda pertanggungjawabkan karena ada pengujian ratchet yang menuntutnya.WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — satu direktori per ruang kerja, yang memuat satu berkas SQLite plus semua hal lain yang hanya milik ruang kerja itu, sehingga worktree suatu percakapan dan berkas agen berada di samping basis data dan ikut terhapus bersamanya. Hampir semuanya berada di sini: agen, space, tiket, memori, pipeline, rapat, graf kode, tinjauan, repo. Berkas dibuka secara lazy saat pertama kali disentuh melaluiWorkspaceDatabaseManager.of(workspaceId)dan saat itu juga menanggungquick_check, pemasangan FTS/trigger, sertavector_initmiliknya sendiri.
Kedua bagian dimulai dari baseline v1 yang sudah dipadatkan — onCreate membangun skema saat ini — dengan langkah migrasi yang ditambahkan hanya ketika basis data yang telah di-deploy harus dibawa ke depan.
Karena satu ruang kerja adalah satu berkas, mengekspor ruang kerja cukup dengan satu pernyataan VACUUM INTO (workspace.export), bukan dump tabel demi tabel; workspace.import mengadopsi berkas semacam itu dan menggunakan workspace_meta yang tertanam untuk membedakan berkasnya sendiri yang diadopsi ulang dari berkas yang berasal dari instalasi lain.
Kedua basis data dimiliki oleh cc_persistence dan hanya dibuka oleh cc_server; tidak ada klien yang membuka DB — setiap klien membaca/menulis melalui RPC. Tabel didefinisikan di packages/cc_persistence/lib/database/tables/, DAO di .../daos/; entitas domain adalah Dart murni, terpisah dari kelas tabel Drift, dengan pemetaan di lapisan data. FTS5 menangani pencarian teks lengkap dan embedding sqlite_vector, keduanya khusus server; kedua basis data menjalankan PRAGMA foreign_keys=ON dalam mode jurnal WAL.
Perutean
Bagian berjudul “Perutean”go_router, dengan satu fakta yang mengatur sisanya: setiap tujuan di dalam aplikasi diberi prefiks ruang kerja sebagai /workspaces/:workspaceId/…. Id ruang kerja di URL adalah satu-satunya sumber kebenaran untuk ruang kerja aktif — activeWorkspaceIdProvider digerakkan dari rute, bukan sebaliknya — itulah sebabnya membaca data bercakupan ruang kerja di luar rute berprefiks tidak punya tempat untuk mendapatkan id-nya.
- Fungsi pembangun rute di
router/routes.dart, masing-masing menerima id ruang kerja sebagai argumen pertamanya, bukan konstanta string - Empat rute pra-konteks tidak berprefiks:
/splash,/onboarding,/signed-out, dan/workspaces(pemilih) ShellRoutemembungkus shell aplikasi; keempat permukaan tersebut dirender layar penuh di luarnya- Penjaga autentikasi: penyiapan yang belum selesai dialihkan ke
/onboarding; penyiapan yang sudah selesai tetapi kredensial forge-nya hilang dialihkan ke/signed-out. Selesai berarti setidaknya satu forge terhubung untuk pengguna yang sudah masuk dan setidaknya satu ruang kerja ada — bukan khusus GitHub, dan CLIghbukanlah metode autentikasi - Gerbang onboarding di
features/auth/providers/
Jaringan (sisi server)
Bagian berjudul “Jaringan (sisi server)”Semua HTTP eksternal berada di server. Klien tidak pernah menghubungi GitHub, Linear, atau Google secara langsung — mereka memanggil operasi RPC server dan bahkan media jarak jauh diambil melalui endpoint /proxy/media server, bukan dari origin hulu.
- Klien dio di
packages/cc_infra/lib/src/:GitHubApiClient,GitHubPrClient,GitHubContentClient,GitHubGraphQLClient,GitLabApiClient,BitbucketApiClient,LinearGraphQlClient, danGoogleCalendarApiClient - Penyuntikan token autentikasi melalui interceptor, termasuk interceptor Google OAuth per akun di
cc_server_coreyang menyegarkan token saat 401 - Semua error dipetakan melalui
packages/cc_infra/lib/src/network/error_mapper.dart→ subclassAppExceptionbertipe yang didefinisikan dicc_domain
Keamanan
Bagian berjudul “Keamanan”Server memegang kredensial yang penting: token GitHub dan tiket untuk peluncuran agen, kunci penyedia LLM, dan token OAuth semuanya berada di bawah direktori data server atau lingkungannya. Yang masih dipegang keychain klien hanyalah kredensial perangkatnya sendiri dan kunci pemasangan untuk server jarak jauh.
- Rahasia di
flutter_secure_storage(keychain macOS, credential manager Windows, libsecret Linux) — kredensial perangkat klien dan kunci pemasangan jarak jauh, bukan token penyedia shared_preferenceshanya untuk preferensi tidak sensitif (tema, font, tata letak)- Rahasia penyedia sisa dari instalasi lama diserahkan ke server sekali oleh
features/auth/providers/credential_migration.dart, lalu dihapus dari keychain klien
Penegakan arsitektur
Bagian berjudul “Penegakan arsitektur”Batasan arsitektur divalidasi oleh test/core/architecture_constraints_test.dart, yang gagal jika aturan dependensi dilanggar.
Konsep terkait
Bagian berjudul “Konsep terkait”- Ruang kerja dan isolasi: bagaimana isolasi ditegakkan
- Siklus hidup pengiriman agen: alur pengiriman
- Peristiwa domain: komunikasi antarfitur
- Deployment dan klien: model server klien tipis dan matriks klien