# 底层沙箱

本指南解释了 [Sandboxes](../guides/sandbox) 的内部工作原理，以及它们为何采用当前的构建方式及其局限性。如果您只是想使用沙箱，[Sandboxes 指南](../guides/sandbox) 就足够了；如果您希望了解其机制、评估信任模型或调试某些问题，请继续阅读。

> [!NOTE]
> Sandbox API 目前处于实验阶段。其 API 和行为可能会在不另行通知的情况下发生变化。

## 不存在“沙箱服务”

首先要理解的一点是，并没有专门的沙箱后端。沙箱只是一个运行着单个小型静态二进制文件 `sbx-server` 的 [HF Job](../guides/jobs)（即虚拟机），该二进制文件通过 HTTP 进行通信。客户端通过 Jobs 代理（即 Job 暴露的 `*.hf.jobs` URL）与该服务器通信。其余所有内容——身份验证、服务发现、将多个沙箱打包到一个 Job 中——都是基于现有的 Jobs 原语构建的：标签、环境变量和密钥。

```mermaid
flowchart LR
    subgraph local["Your machine"]
        C["huggingface_hub<br/>Sandbox / SandboxPool<br/>(or hf sandbox CLI)"]
    end
    subgraph hf["Hugging Face"]
        P["Jobs proxy<br/>(namespace auth)"]
        subgraph job["HF Job (a VM)"]
            S["sbx-server<br/>(static binary, port 49983)"]
        end
    end
    C -- "HTTPS + X-Sandbox-Token" --> P
    P --> S
    S -- "exec / files / procs" --> S
```

这种“无需新增基础设施”的设计，正是沙箱能够在任意 Docker 镜像中运行，并免费继承 Jobs 的计费、硬件规格和命名空间权限的原因。

### 启动服务器

在任务启动时，该任务的命令是一个小型的 `/bin/sh -c` 脚本，用于获取 `sbx-server` 二进制文件，赋予其可执行权限，并 `exec` 执行它。该二进制文件是一个约 640KB 的静态 [musl](https://musl.libc.org/) 构建版本，没有任何运行时依赖项，因此可以在任何 `x86_64` Linux 镜像中运行。

有几个值得指出的决策：

- **下载，并带有挂载回退机制。** 快速路径使用 `wget` 或 `curl`（所有常见的 base image 都自带其中之一）从 HF CDN 下载二进制文件，速度快且免费。作为安全网，服务器的 Hub 仓库也会作为卷挂载到每个作业上：如果镜像中既没有 `wget` 也没有 `curl`，脚本会改为从该挂载点复制二进制文件。挂载是透明的——除非实际读取，否则不产生任何开销——但读取它会使冷启动增加约 2-3 秒，因此它不是默认方式。对镜像的唯一硬性要求是 `/bin/sh`。
- **手写 HTTP/1.1 服务器，不依赖任何框架。**实时输出流要求在生成每个数据块时立即将其刷新发送。常见的极简 Rust HTTP 服务器（例如 `tiny_http`）会缓冲分块响应，直到整个响应完成，这会破坏流式传输。因此，该服务器手动实现了 HTTP/1.1：针对 `exec` 使用 NDJSON 事件流，针对文件使用原始请求体，并对每个数据块执行显式刷新。
- **端口 49983。**服务器监听一个刻意选用的不常用端口，以便将常见的开发端口（3000、8000、8080 等）留给您的代码使用。

> [!TIP]
> 该服务器在 [github.com/huggingface/sandbox-server](https://github.com/huggingface/sandbox-server) 上开源。

## 身份验证是无状态的

两个独立的层保护沙箱：

1. **代理网关。**Jobs 代理仅转发携带具有该作业命名空间读取权限的 HF 令牌的请求。互联网上的随机用户无法访问该 URL。
2. **应用层网关。** `sbx-server` 还会在每个请求（`/health` 除外）中检查 `X-Sandbox-Token` 请求头。这是一种纵深防御机制：即使只读命名空间成员能够访问代理，也无法执行命令。

沙箱令牌是派生的，而非存储的：

```text
nonce  = random 128-bit hex                       # stored in the job label "hf-sandbox-nonce"
token  = HMAC-SHA256(key=your_hf_token, msg="hf-sandbox:" + nonce)
```

nonce 是公开的，HMAC 密钥即您的 HF token，因此任何持有该 token 的机器都可以重新计算沙箱 token——这正是重连能够保持无状态的原因。这也意味着该 token 是从您的凭据派生出的*能力*，而非对调用方的身份验证：服务器验证的是调用方是否知晓该 token，而非其身份。

每个沙箱作业还带有两个用于发现的稳定标签 — `hf-sandbox=1`（所有作业均包含）以及 `hf-sandbox-mode=dedicated` 或 `hf-sandbox-mode=pool` — 因此您可以在服务端列出或筛选它们，例如 `hf jobs ps --label hf-sandbox=1`。

该令牌通过 Job secret 传递到服务器。客户端会根据标签中的公开 nonce 按需重新推导该令牌。这带来了一些有益的效果：

- **无状态重连。** `Sandbox.connect(id)` 可在任何持有相同 HF token 的机器上运行——从标签中读取 nonce，重新计算 token。无需本地文件，也无需复制任何状态。
- **HF token 不会作为环境变量或作业密钥传递给沙箱**（除非您通过 `forward_hf_token=True` 选择启用）。这并非保证您的凭据绝对无法被访问的硬性承诺：监听沙箱端口的进程是镜像启动时最先运行的进程，因此不受信任的镜像可能能够观察客户端发送的请求——包括其 `Authorization` 请求头。应将可从沙箱访问的凭据视为可能已暴露给该沙箱。

### 令牌范围

每个 **Job** 会铸造一个 nonce，因此派生出的 token 是 *job* 凭证。在专用模式下，job 即沙箱，因此两者一致。在池化模式下，job 是主机，而一台主机承载多个沙箱——因此池化沙箱会获得一个属于它自己的、范围更窄的第二凭证：

| credential | 如何推导 | 授权 |
| --- | --- | --- |
| **专用沙箱令牌** | 作业标签中的 `HMAC(hf_token, nonce)` | 该沙箱（即整个任务） |
| **池主机令牌** | 来自主机作业标签的`HMAC(hf_token, nonce)` | 池管理：在该主机上创建、列出和删除沙箱，并恢复其令牌 |
| **池化沙箱令牌** | 由主机服务器为每个沙箱生成的随机 256 位 | 那个沙箱——不是同级，也不是池 |

客户端会自动使用较窄的那个：`pool.create()` 在创建响应中接收它，`Sandbox.connect("<host>.<id>")` 使用主机令牌恢复它，而 `proxy_headers` 则提供沙箱的令牌而非主机的令牌（在您读取时解析 HF 承载令牌，因此长期有效的句柄不会提供过期的令牌）——这些标头通常最终会出现在浏览器或 WebSocket 客户端中，因此它们应仅授予对单个沙箱的访问权限，而不授予其他任何权限。

这对泄露意味着什么：**池化沙箱令牌**的泄露会危及该沙箱。**主机令牌**的泄露会危及主机——包括其上当前及未来的所有沙箱，以及其管理路由——因此应将其视为池的管理员凭证。您命名空间中的成员持有不同的 HF 令牌，无法推导出您的令牌，但请参阅[已知限制](#known-limitations)，了解令牌如何可能被*传递*到错误的位置。

客户端要求用于池化操作的沙箱范围令牌不能为空，并且会拒绝未提供该令牌的主机。它绝不会使用主机管理凭证作为替代。在使用此 API 之前，请升级客户端并回收较旧的池主机。

## 专用沙盒（`Sandbox.create`）

直接模型：**每个沙箱一个 Job**。Job 是一个真实的虚拟机，因此提供了最强的隔离（虚拟机级别），支持包括 GPU 在内的任意硬件配置，并且是处理互不信任代码的正确选择。其 API 路由位于 `/v1/*`，文件路径在容器文件系统上是绝对路径。`kill()` 仅取消该 Job。

专用沙箱为你提供的边界位于 **Job 与其外部的一切** 之间。在虚拟机内部，你的代码和 `sbx-server` 都在同一个容器中以 root 身份运行：`sbx-server` 不会对你执行的命令进行沙箱隔离，它只是通过 HTTP 暴露该命令。因此，一旦你不信任的代码在专用沙箱中执行过，就应将整个虚拟机——包括其控制平面——视为属于该代码：它可以终止或替换服务器，而你在该沙箱中后续执行的一切操作都将受其支配。请为每个不受信任的工作负载使用一个专用沙箱，并在完成后使用 `kill()` 将其终止，而不是在信任边界之间复用同一个沙箱。

```mermaid
sequenceDiagram
    participant U as Sandbox.create()
    participant J as Jobs API
    participant S as sbx-server (in Job)
    U->>J: run_job(image, expose 49983, labels hf-sandbox=1 + hf-sandbox-nonce, secret token)
    J-->>U: job_id + proxy URL
    U->>S: poll /health until ready (~6s VM boot)
    U->>S: POST /v1/exec  (run a command, stream NDJSON back)
    U->>J: cancel_job  (on kill / context-manager exit)
```

成本就体现在图中：每个沙箱都要支付约 6 秒的完整虚拟机冷启动时间，并按整台机器计费。对于单个沙箱或 GPU 工作负载，这正是您所需要的。但对于 100 到 1000 个短时的 CPU 任务来说，这样做是浪费的——而这正是资源池存在的意义。

## 池：在一个 Job 中运行多个沙箱（`SandboxPool`）

> [!WARNING]
> **池（Pools）适用于同一信任边界内的工作负载。** 池化沙箱是共享虚拟机内的一个 uid 加上一组 Landlock 规则集，而非独立的虚拟机，其宿主机运行着一个特权控制平面，该池中的所有沙箱都会与其通信。使用池可以低成本地并行运行*您自己的*代码。对于相互不信任的工作负载——即任何您不希望其读取其他沙箱文件的情况——请使用 [Sandbox.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.create)，它将为每个工作负载提供独立的虚拟机。有关具体的局限性，请参阅 [已知限制](#known-limitations)。

典型的强化学习 rollout 或工具执行沙箱仅需几 MB 内存和一个核心，运行几秒钟。为每个沙箱支付 2 vCPU 虚拟机并承受 6 秒的冷启动时间——甚至触发 1000 个虚拟机的调度突发——是一种错误的权衡。因此，[SandboxPool](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxPool) 将单个 Job 作为主机运行，并在其中复用多个沙箱。

池化沙箱不是嵌套的虚拟机或容器。它是经典的 Unix 多用户原语：

- 一个**专用的 uid**（≥ 20000），
- 一个由该 uid 拥有的**私有 `0700` 主目录**，
- 以该 uid 身份执行的 `exec` 命令，其 **环境已清理**（`env_clear`，因此宿主机的密钥绝不会泄露），并启用了 `NO_NEW_PRIVS`、针对每个进程的 **rlimits** 以及针对每个沙箱的 **Landlock 规则集**。

因此，创建沙箱就是 `mkdir + chown + build ruleset` ≈ 1ms 服务端操作——无需启动第二个虚拟机。客户端可见的唯一延迟是代理往返时间。

```mermaid
flowchart TB
    Pool["SandboxPool<br/>image, flavor, sandboxes_per_host"]
    subgraph h1["Host Job #1 (one VM)"]
        S1["sbx-server"]
        b1["sbx · uid 20001 · ~/ 0700 · landlock"]
        b2["sbx · uid 20002 · ~/ 0700 · landlock"]
        b3["sbx · uid 20003 · ~/ 0700 · landlock"]
    end
    subgraph h2["Host Job #2 (one VM)"]
        S2["sbx-server"]
        b4["sbx · uid 20001 · landlock"]
        b5["sbx · uid 20002 · landlock"]
    end
    Pool --> h1
    Pool --> h2
```

池化沙箱的公开 ID 为 `<host_job_id>.<local_id>`，因此 `connect`/`exec`/`kill` 与专用沙箱一样以无状态方式工作。对池化沙箱调用 `kill()` 会向其宿主发送 `DELETE` 请求（释放一个槽位）；宿主将继续运行。

### 池中的隔离：uid + Landlock

这是池设计的核心所在，因此有必要精确说明哪些内容被隔离，哪些内容未被隔离。

标准的 Job 在用户命名空间内以 root 身份运行，该命名空间仅映射 uid 0..65535，启用了 seccomp 过滤器，且不具备 `CAP_SYS_ADMIN` / `CAP_NET_ADMIN` / `CAP_NET_RAW` 权限。这排除了常用的重量级隔离工具：无法创建嵌套命名空间，无法创建新挂载点，也无法进行 cgroup 委托（`unshare`、`mount`、向 `/sys/fs/cgroup/...` 写入均会失败）。内核提供的是 [**Landlock**](https://docs.kernel.org/userspace-api/landlock.html)（ABI 6），这是一个 Linux 安全模块，允许任何非特权进程限制自身及其子进程——这正是我们所需的每个沙箱的边界。对于每个沙箱，服务器都会构建一个规则集；exec 子进程会降级到沙箱的 uid/gid，并在运行命令前应用 `NO_NEW_PRIVS`、`landlock_restrict_self` 以及 rlimits。

将不同的 uid（自主访问控制）与 Landlock 结合使用，其设计和测试旨在提供：

- ✅ 进程 A 无法读取其他进程的 `environ`，从而防止沙箱之间直接访问 HF 和沙箱令牌。
- ✅ A 无法对 B 的进程执行 `SIGKILL` / `ptrace` / 读取其内存，无法 `setuid` 到 B，也无法读取 B 的 `0700` 主目录。
- ✅ 拒绝访问 `/tmp` 和 `/dev/shm` — 每个沙箱都通过 Landlock 限制在其自身的 home 目录内（其 `TMPDIR` 指向 `$HOME` 内部）。
- ✅ A 无法`bind` TCP 端口，因此沙箱之间不存在本地主机服务（出站`connect`仍然被允许，因此互联网可以正常工作）。
- ✅ Cross-sandbox abstract unix sockets are blocked (`LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET`; uid isolation alone does *not* block these).

这些控制措施用于约束工作负载。根控制平面仍是一个独立的信任边界：其主机模式下的文件操作会拒绝符号链接，其套接字代理会固定套接字 inode 并检查对端 uid，且专用路由在主机模式下不可用。剩余的共享通道在[已知限制](#known-limitations)中进行了说明。

> [!NOTE]
> **为什么这不能替代虚拟机。**Landlock 和 uid 隔离旨在用于同一信任边界内的工作负载。由于池化沙箱共享内核、虚拟机和控制平面，无法保证能防御所有跨沙箱攻击；资源以及部分进程列表元数据也仍然共享。对于互不信任的代码，或对于 GPU，请使用 [Sandbox.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.create)，它将为每个沙箱提供独立的虚拟机。

### 池中的文件模型

由于池化沙箱中唯一可写的区域是其受 Landlock 限制的主目录（该目录同时也是其默认工作目录），文件 API 会将所有路径的根定位到该主目录：`files.write("data/in.txt", ...)` 会写入 `$HOME/data/in.txt`，开头的 `/` 被视为相对于主目录的路径，且 `..` 无法逃逸出该目录。通过 API 写入的文件会被 `chown` 到沙箱的 uid，以便沙箱自身的代码能够读取它们。这提供了一种“以沙箱为根的文件系统”的清晰模型，与沙箱内部代码实际可触及的范围完全一致——这与专用沙箱不同，在专用沙箱中，路径是容器文件系统上的绝对路径。

路径会先进行词法规范化，然后相对于一个已打开的主目录描述符进行解析，并且对每个路径组件都拒绝符号链接。文件 API 不会跟随符号链接，包括同一主目录内的链接；沙箱代码仍可在其 Landlock 规则下使用符号链接。

### Pools 没有权威的本地状态

Pool 并非有意设计为本地配置文件。Pool 即其正在运行的主机 Job 集合，这些 Job 共享 `hf-sandbox-pool=<id>` 标签。这使 Pool 与沙箱 API 的其余部分保持一致（所有内容均可通过标签发现，并可在任意机器上重新附加），也意味着一旦其最后一台主机消失，Pool 便不复存在。

- 主机在其作业环境变量中承载池的配置（镜像、规格、`sandboxes_per_host`、空闲超时）——标签仅用于过滤。当客户端需要启动一个重复的主机时，它会从一个正在运行的主机中读取该配置（`inspect_job`），从而确保池中的所有主机保持一致，而无需中央记录。
- 环境变量和密钥是按沙箱隔离的，在创建时传入——绝不在池级别设置。池化沙箱的环境变量在其生命周期内保存在宿主服务器的内存中（每次执行命令时都会重新应用）；它们永远不会写入磁盘，也不会出现在任何 Job 的元数据中。池化沙箱没有加密密钥通道——请使用 `env`，并将这些值视为“未持久化，但也未加密”。对于需要加密存储的值，请使用专用沙箱的 `secrets`。
- 容量由服务器端权威管理。当创建数量超过 `sandboxes_per_host` 时，主机将拒绝创建请求（返回 `{"rejected": N}`）；客户端会将超出部分打包到另一台主机上，或启动一个副本。这确保了即使多个进程并发地向同一资源池创建资源，打包过程依然精确无误。
- 空闲驱逐分为两级。每个沙箱在自身 `idle_timeout` 时间内无活动后会被驱逐（除非仍有正在运行的进程）；当一台主机在主机空闲超时时间内没有任何沙箱时，它将自行关闭——即使所有客户端都消失，这也是一道计费兜底措施。

### 尽力而为的缓存可让 `create --pool` 保持快速

缺乏权威的本地状态对正确性很有利，但会增加延迟。一个冷启动的 `hf sandbox create --pool <id>`（一个全新的 CLI 进程）在创建沙箱之前，否则必须通过网络重新发现所有信息：`list_jobs` 扫描命名空间 → `inspect_job` 检查每个主机以重建其 URL 和 nonce → `GET /v1/sandboxes` 查看每个沙箱的填充程度 → 最后 `POST` 创建。每次调用都会产生多次纯粹开销的网络往返。

位于 `$HF_HOME/sandbox/pools/<context>/<pool-id>.json` 的尽力而为缓存可消除该开销。在任何创建/预热操作之后，进程会记录池配置以及每个主机的代理 URL、认证 nonce 和最近一次观察到的空闲槽位。下一个进程将直接从该文件重建主机传输（无需 HTTP），并直接执行 `POST`。

`<context>` 是端点、凭据以及条目所写入的命名空间的摘要——凭据以指纹形式呈现；您的令牌本身绝不会写入缓存。目录名和文件都包含该摘要，如果读取时这三者中有任何一项不匹配，则视为普通的缓存未命中，因此同一台机器上的其他用户、其他端点，或 `connect(pool, namespace="other-org")` 将走冷路径，而不是使用为其他目的缓存的主机。文件创建权限为 `0600`，目录为 `0700`。

```mermaid
flowchart TD
    start["hf sandbox create --pool ID"] --> rc{"cache hit?<br/>pools/&lt;context&gt;/ID.json<br/>(endpoint + credential + namespace)"}
    rc -- "yes, written &lt;15 min ago" --> seed["rebuild host transport<br/>from cached URL + nonce<br/>(no HTTP)"]
    rc -- "yes, older" --> check["inspect_job: still a running host<br/>of this pool, on that URL?"]
    check -- "yes" --> seed
    check -- "no" --> slow
    seed --> post["POST /v1/sandboxes"]
    post -- "ok" --> done["sandbox ready<br/>(~1 round-trip)"]
    post -- "host gone / full" --> slow
    rc -- "no / corrupt / another context" --> slow["fallback: list_jobs +<br/>inspect_job + GET (the cold path)"]
    slow --> post2["POST /v1/sandboxes"] --> done2["sandbox ready<br/>+ refresh cache"]
```

缓存的用途与限制：

- **对容量不具有权威性。** 作业内的服务器始终保持权威性，因此过时的 `live` 计数只会导致一次无效请求，而绝不会造成主机装箱错误。
- **不信任主机所在的地址。** 缓存的 URL 仅在其与同一条目所命名的作业的 HTTPS jobs-proxy URL 一致时才会被使用，因此缓存文件无法决定您的 HF bearer 和 sandbox token 的发送目标；任何命名了其他内容的条目都将从文件中丢弃。此外，条目超过 15 分钟后也不会再单独被采信：`inspect_job` 必须确认该作业仍在运行、仍标记为属于此池，并且仍位于该 URL 上，之后才会构建传输。
- **因其形状而不可信。**类型和范围会在读取时进行检查，因此手动编辑、截断或采用字符串类型的文件会导致缓存未命中，而不是在`create()`执行过程中出现错误。
- **自愈。** 已消失的缓存主机会在首次请求失败时被丢弃，并从文件中清除；创建操作会透明地回退到标签发现。
- **并发安全。**写入操作在文件锁（以 `job_id` 为键）下合并并原子提交，因此并行的 `create` 进程不会相互覆盖，读取方也永远不会看到写入一半的文件。
- **可丢弃的。**删除它只会导致缓存未命中：冷路径会执行，一切功能依然正常。它绝不会在多台机器之间共享。

有两点需要注意。请将 `$HF_HOME` 视为敏感信息：其中一条记录仍然是一个可复用的主机 URL 加上其令牌所派生的公开 nonce。虽然在其他凭据下写入的文件会被忽略，但在 *您* 的凭据下写入的文件会在上述 15 分钟窗口内被计入。此外，由于命名空间是键的一部分，在显式 `namespace=` 下缓存的池只有在传入相同的 `namespace=` 时才能再次找到——`hf sandbox create --pool <id>` 需要与创建该池时使用的 `--namespace` 相同，否则它将走冷路径并在您自己的命名空间中查找。

## 已知限制

**池适用于同一信任边界内的工作负载。对于您不信任的代码，请使用专用沙箱。** 以下检查可降低特定风险；它们不会将池化沙箱转变为独立的虚拟机。

### 威胁模型概览

| Actor | 专用 | 池 |
| --- | --- | --- |
| 匿名用户 | Jobs 代理需要一个具有命名空间读取权限的 HF 令牌。 | 相同。 |
| 具有读取权限的命名空间成员 | 可以访问代理和公共健康检查，但 API 操作需要沙箱令牌。 | 相同。 |
| 可以创建作业的命名空间成员 | 无法从其他用户的 nonce 推导出其令牌。 | 可以复制池标签，但默认采用操作会检查 Jobs API 的发起者。`adopt_hosts="namespace"` 表示选择信任其他创建者。 |
| 在沙箱中运行的代码 | 与虚拟机内的服务器共享根目录；请勿跨信任边界重复使用该虚拟机。 | 受 uid 和 Landlock 限制，但共享内核、虚拟机和特权控制平面。 |
| 您的客户端 | 保存 HF 和专用沙箱的凭据。 | 保存主机管理凭据以及每个沙箱的独立权限。 |

### 隔离和资源缺口

- **主机发现不等于证明。** 默认的 `adopt_hosts="own"` 会检查发起者、镜像、flavor、命令以及暴露的 URL。它不会对正在运行的二进制文件进行证明，也不会使可变镜像变得可信。仅在你信任命名空间成员的情况下，才使用 `adopt_hosts="namespace"`。
- **主机凭据仍然具有强大权限。**它们用于管理资源池并恢复每个沙箱的令牌。此客户端绝不会用主机凭据来替代缺失的作用域令牌。由此版本启动的主机运行的是 sbx-server 0.7.0，该版本会在作用域路由上拒绝主机凭据；而由旧版客户端启动的主机则会保留其服务器（及其主机令牌兼容性），直到被回收。
- **共享通道仍然存在。**出站 TCP、对控制服务器的回环访问、UDP、内核 IPC 以及可读的进程列表元数据均未被隔离。系统目录和部分设备节点仍可访问。GPU 池隔离尚未经过测试；对于 GPU，请使用专用模式。
- **限制并非聚合配额。** 进程级 rlimit 和有界输出队列可降低资源放大效应。它们不会在沙箱之间划分 CPU 份额、总内存、磁盘空间、inode 或网络容量。目录分页可限制响应大小，但仍会在服务器上实例化该目录。API 文件写入并非磁盘配额，且专用模式下对特殊文件的写入可能会阻塞。
- **可以显式地削弱隔离。**主机模式默认要求 Landlock ABI 6，并会拒绝失败的规则集。降低 `SBX_MIN_LANDLOCK_ABI` 或使用服务器的 `--allow-unconfined` 开发标志会接受更少的保证；请检查经过身份验证的 `/health` 以获取有效的 ABI 和功能。
- **代理连接仅进行一次身份验证。**在首次请求之后，代理会将字节复制到该后端，直到遇到 EOF。该连接上后续的 HTTP 请求不会独立进行身份验证或路由。WebSocket/SSE 兼容性并不能确保对上游连接复用的安全性。

### 生命周期和凭据

- **分离的后代进程可能在进程终止或超时后继续存活。** 信号会发送给整个进程组；调用了 `setsid()` 的后代进程会脱离该进程组。要结束整个工作负载，请删除池化沙箱或终止专用 Job。
- **`max_hosts` 是尽力而为的。** 不同的客户端可能会统计到相同的主机，并各自部署更多实例。要实现严格的共享上限，需要后端准入控制或幂等性机制。
- **Teardown 可能会失败。** 主机取消失败时会抛出异常并保留缓存记录。`close()` 会限制其对进行中创建操作的等待时间；如果某个创建操作超出了该等待时间，可能会留下一个可计费的 Job。请使用 Jobs API 或 CLI 来核对正在运行的资源。
- **Bearer 轮换不会轮换沙箱功能。**代理身份验证会针对每个请求解析当前的 HF 令牌，但无状态重连会从确切的原始 Bearer 值派生主机令牌。轮换该值可能会阻止与现有主机重新连接。
- **本地缓存作为受信任输入保留 15 分钟。** 会检查其 URL 和安全上下文，但它没有完整性 MAC；请保护 `$HF_HOME` 免受不受信任代码的写入。
- **摘要固定（digest pin）并非签名的来源证明。** 客户端仅验证某一个特定的服务器二进制文件。发布和更新该固定值仍属于发布流程的一部分；工作流的清单（manifest）并非密码学证明。显式的 `SBX_ALLOW_UNVERIFIED_SERVER=1` 逃生机制仅在两个哈希工具均缺失时跳过验证。

未认证的 `/health` 仅暴露存活状态和协议信息。获取详细元数据需要主机凭据。沙箱 ID 和令牌需要内核随机源；UID 复用遵循已验证的进程和主目录清理。

## 性能

所有数值均基于 `cpu-basic` 上的真实 HF Jobs 测量，客户端位于笔记本电脑上，所有流量均通过 Jobs 代理传输。

**专用沙盒：**

| metric                                            | value                                                           |
| ------------------------------------------------- | --------------------------------------------------------------- |
| 冷启动（`create()` 返回，服务器开始响应） | ~5.8s 中位数                                                    |
| `run()` 往返                                | p50 ~110ms（代理 RTT 下限约为 ~105ms；客户端开销 ≈ 0） |
| 文件传输（并行范围，>8 MiB）           | ~340 MiB/s 下载，~441 MiB/s 上传                                  |

**池（共享/主机模式）：**

| N 个沙箱 | 主机（50 台/主机） | provision + 创建所有 | 在所有位置执行 | 全部终止 | 总计     |
| ----------- | --------------- | ---------------------- | ----------- | -------- | --------- |
| 100         | 2               | 6.1s                   | 1.5s        | 0.6s     | **8.2s**  |
| 1000        | 20              | 7.4s                   | 4.2s        | 4.2s     | **15.8s** |

创建、执行并销毁 1000 个沙箱耗时约 16 秒，其成本大致相当于一次主机冷启动（约 6 秒）在所有沙箱上的摊销——总计约 **$0.0009**（20 × `cpu-basic`），而若为每个沙箱创建一个 Job，则需约 $0.06 并引发 1000 个 VM 的调度突发。服务端的创建/执行/删除操作各耗时约 1 毫秒；预算完全由网络往返时间构成。

## 设计决策，回顾

| 决策                                                              | 为什么                                                                             |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| 基于任务构建，无需新服务                                         | 继承计费、硬件和权限；可在任意镜像中运行                     |
| 在启动时下载的静态 Rust 二进制文件                             | 无需 Python/pip；冷启动时间约 6 秒，而基于 pip 的引导过程需要 30–90 秒               |
| 手工编写的 HTTP/1.1                                                  | 最小化框架会缓冲分块响应，从而破坏实时流式传输（已验证） |
| 无状态 HMAC 身份验证                                                   | 可从任何位置重新连接；使用派生令牌而非 HF 令牌本身（按任务限定范围——参见 [令牌范围](#token-scope)） |
| `run()` 在退出码非零时抛出异常（设置 `check=False` 可禁用此行为）              | “运行代码，查看错误”循环的最佳开发体验（E2B 风格）                         |
| `idle_timeout` 看门狗，而非客户端清理                | 持久沙箱是一项功能；泄漏的沙箱仍会终止                       |
| Pools = uid + Landlock，服务器端权威容量，无本地状态 | 快速同用户扇出；并发下正确；可在任意位置重新连接        |

