# 多租户与服务器部署

多用户服务器模式是指在为多个用户、租户、工作区或集成账户提供服务的后端代码中运行 Copilot SDK。 在此设置中，应用程序拥有请求路由和授权，而 SDK 和运行时提供每会话状态、每会话身份验证和显式工具注册，因此一个用户的会话不会继承其他用户的工具或标识。

<!-- markdownlint-disable GHD046 GHD005 -->

<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->

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

## 在以下情况下使用本指南

构建时，请参阅本指南：

* 一款嵌入了由 Copilot 提供支持的智能体的多用户 SaaS 产品
* 用于合作伙伴集成的后端，例如 Copilot Studio 或 Fabric 风格的模式
* 处理并发用户、工作区、租户或请求的任何服务器
* 共享运行时，其中多个 SDK 客户端连接到一个Copilot运行时进程

本指南是 [缩放和多租户](/zh/copilot/how-tos/copilot-sdk/setup/scaling) 的妹妹。 将该指南用作拓扑、负载均衡和存储模式方面的参考。 对于 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 的编码代理，可以公开环境主机文件系统功能。

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

```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-5.4",
    availableTools: ["custom:lookupOrder", "custom:createTicket"],
    gitHubToken: user.githubToken,
});
```

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

```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-5.4",
    available_tools=["custom:lookupOrder", "custom:createTicket"],
    github_token=user.github_token,
    on_permission_request=PermissionHandler.approve_all,
)
```

</div>

<div class="ghd-codetab" data-lang="go" data-label="Go"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Go</div>

```golang
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-5.4",
    AvailableTools: []string{"custom:lookupOrder", "custom:createTicket"},
    GitHubToken:    user.GitHubToken,
})
```

</div>

<div class="ghd-codetab" data-lang="dotnet" data-label=".NET"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">.NET</div>

```csharp
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-5.4",
    AvailableTools = ["custom:lookupOrder", "custom:createTicket"],
    GitHubToken = user.GitHubToken,
});
```

</div>

<div class="ghd-codetab" data-lang="java" data-label="Java"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Java</div>

```java
// 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-5.4")
    .setAvailableTools(List.of("custom:lookupOrder", "custom:createTicket"))
    .setGitHubToken(user.gitHubToken())
).get();
```

</div>

<div class="ghd-codetab" data-lang="rust" data-label="Rust"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Rust</div>

```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-5.4")
        .with_available_tools(["custom:lookupOrder", "custom:createTicket"])
        .with_github_token(user.github_token),
).await?;
```

</div>

</div>

### `sessionIdleTimeoutSeconds`

在服务器上设置 `sessionIdleTimeoutSeconds` ，以便自动清理非活动会话。 这可以防止长时间运行的进程中的僵尸会话，并减少内存和文件系统压力。

| 语言         | 公开选项                                     |
| ---------- | ---------------------------------------- |
| TypeScript | `sessionIdleTimeoutSeconds`              |
| Python     | `session_idle_timeout_seconds`           |
| Go         | `SessionIdleTimeoutSeconds`              |
| .NET       | `SessionIdleTimeoutSeconds`              |
| Java       | `setSessionIdleTimeoutSeconds(...)`      |
| Rust       | `with_session_idle_timeout_seconds(...)` |

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

### `baseDirectory`

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

```typescript
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 可通过应用程序存储而非运行时的本地磁盘进行处理。 当本地磁盘是临时磁盘、会话状态需要位于对象存储中时，或者当平台需要强制实施租户感知存储路径时，请使用它。

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

对于公开提供程序回调的语言，请在客户端级别配置 `sessionFs`，并在创建或恢复会话时提供每会话文件系统处理程序。 请参阅 [会话恢复和持久性](/zh/copilot/how-tos/copilot-sdk/features/session-persistence) 了解持久性概念和存储权衡。

已验证的公共 SDK 图面：

| 语言                                | 客户端级配置                 | 每会话提供程序                         |
| --------------------------------- | ---------------------- | ------------------------------- |
| TypeScript                        | `sessionFs`            |                                 |
| `createSessionFsAdapter` / 提供程序回调 |                        |                                 |
| Python                            | `session_fs`           | `create_session_fs_handler`     |
| Go                                | `SessionFS`            | `CreateSessionFSProvider`       |
| .NET                              | `SessionFs`            | `CreateSessionFsProvider`       |
| Rust                              | `with_session_fs(...)` | `with_session_fs_provider(...)` |

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

### `RuntimeConnection.forUri(url)`

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

| 语言         | 外部运行时连接                                                |
| ---------- | ------------------------------------------------------ |
| TypeScript | `RuntimeConnection.forUri(url)`                        |
| Python     | `RuntimeConnection.for_uri(url)`                       |
| Go         | `copilot.URIConnection{URL: url}`                      |
| .NET       | `RuntimeConnection.ForUri(url)`                        |
| Java       | `setCliUrl(url)`                                       |
| Rust       | `Transport::External { host, port, connection_token }` |

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

### 每会话 `gitHubToken`

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

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

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

## 集成标识

构建品牌化智能体的合作伙伴可以为任务控制请求设置集成 ID。 运行时读取 `GITHUB_COPILOT_INTEGRATION_ID`，并在每个任务控制请求上将其标记为 `Copilot-Integration-Id` HTTP 标头。

```bash
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 文件系统访问权限，因此不要将该模式用于多用户服务器模式。

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

## 模式比较

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

### 模式 2：使用共享 CLI `mode: "empty"`

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

![图示：显示所述过程的流程图。](/assets/images/help/copilot/copilot-sdk/setup-multi-tenancy-diagram-0.png)

使用以下规则：

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

## 常见陷阱

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

## 另请参阅

* [缩放和多租户](/zh/copilot/how-tos/copilot-sdk/setup/scaling)：部署拓扑、存储模式和隔离比较
* [后端服务设置](/zh/copilot/how-tos/copilot-sdk/setup/backend-services)：在无外设服务器模式下运行运行时
* [BYOK （自带密钥）](/zh/copilot/how-tos/copilot-sdk/auth/byok)：使用您自己的模型提供商凭据
* [云会话](/zh/copilot/how-tos/copilot-sdk/features/cloud-sessions)：将所选工作路由到云会话
* [会话恢复和持久性](/zh/copilot/how-tos/copilot-sdk/features/session-persistence)：管理可恢复会话状态
* [功能](/zh/copilot/how-tos/copilot-sdk/features)：工具、事件、挂钩和高级 SDK 功能