# 沙盒

> [!NOTE]
> 沙箱是一项**实验性**功能。其 API、默认设置和行为可能会在不另行通知的情况下发生变更。共享沙箱旨在用于同一信任边界内的工作负载；其隔离机制并不能保证防范所有跨沙箱攻击。对于互不信任的工作负载，请使用专用沙箱，并避免向沙箱工作负载提供长期有效或权限范围过大的凭据。

沙箱（Sandbox）是一台可在数秒内启动的隔离云机器，您可以在其中运行命令并实时流式查看输出，也可以在其中传输文件——所有操作均可通过 Python 或 CLI 完成。沙箱构建于 [Jobs](./jobs) 之上：在底层，沙箱只是一个运行着小型服务器的 Job，该服务器通过 HTTP 暴露命令执行和文件传输功能。

当您需要在本机以外的环境中运行代码时，它们是一个很好的选择：

- **运行不受信任或 AI 生成的代码** — 允许智能体执行任意代码，但不授予其访问文件系统的权限。请为此使用一个*专用*沙箱：它是一个独立的虚拟机。
- **可复现的构建与实验** — 在干净、定义明确的镜像上运行，支持 CPU 或 GPU。
- **分发任务** — 以低成本启动数百个并行环境（RL 推演、评估、批量工具执行）。

任何包含 `/bin/sh` 的 Docker 镜像均可使用——无需预装 Python、pip 或 agent（启动时会注入一个小型静态服务器二进制文件）。

> [!TIP]
> 想知道其底层工作原理——任务内服务器、无状态身份验证，或单个 Job 如何托管多个隔离的沙箱？请参阅 [沙箱概念指南](../concepts/sandbox)。

## 两种沙盒

获取沙箱有两种方式。两种方式都会返回相同的 [Sandbox](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox) 对象（具有相同的 `run`、`files`、`connect`、`kill`）；区别仅在于底层机器的分配方式：

|            | [Sandbox.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.create) — **专用**              | [SandboxPool](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxPool) — **共享 / 池**                                                     |
| ---------- | ----------------------------------------------- | --------------------------------------------------------------------------------------- |
| 映射    | 一个 Job = **一个沙箱**（一台完整的虚拟机）          | 一个 Job = **多个沙箱**（一个 VM，打包）                                           |
| 隔离  | 完整虚拟机                                         | uid + [Landlock](https://docs.kernel.org/userspace-api/landlock.html)（同一用户信任） |
| 冷启动 | 每个沙箱约 6 秒                                 | 第一个主机约需 6 秒，之后每次约需 1 个往返时间                                         |
| cost       | 每个沙箱一个虚拟机                              | 每个 **host** 一个 VM，在多个沙箱之间分摊                                    |
| GPU        | ✅                                               | ❌ (仅限 CPU)                                                                            |
| 最适合   | 单个沙箱、GPU 工作负载、不受信任的代码 | 许多廉价的 CPU 沙箱（RL 展开、扇出）                                         |

经验法则：需要 GPU 或运行互不信任的代码 → 使用专用实例。需要数百个廉价的 CPU 沙箱 → 使用资源池。

## 快速入门

```python
>>> from huggingface_hub import Sandbox

>>> with Sandbox.create() as sbx:                      # ready in ~6s
...     result = sbx.run("python -c 'print(40 + 2)'")  # ~100ms per command
...     print(result.stdout)
42
```

选择任意图像和硬件[类型](./jobs#select-the-hardware)：

```python
>>> sbx = Sandbox.create(image="alpine:3.20")
>>> sbx = Sandbox.create(image="pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel", flavor="a10g-small")
```

## 运行命令

[Sandbox.run()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.run) 会执行命令并等待其完成。请传入 shell 字符串或 argv 列表：

```python
>>> sbx.run("pip install -q numpy")                       # string  → runs through /bin/sh -c
>>> sbx.run(["python", "-c", "import numpy; print(numpy.__version__)"])  # list → exec'd directly

# Live output streaming, plus env, cwd, timeout, stdin
>>> sbx.run("make -j4", cwd="/app", env={"CC": "gcc"}, timeout=600, on_stdout=print, on_stderr=print)
```

默认情况下，模式会根据类型推断（字符串会通过 `/bin/sh -c` 执行，列表则直接作为 argv 执行）。传入 `shell=` 可以显式指定模式——这有助于避免一个常见的陷阱：像 `["echo hi"]` 这样的单元素列表会被当作一个名为 `"echo hi"` 的程序来执行：

```python
>>> sbx.run("echo $HOME && ls | wc -l", shell=True)   # force the shell (pipes, globs, $VARS)
>>> sbx.run(["git", "commit", "-m", msg], shell=False)  # force argv (no quoting surprises)
```

`shell=True` 需要一个字符串，而 `shell=False` 需要一个列表；传入错误的类型会引发 `ValueError`。

以非零状态码退出的命令会引发 `SandboxCommandError`（附带 `stdout`、`stderr` 和 `exit_code`）。传入 `check=False` 即可返回 [SandboxCommandResult](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxCommandResult) 而不是引发异常：

```python
>>> result = sbx.run("test -f /tmp/missing", check=False)
>>> result.exit_code
1
```

### 后台进程

传入 `background=True` 以启动长时间运行的进程（服务器、监视器、训练任务），而无需等待其完成。`run` 会立即返回一个 [SandboxProcess](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxProcess)，而不是 [SandboxCommandResult](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxCommandResult)：

```python
>>> proc = sbx.run("python -m http.server 8000", background=True)
>>> proc
SandboxProcess(pid=1234, cmd='python -m http.server 8000', tag=None, started_at_ms=1700000000000, running=True, exit_code=None)
```

列出沙箱中的进程，并在完成后停止其中一个。已完成的进程会保留在列表中（带有 `running=False` 及其 `exit_code`），直到沙箱被删除，因此您可以判断某个进程是否仍在运行或已经退出：

```python
>>> sbx.processes()
[SandboxProcess(pid=1234, cmd='python -m http.server 8000', tag=None, started_at_ms=1700000000000, running=True, exit_code=None)]
>>> proc.kill()
```

流式/仅等待选项（`timeout`、`stdin`、`on_stdout`、`on_stderr`、`check`）在后台模式下不适用——只有 `env`、`cwd` 和 `shell` 会被处理。

## 文件

```python
>>> sbx.files.write("/app/script.py", "print('hi')")     # str | bytes | file-like
>>> sbx.files.read_text("/app/script.py")
"print('hi')"
>>> sbx.files.upload("local_data.csv", "/data/data.csv")  # local -> sandbox
>>> sbx.files.download("/data/results.bin", "results.bin")  # sandbox -> local
>>> sbx.files.list("/data")
[FileEntry(name='data.csv', path='/data/data.csv', type='file', size=5324, ...)]
```

其他辅助函数：`stat`、`exists`、`mkdir`、`delete`。

## 访问沙箱内的服务器

在沙箱中（在后台）启动一个服务器，然后从外部通过 [Sandbox.proxy_url_for()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.proxy_url_for) 访问它——请求由作业内的沙箱服务器转发到您的内部服务器，因此无需暴露额外的公共端口。它支持纯 HTTP、Server-Sent Events 和 WebSocket。将该 URL 与 `Sandbox.proxy_headers` 配合使用以实现身份验证（您的 WebSocket/HTTP 客户端必须发送这些请求头）：

```python
>>> import httpx2
>>> with Sandbox.create() as sbx:
...     sbx.files.write("app.py", "...")  # a server exposing e.g. /hello and /ws
...     sbx.run("uvicorn app:app --host 127.0.0.1 --port 8000", background=True)
...     # plain HTTP
...     r = httpx2.get(sbx.proxy_url_for(8000, "/hello"), headers=sbx.proxy_headers)
...     # WebSocket: ask for a wss:// URL
...     ws_url = sbx.proxy_url_for(8000, "/ws", scheme="wss://")
```

使用 `proxy_headers` 时，请在 HTTP 客户端中禁用重定向（httpx 中为 `follow_redirects=False`，requests 中为 `allow_redirects=False`）。应用程序可能会重定向到其他源，而这些自定义身份验证标头不应被转发到该源。

Pooled sandboxes 需要一个返回每个沙箱 capability token 的服务器。缺少 token 视为错误；应升级并回收该主机，而不是使用其管理 token。

内部服务器必须如何监听取决于沙箱类型：

- **专用**（[Sandbox.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.create)）：在 `127.0.0.1:<port>` 上绑定一个普通的 TCP 端口。
- **池化 / 共享**（[SandboxPool](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxPool)）：池化沙箱无法绑定 TCP 端口（Landlock），因此请在 `$SBX_PROXY_DIR/<port>.sock` 处监听 **unix socket**（该环境变量在每个沙箱中均已设置），例如 `uvicorn app:app --uds $SBX_PROXY_DIR/8000.sock`。客户端（`proxy_url_for` / `proxy_headers`）在两种情况下完全相同。

## 生命周期

沙箱的生命周期长于创建它的进程——您可以现在创建它，之后从任何持有相同 HF token 的机器重新连接，无需复制任何状态：

```python
>>> sbx = Sandbox.create()
>>> sbx.id
'687f911eaea852de79c4a50a'

# Later, from anywhere:
>>> sbx = Sandbox.connect("687f911eaea852de79c4a50a")
>>> sbx.kill()           # terminate now
```

- `idle_timeout`（默认 10 分钟）是关键所在：当没有 API 调用且没有进程运行时，它会关闭沙箱，从而停止对闲置沙箱的计费。您可以在创建时设置（`Sandbox.create(idle_timeout="30m")`），或传入 `None` 以禁用该功能。
- 该任务还设有一个固定的 24 小时最大生命周期，作为硬性兜底限制（不可配置）。
- `forward_hf_token=True` 会将你的 HF token 显式暴露给在沙箱中运行的代码（作为 `HF_TOKEN`）。即使设置了 `forward_hf_token=False`，也不要将沙箱视为凭据的硬性边界——请参阅本页顶部的警告。

## 同时使用多个沙箱：SandboxPool

当您需要大量沙箱（例如并行 RL 推演、扇出评估、批量工具执行）时，为每个沙箱单独创建一个 Job 会造成资源浪费：每个沙箱都需要经历完整的虚拟机冷启动，并且会占用整台机器，而该工作负载实际上仅需几 MB 的内存。[SandboxPool](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxPool) 将许多轻量级沙箱打包到少数共享的主机 Job 中——一台计费的虚拟机即可服务数十个沙箱，因此每个沙箱的成本按此比例降低，且每个沙箱的冷启动时间约为一次网络往返。

```python
>>> from huggingface_hub import SandboxPool

>>> with SandboxPool(image="python:3.12", flavor="cpu-basic", warm_up=2) as pool:
...     boxes = [pool.create() for _ in range(100)]   # packed across the 2 warm host VMs
...     print(boxes[0].run("echo hi").stdout)          # each box is a normal Sandbox
hi
```

每个 `create()` 都会返回一个完整的 [Sandbox](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox)；可以反复调用它以实现扇出。该池会按需启动主机上的 Job，每台主机打包 `sandboxes_per_host` 个沙箱，并在 `close()` 时（或当主机空闲时，作为计费兜底）终止所有任务。典型的扇出模式如下：

```python
>>> from concurrent.futures import ThreadPoolExecutor
>>> with SandboxPool(image="python:3.12", warm_up=4) as pool:
...     boxes = [pool.create() for _ in tasks]
...     with ThreadPoolExecutor(32) as ex:
...         outputs = list(ex.map(lambda b, t: b.run(t.cmd).stdout, boxes, tasks))
```

Env 和 `idle_timeout` 是按沙箱设置的（它们属于 `create()`，而非池），因此同一个池中的沙箱可以拥有不同的环境：

```python
>>> sbx = pool.create(env={"SEED": "42"}, idle_timeout="5m", forward_hf_token=True)
```

池化沙箱是一个完整的 [Sandbox](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox)，但部分输入由宿主固定，而非针对每个沙箱单独设置——因此 [SandboxPool.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxPool.create) 接受的参数比 [Sandbox.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.create) 更少：

| 输入              | `Sandbox.create`（专用） | `SandboxPool.create`（池化）            |
| ------------------ | ---------------------------- | ---------------------------------------- |
| `image`, `flavor`  | 每个沙箱                  | 由池固定（在主机上设置一次） |
| `volumes`          | 每个沙箱                  | **不可用**（在主机启动时挂载） |
| `env`              | 每个沙箱                  | 每个沙箱                              |
| `idle_timeout`     | 每个沙箱                  | 每个沙箱                              |
| `forward_hf_token` | 每个沙箱                  | 每个沙箱                              |

Pooled sandboxes share a long-lived host job, so there's no encrypted-Job-secrets channel like dedicated sandboxes get. But a pooled sandbox's `env` is delivered to the host server at sandbox creation (not at job start), so it's never stored in any job's metadata — pass would-be secrets as plain `env`.

### 按需增长和预热

`pool.create()` 每次创建一个沙箱，在启动新主机之前会优先复用仍有空闲容量的主机——因此您可以在工作到达时随时创建沙箱，它们会自动打包到已预热的主机上：

```python
>>> pool = SandboxPool(image="python:3.12", flavor="cpu-basic")
>>> sbx = pool.create()    # boots the first host (~6s)
>>> sbx = pool.create()    # packs onto the same warm host (~one round-trip, no new VM)
```

为避免前几次调用时出现主机冷启动，请预先配置主机：使用 `warm_up=N`（在首次 `create()` 时启动）或提前调用 `pool.warm(N)`。

### 跨进程和跨机器复用池

Warm 主机通过作业标签发现，因此复用可以跨进程生效：一个全新的 `SandboxPool`，只要具有相同的 `image`/`flavor`/`name`，就会连接到先前运行遗留下来的主机，而不是自行启动新的主机。传入 `name=` 可确保不同的池不会共享主机。

若要从另一台没有本地状态的机器重新连接，请使用池 ID 通过 [SandboxPool.connect()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.SandboxPool.connect) 重新连接——它会找到正在运行的主机，从该主机作业中重建池的配置（镜像、规格、打包密度），并准备好 `create()` 更多实例：

```python
>>> pool = SandboxPool.connect("pool-ae9f7efe0bc7")   # from anywhere, no config needed
>>> sbx = pool.create()
```

通过 `connect()` 连接的池并不拥有共享的主机（其他客户端可能正在使用它们），因此——与 [Sandbox.connect()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.connect) 类似——离开其 `with` 块（或调用 `close()`）只会释放本地的 HTTP 客户端，而主机仍会继续运行。请使用 `pool delete` / `hf sandbox pool delete <id>` 显式终止池的主机。

> [!NOTE]
> 主机内的沙箱通过不同的 uid 和每个沙箱独立的 Landlock 规则集进行隔离。共享沙箱旨在用于*同一用户自己的*并行工作负载，并不保证能抵御所有跨沙箱攻击。对于互不信任的代码，或涉及 GPU 的场景，请使用 [Sandbox.create()](/docs/huggingface_hub/v2.4.0/zh/package_reference/sandbox#huggingface_hub.Sandbox.create)（每个沙箱对应一个独立的虚拟机）。相关权衡详见概念指南：[隔离机制的工作原理](../concepts/sandbox#isolation-in-a-pool-uid--landlock)，以及——如果您正在两种模式之间做选择——[已知限制](../concepts/sandbox#known-limitations)的完整列表。

## 从 CLI

`hf sandbox` 命令与 Python API 保持一致。专用沙箱：

```bash
>>> hf sandbox create
✓ Sandbox ready id=687f911eaea852de79c4a50a image=python:3.12 elapsed=6.0s

# Attach labels to the underlying Job
>>> hf sandbox create --label controller-run=run-42 --label team=data-infra

>>> hf sandbox exec 687f911eaea852de79c4a50a -- python -c "print('hi')"
hi

>>> hf sandbox cp data.csv 687f911eaea852de79c4a50a:/data/data.csv
>>> hf sandbox kill 687f911eaea852de79c4a50a
```

`hf sandbox exec` 会实时流式输出，并以该命令的退出码退出，因此可在脚本中组合使用：

```bash
hf sandbox exec $ID -- pytest && echo "tests passed"
```

使用 `hf sandbox spawn` 在后台启动一个长时间运行的进程（会打印其 pid），然后列出或停止进程。列表会显示每个进程的状态（`running` 或 `exited (<code>)`）：

```bash
>>> hf sandbox spawn $ID -- python -m http.server 8000
✓ Process started sandbox=687f... pid=1234

>>> hf sandbox process ls $ID
pid   status   cmd
1234  running  python -m http.server 8000

>>> hf sandbox process kill $ID 1234
```

对于许多廉价的共享沙箱，可以先预热一个池子，然后按需在其中创建：

```bash
# Warm a pool -> prints a pool id (billing starts: a host VM is now running)
>>> hf sandbox pool create python:3.12 --flavor cpu-basic
✓ Pool created id=pool-ae9f7efe0bc7 image=python:3.12 flavor=cpu-basic host=687f... elapsed=5.7s

# Each create packs onto a host with room (found by the pool id, from any machine);
# only when every host is full does it boot a duplicate. Env is per-sandbox (pooled
# sandboxes share a host, so there's no encrypted-secrets channel — use --env).
>>> hf sandbox create --pool pool-ae9f7efe0bc7 --env LOG_LEVEL=debug
>>> hf sandbox create --pool pool-ae9f7efe0bc7

>>> hf sandbox pool ls
>>> hf sandbox pool delete pool-ae9f7efe0bc7    # terminate the pool's hosts (and their sandboxes)
```

`hf sandbox create --pool` 会创建一个共享沙箱；其 id 形如 `<host_job_id>.<local_id>`，并且可以在任何使用专用 id 的地方使用（`exec`、`cp`、`kill`）。池没有本地状态——它只是其正在运行的主机 VM，通过池 id 查找——因此它可以从任何机器访问，并且一旦其所有主机都不复存在（被终止或因空闲超时），它便不再存在。

