Skip to main content

マルチテナントとサーバーの展開

Copilot SDK は、状態、認証、およびツールのセッションごとの分離を使用して、マルチユーザー サーバーデプロイで実行します。

次の場合に最適です。 同時ユーザーを処理する SaaS 製品、パートナー統合、内部プラットフォーム、バックエンド サービス。

このガイドは、次の場合に使用します。

ビルドする場合は、次のガイドを使用します。

  • Copilotを利用するエージェントを埋め込むマルチユーザー SaaS 製品
  • Copilot Studio または Fabric スタイルのパターンのような、パートナー連携のバックエンド
  • 同時実行ユーザー、ワークスペース、テナント、または要求を処理するすべてのサーバー
  • 複数の SDK クライアントが 1 つの Copilot ランタイム プロセスに接続する共有ランタイム

このガイドは スケーリングとマルチテナント の姉妹です。 トポロジ、負荷分散、およびストレージ パターンについては、このガイドを使用してください。 SDK レベルのオプションとランタイム分離の選択肢については、このガイドを使用してください。

主要な SDK オプション

オプション次の目的に使用します:メモ
mode: "empty"アンビエント OS ツールと CLI の既定値を無効にするマルチユーザーまたは共有のシナリオに必要です。
sessionIdleTimeoutSecondsアイドル状態のセッションのクリーンアップ実行時間の長いプロセスのサーバー側タイムアウトを設定します。
baseDirectoryランタイム インスタンスごとの COPILOT_HOME の分離既存のランタイムに接続するときに無視されます。
sessionFsローカル ディスクからセッション ファイル システム ストレージをルーティングするセッションごとのファイルシステム プロバイダーとペアにします。
RuntimeConnection.forUri(url)既に実行されているランタイムの共有言語名は異なります。以下のサンプルを参照してください。
セッションごと gitHubToken認証のスコープをリクエスト元のユーザーに限定するこれは、単一の共有ユーザー トークンよりも優先されます。

mode: "empty"

mode: "empty" は、オプションの Copilot CLI の動作を既定で無効にします。 マルチユーザー サーバー モードでは、セッションがアクセスできるツール、MCP サーバー、スキル、ワークスペース パスをアプリケーションが明示的に決定する必要があるため、これは安全なベースラインです。

共有サーバーには既定の mode: "copilot-cli" を使用しないでください。 このモードは、CLI に似たコーディング エージェントを対象としており、アンビエント ホスト ファイルシステムの機能を公開できます。

TypeScript
import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk";

// baseDirectory and sessionIdleTimeoutSeconds apply when the SDK spawns the
// runtime. With RuntimeConnection.forUri(...) configure COPILOT_HOME and the
// idle timeout on the runtime process itself.
const client = new CopilotClient({
    mode: "empty",
    connection: RuntimeConnection.forUri(process.env.COPILOT_RUNTIME_URL!),
});

const session = await client.createSession({
    sessionId: `user-${user.id}-${crypto.randomUUID()}`,
    model: "gpt-4.1",
    availableTools: ["custom:lookupOrder", "custom:createTicket"],
    gitHubToken: user.githubToken,
});
Python
from copilot import CopilotClient, RuntimeConnection
from copilot.session import PermissionHandler

client = CopilotClient(
    mode="empty",
    base_directory=f"/var/lib/my-app/copilot/{runtime_instance_id}",
    session_idle_timeout_seconds=900,
    connection=RuntimeConnection.for_uri(runtime_url),
)
await client.start()

session = await client.create_session(
    session_id=f"user-{user.id}-{request_id}",
    model="gpt-4.1",
    available_tools=["custom:lookupOrder", "custom:createTicket"],
    github_token=user.github_token,
    on_permission_request=PermissionHandler.approve_all,
)
Go
package main

import (
    "context"
    "fmt"

    copilot "github.com/github/copilot-sdk/go"
)

type appUser struct {
    ID          string
    GitHubToken string
}

func main() {
    ctx := context.Background()
    runtimeInstanceID := "instance-1"
    runtimeURL := "http://127.0.0.1:8080"
    requestID := "req-1"
    user := appUser{ID: "alice", GitHubToken: "YOUR_GITHUB_TOKEN"}

    client := copilot.NewClient(&copilot.ClientOptions{
        Mode:                      copilot.ModeEmpty,
        BaseDirectory:             fmt.Sprintf("/var/lib/my-app/copilot/%s", runtimeInstanceID),
        SessionIdleTimeoutSeconds: 900,
        Connection:                copilot.URIConnection{URL: runtimeURL},
    })

    session, err := client.CreateSession(ctx, &copilot.SessionConfig{
        SessionID:      fmt.Sprintf("user-%s-%s", user.ID, requestID),
        Model:          "gpt-4.1",
        AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"},
        GitHubToken:    user.GitHubToken,
    })
    _ = session
    _ = err
}
client := copilot.NewClient(&copilot.ClientOptions{
    Mode:                      copilot.ModeEmpty,
    BaseDirectory:             fmt.Sprintf("/var/lib/my-app/copilot/%s", runtimeInstanceID),
    SessionIdleTimeoutSeconds: 900,
    Connection:                copilot.URIConnection{URL: runtimeURL},
})

session, err := client.CreateSession(ctx, &copilot.SessionConfig{
    SessionID:      fmt.Sprintf("user-%s-%s", user.ID, requestID),
    Model:          "gpt-4.1",
    AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"},
    GitHubToken:    user.GitHubToken,
})
.NET
using GitHub.Copilot;

var runtimeInstanceId = "instance-1";
var runtimeUrl = "http://127.0.0.1:8080";
var requestId = "req-1";
var user = new { Id = "alice", GitHubToken = "YOUR_GITHUB_TOKEN" };

var client = new CopilotClient(new CopilotClientOptions
{
    Mode = CopilotClientMode.Empty,
    BaseDirectory = $"/var/lib/my-app/copilot/{runtimeInstanceId}",
    SessionIdleTimeoutSeconds = 900,
    Connection = RuntimeConnection.ForUri(runtimeUrl),
});

await using var session = await client.CreateSessionAsync(new SessionConfig
{
    SessionId = $"user-{user.Id}-{requestId}",
    Model = "gpt-4.1",
    AvailableTools = ["custom:lookupOrder", "custom:createTicket"],
    GitHubToken = user.GitHubToken,
});
var client = new CopilotClient(new CopilotClientOptions
{
    Mode = CopilotClientMode.Empty,
    BaseDirectory = $"/var/lib/my-app/copilot/{runtimeInstanceId}",
    SessionIdleTimeoutSeconds = 900,
    Connection = RuntimeConnection.ForUri(runtimeUrl),
});

await using var session = await client.CreateSessionAsync(new SessionConfig
{
    SessionId = $"user-{user.Id}-{requestId}",
    Model = "gpt-4.1",
    AvailableTools = ["custom:lookupOrder", "custom:createTicket"],
    GitHubToken = user.GitHubToken,
});
Java
import java.util.List;
import com.github.copilot.CopilotClient;
import com.github.copilot.rpc.CopilotClientOptions;
import com.github.copilot.rpc.CopilotClientMode;
import com.github.copilot.rpc.SessionConfig;

public class MultiTenancyExample {
    record User(String id, String gitHubToken) {}

    public static void main(String[] args) throws Exception {
        String runtimeUrl = "http://localhost:4321";
        String requestId = "req-1";
        User user = new User("u1", "ghu_token");

        // setCopilotHome and setSessionIdleTimeoutSeconds are ignored when
        // setCliUrl is used; configure those on the runtime process instead.
        var client = new CopilotClient(new CopilotClientOptions()
            .setMode(CopilotClientMode.EMPTY)
            .setCliUrl(runtimeUrl)
        );

        var session = client.createSession(new SessionConfig()
            .setSessionId("user-" + user.id() + "-" + requestId)
            .setModel("gpt-4.1")
            .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket"))
            .setGitHubToken(user.gitHubToken())
        ).get();
    }
}
// setCopilotHome and setSessionIdleTimeoutSeconds are ignored when
// setCliUrl is used; configure those on the runtime process instead.
var client = new CopilotClient(new CopilotClientOptions()
    .setMode(CopilotClientMode.EMPTY)
    .setCliUrl(runtimeUrl)
);

var session = client.createSession(new SessionConfig()
    .setSessionId("user-" + user.id() + "-" + requestId)
    .setModel("gpt-4.1")
    .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket"))
    .setGitHubToken(user.gitHubToken())
).get();
Rust
use std::path::PathBuf;
use github_copilot_sdk::{Client, ClientOptions, Transport};
use github_copilot_sdk::mode::ClientMode;
use github_copilot_sdk::types::SessionConfig;

let client = Client::start(
    ClientOptions::new()
        .with_mode(ClientMode::Empty)
        .with_base_directory(PathBuf::from(format!(
            "/var/lib/my-app/copilot/{runtime_instance_id}"
        )))
        .with_session_idle_timeout_seconds(900)
        .with_transport(Transport::External {
            host: runtime_host.to_string(),
            port: runtime_port,
            connection_token: None,
        }),
).await?;

let session = client.create_session(
    SessionConfig::default()
        .with_session_id(format!("user-{}-{request_id}", user.id))
        .with_model("gpt-4.1")
        .with_available_tools(["custom:lookupOrder", "custom:createTicket"])
        .with_github_token(user.github_token),
).await?;

sessionIdleTimeoutSeconds

非アクティブなセッションが自動的にクリーンアップされるように、サーバーに sessionIdleTimeoutSeconds を設定します。 これにより、実行時間の長いプロセスでのゾンビ セッションが防止され、メモリとファイル システムの負荷が軽減されます。

Languageパブリック オプション
TypeScriptsessionIdleTimeoutSeconds
Pythonsession_idle_timeout_seconds
GoSessionIdleTimeoutSeconds
.NETSessionIdleTimeoutSeconds
JavasetSessionIdleTimeoutSeconds(...)
Rustwith_session_idle_timeout_seconds(...)

製品の会話の有効期間に一致する値を使用します。 チャット バックエンドの場合、通常は 15 分から 30 分が適しています。 ワークフロー エージェントの場合は、ワークフローの完了時にタイムアウト時間が長くなり、明示的に削除されます。

baseDirectory

baseDirectory は、ランタイム インスタンスの COPILOT_HOME を設定します。 これを使用して、プロセス、ポッド、ワーカー、またはテナントの境界ごとにランタイム状態、資格情報、セッション データを分離します。

const client = new CopilotClient({
    mode: "empty",
    baseDirectory: `/var/lib/my-app/copilot/runtime-${process.env.HOSTNAME}`,
    sessionIdleTimeoutSeconds: 900,
});

ランタイムは、COPILOT_HOMEを含め、構成されたsession-state/{sessionId}の下にセッション状態を格納します。 アプリで複数のランタイム インスタンスを実行する場合は、意図的に共有ストレージを使用しない限り、各インスタンスに個別のディレクトリを指定します。

SDK が RuntimeConnection.forUri(url) を使用してすでに実行中のランタイムに接続する場合、baseDirectory は SDK クライアントによって無視されます。 代わりに、ランタイム プロセスで COPILOT_HOME を構成します。

sessionFs

sessionFs はカスタム セッション ファイルシステム プロバイダーを登録するため、セッション スコープのファイル I/O をランタイムのローカル ディスクではなくアプリケーション ストレージ経由でルーティングできます。 ローカル ディスクが一時的な場合、セッション状態がオブジェクト ストレージに存在する必要がある場合、またはプラットフォームでテナント対応のストレージ パスを適用する必要がある場合に使用します。

const client = new CopilotClient({
    mode: "empty",
    sessionFs: {
        initialCwd: "/workspace",
        sessionStatePath: "/session-state",
        conventions: "posix",
    },
});

プロバイダー コールバックを公開する言語の場合は、クライアント レベルで sessionFs を構成し、セッションの作成時または再開時にセッションごとのファイルシステム ハンドラーを提供します。 永続化の概念とストレージのトレードオフについては、 セッションの再開と永続化 を参照してください。

検証済みのパブリック SDK サーフェス:

Languageクライアント レベルの構成セッションごとのプロバイダー
TypeScriptsessionFs
createSessionFsAdapter / プロバイダーコールバック
Pythonsession_fscreate_session_fs_handler
GoSessionFSCreateSessionFSProvider
.NETSessionFsCreateSessionFsProvider
Rustwith_session_fs(...)with_session_fs_provider(...)

Javaは現在、検証済みのパブリック sessionFs オプションを公開していないため、このガイドでは Java sessionFs サンプルは表示されません。

RuntimeConnection.forUri(url)

複数の SDK クライアントが既に実行されている 1 つのランタイムを共有する必要がある場合は、外部ランタイム接続を使用します。 これは、ランタイム プロセスが要求ハンドラーとは別に管理されるバックエンド サービスで一般的です。

Language外部ランタイム接続
TypeScriptRuntimeConnection.forUri(url)
PythonRuntimeConnection.for_uri(url)
Gocopilot.URIConnection{URL: url}
.NETRuntimeConnection.ForUri(url)
JavasetCliUrl(url)
RustTransport::External { host, port, connection_token }

外部ランタイムは、独自のプロセス レベルの認証とストレージを管理します。 ユーザー固有の認証が必要な場合は、セッションごとのトークンを createSession または resumeSession に渡します。

セッションごと gitHubToken

各セッションで gitHubToken を設定して、GitHub 認証の対象を要求元のユーザーに限定します。 これは、ランタイム プロセスを認証するクライアント レベルのトークンとは異なります。

const session = await client.createSession({
    sessionId: `user-${user.id}-support`,
    model: "gpt-4.1",
    availableTools: ["custom:*"],
    gitHubToken: user.githubToken,
});

コンテンツの除外、モデル ルーティング、クォータ チェック、およびユーザー固有のCopilot アクセスには、セッションごとのトークンを使用します。 製品でサービス アカウント セマンティクスを意図的に使用しない限り、ユーザー間で 1 つのサービス トークンを共有しないようにします。

統合ID

ブランド化されたエージェントを構築するパートナーは、Mission Control 要求の統合 ID を設定できます。 ランタイムは、GITHUB_COPILOT_INTEGRATION_ID を読み取り、すべての Mission Control 要求で Copilot-Integration-Id HTTP ヘッダーとしてスタンプします。

GITHUB_COPILOT_INTEGRATION_ID=my-product-agent copilot --headless --port 4321

既定の統合 ID は copilot-developer-cli。 属性やルーティングには、 my-product-agent などの安定した値を使用します。 統合 ID は現在、環境変数によってのみ構成されています。これは、ファースト クラスの SDK オプションではありません。

SDK がランタイムを生成する場合は、クライアント環境オプションを使用して環境変数を渡します。 RuntimeConnection.forUri(url)で接続する場合は、ランタイム プロセス自体に環境変数を設定します。

セッションレベルの分離保証

セッション レベルの分離とは、ランタイムがグローバル共有状態ではなく、ユーザー固有のモデルと状態の情報をセッションのスコープに保持することを意味します。

Surface分離動作
モデル リスト キャッシュセッションごと。 モデル検索では、セッションのモデル リスト キャッシュが使用されます。
セッションの状態
COPILOT_HOME/session-state/{sessionId}配下のセッションIDごと。
GitHub ID 情報
gitHubToken がセッションに設定されている場合は、セッションごと。
Tools
mode: "empty"では明示的、mode: "copilot-cli"では暗黙的です。
ホスト ファイルシステムホスト ツールが使用可能な場合は、ランタイム プロセスによって共有されます。

mode: "empty" は、共有ランタイム パターンを実行可能にする機能です。アプリケーションが登録または許可しない限り、アンビエント OS ツールは公開されません。 mode: "copilot-cli"では、OS ファイルシステム アクセスはホスト プロセスを通じて共有されるため、マルチユーザー サーバー モードにはそのモードを使用しないでください。

セッションの状態は、COPILOT_HOME/session-state/{sessionId}経由でルーティングしない限り、sessionFsに格納されます。 独自のテナントまたはユーザーの境界を含む一意のセッション ID を使用し、セッションを再開または削除する前にアクセス制御を適用します。

パターンの比較

Pattern次の場合に使用します。Trade-offs
パターン 1: ユーザーごとに分離された CLIユーザーごとに最も強力な分離境界または個別のプロセス資格情報が必要です。強力な分離。より高いリソース コスト。 「スケーリングとマルチテナント」を参照してください。
パターン 2: CLI との共有: mode: "empty"アプリがツール、認証、およびセッション ID を制御している間、1 つのランタイムで多くのユーザーにサービスを提供する必要があります。効率的;には、慎重なツール登録、セッションごとのトークン、およびアプリケーション レベルのアクセス チェックが必要です。
パターン 3: ハイブリッドコンピューティング負荷の高い作業をクラウド セッションにルーティングし、軽い作業をローカル セッションにルーティングします。柔軟;には、ワークロード ルーティングとポリシー処理が必要です。 「クラウド セッション」を参照してください。

パターン 2: mode: "empty" を伴う共有 CLI

このパターンでは、すべてのユーザーがバックエンド経由で 1 つのランタイム プールに接続します。 アプリケーションはユーザー認証を実行し、セッション ID を選択し、セッションでユーザーのGitHub トークンを渡し、明示的なツール許可リストを提供します。

図: 説明されたプロセスを示すフローチャート。

次の規則を使用します。

  • 常にクライアントまたはランタイムを mode: "empty"で起動します。
  • 一意のセッション ID を使用し、所有権メタデータをアプリケーション データベースに格納します。
  • セッション ID を参照する resumeSessiondeleteSession、または UI アクションの前に所有権を確認します。
  • 要求をユーザーとして実行する必要がある場合は、セッションごとに gitHubToken を渡します。
  • セッションに必要なツールのみを登録し、 custom:*mcp:search_docsなどのソース修飾許可リストを優先します。
  • sessionIdleTimeoutSeconds設定し、完了したワークフロー セッションを明示的に削除します。

よくある落とし穴

  • mode: "empty"を忘れること。 既定の copilot-cli モードでは、CLI スタイルの動作が公開され、アンビエント ツールを使用してホスト ファイル システムが公開される場合があります。
  • sessionIdleTimeoutSecondsを設定していません。 実行時間の長いサーバーは、クリーンアップしないとアイドル 状態のセッションを蓄積する可能性があります。
  • セッションごとのトークンを渡すのではなく、ユーザー間で 1 つの gitHubToken を共有します。
  • バックエンドの所有権を確認せずに、クライアントが提供するセッション ID を信頼する。
  • 既存のランタイムに接続するクライアントで baseDirectory を設定し、ランタイム ストレージを移動することを想定しています。 代わりにランタイム プロセスを構成します。
  • 各ツールがユーザーに適しているかどうかを確認することなく、 builtin:* などの広範なツール パターンを許可します。

参照