Langkau ke kandungan

Seni bina

Control Center mengikut Clean Architecture berteraskan ciri dengan corak port dan penyesuai. Pangkalan kod disusun mengikut domain perniagaan (ciri), dengan peraturan kebergantungan yang ketat dikuatkuasakan oleh ujian seni bina automatik.

Control Center ialah seni bina klien nipis. Tiada klien membuka pangkalan data — proses cc_server tanpa kepala memiliki data (satu global.db kecil serta satu fail SQLite bagi setiap ruang kerja, dibuka secara malas) dan menyajikannya melalui RPC WebSocket. Aplikasi desktop, binaan web dan aplikasi pendamping telefon semuanya merupakan pemapar di atas satu sambungan RPC itu.

Desktop memilih cara ia mencapai pelayannya pada pelancaran pertama: setempat (ia melancarkan cc_server pada mesin ini dan berkomunikasi melalui loopback — lalai) atau jauh (ia mendail cc_server di tempat lain melalui WebSocket yang selamat). Klien web sentiasa jauh. Penyelesai permulaan (bootstrap/server_backend.dart) memilih laluan sebelum Riverpod wujud, kemudian mengatasi rpcClientProvider dengan klien yang telah bersambung — jadi setiap ciri membaca dan menulis melalui pelayan dan bukan pangkalan data setempat.

Lapisan RPC itu sendiri tanpa keadaan: sesi disahkan, tetapi ia tidak terikat pada mana-mana ruang kerja. Setiap panggilan berskop ruang kerja membawa workspace_id sendiri dalam argumennya dan penghantar menolak panggilan sedemikian tanpanya. Itulah yang membolehkan dua klien pada satu pelayan memegang ruang kerja aktif yang berbeza tanpa “ruang kerja semasa” yang dipegang pelayan untuk diperselisihkan — dan ini bermakna pengasingan dikuatkuasakan bagi setiap panggilan, bukan setiap sambungan.

Ini ialah ruang kerja Dart dengan satu fail kunci yang telah diselesaikan, terdiri daripada dua puluh satu ahli — lima belas pakej serta enam aplikasi — dengan aplikasi desktop+web (control_center) sebagai akar ruang kerja. Enam aplikasi itu ialah binari pelayan tanpa kepala (cc_server), pelaksana armada tanpa kepala (cc_worker), PWA telefon (cc_remote), broker geganti WebSocket (cc_signaling_server), galeri Widgetbook (cc_gallery) dan hos demo awam (cc_demo_server). Lima belas pakej itu ialah cc_ui (sistem reka bentuk), cc_domain (kernel dikongsi Dart tulen: entiti, objek nilai, port, acara dan lapisan domain setiap ciri), cc_harness (kernel ejen terbina: gelung ejen Dart tulen — mesej, port pembekal, alat, pemadatan, pemanduan, cangkuk, subejen — tanpa dart:io dan tanpa kebergantungan cc_* yang lain), cc_harness_runtime (penyesuai masa jalan harness terbina: pembekal penstriman VM sahaja, perantaraan kelayakan OAuth/PKCE, stor kelayakan dan set alat generik — menjalankan ejen tanpa sebarang CLI luaran dipasang), cc_rpc (pengangkutan klien), cc_host (kernel pelayan), cc_data (repositori jauh), cc_persistence (DB sebelah pelayan), cc_server_core (komposisi pelayan aplikasi + masa jalan identiti/kehadiran/armada/evals), cc_infra (penyesuai VM sahaja), cc_mcp (alat MCP), cc_mcp_client (klien untuk pelayan MCP luaran, menjambatkan alat mereka ke dalam daftar setempat), cc_markdown (enjin markdown dalam repo), cc_natives (daun FFI) dan system_audio_capture. Lihat Penerapan dan klien untuk matriks klien penuh dan apa yang berjalan di mana.

Presentation → Application/Providers → Domain ← Infrastructure
  • Lapisan domain: entiti Dart tulen, objek nilai, antara muka repositori, port dan perkhidmatan domain. Sifar import infrastruktur (tiada dio, drift atau model rangkaian).
  • Lapisan persembahan: skrin, widget, pemberitahu. Tiada akses terus drift/DAO/lapisan data; semuanya melalui pembekal Riverpod → repositori.
  • Lapisan infrastruktur: pelaksanaan konkrit bagi port domain dan antara muka repositori. Bergantung pada domain, tidak pernah sebaliknya.

Bentuk penuh sesuatu ciri ialah:

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

Sangat sedikit ciri bawah lib/features/ yang benar-benar membawa keempat-empatnya dan jurang itu bersifat bermaklumat dan bukan kebetulan. Kernel dikongsi telah menyerap domain/ setiap ciri ke dalam cc_domain dan klien nipis tidak memegang sebarang pelaksanaan repositori, jadi kebanyakan ciri bawah lib/ hanyalah presentation/ serta providers/ dan tiada yang lain. Direktori data/ atau domain/ yang masih kekal dalam klien menandakan sesuatu yang benar-benar milik klien.

Dua pengecualian yang dinamakan wajar diketahui:

  • mcp hanya mempunyai pembekal. UI tetapan dan statusnya terletak bawah settings/ dan permukaan alat itu sendiri ialah pakej (cc_mcp), bukan folder ciri.
  • orchestration dan plan_studio hanya membawa presentation/ dan providers/.

Ciri guardrails meletakkan lapisan domainnya dalam cc_domain/features/guardrails/ dan bukan dalam aplikasi akar, kerana pelayan yang menguatkuasakannya: sesuatu peraturan memetakan ActionClass (atau commandPrefix shell) kepada keputusan — allow, prompt atau deny — pada sesuatu skop, peraturan disimpan bagi setiap ruang kerja dan setiap tindakan ejen yang ditadbir diselesaikan melalui rantaian polisi sebelum ia dijalankan. Peleraian menelusuri space > agent > workspace > mode preset > built-in default dan skop pertama yang mempunyai peraturan sepadan yang membuat keputusan; yang paling ketat hanyalah penentu seri dalam satu skop dan semasa menggabungkan beberapa kelas yang diisytiharkan oleh satu tindakan. Permukaan yang berhadapan dengan pengendali ialah Tetapan → Ruang kerja → Kebenaran ejen, di /workspaces/<id>/settings/workspace/permissions. Lihat Guardrail untuk modelnya.

packages/cc_domain/lib/core/domain/ memegang entiti dan repositori yang dikongsi merentas 3+ ciri. Ia Dart tulen dengan sifar kebergantungan infrastruktur, dan itulah yang membolehkan kedua-dua klien Flutter dan binari pelayan tanpa Flutter mengimport definisi yang sama:

  • Entiti teras: Agent, AgentRunLog, Workspace, Repo, ReviewSpaceAssociation
  • Identiti dan multipemain: User, Principal (tersegel UserPrincipal | AgentPrincipal), WorkspaceMember, WorkspaceRole
  • Entiti memori: MemoryFact, MemoryPolicy, AgentWorkingMemory, MemoryAccessGrant
  • Objek nilai dikongsi: AgentSkills, AgentRole, Mode, SandboxBackend, SandboxSpec, RunCost
  • Port dikongsi: SandboxPort, CredentialBrokerPort, WorkspaceFilesystemPort, GitRepoInspectorPort, EmbeddingPort
  • Perkhidmatan domain: MemoryAccessPolicy, ActivityLogger, MentionResolver, RunLivenessClassifier
  • DomainEventBus + jenis acara (ruang kerja/ejen, PR/semakan, pemesejan, penetikan, pipeline, orkestrasi, memori, kalendar/mesyuarat, identiti dan keahlian, kebolehcerapan)

Control Center ialah berbilang pengguna: manusia dan ejen merupakan pelaku sama taraf yang disatukan oleh Principal. Ahli ruang kerja ialah User yang terikat pada Workspace pada WorkspaceRole (owner/admin/member/viewer/guest). Keahlian ialah sempadan akses — bukan kunci penggandingan — dan geran setiap repo menghalang keahlian ruang kerja daripada memberi keistimewaan melebihi hos kod. Pengguna pertama yang dicipta pada pelayan baharu memiliki pemasangan itu; yang lain menyertai melalui jemputan atau OIDC. Mencipta ruang kerja menandakan pengguna itu sebagai owner.

Kerjasama masa nyata ialah pelayan berwibawa + penulis-terakhir-menang (LWW) bagi setiap medan, bukan CRDT. Kehadiran (siapa berada di sini, di mana mereka, kursor, penulisan, status langsung dan kos berjalan seseorang ejen) ialah lorong fana yang tidak pernah dikekalkan secara berasingan; keadaan kekal menaiki tulang belakang mutasi optimistik + rebase pelayan + LWW. Manusia dan ejen berkongsi satu senarai nama. Dengan seorang sahaja pengendali manusia, lorong kehadiran melahu dan tiada krom antara muka senarai nama muncul — multipemain tidak membebankan pengguna tunggal.

Port ialah antara muka abstrak dalam lapisan domain. Penyesuai ialah pelaksanaan konkrit dalam infrastruktur. Kerana pelayan tanpa kepala memiliki data dan mesti dibina tanpa Flutter, hampir setiap penyesuai adalah VM sahaja dan terletak dalam packages/cc_infra/lib/src/, disusun mengikut bidang:

Port Lokasi penyesuai
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/ (sebelah klien: memaparkan acara pelayan secara setempat)
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 ialah penyesuai penyegerakan tiket dalam cc_infra/lib/src/tickets/sync/; tiket setempat hidup dalam pangkalan data ruang kerja tanpa TicketProviderPort.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (perkhidmatan domain Dart tulen — ia tidak memerlukan infrastruktur)

Akar komposisi aplikasi (di/providers.dart) mengikat port sebelah klien kepada pelaksanaan melalui pembekal Riverpod; pelayan menyusun penyesuainya dalam cc_server_core.

Riverpod untuk semua keadaan klien:

  • Notifier<T> dan AsyncNotifier<T> untuk keadaan boleh ubah
  • FutureProvider<T> untuk data tak segerak
  • Senarai dan baris langsung tiba melalui langganan RPC sebagai AsyncValue<List<T>> — pelayan menggunakan Drift .watch() secara dalaman; tiada klien membuka Drift
  • Alat MCP menerima kebergantungan sebagai parameter pembina bertip, tidak pernah Ref

Drift (SQLite), dipecahkan kepada dua pangkalan data, kedua-duanya dalam cc_persistence:

  • GlobalDatabase (<dataDir>/global.db) — senarai pendek jadual yang benar-benar merentas seluruh pelayan: daftar ruang kerja, pengguna/keutamaan/peranti berganding, sambungan SSO, tetapan pelayan seluruh pemasangan, polisi tindakan terurus (pengapit seluruh pemasangan yang hanya boleh mengetatkan guardrail ruang kerja), suapan berita setiap pengguna, baris gilir armada (pekerja/kerja/log penempatan), indeks workspace_routes pra-pengesahan dan metadata pemasangan. Permulaan hanya membuka fail ini, jadi kos permulaan kekal rata tidak kira berapa banyak sejarah yang dikumpulkan oleh ruang kerja. Menambah jadual di sini ialah keputusan pengasingan yang perlu anda pertahankan melalui ujian ratchet.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) — satu direktori bagi setiap ruang kerja, memegang satu fail SQLite serta segala yang lain yang hanya milik ruang kerja itu, jadi worktree sesuatu perbualan dan fail ejen duduk bersebelahan pangkalan data dan dipadam bersamanya. Hampir semuanya hidup di sini: ejen, ruang, tiket, memori, pipeline, mesyuarat, graf kod, semakan, repo. Fail dibuka secara malas pada sentuhan pertama melalui WorkspaceDatabaseManager.of(workspaceId) dan membayar quick_check, pemasangan FTS/pencetus dan vector_init sendiri pada masa itu.

Kedua-dua bahagian bermula daripada garis dasar v1 yang digabungkan — onCreate membina skema semasa — dengan langkah migrasi hanya ditambahkan apabila pangkalan data yang telah diterapkan perlu dibawa ke hadapan.

Kerana satu ruang kerja ialah satu fail, mengeksport ruang kerja ialah satu pernyataan VACUUM INTO (workspace.export), bukan lambakan jadual demi jadual; workspace.import menerima pakai fail sedemikian dan menggunakan workspace_meta terbenam untuk membezakan failnya sendiri yang diterima pakai semula daripada fail yang datang daripada pemasangan lain.

Kedua-dua pangkalan data dimiliki oleh cc_persistence dan dibuka hanya oleh cc_server; tiada klien membuka DB — setiap klien membaca/menulis melalui RPC. Jadual ditakrifkan dalam packages/cc_persistence/lib/database/tables/, DAO dalam .../daos/; entiti domain ialah Dart tulen, berasingan daripada kelas jadual Drift, dengan pemetaan dalam lapisan data. FTS5 mengendalikan carian teks penuh dan embed sqlite_vector, kedua-duanya hanya di pelayan; kedua-dua pangkalan data menjalankan PRAGMA foreign_keys=ON dalam mod jurnal WAL.

go_router, dengan satu fakta yang mengawal selebihnya: setiap destinasi dalam aplikasi berawalan ruang kerja sebagai /workspaces/:workspaceId/…. Id ruang kerja dalam URL ialah satu-satunya sumber kebenaran bagi ruang kerja aktif — activeWorkspaceIdProvider dipandu daripada laluan, bukan sebaliknya — dan itulah sebabnya membaca data berskop ruang kerja di luar laluan berawalan tiada tempat untuk mendapatkan idnya.

  • Fungsi pembina laluan dalam router/routes.dart, setiap satu mengambil id ruang kerja sebagai argumen pertamanya, bukan pemalar rentetan
  • Empat laluan pra-konteks tidak berawalan: /splash, /onboarding, /signed-out dan /workspaces (pemilih)
  • ShellRoute membalut cangkerang aplikasi; keempat-empat permukaan itu dipaparkan skrin penuh di luarnya
  • Pengawal pengesahan: persediaan yang tidak lengkap pergi ke /onboarding; persediaan lengkap yang kelayakan hos kodnya telah hilang pergi ke /signed-out. Lengkap bermaksud sekurang-kurangnya satu hos kod disambungkan bagi pengguna yang telah daftar masuk dan sekurang-kurangnya satu ruang kerja wujud — bukan GitHub secara khusus, dan CLI gh bukan kaedah pengesahan
  • Gerbang onboarding dalam features/auth/providers/

Semua HTTP luaran hidup dalam pelayan. Klien tidak pernah mendail GitHub, Linear atau Google secara langsung — mereka memanggil operasi RPC pelayan malah media jauh diambil melalui titik akhir /proxy/media pelayan dan bukan daripada asal huluan.

  • Klien dio dalam packages/cc_infra/lib/src/: GitHubApiClient, GitHubPrClient, GitHubContentClient, GitHubGraphQLClient, GitLabApiClient, BitbucketApiClient, LinearGraphQlClient dan GoogleCalendarApiClient
  • Suntikan token pengesahan melalui pencegat, termasuk pencegat Google OAuth bagi setiap akaun dalam cc_server_core yang menyegarkan token pada 401
  • Semua ralat dipetakan melalui packages/cc_infra/lib/src/network/error_mapper.dart → subkelas AppException bertip yang ditakrifkan dalam cc_domain

Pelayan memegang kelayakan yang penting: token GitHub dan penetikan untuk pelancaran ejen, kunci pembekal LLM dan token OAuth semuanya hidup di bawah dir data pelayan atau persekitarannya. Apa yang masih dipegang oleh rantai kunci klien ialah kelayakan perantinya sendiri dan kunci penggandingan untuk pelayan jauh.

  • Rahsia dalam flutter_secure_storage (rantai kunci macOS, pengurus kelayakan Windows, libsecret Linux) — kelayakan peranti klien dan kunci penggandingan jauh, bukan token pembekal
  • shared_preferences untuk keutamaan tidak sensitif sahaja (tema, fon, susun atur)
  • Rahsia pembekal yang tertinggal daripada pemasangan lama diserahkan kepada pelayan sekali oleh features/auth/providers/credential_migration.dart, kemudian dipadam daripada rantai kunci klien

Kekangan seni bina disahkan oleh test/core/architecture_constraints_test.dart, yang gagal jika peraturan kebergantungan dilanggar.