Skip to main content

多租户与服务器部署

在多用户服务器部署中运行Copilot SDK,对状态、身份验证和工具进行按会话隔离。

最适合: SaaS 产品、合作伙伴集成、内部平台和处理并发用户的后端服务。

在以下情况下使用本指南

构建时,请参阅本指南:

  • 一款嵌入了由 Copilot 提供支持的智能体的多用户 SaaS 产品
  • 用于合作伙伴集成的后端,例如 Copilot Studio 或 Fabric 风格的模式
  • 处理并发用户、工作区、租户或请求的任何服务器
  • 共享运行时,其中多个 SDK 客户端连接到一个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 ,以便自动清理非活动会话。 这可以防止长时间运行的进程中的僵尸会话,并减少内存和文件系统压力。

语言公开选项
TypeScriptsessionIdleTimeoutSeconds
Pythonsession_idle_timeout_seconds
GoSessionIdleTimeoutSeconds
.NETSessionIdleTimeoutSeconds
JavasetSessionIdleTimeoutSeconds(...)
Rustwith_session_idle_timeout_seconds(...)

使用与您的产品会话持续时间相匹配的值。 对于聊天后端,15 到 30 分钟通常是一个很好的起点。 对于工作流代理,请在工作流完成时使用更长的超时时间,并显式执行删除操作。

baseDirectory

baseDirectory 为运行时实例设置 COPILOT_HOME 。 使用它可以隔离每个进程、Pod、辅助角色或租户边界的运行时状态、凭据和会话数据。

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 图面:

语言客户端级配置每会话提供程序
TypeScriptsessionFs
createSessionFsAdapter / 提供程序回调
Pythonsession_fscreate_session_fs_handler
GoSessionFSCreateSessionFSProvider
.NETSessionFsCreateSessionFsProvider
Rustwith_session_fs(...)with_session_fs_provider(...)

Java当前不公开已验证的公共 sessionFs 选项,因此本指南不显示 Java sessionFs 示例。

RuntimeConnection.forUri(url)

当多个 SDK 客户端应共享一个已运行的运行时时,请使用外部运行时连接。 这在后端服务中很常见,其中运行时进程独立于请求处理程序进行管理。

语言外部运行时连接
TypeScriptRuntimeConnection.forUri(url)
PythonRuntimeConnection.for_uri(url)
Gocopilot.URIConnection{URL: url}
.NETRuntimeConnection.ForUri(url)
JavasetCliUrl(url)
RustTransport::External { host, port, connection_token }

外部运行时管理自己的进程级身份验证和存储。 当需要用户专属身份验证时,请在 createSessionresumeSession 上传递会话级令牌。

每会话数 gitHubToken

在每个会话上设置 gitHubToken,以将GitHub身份验证范围限定为请求用户。 这不同于对运行时进程进行身份验证的客户端级别令牌。

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

使用按会话令牌来实现内容排除、模型路由、配额检查和用户专属的 Copilot 访问权限。 除非产品有意使用服务帐户语义,否则请避免在用户之间共享一个服务令牌。

集成标识

构建品牌化代理的合作伙伴可以为 Mission Control 请求设置集成 ID。 运行时读取 GITHUB_COPILOT_INTEGRATION_ID,并在每个任务控制请求上将其标记为 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标识当在会话上设置 gitHubToken 时,按会话。
工具mode: "empty" 中显式;在 mode: "copilot-cli" 中隐式。
主机文件系统如果主机工具可用,则由运行时进程共享。

mode: "empty" 是使共享运行时模式可行:除非应用程序注册或允许它们,否则不会公开任何环境 OS 工具。 通过 mode: "copilot-cli"主机进程共享 OS 文件系统访问权限,因此不要将该模式用于多用户服务器模式。

除非通过 sessionFs 路由会话状态,否则会话状态将存储在 COPILOT_HOME/session-state/{sessionId} 下。 使用包含你自己的租户或用户边界的唯一会话 ID,并在恢复或删除会话之前强制实施访问控制。

模式比较

图案何时使用Trade-offs
模式 1:每位用户一个独立的 CLI每个用户需要最强的隔离边界或单独的进程凭据。强隔离;资源成本更高。 请参阅“可扩展性和多租户”。
模式 2:使用共享 CLI mode: "empty"你希望一个运行时在应用控制工具、身份验证和会话 ID 的同时为多个用户提供服务。高效;但要求仔细进行工具注册、为每个会话配置令牌,并执行应用程序级访问检查。
模式 3:混合将计算密集型工作路由到云会话,并将轻量工作路由到本地会话。灵活;需要工作负载路由和策略处理。 请参阅“云会话”。

模式 2:共享 CLI 与 mode: "empty"

在此模式中,所有用户都通过后端连接到一个运行时池。 应用程序执行用户身份验证,选择会话 ID,在会话上传递用户的GitHub令牌,并提供显式工具允许列表。

图示:显示所述过程的流程图。

使用以下规则:

  • 始终在mode: "empty"中启动客户端或运行时。
  • 使用唯一会话 ID 并在应用程序数据库中存储所有权元数据。
  • 在执行 resumeSessiondeleteSession 或任何引用会话 ID 的 UI 操作之前,请先检查所有权。
  • 当请求应以用户身份运行时,请在每个会话中传递 gitHubToken
  • 仅注册会话所需的工具,并首选源限定的允许列表,例如 custom:*mcp:search_docs
  • 显式设置 sessionIdleTimeoutSeconds,并删除已完成的工作流会话。

常见陷阱

  • 忘记mode: "empty"。 默认 copilot-cli 模式公开 CLI 样式的行为,并且可能通过环境工具公开主机文件系统。
  • 未设置 sessionIdleTimeoutSeconds。 如果长期运行的服务器不清理空闲会话,就可能会积累这些空闲会话。
  • 在用户之间共享同一个gitHubToken,而不是传递每个会话的令牌。
  • 信任客户端提供的会话 ID,而无需检查后端的所有权。
  • 在连接到现有运行时的客户端上设置 baseDirectory,并期望这会迁移运行时存储。 改为配置运行时进程。
  • 允许广泛的工具模式,例如 builtin:* ,无需查看每个工具是否适合你的用户。

另请参阅