エンタープライズ管理の AI クライアント
| Status | Authors | Coach | DRIs | Owning Stage | Created |
|---|---|---|---|---|---|
| proposed | erran
ohoral | devops ai_powered | 2026-09-09 |
概要
ユーザーと管理者が AI クライアントの設定を管理するための、一貫したインターフェースを提供します。複数のソースからのクライアント設定の解決をサポートし、明確なマージ戦略を適用して文書化します。
動機
GitLab インスタンス設定、バージョン付き API エンドポイント、クライアントごとに異なる場所に保存されるユーザー設定を通じて、クライアント設定と管理者による強制設定が一貫性なく実装されることを避けます。
競合製品では、クライアント設定の優先順位と解決方法が明確に文書化されています。
目標
何を実現しようとしていますか?
- クライアント設定の配置場所を明確にする
成功したことをどのように判断しますか?
- 次のユーザー設定を実装する:
- テレメトリを有効にする
- 新しいセッションにワークアイテムを表示する
- 通知
- テーマ
- 自動モードを有効にする
- ワークスペースエージェントを有効にする
- 次の管理者による管理設定を実装する:
- テレメトリを有効にする
- 自動モードを有効にする
- ワークスペースエージェントを有効にする
- 推奨プラグインマーケットプレイス
- 推奨プラグイン
- 必須プラグインマーケットプレイス
- 必須プラグイン
- 上記の設定の優先順位を文書化する
ほかに、ここにはどのような目に見えにくい機会がありますか?
- クライアント設定のドキュメントの自動生成。
対象外
このドキュメントの対象外は何ですか?
- 従来の設定からの移行手順。ただし、唯一の例外として、Duo CLI は記録済みの
テレメトリのオプトアウトを
settings.jsonに 1 回だけ引き継ぎ、確認せずに データ収集が再び有効になることを防ぎます。その他の設定はデフォルトから開始し、 古い保存先からは何も削除しません。 - クライアント設定を取得する Rails エンドポイントの実装
- ツールの権限を解決する Rails エンドポイントの実装。
提案
次の読み込みをサポートする設定解決サービスを導入します。
- ユーザー全体に適用する
settings.jsonファイル - MDM/Jamf が管理する
settings.managed.jsonファイル
将来のソース:
- 共有プロジェクト設定
- リモートではなく、現在の作業ディレクトリから読み込む
- ローカルのプロジェクト設定の上書き(例:
settings.local.json)- リモートではなく、現在の作業ディレクトリから読み込む
- 一元化された
$root_namespace/duo-workspaceプロジェクトのsettings.managed.jsonファイル- 既存セッション向けにキャッシュし、バックグラウンドで定期的に更新する
設計と実装の詳細
設定のソース
設定のソースとは、設定値を供給できるものすべてを指します。リゾルバーとファイルソースは、すべてのクライアントで共有します。各クライアントは、設定のソースを 1 つの順序付きリストにまとめ、起動時にリゾルバーへ渡します。これにより、そのクライアントの優先順位全体を 1 か所で確認できます。プロジェクト設定やリモートファイルなど、新しい種類のソースを追加する場合は、そのソースを実装してリストに挿入します。既存の設定やソースを変更する必要はありません。
優先順位の高いものから低いものへ、次の順序を提案します。
| # | ソース | 場所 | 強制適用 | ステータス |
|---|---|---|---|---|
| 1 | MDM、Jamf、グループポリシーを通じて配布する管理設定ファイル | プラットフォームごとの固定パス。管理設定ファイルの場所を参照 | はい | 最初のイテレーション |
| 2 | リモートの管理設定 | $root_namespace/duo-workspace プロジェクト内の settings.managed.json | はい | 将来 |
| 3 | クライアントが提供するセッションの上書き | CLI フラグと環境変数、IDE 設定、CI ジョブ変数 | いいえ | 最初のイテレーション |
| 4 | ローカルのプロジェクト上書き | 作業ディレクトリ内の .gitlab/duo/settings.local.json | いいえ | 将来 |
| 5 | 共有プロジェクト設定 | 作業ディレクトリ内の .gitlab/duo/settings.json | いいえ | 将来 |
| 6 | アカウント設定 | ~/.gitlab/duo/accounts/<host>/<user>/settings.json。GitLab アカウントごとに 1 つのフラットなファイル | いいえ | 将来 |
| 7 | ユーザー設定 | ~/.gitlab/duo/settings.json | いいえ | 最初のイテレーション |
| 8 | インストール済みプラグインが提供する設定 | プラグインのマニフェスト。優先順位は要確認 | いいえ | 将来 |
ローカルのプロジェクト上書きは共有プロジェクト設定より上位に置き、開発者が自身のチェックアウトに合わせてチームのデフォルトを調整できるようにします。
ほとんどの設定では、その値を設定しているソースのうち、優先順位が最も高いものの値を使用します。どのソースも設定していなければ、設定の定義で宣言されたデフォルトを適用します。すべての設定にデフォルトがあるため、解決処理は常に値を生成します。
設定の定義
各設定は、LSP リポジトリ内でリゾルバーやファイルソースとともに、1 回だけ宣言します。この宣言から、解決処理、検証、対話的な設定 UI、生成される JSON Schema、および各クライアントがサポートする設定についての生成ドキュメントを導出します。Rails のワークアイテム AI クライアント設定スキーマの作成では、別のスキーマを作成しません。代わりに、生成した JSON Schema を LSP のビルド成果物として公開し、自動化によって Rails プロジェクトへ同期します。この作業は gitlab-org/gitlab#628967で追跡しています。
type SourceId =
| 'managed'
| 'remote'
| 'session'
| 'project-local'
| 'project'
| 'account'
| 'user'
| 'plugin';
type MergeStrategy =
| 'replace'
| 'union'
| { restrict: 'intersection' | 'union' | 'min' | 'max' | 'anyTrue' | 'allTrue' }
| { perKey: MergeStrategy }; // records: each key resolves on its own
interface SettingDefinition<T> {
schema: ZodType<T>; // validation and schema generation
default: T; // required; applies when no source sets the setting
description: string; // used for docs and $schema
merge: MergeStrategy; // see merge strategies below
allowedSources?: SourceId[]; // omit = any source may set it
onUnavailable?: 'fallthrough' | { failClosedTo: T }; // when an administrator source cannot be loaded
clients?: Array<'cli' | 'editor' | 'ci'>; // where the setting applies; omit = every client
}
interface StartupContext {
workingDirectory: string; // for project sources
homeDirectory: string; // for the user file
platform: 'darwin' | 'linux' | 'win32'; // for the managed file location
instanceUrl?: string; // for the remote source
namespace?: string; // detected project namespace, for the remote source
client: { kind: 'cli' | 'editor' | 'ci'; version: string };
}
interface SettingsSource {
readonly id: SourceId;
readonly enforced: boolean; // true for 'managed' and 'remote'
load(context: StartupContext): Promise<SourceResult>;
}
type SourceResult =
| { status: 'ok'; values: Record<string, unknown> }
| { status: 'absent' } // nothing to say, e.g. file missing
| { status: 'unavailable'; reason: string }; // could not load, e.g. fetch failed
interface Resolved<T> {
value: T;
source: SourceId | 'default'; // which source supplied the value
readOnly: boolean; // true when the source is enforced
keys?: Record<string, { source: SourceId | 'default'; readOnly: boolean }>; // perKey settings only
}
interface SettingsResolver {
get<T>(definition: SettingDefinition<T>): Resolved<T>;
}
clients は、設定を適用する AI クライアントを列挙します。cli は Duo CLI、editor はエディタ拡張機能に組み込まれた言語サーバー、ci はワークフロー Executor が GitLab CI/CD ジョブ内で実行するフローです。3 つとも同じリポジトリからビルドし、定義を共有します。clients は任意で、省略するとすべてのクライアントに設定を適用します。Duo CLI が起動する ACP サーバーは、ACP 独自の値が必要になるまでは cli プロファイルを継承し、必要になった時点で独自のクライアント種別になります。
load は、解決処理の前にクライアントが 1 回収集した StartupContext を受け取ります。これには、作業ディレクトリとホームディレクトリの場所、クライアントの実行プラットフォーム、検出した GitLab インスタンスとプロジェクトの名前空間、要求元のクライアント種別が含まれます。ソースはその中から必要な情報を読み取ります。セッションの上書きソースは例外で、クライアントが解析したフラグまたは IDE 設定を渡して構築し、このコンテキストを無視します。
各ソースは自身の優先順位を知らず、他のソースを参照できません。クライアント固有なのはセッションの上書きソースだけで、それ以外のソースはすべて共有します。
allowedSources は、設定値を指定できるソースを制限します。requiredPlugins のように管理者による指定にのみ意味がある設定もあれば、テーマのようにユーザーによる指定にのみ意味がある設定もあります。ほとんどの設定はどのソースからでも指定でき、フィールドを省略した場合のデフォルトもその動作です。この制限は 3 か所で適用します。
- 読み込み時には、リゾルバーは定義にそのソースが列挙されていないキーの値を破棄し、ファイルパスとキーを示して 1 回警告します。
- 書き込み時には、対話的な設定 UI は
userソースを許可するキーだけを書き込みます。 - 生成される JSON Schema では、同じ定義からファイル種別ごとにスキーマを生成し、クライアントが実行される前にエディタが配置場所の誤ったキーを指摘できるようにします。
allowedSources と enforced は独立しています。前者はどのソースが値を設定できるか、後者はどのソースの値が最終決定になるかを示します。
設定のスコープ
ユーザー設定ファイルは、オペレーティングシステムのユーザーごとに用意します。その中の設定は、 ファイルを読み取るすべてのクライアントで同じ値を持ち、ファイル内にクライアント別のセクションはありません。 クライアント固有の調整項目は各クライアント自身の設定に置きます。エディタでは IDE 設定、 CLI ではフラグや環境変数を使用します。クライアント別の上書きが必要になった場合も、 共有ファイル内のセクションではなく、クライアントごとに別のフラットなファイルを用意します。
あるインスタンスには存在しても別のインスタンスには存在しないモデルの選択など、GitLab アカウント間で
異なる必要がある設定は、allowedSources に account ソースを指定し、そのアカウント専用の
ファイルに保存します。このファイルはユーザーファイルと同じスキーマを使用します。クライアントは各設定を
そのスコープが指すファイルに書き込み、ユーザーにファイルの選択を求めることはありません。
定義の例:
const settings = defineSettings({
telemetry: {
enabled: setting({
schema: z.boolean(),
default: true,
description: 'Send anonymous usage data to improve GitLab Duo.',
merge: 'replace',
clients: ['cli', 'editor'],
}),
url: setting({
schema: z.string().url(),
default: 'https://telemetry.gitlab.com',
description: 'Endpoint that receives telemetry events.',
merge: 'replace',
allowedSources: ['managed'],
clients: ['cli', 'editor'],
}),
},
});
resolver.get(settings.telemetry.enabled); // Resolved<boolean>
対応するファイルも、同じようにネストされています。
{
"telemetry": {
"enabled": false
}
}
強制適用
値を強制適用するかどうかは、設定の種類ではなく、その値の供給元によって決まります。管理者が制御するソースは 2 つあり、ローカルの管理ファイルと duo-workspace プロジェクトのリモート管理設定です。それらが設定したものはすべて強制適用され、他のソースからのものは強制適用されません。強制適用された値について、リゾルバーは読み取り専用の印を付け、対話的な設定 UI は供給元とともに無効化された状態で表示します。たとえば「組織によって設定」と表示します。それ以外のソースの値は、より上位のソースで上書きできます。
強制適用の意味は、設定のマージ戦略によって異なります。
replace: 管理された値をそのまま値として使用します。union: 管理されたエントリは削除できません。ユーザーはエントリを追加できるため、結果は管理リストとユーザーのリストを合わせたものになります。restrict: 管理された値が制限を定めます。ユーザーは、許可リストからエントリを削除するなど、設定をより厳しくすることはできますが、緩めることはできません。
強制適用の対象は、管理ソースが実際に設定した設定だけです。管理者が何も指定していないものについては、ユーザーが制御を維持します。
管理者による推奨は、強制適用の弱い段階ではなく、別の設定として表現します。たとえば recommendedPlugins はインストールするものとしてクライアントとユーザーに提示しますが、requiredPlugins はクライアントがインストールする強制適用リストです。
管理設定ファイルの場所
管理設定ファイルは、各プラットフォームの固定パスから読み込みます。デフォルトでは、 管理者だけがこれらの場所に書き込めるため、 ファイルの値を強制適用することに意味があります。
- Linux:
/etc/gitlab/duo/settings.managed.json - macOS:
/Library/Application Support/GitLab/duo/settings.managed.json - Windows:
C:\Program Files\GitLab\duo\settings.managed.json
Windows では、このファイルを %ProgramData% から読み込みません。標準ユーザーは
C:\ProgramData の下にフォルダーを作成し、その所有者になれるため、ポリシーの配布前に
GitLab\duo を作成して、後からファイルを置き換えたり削除したりできてしまいます。
環境変数はユーザーが制御できるため、このパスはリテラルで指定し、
%ProgramFiles% などの環境変数から組み立てません。そのため、Program Files が
C: にないマシンでは、クライアントはこのファイルを見つけられません。
WSL は対象外です。WSL 内で実行されるクライアントは、Windows のグループポリシーでは管理されない、 ディストリビューション内の Linux パスを読み込みます。
マージ戦略
各設定は、複数のソースの値をどのように組み合わせるかを宣言します。戦略は、その設定を指定しているソースを対象に実行し、デフォルトはどのソースも指定していない場合にのみ使用します。管理者が許可リストを追加するまでは制限が存在しない場合のように、「未設定」という状態に意味がある設定では、その状態を型とデフォルトに宣言します。たとえば string[] | null とし、null は制限なしを意味します。
replace: 設定を指定しているソースのうち、優先順位が最も高いものの値を採用します。テレメトリ、テーマ、自動モードなどのスカラー値の設定に適用します。union: すべてのソースの値を集合として組み合わせます。追加のマーケットプレイスや推奨プラグインなど、追加型のリストに適用します。restrict: 下位のソースは値を厳しくすることはできますが、緩めることはできません。「より厳しい」の意味は値の形によって異なるため、この戦略には方向があります。- 許可リストでは
intersection: すべてのソースに存在するエントリだけが残ります。 - ブロックリストでは
union: いずれかのソースでブロックされたエントリは、ブロックされたままになります。 - 数値や順序付きの選択肢では
minとmax: 小さい値または大きい値のどちらかを、より厳しい値とします。列挙型の順序はスキーマに由来します。 - 真偽値では
anyTrueとallTrue:trueとfalseのどちらが安全な値かによって選びます。
- 許可リストでは
perKey: プラグイン名から有効化フラグへのマップのようなレコードに使用します。各キーは、そのキーに言及するソースを対象として、内側の戦略で個別に解決します。強制適用もキーごとに行います。
設定を厳しくするための特別な構文は、ユーザーには必要ありません。自分のファイルに同じキーを記述すると、戦略が値を組み合わせます。
障害の処理
- 存在しないファイルは
absentです。リゾルバーは次のソースへ進みます。 - 解析できないファイルは
unavailableです。クライアントはパスと解析エラーを示して 1 回警告し、次へ進みます。エラーから復旧するために設定ファイルを削除したり、その内容を置き換えたりすることはありません。ユーザーが修正できるよう、ファイルはそのまま残します。 - 検証に失敗した値は、キーと期待する型を示す警告とともに破棄します。ファイルの残りの部分は保持します。
- 未知のキーは無視するため、時間の経過とともにキーを追加または削除できます。
- ある設定を指定できる管理者のソースが、管理ファイルを読み取れない、リモート取得が失敗するなどの理由で読み込めない場合や、その設定のスキーマに適合しない値を提供した場合、デフォルトの動作は
fallthroughです。そのソースは存在しないものとして扱い、次のソースを適用します。好みの設定には適切ですが、ガバナンスの設定では、まさにポリシーにアクセスできなくなった時点で制限を解除することになります。そのような設定はonUnavailable: { failClosedTo: value }を宣言し、空の許可リストや有効化フラグのfalseなど、ポリシーを読み取れない間に維持する値を指定します。ほとんどのマージ戦略では、クライアントがスキーマから安全な方向を導き出せないため、値を明示的に宣言します。また、defaultは、どのソースもその設定を指定していないときの値という、本来の意味を保ちます。ポリシーを再び読み取れるようになるまで、クライアントは制限された状態で利用可能なままとなり、その設定は読み取り専用になります。 - 設定ファイルに書き込めない場合も、変更は実行中のセッションに適用し、クライアントは保存できなかったことを報告します。
ファイル形式
- ファイルは JSON で、読み込み時にはコメントと末尾のカンマを許可します。クライアントによる書き込みでは、コメントと未知のキーを保持します。
- 各ファイルには
$schemaキーを含めることができ、エディタがファイルを検証し、補完を提供できるようにします。クライアントはユーザーファイルの作成時に$schemaの行を書き込みます。 - 単一の設定定義リストから、設定ファイルの種類ごとに JSON Schema を生成し、固定 URL で公開します。各スキーマには、そのファイルのソースが設定できるキーを含めるため、複数のソースで許可されるキーは、それぞれのスキーマに現れます。
代替案
サーバーによる管理設定
メリット:
- 唯一の情報源は GitLab インスタンス
デメリット:
- アップグレードを待つ必要があります。複数のマイルストーンを要する場合があり、バグ/セキュリティ修正以外ではバックポートできません。
- インスタンスのバージョン間で設定をマージする必要があります
- インスタンスのバージョンごとにデフォルトを実装する必要があります
- デフォルト値を決定するためにフィーチャーフラグの値を考慮する必要があります
- 新しい GraphQL クエリを導入し続けることになります
何もしない
メリット:
- 変更が不要
デメリット:
- 異なる AI クライアント設定の優先順位/マージ戦略について、不明確な要件に直面し続けます。
5d840611)