ワークスペースと分離
ワークスペースとは何か
「ワークスペースとは何か」というセクションワークスペースは Control Center における最上位の分離テナントです。エージェント、リポジトリ、スペース、チケット、メモリ、パイプライン、メンバーをひとつの境界づけられたコンテキストにまとめます。ワークスペース内で起きることは、すべてその中にとどまります。
通常、ワークスペースは 1 つのプロジェクトまたは 1 つのコードベースに対応します。「api-service」「frontend-v2」「infrastructure」に別々のワークスペースを用意することもあるでしょう。
ワークスペース単位のデータ
「ワークスペース単位のデータ」というセクションワークスペースの行は、そのワークスペース自身の SQLite ファイル、<dataDir>/<workspaceId>/workspace.db に存在します。各ワークスペースに裸のファイルではなくディレクトリが割り当てられるのは、ワークスペースがデータベース以上のものを蓄積するからです — 会話ごとのワークツリー、エージェントとスキルのファイル、チャットボットの資格情報 — フォルダを与えることで、それらすべてが一緒に存在し、一緒に削除されます。
| エンティティ | スコープ |
|---|---|
| エージェント | ちょうど 1 つのワークスペースに属する |
| リポジトリ | 1 つのワークスペースに登録される(後述) |
| スペース、会話、メッセージ | ワークスペース単位 |
| チケット、プロジェクト、プラン、オーケストレーション | ワークスペース単位 |
| メモリのファクト、ポリシー、ドメイン、アクセスグラント | ワークスペース単位 |
| パイプラインの実行、テンプレート、トリガー | ワークスペース単位 |
| プルリクエストの状態、レビュースペース、レビューコホート | ワークスペース単位 |
| エージェント実行ログとワーキングメモリ | ワークスペース単位 |
| メンバー、招待、リポジトリ単位のグラント | ワークスペース単位 |
| コードグラフのシンボルとエッジ | ワークスペースファイル内で repoId によってスコープされる |
これらの行のほとんどは workspaceId 列を持ちますが、分離を強制するのはデータベースファイルを分けていることです。後述の分離の不変条件を参照してください。
リポジトリはワークスペース単位。ワークスペースをまたぐ同一性はパスで決まる
「リポジトリはワークスペース単位。ワークスペースをまたぐ同一性はパスで決まる」というセクションリポジトリの行は、そのワークスペース自身のデータベースファイルに存在します — ファイルが_スコープそのもの_であるため、テーブルに workspaceId 列も結合テーブルもありません。
リポジトリをワークスペースに追加するには 設定 → ワークスペース → リポジトリ を使います。同じチェックアウトを 2 つのワークスペースに登録すると、2 つの id を持つ 2 つの独立した行が作られます。_ワークスペースをまたいだ_リポジトリの同一性はファイルシステムのパス(または GitHub の owner/name)で決まり、id で決まることはありません。
登録は厳格です。パスは git ワークツリーの内側にあり、origin リモートを持ち、そのリモートがサポート対象のコードホスト(github.com、gitlab.com、bitbucket.org)を指している必要があります。コードホストはリモートから読み取られてリポジトリに保存されるため、1 つのワークスペースが 3 つすべてのホストのリポジトリを保持できます。それ以外は拒否されます。フォルダブラウザーが一覧するのは サーバーホストのファイルシステムで、サーバーの --repo-roots 許可リスト(既定ではサーバーユーザーのホームディレクトリ)に限定されます。
リポジトリをワークスペースから削除すると、行はそのまま削除され、そのコードシンボル、エッジ、ファイル、インデックスのチェックポイント、メンバー単位のグラントがカスケード削除されます。ディスク上のチェックアウトはそのまま残ります。
コードグラフが同じファイルに repoId をキーとして置かれるのは、異なるワークスペースが同じリポジトリを異なるブランチで、異なるコードとして持つ可能性があるからです。
分離の不変条件
「分離の不変条件」というセクションワークスペースをまたぐデータ漏えいはバグとして扱われます。分離は構造的であり、複数の層で強制されます:
- データベース層。 各ワークスペースは独自の SQLite ファイルを持ち、
WorkspaceDatabaseManager.of(workspaceId)によって払い出されます。ワークスペースのデータベースはusers、workspaces、その他のワークスペースのテーブルを宣言していないため、ワークスペースをまたぐ読み取りは書こうとしても書けないバグです — コンパイルが通りません。リポジトリは DAO をキャッシュせず呼び出しごとに解決し、すべてのリポジトリメソッドが必須のworkspaceIdを受け取るため、ワークスペースを推測したり既定値にしたりすることはできません。 - ワークスペース id はパスのセグメント。 id はディレクトリ名になるため、安全な単一セグメントのパターン(先頭は英数字、以降は英数字、ドット、アンダースコア、ハイフン。
..は不可)に一致しなければなりません。製品内のどこでも id は UUID です。それ以外はサニタイズされるのではなく例外を投げます — これがパストラバーサル対策であり、ワークスペースのデータを誰かが選んだパスに書き込むのではなく、大きな音を立てて失敗します。 - 多層防御。
workspaceId列は今も存在し、今も書き込まれます。1 つのファイルの中では冗長ですが、同期フィードのトリガー、FTS インデックス、既存の行の形を変えずに保ち、ファイルを単体で検査したときに自己記述的にします — エクスポートとインポートが依存しているのはこれです。 - ドメイン層。 id によってエンティティを変更するサービスは、処理を進める前に単一のチョークポイントで
entity.workspaceId == workspaceIdを検証し、一致しない場合はWorkspaceMismatchExceptionを投げます — 誤ったコンテキストから届いた id に対する、大きな音を立てる失敗です。大きな音を立てて拒否することは重要です。静かな無操作はバグを隠し、処理を続ければ漏えいに至ります。 - RPC 層。 リポジトリ RPC ディスパッチャーがチョークポイントです。ワークスペース単位の操作はすべて、自身の引数に
workspace_idを持たなければなりません — 漏えいしうるセッション単位の「現在のワークスペース」は存在せず、未知の id はデータベースを開く_前に_ not-found として拒否されるため、不正な id が幽霊のworkspace.dbを実体化することはできません。真にグローバルな操作(ニュースフィード、フリートキュー)は、明示的でレビュー済みの決定として自らをスコープ外と宣言します。MCP ツールも同じ規則に従います。ワークスペース単位のデータに触れるツールはworkspace_idを必須とします。 - ワークスペースをまたぐファンアウトは列挙可能。 複数のワークスペースに関する問いに答えるには、複数のデータベースを開くことになります。そのファンアウトは 1 つのヘルパー
CrossWorkspaceQueriesに閉じ込められているため、正当に境界を越えるものの完全なリストは、doc コメントの背後に拡散するのではなく、1 つのファイルの呼び出し箇所に存在します。その呼び出し箇所は、サーバー全体の集約、起動時のリコンサイラー、保持と GC のスイープ、バックアップ、イベントルーター — そして後述のメンバーシップの参照です。 - id だけによるアクセスでは不十分。 エンティティを UUID で参照しても、それが呼び出し元のワークスペースに属することの証明にはなりません。ファイルの分割は誤ったファイルを到達不能にし、ドメイン層の検証は正当なファイルを通って届いた誤った id を捕捉します。
アクセスを許可するのはペアリングキーではなくメンバーシップ
「アクセスを許可するのはペアリングキーではなくメンバーシップ」というセクションデバイスの資格情報を保持することは、_デバイス_を認証するだけです。ワークスペース内部の何かを認可するものではありません。ワークスペース単位のすべての操作は、呼び出し元のメンバーシップロールを解決し、操作の種類から導かれる下限を強制します — 読み取りには guest、変更には member、破壊的な操作には admin — さらにリポジトリの内容を露出する操作は、呼び出し元のリポジトリ単位のグラントを追加で確認します。メンバーシップを持たない有効なペアリングキーは Not a member of this workspace を受け取ります。
分離にとっての帰結は明快です。メンバーシップの行は_ワークスペース自身の_ファイルに存在するため、「このワークスペースには誰がいるか?」はごく普通のスコープ付き読み取りです。逆の問い — 「自分はどのワークスペースにいるか?」 — は定義上ファイルをまたぐため、数少ない公認の CrossWorkspaceQueries ファンアウトの 1 つであり、ワークスペースがまだ選ばれていない段階でワークスペースピッカーが実行するものです。
ロールのはしごとリポジトリ単位のグラントについては、マルチプレイヤーを参照してください。
グローバルレジストリ
「グローバルレジストリ」というセクションすべてがワークスペース単位というわけではありません。2 つ目のデータベース <dataDir>/global.db には、真にサーバー全体にわたるものが入ります:
- ワークスペースレジストリ — 切り替え UI がワークスペースファイルを 1 つも開かずにあらゆるワークスペースを一覧できるようにし、またワークスペースの名前、ロゴ、オーナー、手動の並び順、ソフト削除マーカーが、それらが記述するファイルの外に置かれるようにします。
- アイデンティティ: ユーザー、ユーザー設定、ペアリング済みデバイス。1 人の人間はすべてのワークスペースを通じて 1 人のユーザーであり、ペアリング済みデバイスはワークスペースの削除を越えて存続するからです。
- フリートキュー(ワーカー、ジョブ、配置ログ)。そのスケジューラーはティックごとにキュー全体をすべてのワーカーと突き合わせます。
- ニュースフィード。これはワークスペース単位ではなくユーザー単位です — フィードを追加すると、すべてのワークスペースに付いてきます。
workspace_routes。認証前のインデックスで、シークレットまたは不透明な id(招待ハッシュ、Webhook トークン、ディープリンク)だけを持ちワークスペースを持たないエントリーポイントに対して「このキーを所有するのはどのワークスペースか?」に答えます。ここで見つからなければ not-found です。スキャンによるフォールバックは意図的にありません。- インストール全体の設定とアイデンティティ: インストール id、ホストレベルの設定、SSO 接続、管理対象アクションポリシー(ワークスペースのガードレールを厳しくすることしかできないインストール全体のクランプ)。
起動時に開くのはこのファイルだけです。ワークスペースファイルは最初のアクセス時に遅延して開かれるため、ワークスペースがどれだけの履歴を蓄積しても起動コストは一定に保たれ、ファイル単位の quick_check は準備完了バナーに至る経路ではなく、最初の使用時に支払われます。
ワークスペースファイルが自己識別のために持つのは、単一行の workspace_meta だけです(その id、作成したインストール、作成時のスキーマバージョン、作成日時)。ワークスペースに関する説明的な情報はすべてレジストリにあります。
ワークスペースの作成
「ワークスペースの作成」というセクションワークスペースの作成は、レジストリの行を挿入するだけでは終わりません。1 つの操作で、サーバーは作成したユーザーを ownerUserId として記録し、そのユーザーのオーナーロールのメンバーシップを登録し、WorkspaceCreated ドメインイベントを発行します。このイベントのリスナーが、冪等に、バックグラウンドで次をシードします:
- CEO エージェントと 4 人のスペシャリスト —
qa、architect、engineer、librarian— スペシャリストは CEO に報告します。 - 組み込みのパイプラインテンプレートとそのトリガー(イベントトリガーと手動トリガーは有効な状態で提供されます。スケジュールトリガーは、日次のワークツリー GC スイープを除いてオプトインです)。
- スターター評価スイート。
これは最初のワークスペースだけでなく、_すべての_ワークスペースで起こります。シードされたエージェントはアダプターなしで作成されるため、設定 → ワークスペース → エージェントでエージェントごとにアダプターとモデルを設定するまで、Control Center (built-in) 上で Anthropic の既定モデル(別のモデルを固定しない限り claude-opus-4-8)で実行されます。エージェントモデルを参照してください。
オーナーメンバーシップの書き込みは見た目以上に重要です。これがなければ、作成されたばかりのワークスペースにはメンバーが 1 人もおらず、作成者が次に行うワークスペース単位の呼び出しはすべて拒否されてしまいます。
削除、エクスポート、インポート
「削除、エクスポート、インポート」というセクションワークスペースを削除すると、レジストリの行にマークを付け、ディレクトリのリンクを解除します。ワークスペースはあらゆる一覧と参照から消えます。レジストリの行は(ソフト削除として)残るため、id はバックアップとメンテナンスに引き続き知られていますが、workspace.delete はそのワークスペースのディレクトリ — データベース、ワークツリー、エージェントファイルなど — を WorkspaceDatabaseManager.dropAndClose によって直ちに破棄します。削除できるのはワークスペースのオーナーだけです。
1 つのワークスペースが 1 つのファイルであるため、ワークスペースの受け渡しはテーブルごとのダンプではなくファイル操作になります:
workspace.exportは単一のVACUUM INTOです — サーバーが提供を続けるなか、読み取りトランザクションの下で取られる、一貫性がありデフラグされたスナップショットです。workspace.importはそのようなファイルを採用し、対象のワークスペースが現在保持しているものを置き換えます。埋め込まれたworkspace_metaにより、インポートは「自分のファイルを再採用した」のか「別のインストールのファイル」なのかを区別できます(後者も許可されますが、見えないのではなくログに残ります)。server.backupNowはインストール全体を、ライブのデータディレクトリを写したタイムスタンプ付きディレクトリにスナップショットします —manifest.json、global.db、そしてワークスペースごとの 1 つの<workspaceId>/workspace.db— そのため、復元はそれをコピーして戻すだけです。server.listBackupsはそのディレクトリを新しい順に読み戻し、各スナップショットのサイズとワークスペース一覧を返します。マニフェストが欠けている、または存在しないファイルを名指ししているスナップショットも、不完全としてフラグを立てたうえで報告されます — それを隠すことが、オペレーターに持っていないバックアップを持っていると思い込ませる道です。
バックアップ操作は 設定 → サーバー → バックアップと復元 にあります:
今すぐバックアップ はインストールのスナップショットを取り、一覧はどのスナップショットが
完全かを示し、インストールのオーナーが確認後に削除できるようにします。また各
ワークスペースはエクスポートとインポートを提供します。組み込みの Workspace backup パイプライン
は、そのワークスペースのデータベースだけを週次スケジュールで取得し、設定された保持期間より
古い自身のバックアップを削除します。インストールスナップショットからワークスペースを
復元するのは、スナップショットの <workspaceId>/workspace.db を指した
workspace.import です。インストール全体の復元は、これまでどおりサーバーを停止したうえでの
スナップショットディレクトリのコピー戻しです。
いずれも運ばないのは、ワークスペースディレクトリの残りの部分です。ワークスペースはデータベース以上のものを蓄積し — 貼り付けた画像、スキルとエージェントのファイル、チャットの資格情報、ワークツリー — エクスポートとインポートが移動するのはファイルなので、その隣にあるものはすべてその場にとどまります。これは資格情報については意図的であり、ワークツリーについては使い捨てであり、それ以外については機能ではなく知っておくべき制約です。バックアップリファレンスに完全な表があります。
サーバーとデバイスの間でバックアップを移動する
「サーバーとデバイスの間でバックアップを移動する」というセクション上記のすべての操作はサーバー上のパスで語られます。それが完全な答えになるのは、サーバーが自分のマシンである場合だけです。3 つの署名付き HTTP ルート — /backup/workspace、/backup/snapshot、/backup/restore — が、それ以外のすべてのトポロジー向けにバイト列を運びます。メディアプロキシと画像ストアがすでに使っているのと同じ認証済みレーンです。ページ上ではこれが、各エクスポートと各スナップショットの横の ダウンロード ボタン(保存先は自分で選びます)と、使っているマシン上にあるファイルから復元するための ファイルを選んでアップロード です。サーバーが自分のマシンでないときに機能するのはアップロードのほうです。workspace.import はファイルがすでにホスト上にあることを必要とし、それを届けるのがこれです。
知っておく価値のある 3 つの性質があります:
- ダウンロードは新しいコピーを作り、サーバーはそのどれも保持しません。 ダウンロードは「エクスポートボタンが作ったファイルを取ってくる」ことではありません — もう一度エクスポートし、それをストリーミングし、削除します。応答が
no-storeでAccept-Ranges: noneなのも同じ理由です。レンジを再開すると 2 つの異なるエクスポートがつなぎ合わされ、有効に見えて実際にはそうでない 1 つのファイルになってしまいます。 - アップロードは両端でディスクへストリーミングされ、バッファリングされることは決してありません。また、ステージングされたコピーは、拒否された場合を含め、あらゆる出口で削除されます。ワークスペースのデータベースはスクリーンショットではありません。
- リレー接続には HTTP オリジンがありません。そのため、そこでは転送コントロールが無効になり、理由を示す注記が付きます。サーバー側のパスで語られるものはすべてリレー経由でも動き続けます。直接接続を必要とするのはバイトのレーンだけです。
複数のワークスペース
「複数のワークスペース」というセクション複数のワークスペースを並行して実行できます。それぞれが独立して動作します。あるワークスペースのエージェントは別のワークスペースのエージェントにメッセージを送れず、別のワークスペースのメモリを読めず、そのチケットを見ることもできません。エージェント間メッセージングの宛先解決は厳密で、ワークスペースをまたぐことは決してありません。
ワークスペースをまたぐダッシュボードはありません。/workspaces/:workspaceId/observability の分析サーフェスは、最近の実行の限定的なグローバルフィードを購読し、それを直ちにアクティブなワークスペースへ絞り込むため、そこで見えるのは常に 1 つのワークスペースです。人が触れる唯一の真にマルチワークスペースなサーフェスは、ワークスペースピッカーと切り替え UI です。
知っておく価値のある実用的な制限が 1 つあります。誰かが使用中のワークスペースは開いたままです — LRU はありません。それが 32 を超えると、サーバーは拒否するのではなく警告します。開かれた各ファイルがバックグラウンドアイソレートとページキャッシュを保持するからです。ワークスペースをまたぐファンアウトはファイルを一時的に開き、他に誰もそれらを要求していなければ後で閉じます。