コンテンツにスキップ

デプロイメントとクライアント

Control Center は 1 つのプログラムではありません。シンクライアントアーキテクチャです。データを所有するヘッドレスサーバーと、そこへの単一の RPC 接続を介してインターフェースを描画する一連のクライアントで構成されます。この分離を理解すると、なぜ一部の処理がその場所で動くのか、各クライアントが何に到達できて何に到達できないのかがわかります。

cc_server というプロセスがデータベースを保持します。サーバー全体の状態を保存する小さな global.db と、ワークスペースごとに 1 つの SQLite ファイル(遅延して開かれます)です。そして、長期間動き続ける処理、つまりパイプラインエンジン、リコンサイラー、MCP ツールサーフェス、オーケストレーションリスナーを実行します。ウィンドウより長く生き残る必要があるもの、あるいは 2 つのクライアントが同一に見えるべきものは、すべてそこに存在します。

どのクライアントもデータベースファイルを自ら開くことはありません。代わりに、各クライアントは 1 つの RemoteRpcClient を保持し、それを介してリポジトリ操作とサブスクリプションを発行します。サーバーは、ペアリング済みデバイスの事前共有キーに対して接続を認証し、その後は接続を気にかけなくなります。セッションはワークスペースに束縛されません。 ディスパッチャーはステートレスです。ワークスペーススコープのすべての呼び出しは、引数に自身の workspace_id を含み、それを省略した呼び出しは拒否されます。

これは意図的な設計であり、1 つのサーバー上の 2 つのクライアントが、サーバー側に保持された「現在のワークスペース」をめぐって食い違うことなく、異なるワークスペースに留まれるようにするものです。代わりに、分離は呼び出しごとに次の 4 つのステップで強制されます。

  1. 操作は自身がワークスペーススコープかどうかを宣言します。スコープであり、workspace_id が届かなければ、その呼び出しは拒否されます。
  2. 指定された id は登録済みのワークスペースでなければならず、これはデータベースを開く前にチェックされます。そうでなければ、未知の id がリクエストのたびに空の幽霊 workspace.db を実体化させてしまいます。見つからなければ not-found です。
  3. 呼び出し元の WorkspaceRole は、操作の種類から導かれる下限を満たす必要があります。読み取りはゲスト、変更はメンバー、破壊的操作は管理者が必要です。
  4. リポジトリの内容を公開する操作は、さらに自身が宣言したレベルでのリポジトリ単位の許可(grant)を必要とします — none / read / review / write — これにより、ワークスペースのメンバーシップが暗黙にコードホストを上回る権限を得ることはありません。

重要な一文はこれです。ペアリングキーを保持していることはアクセスの境界ではありません。メンバーシップが境界です。

サーバーへ向かわせた 3 つの圧力がありました。

  1. 唯一の信頼できる情報源。 デスクトップとスマートフォン、あるいはノート PC とデスクトップは、同じフリート状態を見るべきです。それぞれが独自のデータベースを持てば、それらは乖離していきます。単一のサーバーであれば、すべてのクライアントが同じライブデータを読み取ります。
  2. Web は自己完結できない。 ブラウザーはサブプロセスを起動したりローカルファイルを開いたりできないため、Web ビルドがデータベースを所有したりエージェントを実行したりすることは決してできません。それを実行するサーバーと通信する必要があります。Web 専用の 2 つ目のデータ経路を構築するのではなく、デスクトップが同じ経路に載せられました。
  3. ヘッドレス運用。 小さなマシンで実行でき、電車の中のスマートフォンやどこからでもブラウザーから到達できるサーバーは、開いたままにしておかなければならない GUI よりも便利です。

トレードオフは、サーバーが依存先になったことです。サーバーが動いていなければ、何も動きません。デスクトップのデフォルトの答えは、自分でサーバーを起動することです。

5 つのクライアントがサーバーに到達し、それぞれ形と信頼プロファイルが異なります。

Flutter のデスクトップアプリケーション(macOS、Windows、Linux)はフルクライアントです。初回起動時にどのように実行するかを尋ね、その選択を記憶します。

  • ローカル: このマシンで cc_server を起動して監視します。デフォルトの、自己完結したシングルユーザー構成です。デスクトップがサーバーを起動し、サーバーはアプリサポートルート配下のデータベースを所有して、ループバック経由で通信します。
  • リモート: 別の場所で動いている cc_server に、安全な WebSocket 経由で接続します。データはそのサーバー上にあり、このデスクトップは純粋にレンダラーです。

どちらの場合も、得られた RPC クライアントがアプリのデータプロバイダーを上書きするため、UI 全体(すべての画面、すべての機能)がサーバーを介して読み書きします。デスクトップがデータベースファイルに直接触れることはありません。

Web ビルドは、ブラウザーで動くようにコンパイルされた同じ Flutter アプリで、app.usectrl.dev でホストされているためインストールする必要はありません。ブラウザーはサブプロセスを起動できないため、Web クライアントは常にリモートです。wss:// 経由で cc_server に接続し、デスクトップの完全なインターフェースを描画します。接続後は、同じコードを同じ RPC 上で動かしているため、同じフリートに対して同じ画面を表示します。欠けているのは、ブラウザーにはないネイティブ機能を必要とするわずかな箇所です。以下で説明するミーティングキャプチャの品質レイヤーが主なもので、OS 通知ももう 1 つです。

これが、「ダウンロード不要」と「サーバー不要」が異なる主張である理由です。Web クライアントはインストールを不要にしますが、サーバーを不要にはしません。接続先は同じノート PC 上の cc_server でもかまいません — ws://localhost:9030 は、ブラウザーがループバックを信頼できるものとして扱うため証明書を必要としません — あるいは別のマシン上のサーバーで、ブラウザーが信頼する証明書を伴う wss:// 経由で到達可能でなければなりません。cc_server は TLS なしで非ループバックアドレスをバインドすることを拒否するため、リモートサーバーとは、それ自身の証明書、TLS 終端プロキシ、または証明書を提供するトンネルか VPN のいずれかを意味します。

バンドル自体は、通信先のサーバーとは別にホストされます。スタンドアロンの cc_server は RPC エンドポイント、MCP サーフェス、およびそのプロキシを提供しますが、Web バンドルは提供しません — 静的ファイルの経路はサーバー内に存在しますが、スタンドアロンバイナリには Web ルートが構成されていないため、/ で提供するものがありません。公開されている cc-webapp コンテナイメージがバンドルをホストし、scripts/build_web.sh がそれをビルドします。サーバーは --allowed-origins リストにあるブラウザーオリジンのみを受け入れ、そのデフォルトは https://app.usectrl.dev です。したがって、バンドルをセルフホストする場合はそのオリジンを追加する必要があります。

メディア(アバター、フィードの画像、PR のスクリーンショット)は、上流のホストに直接アクセスするのではなく、サーバーのプロキシエンドポイントを経由するため、ブラウザーが任意のオリジンを取得することはありません。デプロイされたクライアントは、機密性のない Cookie からリクエストごとの Content-Security-Policy を付与し、接続中のサーバーのオリジンのみを許可します。

Remote アプリは、別個のより軽量なクライアントです。remote.usectrl.dev にある Flutter Web の PWA です。意図的にフルアプリではありません。ブローカー経由のエンドツーエンドで密封された WebSocket リレーでペアリングし、ツールサーフェスの読み取り中心のスライスを扱います。チケット、メッセージ、ニュースフィードを読み取り、返信の送信やチケットの更新を行います。

スマートフォンは、ローカルエージェントよりも低い権限のプリンシパルです。デフォルト拒否のツールポリシー、すべてのフレームを密封する事前共有キー、特権操作(たとえばペアリング)をコンパニオンクライアントから締め出すセッションケイパビリティ、上記の呼び出しごとのロールとリポジトリ許可のチェック、およびレート制限によって、承認された(あるいは漏洩した)ペアリングが完全なリモート操作になることを防ぎます。セキュリティモデルの全体はリモートコントロールとモバイルを参照してください。

エディター、他のエージェント、CLI など、Model Context Protocol を話す外部ツールが、サーバーの MCP サーフェスに接続し、型付きのツールレジストリを利用します。これはプログラムによるインテグレーションの経路であり、GUI ではありません。MCP サーバーを使うと MCP ツールリファレンスを参照してください。

cc_worker は、cc_server とペアリングするヘッドレスの純粋な Dart バイナリで、ホストのケイパビリティを宣言し、実行するリース済みジョブを取得します。同じ RPC を介してプロセスイベントをストリームで返し、永続状態を一切持ちません。データベース、認証、承認、予算はなく、それらが cc_server を離れることはありません。ワーカーは四肢であり、第二の脳ではありません。1 つの権威あるサーバーと N 個の愚直な四肢で、コンセンサスもワーカー間の通信もありません。

リース実行は依然としてリースの配管です。実際のコマンドを実行するのは agentRun だけです(CC_JOB_COMMAND、指定がなければエコーされたプロンプト)。pipelineStep、codeIndex、goldenRender、benchmark、evalBatch は現在、ストリーミング経路がそれらのジョブを実行したふりをすることなく動くよう、git --version / rev-parse HEAD でプローブします。

ワーカーが公開するのは、オペレーティングシステム、CPU アーキテクチャ、コア数、RAM、Flutter SDK に到達可能かどうか、およびホストが持つサンドボックスバックエンド(Windows ではなし)です。専用ワーカーはさらに常に alwaysOn と acceptsParallel を公開し、pin → prefer → spill スケジューラーはこれを基準に配置を行います。GPU や ML のプローブはありません。

単独のデスクトップはこれらを一切意識しません。通常のディスパッチはフリートをまったく経由しないためです。サーバーホストに対して暗黙のインプロセスワーカーが登録されて_います_が、それはランナーが接続されていない登録用のシームです。ローカルワーカーに実際に配置されたジョブは、実行されずに失敗します。現時点でリース済みジョブを実行するのは、実際の cc_worker だけです。

関心事 サーバー(cc_server) フリートエグゼキューター(cc_worker) デスクトップクライアント Web / スマートフォンクライアント
データベース(Drift/SQLite) 所有する — — —
認証、承認、予算、ID 所有する — — —
パイプライン、リコンサイラー、オーケストレーションリスナー 実行する — — —
MCP ツールサーフェス 提供する — — —
エージェント実行 実行する(またはリースで外部に出す) リース済みジョブを実行する — —
UI 描画 — — フル フル(Web)/ 読み取り中心(スマートフォン)
ミーティングキャプチャ(マイク + ミーティング音声) — — ネイティブキャプチャ Web のみ、画面共有音声経由
カレンダー OAuth(Google) トークンを保存する — — —

この 2 つの階層は同じようにはサンドボックス化しません。cc_server は、自ら実行するランを、バックエンドが存在する場合はホストの OS サンドボックス — macOS では Seatbelt、Linux と WSL2 では bubblewrap — で包み、Windows や bwrap と socat がない場合は他の境界にフォールバックします。cc_worker はまったくサンドボックス化しません。 サーバーがプラットフォーム別にジョブを配置できるよう sandbox ケイパビリティを公開しますが、ワーカー側でサンドボックスを適用するものは何もありません。リースされたジョブは直接起動され、リースの環境と与えられたワークツリーによって制限されます。したがって、作業を外部にリースすることは、サンドボックスを持つホストであっても実行をサンドボックスの外に移します。それぞれの場合にランを何が制限するかはサンドボックスとセキュリティを参照してください。

ミーティングキャプチャはクライアント上で行われます。マイクとミーティングの音声が必要であり、物置の中のサーバーは通話を録音できないためです。録音はその後サーバーのパイプラインで要約され、ノートはどのクライアントからも読める場所に保存されます。

Web クライアントも録音しますが、これは表の形からは見えにくいかもしれません。ブラウザーでマイクと画面共有の音声トラックを取り込み、16 kHz の PCM をサーバーにストリーム配信します。この音声トラックを_必須_とするため、ブラウザーが提供しない場合は明示的に失敗します。本当にデスクトップ専用なのは、キャプチャ周辺の品質レイヤーです — ネイティブプロセッサによる信号レベルのエコーキャンセリング、入力レベルのメーターとマイク無音の警告、要約の再実行、処理のキャンセル、カレンダーの録音してリンクするアクションです。

すべてのクライアントは、一度プロビジョニングされた事前共有キーで自身を証明します。

  • ローカルモードのデスクトップは、起動のたびに新しいキーを生成し、環境変数を介して起動したサーバーに渡すため、デスクトップに秘密情報が永続化されません。
  • リモートクライアント(デスクトップのリモート、Web、スマートフォン)は、サーバーの pair コマンドで生成されたキーを使用し、OS のキーチェーン(スマートフォンではローカル)に保存します。

失効は遅延ではなくライブです。サーバーはペアリング済みデバイスのテーブルを監視し、デバイスがアクティブセットから外れた開いているセッションを数秒以内に切断します — 直接の WebSocket 経路でもリレー経路でも同様です。切断するためにデバイスが再接続するのを待つ必要はありません。

キーは、静的ホストが見ることのできる URL を通ることはありません。リンクで届く場合 — スマートフォンやブラウザーで — は、URL フラグメント、つまり # の後の部分に乗り、ブラウザーがそれをページをホストするサーバーに送ることはありません。これは覚えておく価値のある不変条件であり、どちらにも当てはまります。Web クライアントには他に 2 つのエントリーポイントがあります。サーバーとキーを直接貼り付ける接続フォームと、そのブラウザー専用に発行された資格情報と引き換える招待コードです。