# Sandboxes bên trong

Hướng dẫn này giải thích cách [Sandboxes](../guides/sandbox) hoạt động bên trong, lý do chúng được xây dựng theo cách đó và những hạn chế của chúng. Nếu bạn chỉ muốn sử dụng sandboxes, [Hướng dẫn Sandboxes](../guides/sandbox) là đủ; hãy tiếp tục đọc nếu bạn muốn hiểu cơ chế, đánh giá mô hình tin cậy hoặc gỡ lỗi một vấn đề nào đó.

> [!NOTE]
> API Sandbox mang tính thử nghiệm. API và hành vi của nó có thể thay đổi mà không cần thông báo trước.

## Không có "dịch vụ sandbox"

Điều đầu tiên cần hiểu là không có backend sandbox riêng biệt. Một sandbox đơn giản chỉ là một [HF Job](../guides/jobs) (một VM) chạy một binary tĩnh nhỏ duy nhất `sbx-server` giao tiếp qua HTTP. Client nói chuyện với server đó thông qua Jobs proxy (URL `*.hf.jobs` mà Job công bố). Mọi thứ khác — xác thực, khám phá, đóng gói nhiều sandbox vào một Job — đều được xây dựng từ các nguyên thủy Jobs hiện có: labels, biến môi trường và secrets.

```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
```

Thiết kế “không cần hạ tầng mới” này là lý do một sandbox hoạt động trong bất kỳ Docker image nào và kế thừa miễn phí hóa đơn, các loại phần cứng và quyền namespace của Jobs.

### Khởi tạo máy chủ

Khi khởi động job, lệnh của Job là một script `/bin/sh -c` nhỏ, có nhiệm vụ tải binary `sbx-server`, cấp quyền thực thi cho nó và `exec` nó. Binary này là bản build tĩnh [musl](https://musl.libc.org/) cỡ ~640KB, không có bất kỳ phụ thuộc nào lúc chạy, nên có thể chạy trên bất kỳ image Linux `x86_64` nào.

Một vài quyết định đáng lưu ý:

- **Tải xuống, với phương án dự phòng là mount.** Lối tắt tải binary từ HF CDN bằng `wget` hoặc `curl` (mọi base image phổ biến đều có sẵn một trong hai), nhanh và miễn phí. Để an toàn, Hub repo của server cũng được mount dưới dạng volume trên mọi job: nếu image không có cả `wget` lẫn `curl`, script sẽ copy binary từ mount đó thay thế. Mount là trong suốt — không tốn chi phí trừ khi thực sự đọc — nhưng việc đọc nó làm tăng ~2-3s cho cold start, vì vậy đây không phải là mặc định. Yêu cầu duy nhất bắt buộc đối với image là `/bin/sh`.
- **Một máy chủ HTTP/1.1 tự viết, không dùng framework.** Việc phát trực tiếp đầu ra yêu cầu phải xả (flush) từng khối (chunk) ngay khi nó được tạo ra. Các máy chủ HTTP Rust tối giản phổ biến (ví dụ `tiny_http`) sẽ đệm (buffer) các phản hồi dạng chunked cho đến khi phản hồi hoàn tất, điều này làm hỏng việc phát trực tiếp. Do đó, máy chủ này tự triển khai HTTP/1.1: các luồng sự kiện NDJSON cho `exec`, thân (body) thô cho tệp, và một thao tác xả rõ ràng cho từng khối.
- **Cổng 49983.** Máy chủ lắng nghe trên một cổng ít phổ biến một cách có chủ đích để các cổng phát triển thông dụng (3000, 8000, 8080, …) được giữ trống cho mã của bạn.

> [!TIP]
> Máy chủ có mã nguồn mở tại [github.com/huggingface/sandbox-server](https://github.com/huggingface/sandbox-server).

## Xác thực không có trạng thái

Hai lớp độc lập bảo vệ một sandbox:

1. **Cổng proxy.** Proxy của Jobs chỉ chuyển tiếp các yêu cầu mang token HF có quyền đọc trong namespace của job. Một thành viên ngẫu nhiên của internet không thể truy cập được URL.
2. **Cổng ứng dụng.** `sbx-server` kiểm tra thêm tiêu đề `X-Sandbox-Token` trên mọi yêu cầu ngoại trừ `/health`. Đây là phòng thủ nhiều lớp: thành viên namespace chỉ đọc có thể truy cập proxy nhưng vẫn không thể thực thi lệnh.

Token sandbox được suy ra, không được lưu trữ:

```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 là công khai và khóa HMAC chính là token HF của bạn, do đó bất kỳ máy nào nắm giữ token đó đều có thể tính lại token sandbox — đó chính là điều khiến việc kết nối lại trở nên không trạng thái. Điều này cũng có nghĩa là token là một *quyền năng* được suy ra từ thông tin xác thực của bạn, chứ không phải là một bước kiểm tra danh tính của người gọi: máy chủ xác minh rằng người gọi biết token, chứ không xác minh họ là ai.

Mỗi tác vụ sandbox cũng mang hai nhãn ổn định để phục vụ việc tìm kiếm — `hf-sandbox=1` (trên tất cả các tác vụ) và `hf-sandbox-mode=dedicated` hoặc `hf-sandbox-mode=pool` — để bạn có thể liệt kê hoặc lọc chúng phía máy chủ, ví dụ `hf jobs ps --label hf-sandbox=1`.

Token được chuyển đến máy chủ thông qua một Job secret. Client sẽ tự suy ra lại token này khi cần từ public nonce trong label. Điều này mang lại một số hệ quả tích cực:

- **Kết nối lại không trạng thái.** `Sandbox.connect(id)` hoạt động từ bất kỳ máy nào giữ cùng một token HF — đọc nonce từ nhãn, tính lại token. Không có tệp cục bộ, không có trạng thái để sao chép.
- **Token HF không được truyền vào sandbox dưới dạng biến môi trường hoặc bí mật của job** (trừ khi bạn chọn tham gia bằng `forward_hf_token=True`). Đây không phải là đảm bảo tuyệt đối rằng thông tin xác thực của bạn sẽ không bị truy cập: tiến trình lắng nghe trên cổng của sandbox chính là tiến trình được image khởi chạy đầu tiên, do đó một image không đáng tin cậy có thể quan sát được các yêu cầu mà client gửi đi — bao gồm cả header `Authorization` của chúng. Hãy coi các thông tin xác thực có thể truy cập được từ sandbox là có khả năng bị phơi bày cho sandbox.

### Phạm vi token

Một nonce được tạo cho mỗi **Job**, do đó token dẫn xuất là thông tin xác thực của *job*. Ở chế độ dedicated, job chính là sandbox, nên hai khái niệm này trùng nhau. Ở chế độ pool, job là host, và một host chứa nhiều sandbox — do đó một sandbox trong pool sẽ có thêm một thông tin xác thực riêng, hẹp hơn:

| credential | được suy ra như thế nào | cấp quyền |
| --- | --- | --- |
| **token sandbox chuyên dụng** | `HMAC(hf_token, nonce)` từ nhãn của tác vụ | sandbox đó (đó là toàn bộ công việc) |
| **token máy chủ pool** | `HMAC(hf_token, nonce)` từ nhãn của công việc host | quản lý pool: tạo, liệt kê và xóa các sandbox trên host đó, và khôi phục token của chúng |
| **token sandbox đã gộp** | 256 bit ngẫu nhiên, được tạo bởi máy chủ host cho mỗi sandbox | một sandbox đó — không phải sandbox anh em, không phải pool |

Client tự động sử dụng phiên bản hẹp: `pool.create()` nhận nó trong phản hồi tạo, `Sandbox.connect("<host>.<id>")` khôi phục nó bằng token của host, và `proxy_headers` cấp phát token của sandbox thay vì token của host (giải quyết HF bearer tại thời điểm bạn đọc nó, để một handle tồn tại lâu không cấp phát token đã hết hạn) — các header này thường kết thúc trong trình duyệt hoặc client WebSocket, vì vậy chúng chỉ nên cấp quyền truy cập vào một sandbox và không gì khác.

Điều này có nghĩa là gì đối với một vụ rò rỉ: một **token sandbox được pooling** sẽ làm tổn hại sandbox đó. Một **token host** sẽ làm tổn hại host — mọi sandbox trên nó, hiện tại và tương lai, cùng các tuyến quản lý của nó — vì vậy hãy coi nó như thông tin xác thực quản trị viên của pool. Các thành viên trong namespace của bạn giữ một token HF khác và không thể suy ra token của bạn, nhưng xem [Hạn chế đã biết](#known-limitations) để biết cách một token có thể được *gửi* đến sai nơi.

Client yêu cầu một token phạm vi sandbox không rỗng cho các thao tác pooled và từ chối các host không cung cấp token này. Client không bao giờ thay thế bằng thông tin xác thực quản lý host. Hãy nâng cấp client và tái tạo các host pool cũ trước khi sử dụng API này.

## Các sandbox chuyên dụng (`Sandbox.create`)

Mô hình đơn giản: **mỗi sandbox một Job**. Job là một VM thực, nên cách này mang lại mức cách ly mạnh nhất (cấp độ VM), hỗ trợ mọi loại phần cứng bao gồm cả GPU, và là lựa chọn phù hợp cho các mã không tin cậy lẫn nhau. Các API routes nằm tại `/v1/*` và đường dẫn file là tuyệt đối trên hệ thống tệp của container. `kill()` đơn giản là hủy Job.

Ranh giới mà một sandbox chuyên dụng mang lại cho bạn là giữa **Job và mọi thứ bên ngoài nó**. Bên trong VM, mã của bạn và `sbx-server` đều chạy với quyền root trong cùng một container: `sbx-server` không sandbox lệnh mà nó thực thi cho bạn, nó chỉ phơi bày lệnh đó qua HTTP. Vì vậy, một khi mã mà bạn không tin tưởng đã được thực thi trong một sandbox chuyên dụng, hãy coi VM đó — bao gồm cả control plane của nó — là thuộc về mã đó: nó có thể giết hoặc thay thế server, và bất kỳ thứ gì bạn thực hiện trong sandbox đó sau đó đều nằm dưới sự kiểm soát của nó. Hãy sử dụng một sandbox chuyên dụng cho mỗi workload không tin tưởng và `kill()` nó khi hoàn thành, thay vì tái sử dụng một sandbox xuyên qua các ranh giới tin cậy.

```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)
```

Chi phí thể hiện rõ trong sơ đồ: mỗi sandbox phải chịu thời gian khởi động lạnh VM đầy đủ khoảng ~6s và tính phí cho cả một máy. Đối với một sandbox đơn lẻ hoặc tác vụ GPU, đó chính xác là điều bạn cần. Đối với 100–1000 tác vụ CPU ngắn, điều này gây lãng phí — và đó là mục đích của các pool.

## Pools: nhiều sandbox trong một Job (`SandboxPool`)

> [!WARNING]
> **Pools dành cho các tác vụ nằm trong cùng một ranh giới tin cậy.** Một sandbox dạng pool là một uid + một bộ quy tắc Landlock bên trong một VM dùng chung, không phải một VM riêng, và host của nó chạy một mặt phẳng điều khiển có đặc quyền mà tất cả các sandbox của nó đều giao tiếp. Sử dụng pools để triển khai *mã của chính bạn* một cách tiết kiệm. Đối với các tác vụ không tin tưởng lẫn nhau — bất kỳ thứ gì mà bạn không cho phép đọc tệp của các sandbox khác của bạn — hãy dùng [Sandbox.create()](/docs/huggingface_hub/v2.4.0/vi/package_reference/sandbox#huggingface_hub.Sandbox.create), nó cấp cho mỗi tác vụ một VM riêng. Xem [Hạn chế đã biết](#known-limitations) để biết các khoảng trống cụ thể.

Một lượt rollout RL hoặc sandbox thực thi công cụ điển hình chỉ cần vài MB RAM và một lõi CPU trong vài giây. Việc trả tiền cho một VM 2 vCPU và chịu thời gian khởi động lạnh 6 giây cho mỗi lượt — đồng thời kích hoạt một đợt lập lịch 1000 VM — là một sự đánh đổi không hợp lý. Vì vậy, [SandboxPool](/docs/huggingface_hub/v2.4.0/vi/package_reference/sandbox#huggingface_hub.SandboxPool) chạy một Job như một host và đa hợp nhiều sandbox bên trong nó.

Một sandbox được chia sẻ không phải là một VM hoặc container lồng nhau. Nó là nguyên thủy đa người dùng cổ điển của Unix:

- một **uid riêng** (≥ 20000),
- một **thư mục home riêng tư `0700`** thuộc sở hữu của uid đó,
- các lệnh `exec` được thực thi với uid đó trong một **môi trường đã được làm sạch** (`env_clear`, để các bí mật của host không bao giờ bị rò rỉ), `NO_NEW_PRIVS`, **rlimits** cho từng tiến trình và một **bộ quy tắc Landlock** cho từng sandbox.

Do đó, việc tạo một sandbox là `mkdir + chown + build ruleset` ≈ 1ms phía server — không cần khởi động VM thứ hai. Độ trễ duy nhất mà client nhìn thấy là vòng lặp qua proxy.

```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
```

Public id của một pooled sandbox là `<host_job_id>.<local_id>`, do đó `connect`/`exec`/`kill` hoạt động không trạng thái giống như các sandbox dedicated. `kill()` trên một pooled sandbox gửi `DELETE` đến host của nó (giải phóng một slot); host vẫn tiếp tục chạy.

### Cách ly trong một pool: uid + Landlock

Đây là cốt lõi của thiết kế pool, vì vậy cần chính xác về những gì được và không được cách ly.

Một Job mặc định chạy với quyền root bên trong một user namespace chỉ ánh xạ các uid từ 0 đến 65535, có bật bộ lọc seccomp và không có `CAP_SYS_ADMIN` / `CAP_NET_ADMIN` / `CAP_NET_RAW`. Điều này loại trừ các công cụ cách ly nặng nề thông thường: không có namespace lồng nhau, không có mount mới, không có ủy quyền cgroup (`unshare`, `mount`, ghi vào `/sys/fs/cgroup/...` đều thất bại). Điều mà kernel cung cấp là [**Landlock**](https://docs.kernel.org/userspace-api/landlock.html) (ABI 6), một Linux Security Module cho phép bất kỳ tiến trình không đặc quyền nào tự giới hạn bản thân và các tiến trình con của nó — chính xác là ranh giới cho từng sandbox mà chúng ta cần. Đối với mỗi sandbox, server xây dựng một bộ quy tắc; tiến trình con exec hạ xuống uid/gid của sandbox và áp dụng `NO_NEW_PRIVS`, `landlock_restrict_self`, cùng rlimits trước khi chạy lệnh.

Việc kết hợp các uid riêng biệt (kiểm soát truy cập tùy chọn) với Landlock được thiết kế và kiểm thử để cung cấp:

- ✅ A không thể đọc `environ` của một tiến trình khác, ngăn chặn việc truy cập trực tiếp vào các token HF và sandbox giữa các sandbox.
- ✅ A không thể `SIGKILL` / `ptrace` / đọc bộ nhớ của các tiến trình của B, `setuid` vào B, hoặc đọc thư mục home `0700` của B.
- ✅ Truy cập `/tmp` và `/dev/shm` bị từ chối — mỗi sandbox được giới hạn bởi Landlock trong thư mục home riêng của nó (`TMPDIR` của nó trỏ vào bên trong `$HOME`).
- ✅ A không thể `bind` một cổng TCP, do đó không có dịch vụ localhost giữa các sandbox (vẫn cho phép `connect` ra ngoài, nên internet hoạt động bình thường).
- ✅ Các socket unix trừu tượng xuyên sandbox bị chặn (`LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET`; chỉ cách ly uid không *chặn* được chúng).

Các điều khiển này ràng buộc khối lượng công việc. Mặt phẳng điều khiển gốc vẫn là một ranh giới tin cậy riêng biệt: các thao tác tệp ở chế độ host của nó từ chối các liên kết mềm, bộ đại diện socket của nó cố định inode của socket và kiểm tra uid của đối tác, và các tuyến đường riêng biệt không khả dụng ở chế độ host. Các kênh chia sẻ còn lại được mô tả trong [Hạn chế đã biết](#known-limitations).

> [!NOTE]
> **Tại sao đây không phải là sự thay thế cho VM.** Landlock và cách ly uid được thiết kế cho các tác vụ nằm trong cùng một ranh giới tin cậy. Vì các sandbox trong pool chia sẻ một kernel, một VM và một control plane, nên không thể đảm bảo bảo vệ khỏi mọi cuộc tấn công xuyên sandbox; tài nguyên và một số metadata của danh sách tiến trình cũng vẫn được chia sẻ. Đối với các mã không tin cậy lẫn nhau, hoặc cho GPU, hãy sử dụng [Sandbox.create()](/docs/huggingface_hub/v2.4.0/vi/package_reference/sandbox#huggingface_hub.Sandbox.create), cho phép mỗi sandbox có VM riêng của nó.

### Tệp mô hình trong một pool

Vì vùng ghi duy nhất của sandbox được pool là thư mục home bị giới hạn bởi Landlock (đồng thời cũng là thư mục làm việc mặc định), API tệp gốc mọi đường dẫn tại thư mục home đó: `files.write("data/in.txt", ...)` ghi vào `$HOME/data/in.txt`, một `/` đứng đầu được hiểu là tương đối so với thư mục home, và `..` không thể thoát ra ngoài. Các tệp được ghi thông qua API sẽ được `chown` cho uid của sandbox để chính mã của sandbox có thể đọc chúng. Điều này tạo ra mô hình “hệ thống tệp có gốc tại sandbox” sạch sẽ, khớp chính xác với những gì mã bên trong sandbox có thể chạm tới — và khác với các sandbox chuyên dụng, nơi các đường dẫn là tuyệt đối trên hệ thống tệp của container.

Các đường dẫn được chuẩn hóa theo từ điển, sau đó được phân giải tương đối so với bộ mô tả thư mục home đang mở, với việc từ chối symlink trên mọi thành phần. API tệp không theo dõi symlink, bao gồm cả các liên kết nằm trong cùng một thư mục home; mã sandbox vẫn có thể sử dụng chúng dưới các quy tắc Landlock của nó.

### Pools không có trạng thái cục bộ chính thức

Pool được thiết kế để không phải là một tệp cấu hình cục bộ. Một pool chính là tập hợp các Job đang chạy trên các host, tất cả đều chia sẻ nhãn `hf-sandbox-pool=<id>`. Điều này giúp các pool nhất quán với phần còn lại của API sandbox (mọi thứ đều có thể được phát hiện từ nhãn và gắn lại từ bất kỳ máy nào), đồng nghĩa với việc một pool đơn giản là ngừng tồn tại ngay khi host cuối cùng của nó biến mất.

- Một host mang cấu hình của pool (image, flavor, `sandboxes_per_host`, thời gian chờ khi nhàn rỗi) trong các biến môi trường của job — nhãn chỉ được dùng để lọc. Khi một client phải khởi động một host trùng lặp, nó đọc lại cấu hình đó từ một host đang chạy (`inspect_job`), nhờ đó tất cả các host trong một pool luôn nhất quán mà không cần một bản ghi tập trung.
- Env và secrets được cấp theo từng sandbox, được truyền vào tại thời điểm tạo — không bao giờ ở cấp pool. Môi trường của một sandbox trong pool được giữ trong bộ nhớ của máy chủ host trong suốt vòng đời của sandbox (nó được áp dụng lại cho mọi lệnh bạn chạy); nó không bao giờ được ghi xuống đĩa và không bao giờ xuất hiện trong metadata của bất kỳ Job nào. Không có kênh secrets mã hóa cho các sandbox trong pool — hãy dùng `env` và coi các giá trị đó là “không lưu trữ tại chỗ, nhưng cũng không được mã hóa”. Đối với các giá trị cần kho lưu trữ mã hóa, hãy dùng `secrets` của một sandbox chuyên dụng.
- Dung lượng do máy chủ quyết định. Máy chủ từ chối các yêu cầu tạo vượt quá `sandboxes_per_host` (trả về `{"rejected": N}`); client sẽ đóng gói phần vượt sang một máy chủ khác hoặc khởi động một bản sao. Điều này giúp việc đóng gói luôn chính xác ngay cả khi nhiều tiến trình cùng tạo vào cùng một pool đồng thời.
- Việc loại bỏ khi nhàn rỗi diễn ra theo hai cấp. Mỗi sandbox sẽ bị loại bỏ sau khoảng thời gian `idle_timeout` không hoạt động của riêng nó (trừ khi vẫn còn một tiến trình đang chạy); một khi host không còn sandbox nào trong khoảng thời gian nhàn rỗi của host, nó sẽ tự tắt — đây là biện pháp dự phòng về hóa đơn ngay cả khi mọi client biến mất.

### Bộ nhớ đệm cố gắng tối đa giúp `create --pool` chạy nhanh

Việc không có trạng thái cục bộ chính thức là rất tốt cho tính chính xác nhưng lại gây ra độ trễ. Một lệnh `hf sandbox create --pool <id>` lạnh (một tiến trình CLI mới) nếu không sẽ phải khám phá lại mọi thứ qua mạng trước khi có thể tạo sandbox: `list_jobs` để quét namespace → `inspect_job` từng host để xây dựng lại URL và nonce của nó → `GET /v1/sandboxes` để xem mức độ đầy của từng sandbox → cuối cùng là `POST` để tạo. Nhiều vòng lặp chỉ là chi phí thừa, trong mỗi lần gọi.

Một bộ nhớ đệm theo kiểu nỗ lực tối đa tại `$HF_HOME/sandbox/pools/<context>/<pool-id>.json` sẽ loại bỏ điều đó. Sau bất kỳ thao tác tạo/nóng nào, một tiến trình sẽ ghi lại cấu hình pool cùng, cho từng host, URL proxy, mã xác thực ngẫu nhiên (auth nonce) và các slot trống được nhìn thấy gần nhất. Tiến trình tiếp theo sẽ xây dựng lại kết nối host trực tiếp từ tệp (không qua HTTP) và chuyển thẳng đến `POST`.

`<context>` là bản tóm tắt của endpoint, credential và namespace mà entry được ghi cho — credential dưới dạng fingerprint; chính token của bạn không bao giờ được ghi vào cache. Cả tên thư mục và tệp đều chứa thông tin này, và một thao tác đọc không khớp trên cả ba yếu tố sẽ là một cache miss thông thường, do đó một người dùng khác của máy, một endpoint khác, hoặc một `connect(pool, namespace="other-org")` sẽ nhận đường dẫn cold thay vì các host đã được cache cho mục đích khác. Tệp được tạo với quyền `0600`, thư mục với quyền `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"]
```

Cache được tin cậy và không được tin cậy cho những gì:

- **Không có thẩm quyền về sức chứa.** Máy chủ trong job vẫn giữ thẩm quyền, nên một số lượng `live` cũ chỉ gây ra một yêu cầu bị lãng phí, chứ không bao giờ dẫn đến việc đóng gói sai host.
- **Không đáng tin cậy về vị trí của host.** Một URL được cache chỉ được sử dụng nếu nó là URL jobs-proxy HTTPS của job mà cùng entry đó chỉ định, do đó một tệp cache không thể chọn đích mà token bearer HF và token sandbox của bạn được gửi đến; một entry chỉ định bất kỳ thứ gì khác sẽ bị loại khỏi tệp. Sau 15 phút, một entry cũng không còn được tính dựa trên chính nó nữa: `inspect_job` phải xác nhận rằng job vẫn đang chạy, vẫn được gắn nhãn cho pool này và vẫn ở trên URL đó trước khi transport được xây dựng.
- **Không đáng tin cậy về hình dạng.** Kiểu và phạm vi được kiểm tra khi đọc, nên một tệp được chỉnh sửa thủ công, bị cắt cụt hoặc có kiểu chuỗi sẽ là một lần bỏ lỡ bộ nhớ đệm thay vì một lỗi xảy ra giữa chừng `create()`.
- **Tự phục hồi.** Một host được cache đã biến mất sẽ bị loại bỏ ở yêu cầu thất bại đầu tiên và được cắt bỏ khỏi tệp; thao tác tạo sẽ tự động chuyển sang khám phá nhãn.
- **An toàn với đa luồng.** Các thao tác ghi được hợp nhất dưới một khóa tệp (dựa trên `job_id`) và cam kết một cách nguyên tử, do đó các tiến trình `create` chạy song song sẽ không ghi đè lên nhau và người đọc sẽ không bao giờ thấy tệp bị ghi dở.
- **Dùng một lần.** Xóa nó và bạn sẽ gặp lỗi cache: đường dẫn lạnh (cold path) sẽ chạy và mọi thứ vẫn hoạt động bình thường. Nó không bao giờ được chia sẻ giữa các máy.

Có hai điều cần lưu ý. Hãy coi `$HF_HOME` là nhạy cảm: một mục nhập vẫn là một URL host có thể tái sử dụng cùng với nonce công khai mà token của nó được suy ra từ đó, và trong khi một tệp được ghi dưới một thông tin xác thực khác sẽ bị bỏ qua, một tệp được ghi dưới *thông tin xác thực của bạn* sẽ được ghi nhận cho cửa sổ 15 phút nêu trên. Và vì namespace là một phần của khóa, một pool được cache dưới một `namespace=` tường minh chỉ được tìm thấy lại khi cùng một `namespace=` được truyền vào — `hf sandbox create --pool <id>` yêu cầu cùng một `--namespace` mà pool đã được tạo với, nếu không nó sẽ đi theo đường dẫn lạnh và tìm trong namespace của riêng bạn.

## Hạn chế đã biết

**Pools dành cho các tác vụ nằm trong cùng một ranh giới tin cậy. Hãy sử dụng các sandbox riêng biệt cho mã mà bạn không tin tưởng.** Các kiểm tra bên dưới giúp giảm thiểu các rủi ro cụ thể; chúng không biến các sandbox được pool thành các VM riêng biệt.

### Tổng quan về mô hình mối đe dọa

| Actor | Dành riêng | Pool |
| --- | --- | --- |
| Người dùng internet ẩn danh | Proxy Jobs yêu cầu một token HF có quyền đọc namespace. | Tương tự. |
| Thành viên namespace có quyền đọc | Có thể truy cập proxy và kiểm tra sức khỏe công khai, nhưng cần token sandbox cho các thao tác API. | Tương tự. |
| Thành viên của Namespace có thể tạo Jobs | Không thể suy ra token của người dùng khác từ nonce của nó. | Có thể sao chép nhãn pool, nhưng mặc định việc nhận sẽ kiểm tra người khởi tạo của API Jobs. `adopt_hosts="namespace"` chọn cách tin tưởng các người tạo khác. |
| Mã chạy trong sandbox | Chia sẻ thư mục gốc với máy chủ bên trong VM; không tái sử dụng VM đó giữa các ranh giới tin cậy. | Bị giới hạn bởi uid và Landlock, nhưng chia sẻ kernel, VM và mặt phẳng điều khiển đặc quyền. |
| Client của bạn | Chứa thông tin xác thực của HF và sandbox chuyên dụng. | Chứa thông tin xác thực quản lý host và các khả năng riêng biệt cho từng sandbox. |

### Khoảng cách về cách ly và tài nguyên

- **Phát hiện host không phải là xác thực.** Mặc định `adopt_hosts="own"` kiểm tra initiator, image, flavor, command và URL được phơi bày. Nó không xác thực binary đang chạy cũng không làm cho image có thể thay đổi trở nên đáng tin cậy. Chỉ sử dụng `adopt_hosts="namespace"` với các thành viên namespace mà bạn tin tưởng.
- **Thông tin xác thực của host vẫn rất mạnh.** Chúng quản lý pool và khôi phục token của từng sandbox. Chúng không bao giờ được thay thế cho các scoped token bị thiếu bởi client này. Các host được khởi động bởi phiên bản này chạy sbx-server 0.7.0, từ chối chúng trên các scoped routes; các host được khởi động bởi các client cũ hơn giữ lại server của chúng (và khả năng tương thích host-token của nó) cho đến khi chúng được tái chế.
- **Các kênh dùng chung vẫn được giữ lại.** Kết nối TCP ra ngoài, truy cập loopback đến máy chủ điều khiển, UDP, IPC của kernel và metadata của danh sách tiến trình có thể đọc được không bị cách ly. Các thư mục hệ thống và một số nút thiết bị được chọn vẫn có thể truy cập được. Việc cách ly pool GPU chưa được kiểm tra; hãy sử dụng chế độ dành riêng cho GPU.
- **Giới hạn không phải là hạn mức tổng hợp.** rlimits theo tiến trình và hàng đợi đầu ra có giới hạn giúp giảm khuếch đại tài nguyên. Chúng không phân chia phần CPU, bộ nhớ tổng, dung lượng đĩa, inode hoặc băng thông mạng giữa các sandbox. Phân trang thư mục giới hạn phản hồi nhưng vẫn tạo ra thư mục trên máy chủ. Ghi tệp qua API không phải là hạn mức đĩa, và ghi ở chế độ dành riêng vào các tệp đặc biệt có thể bị chặn.
- **Confinement có thể được làm suy yếu một cách rõ ràng.** Chế độ Host yêu cầu Landlock ABI 6 theo mặc định và từ chối ruleset thất bại. Việc hạ thấp `SBX_MIN_LANDLOCK_ABI` hoặc sử dụng cờ phát triển `--allow-unconfined` của server sẽ chấp nhận ít bảo đảm hơn; hãy kiểm tra `/health` đã xác thực để biết ABI và các tính năng hiệu lực.
- **Kết nối proxy chỉ được xác thực một lần.** Sau yêu cầu đầu tiên, proxy sẽ sao chép các byte đến backend đó cho đến khi kết thúc (EOF). Các yêu cầu HTTP tiếp theo trên kết nối đó sẽ không được xác thực hay định tuyến độc lập. Khả năng tương thích WebSocket/SSE không đảm bảo an toàn trước việc tái sử dụng kết nối phía upstream.

### Chu kỳ sống và thông tin xác thực

- **Các tiến trình con bị tách ra có thể tồn tại sau khi tiến trình cha kết thúc hoặc hết thời gian chờ.** Cả hai đều gửi tín hiệu đến nhóm tiến trình; một tiến trình con gọi `setsid()` sẽ thoát khỏi nhóm đó. Hãy xóa sandbox trong pool hoặc kết thúc Job chuyên dụng để chấm dứt toàn bộ khối lượng công việc.
- **`max_hosts` chỉ là nỗ lực tốt nhất.** Các client riêng biệt có thể đếm cùng một nhóm host và cả hai đều provision thêm. Cần có cơ chế chấp nhận phía backend hoặc tính idempotency để đặt giới hạn chung cứng.
- **Teardown có thể thất bại.** Các lệnh hủy host thất bại sẽ raise và giữ lại các bản ghi cache. `close()` giới hạn thời gian chờ cho các thao tác tạo đang chạy; một thao tác tạo kéo dài vượt quá thời gian chờ đó có thể để lại một Job tính phí. Hãy sử dụng Jobs API hoặc CLI để đối soát các tài nguyên đang chạy.
- **Việc xoay vòng Bearer không xoay vòng các khả năng của sandbox.** Xác thực proxy giải quyết token HF hiện tại cho mỗi yêu cầu, nhưng việc kết nối lại không trạng thái suy ra token host từ giá trị bearer gốc chính xác. Việc xoay vòng giá trị đó có thể ngăn cản việc kết nối lại với các host hiện có.
- **Bộ nhớ đệm cục bộ vẫn được coi là đầu vào đáng tin cậy trong 15 phút.** URL và ngữ cảnh bảo mật của nó được kiểm tra, nhưng nó không có MAC toàn vẹn; hãy bảo vệ `$HF_HOME` khỏi việc ghi bởi mã không đáng tin cậy.
- **Một digest pin không phải là nguồn gốc đã ký.** Client xác minh một binary server cụ thể. Việc công bố và cập nhật pin đó vẫn là các bước phát hành; manifest của workflow không phải là một xác nhận mật mã. Lối thoát rõ ràng `SBX_ALLOW_UNVERIFIED_SERVER=1` bỏ qua xác minh chỉ khi cả hai công cụ hash đều thiếu.

Chưa xác thực `/health` chỉ hiển thị trạng thái hoạt động và giao thức. Metadata chi tiết yêu cầu thông tin xác thực của host. Id và token của sandbox yêu cầu nguồn ngẫu nhiên từ kernel; việc tái sử dụng UID tuân theo quy trình xác minh và dọn dẹp thư mục home.

## Hiệu năng

Tất cả các con số được đo lường trên các HF Jobs thực tế chạy trên `cpu-basic`, với client đặt trên laptop và toàn bộ lưu lượng đi qua proxy của Jobs.

**Môi trường sandbox riêng:**

| metric                                            | value                                                           |
| ------------------------------------------------- | --------------------------------------------------------------- |
| khởi động lạnh (`create()` trả về, máy chủ bắt đầu phản hồi) | ~5,8s trung vị                                                    |
| round-trip `run()`                                | p50 ~110ms (mức sàn RTT của proxy là ~105ms; chi phí phát sinh phía client ≈ 0) |
| chuyển tệp (song song theo khoảng, >8 MiB)           | ~340 MiB/s xuống, ~441 MiB/s lên                                  |

**Pool (chế độ shared/host):**

| N sandbox | máy chủ (50/máy chủ) | cấp phát + tạo tất cả | chạy trong tất cả | kill all | tổng     |
| ----------- | --------------- | ---------------------- | ----------- | -------- | --------- |
| 100         | 2               | 6.1s                   | 1.5s        | 0.6s     | **8.2s**  |
| 1000        | 20              | 7.4s                   | 4.2s        | 4.2s     | **15.8s** |

1000 sandbox được tạo, thực thi và hủy trong ~16s có chi phí xấp xỉ một lần khởi động lạnh của host (~6s) được phân bổ cho tất cả chúng — tổng cộng khoảng **$0.0009** (20 × `cpu-basic`), so với ~$0.06 và một đợt lập lịch 1000-VM cho mỗi Job trên một sandbox. Các thao tác tạo/thực thi/xóa phía máy chủ mỗi thao tác mất ~1ms; ngân sách thời gian hoàn toàn nằm ở vòng lặp mạng.

## Quyết định thiết kế, tóm tắt

| quyết định                                                              | tại sao                                                                             |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Xây dựng trên Jobs, không có dịch vụ mới                                         | kế thừa hóa đơn, phần cứng, quyền hạn; hoạt động trong bất kỳ image nào                     |
| Binary Rust tĩnh, được tải xuống khi khởi động                             | không có Python/pip; khởi động lạnh ~6 giây so với 30–90 giây cho một bootstrap dựa trên pip               |
| HTTP/1.1 tự viết                                                  | các framework tối giản đệm các phản hồi dạng chunk và làm hỏng việc phát trực tiếp (đã xác minh) |
| Xác thực HMAC không trạng thái                                                   | kết nối lại từ bất kỳ đâu; một token phái sinh thay vì chính token HF (được giới hạn phạm vi theo từng job — xem [Phạm vi token](#token-scope)) |
| `run()` sẽ gây ra lỗi nếu mã thoát khác không (`check=False` để tắt tính năng này)              | DX tốt nhất cho các vòng lặp "chạy mã, xem lỗi" (kiểu E2B)                         |
| `idle_timeout` watchdog thay vì dọn dẹp phía client                | Các sandbox tồn tại lâu dài là một tính năng; những sandbox bị rò rỉ vẫn sẽ bị hủy                       |
| Pools = uid + Landlock, sức chứa do máy chủ kiểm soát, không có trạng thái cục bộ | fan-out cho cùng một người dùng nhanh; chính xác dưới điều kiện đồng thời; có thể gắn lại ở bất kỳ đâu        |

