アーキテクチャ
Control Center は、フィーチャー優先のクリーンアーキテクチャとポートとアダプターのパターンに従っています。コードベースはビジネスドメイン(フィーチャー)を中心に構成され、厳格な依存関係のルールが自動化されたアーキテクチャテストによって強制されます。
ヘッドレスサーバー上のシンクライアント
「ヘッドレスサーバー上のシンクライアント」というセクションControl Center はシンクライアントアーキテクチャです。どのクライアントもデータベースを開きません。ヘッドレスの cc_server プロセスがデータ(小さな global.db と、ワークスペースごとに 1 つずつ遅延して開かれる SQLite ファイル)を所有し、それを WebSocket RPC 経由で提供します。デスクトップアプリ、Web ビルド、スマートフォンのコンパニオンはすべて、その 1 つの RPC 接続の上に乗るレンダラーです。
デスクトップは初回起動時に、サーバーへの接続方法を選びます。ローカル(このマシン上で cc_server を起動し、ループバック経由で通信します — これが既定です)またはリモート(別の場所にある cc_server にセキュアな WebSocket で接続します)です。Web クライアントは常にリモートです。起動リゾルバー(bootstrap/server_backend.dart)は Riverpod が存在する前に経路を選び、その後 rpcClientProvider を接続済みのクライアントで上書きします。そのため、すべてのフィーチャーはローカルのデータベースではなくサーバーを介して読み書きします。
RPC レイヤー自体はステートレスです。セッションは認証されますが、ワークスペースには紐づきません。ワークスペースを対象とするすべての呼び出しは自身の引数に workspace_id を持ち、それがなければディスパッチャーはその呼び出しを拒否します。これにより、1 つのサーバー上の 2 つのクライアントが、食い違う「現在のワークスペース」をサーバー側に持つことなく、それぞれ異なるアクティブなワークスペースを保持できます。そして、分離は接続単位ではなく呼び出し単位で強制されることになります。
これは、1 つの解決済みロックファイルを共有するDart ワークスペースで、21 のメンバー — 15 のパッケージと 6 つのアプリ — から成り、デスクトップ + Web アプリ(control_center)がワークスペースのルートです。6 つのアプリは、ヘッドレスサーバーバイナリ(cc_server)、ヘッドレスフリート実行者(cc_worker)、スマートフォン PWA(cc_remote)、WebSocket リレーブローカー(cc_signaling_server)、Widgetbook ギャラリー(cc_gallery)、公開デモホスト(cc_demo_server)です。15 のパッケージは cc_ui(デザインシステム)、cc_domain(純粋な Dart の共有カーネル: エンティティ、値オブジェクト、ポート、イベント、およびすべてのフィーチャーのドメイン層)、cc_harness(組み込みエージェントカーネル: 純粋な Dart のエージェントループ — メッセージ、プロバイダーポート、ツール、コンパクション、ステアリング、フック、サブエージェント — で、dart:io も他の cc_* 依存も持ちません)、cc_harness_runtime(組み込みハーネスランタイムアダプター: VM 専用のストリーミングプロバイダー、OAuth/PKCE の資格情報ブローカリング、資格情報ストア、汎用ツールセット — 外部 CLI をインストールせずにエージェントを実行します)、cc_rpc(クライアントトランスポート)、cc_host(サーバーカーネル)、cc_data(リモートリポジトリ)、cc_persistence(サーバー側 DB)、cc_server_core(アプリサーバーのコンポジション + アイデンティティ/プレゼンス/フリート/評価ランタイム)、cc_infra(VM 専用アダプター)、cc_mcp(MCP ツール)、cc_mcp_client(外部 MCP サーバーのクライアントで、それらのツールをローカルレジストリにブリッジします)、cc_markdown(リポジトリ内 markdown エンジン)、cc_natives(FFI リーフ)、system_audio_capture です。クライアントの全一覧と何がどこで動くかについては、デプロイメントとクライアント を参照してください。
依存関係のルール
「依存関係のルール」というセクションPresentation → Application/Providers → Domain ← Infrastructure- ドメイン層: 純粋な Dart のエンティティ、値オブジェクト、リポジトリインターフェース、ポート、ドメインサービス。インフラストラクチャのインポートはゼロです(dio、drift、ネットワークモデルなし)。
- プレゼンテーション層: 画面、ウィジェット、ノーティファイアー。drift/DAO/データ層への直接アクセスはありません。すべては Riverpod プロバイダー → リポジトリを経由します。
- インフラストラクチャ層: ドメインのポートとリポジトリインターフェースの具象実装。ドメインに依存し、その逆は決してありません。
フィーチャー構成
「フィーチャー構成」というセクションフィーチャーの完全な形は次のとおりです。
feature_name/├── data/ # Repository implementations, data sources, services, DTOs, mappers├── domain/ # Entities, repository interfaces, ports, use cases├── presentation/ # Screens, widgets, notifiers└── providers/ # Riverpod providerslib/features/ 配下でこの 4 つをすべて備えているフィーチャーはごくわずかで、その欠けは偶発的ではなく意味を持っています。共有カーネルが各フィーチャーの domain/ を cc_domain に取り込み、またシンクライアントはリポジトリ実装を一切持たないため、lib/ 配下のほとんどのフィーチャーは presentation/ と providers/ だけで、それ以外はありません。クライアントに残っている data/ や domain/ ディレクトリは、本当にクライアント側にあるものの印です。
注目すべき名前付きの例外が 2 つあります。
mcpはプロバイダーのみです。設定とステータスの UI はsettings/配下にあり、ツールサーフェス自体はフィーチャーフォルダーではなくパッケージ(cc_mcp)です。orchestrationとplan_studioはpresentation/とproviders/だけを持ちます。
guardrails フィーチャーは、そのドメイン層をルートアプリではなく cc_domain/features/guardrails/ に置きます。サーバーがこれを強制するからです。ルールは ActionClass(またはシェルの commandPrefix)をあるスコープにおける判定 — allow、prompt、deny — にマッピングし、ルールはワークスペースごとに保存され、統制対象のエージェントアクションは実行前に必ずポリシーチェーンを通って解決されます。解決は space > agent > workspace > mode preset > built-in default をたどり、一致するルールを持つ最初のスコープが決定します。最も制限の厳しいルールが効くのは、1 つのスコープ内でのタイブレークと、1 つのアクションが宣言する複数のクラスを組み合わせる場合だけです。オペレーター向けの画面は 設定 → ワークスペース → エージェントの権限 で、/workspaces/<id>/settings/workspace/permissions にあります。モデルについては ガードレール を参照してください。
共有カーネル
「共有カーネル」というセクションpackages/cc_domain/lib/core/domain/ には、3 つ以上のフィーチャーで共有されるエンティティとリポジトリが入っています。純粋な Dart でインフラストラクチャ依存はゼロであり、これにより Flutter クライアントと Flutter を含まないサーバーバイナリの両方が同じ定義をインポートできます。
- コアエンティティ:
Agent、AgentRunLog、Workspace、Repo、ReviewSpaceAssociation - アイデンティティとマルチプレイヤー:
User、Principal(sealed なUserPrincipal|AgentPrincipal)、WorkspaceMember、WorkspaceRole - メモリエンティティ:
MemoryFact、MemoryPolicy、AgentWorkingMemory、MemoryAccessGrant - 共有値オブジェクト:
AgentSkills、AgentRole、Mode、SandboxBackend、SandboxSpec、RunCost - 共有ポート:
SandboxPort、CredentialBrokerPort、WorkspaceFilesystemPort、GitRepoInspectorPort、EmbeddingPort - ドメインサービス:
MemoryAccessPolicy、ActivityLogger、MentionResolver、RunLivenessClassifier DomainEventBus+ イベント種別(ワークスペース/エージェント、PR/レビュー、メッセージング、チケッティング、パイプライン、オーケストレーション、メモリ、カレンダー/ミーティング、アイデンティティとメンバーシップ、オブザーバビリティ)
アイデンティティとマルチプレイヤー
「アイデンティティとマルチプレイヤー」というセクションControl Center はマルチユーザーです。人間とエージェントは Principal によって統合された対等なアクターです。ワークスペースメンバーは、Workspace に WorkspaceRole(owner/admin/member/viewer/guest)で束縛された User です。メンバーシップがアクセス境界であり、ペアリングキーではありません。また、リポジトリ単位のグラントにより、ワークスペースのメンバーシップがコードホストでの権限を上回ることはありません。新しいサーバーで最初に作成されたユーザーがそのインストールを所有し、他のユーザーは招待または OIDC で参加します。ワークスペースを作成すると、そのユーザーが owner として記録されます。
リアルタイムコラボレーションは 権威サーバー + フィールド単位の最終書き込み優先(LWW) であり、CRDT ではありません。プレゼンス(誰がいるか、どこにいるか、カーソル、タイピング、エージェントのライブステータスと実行中コスト)は、決して永続化されない一時的なレーンです。永続的な状態は、楽観的更新 + サーバーリベース + LWW のバックボーンに乗ります。人間とエージェントは 1 つの名簿を共有します。人間のオペレーターが 1 人だけのときはプレゼンスレーンは休止し、名簿の UI は表示されません。マルチプレイヤー機能は単独ユーザーに何の負担もかけません。
ポートとアダプター
「ポートとアダプター」というセクションポートはドメイン層の抽象インターフェースです。アダプターはインフラストラクチャにある具象実装です。ヘッドレスサーバーがデータを所有し、Flutter を含めずにビルドする必要があるため、ほぼすべてのアダプターは VM 専用で、packages/cc_infra/lib/src/ に配置され、関心事ごとに整理されています。
| ポート | アダプターの場所 |
|---|---|
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/(クライアント側: サーバーイベントをローカルでレンダリング) |
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、GitHub Issues は cc_infra/lib/src/tickets/sync/ にあるチケット同期アダプターです。ローカルチケットは TicketProviderPort を持たず、ワークスペースデータベースに存在します。 |
PipelineEnginePort |
cc_domain の features/pipelines/domain/services/(純粋な Dart のドメインサービスであり、インフラストラクチャを必要としません) |
アプリのコンポジションルート(di/providers.dart)は、クライアント側のポートを Riverpod プロバイダー経由で実装にバインドします。サーバーは cc_server_core でアダプターを組み立てます。
状態管理
「状態管理」というセクションクライアントのすべての状態に Riverpod を使用します。
- 可変状態には
Notifier<T>とAsyncNotifier<T> - 非同期データには
FutureProvider<T> - リストとライブ行は RPC サブスクリプションを通じて
AsyncValue<List<T>>として届きます — サーバーは内部で Drift の.watch()を使用し、クライアントが Drift を開くことはありません - MCP ツールは依存関係を型付きコンストラクターパラメーターとして受け取り、
Refは決して受け取りません
データベース
「データベース」というセクションDrift(SQLite)は、どちらも cc_persistence にある 2 つのデータベースに分割されています。
GlobalDatabase(<dataDir>/global.db) — 真にサーバー全体にわたるテーブルの短いリストです。ワークスペースレジストリ、ユーザー/設定/ペアリング済みデバイス、SSO 接続、インストール全体のサーバー設定、管理対象アクションポリシー(ワークスペースのガードレールを厳しくすることしかできないインストール全体のクランプ)、ユーザーごとのニュースフィード、フリートキュー(ワーカー/ジョブ/配置ログ)、認証前のworkspace_routesインデックス、インストールのメタデータが含まれます。起動時はこのファイルだけを開くため、ワークスペースがどれだけ履歴を蓄積しても起動コストは一定に保たれます。ここにテーブルを追加することは分離に関する判断であり、ラチェットテストがその正当性の説明を強制します。WorkspaceDatabase(<dataDir>/<workspaceId>/workspace.db) — ワークスペースごとに 1 つの_ディレクトリ_で、1 つの SQLite ファイルと、そのワークスペースだけに属する他のすべてを保持します。そのため、会話のワークツリーやエージェントのファイルはデータベースの隣に置かれ、データベースとともに削除されます。ほぼすべてがここにあります: エージェント、スペース、チケット、メモリ、パイプライン、ミーティング、コードグラフ、レビュー、リポジトリです。ファイルは最初のアクセス時にWorkspaceDatabaseManager.of(workspaceId)を通じて遅延して開かれ、そのとき自身のquick_check、FTS/トリガーのインストール、vector_initを実行します。
どちらのデータベースも、v1 を squash した単一のベースラインから始まっており(onCreate が現在のスキーマを構築します)、マイグレーション手順は、デプロイ済みのデータベースを引き継ぐ必要があるときにのみ追加されます。
1 つのワークスペースは 1 つのファイルなので、ワークスペースのエクスポートはテーブルごとのダンプではなく、単一の VACUUM INTO 文(workspace.export)です。workspace.import はそのようなファイルを取り込み、埋め込まれた workspace_meta を使って、自分自身のファイルを再び取り込んだのか、別のインストールから来たものなのかを判別します。
どちらのデータベースも cc_persistence が所有し、開くのは cc_server だけです。どのクライアントも DB を開きません — すべてのクライアントは RPC 経由で読み書きします。テーブルは packages/cc_persistence/lib/database/tables/ で定義され、DAO は .../daos/ にあります。ドメインエンティティは純粋な Dart で Drift のテーブルクラスとは別であり、マッピングはデータ層にあります。全文検索は FTS5 が、埋め込みは sqlite_vector が処理し、どちらもサーバー専用です。どちらのデータベースも WAL ジャーナルモードで PRAGMA foreign_keys=ON を実行します。
ルーティング
「ルーティング」というセクションgo_router を使用し、残りを規定する 1 つの事実があります。アプリ内のすべての遷移先にはワークスペースのプレフィックスが付き、/workspaces/:workspaceId/… の形式になります。URL 内のワークスペース ID がアクティブなワークスペースの唯一の信頼できる情報源です — activeWorkspaceIdProvider はルートから駆動され、その逆ではありません — そのため、プレフィックスのないルートでワークスペーススコープのデータを読もうとしても、ID の取得元がありません。
- ルートビルダーの 関数 は
router/routes.dartにあり、それぞれが最初の引数としてワークスペース ID を受け取ります。文字列定数ではありません - コンテキスト前の 4 つのルートにはプレフィックスが付きません:
/splash、/onboarding、/signed-out、/workspaces(ピッカー) ShellRouteがアプリシェルをラップし、その 4 つの画面はその外側で全画面表示されます- 認証ガード: セットアップが未完了の場合は
/onboardingへ、完了していてもコードホストの資格情報が失われている場合は/signed-outへ送られます。完了とは、サインイン中のユーザーに対して少なくとも 1 つのコードホストが接続され、少なくとも 1 つのワークスペースが存在することです — 特に GitHub である必要はなく、ghCLI は認証方法ではありません - オンボーディングゲートは
features/auth/providers/
ネットワーキング(サーバー側)
「ネットワーキング(サーバー側)」というセクション外部への HTTP はすべてサーバー内にあります。クライアントが GitHub、Linear、Google に直接接続することはありません — サーバーの RPC オペレーションを呼び出し、リモートのメディアでさえ上流のオリジンからではなくサーバーの /proxy/media エンドポイントを通じて取得されます。
packages/cc_infra/lib/src/にある dio クライアント:GitHubApiClient、GitHubPrClient、GitHubContentClient、GitHubGraphQLClient、GitLabApiClient、BitbucketApiClient、LinearGraphQlClient、GoogleCalendarApiClient- インターセプターによる認証トークンの注入。401 でトークンを更新する、
cc_server_core内のアカウントごとの Google OAuth インターセプターも含みます - すべてのエラーは
packages/cc_infra/lib/src/network/error_mapper.dartを通じてマッピングされ、cc_domainで定義された型付きのAppExceptionサブクラスになります
セキュリティ
「セキュリティ」というセクションサーバーは重要な資格情報を保持します。エージェント起動用の GitHub とチケットのトークン、LLM プロバイダーのキー、OAuth トークンはすべて、サーバーのデータディレクトリまたはその環境にあります。クライアントのキーチェーンに残るのは、自身のデバイス資格情報と、リモートサーバーのペアリングキーだけです。
- シークレットは
flutter_secure_storage(macOS キーチェーン、Windows 資格情報マネージャー、Linux libsecret)に保存されます — プロバイダーのトークンではなく、クライアントのデバイス資格情報とリモートのペアリングキーです shared_preferencesは機密でない設定(テーマ、フォント、レイアウト)のみに使用します- 古いインストールから残ったプロバイダーのシークレットは、
features/auth/providers/credential_migration.dartによって 1 回だけサーバーに渡され、その後クライアントのキーチェーンから削除されます
アーキテクチャの強制
「アーキテクチャの強制」というセクションアーキテクチャの制約は test/core/architecture_constraints_test.dart によって検証され、依存関係のルールに違反すると失敗します。
関連するコンセプト
「関連するコンセプト」というセクション- ワークスペースと分離: 分離がどのように強制されるか
- エージェントのディスパッチライフサイクル: ディスパッチの流れ
- ドメインイベント: フィーチャー間の通信
- デプロイメントとクライアント: シンクライアントのサーバーモデルとクライアントの一覧