# 内部のサンドボックス

このガイドでは、[Sandboxes](../guides/sandbox)の内部動作、その設計思想、および制約について説明します。Sandboxesを単に利用するだけの場合は、[Sandboxesガイド](../guides/sandbox)を参照するだけで十分です。メカニズムの理解、トラストモデルの評価、またはデバッグを行う場合は、読み進めてください。

> [!NOTE]
> Sandbox APIは実験的なものです。APIや動作は、事前の通知なしに変更される場合があります。

## 「サンドボックスサービス」は存在しません

まず理解すべきことは、専用のサンドボックスバックエンドが存在しないということです。サンドボックスとは、HTTPを話す単一の小さな静的バイナリ`sbx-server`を実行する[HF Job](../guides/jobs)（VM）に過ぎません。クライアントは、Jobが公開するJobsプロキシ（`*.hf.jobs` URL）を介してそのサーバーと通信します。それ以外のすべて、すなわち認証、ディスカバリー、1つの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の請求、ハードウェアのバリエーション、名前空間の権限を自動的に継承します。

### サーバーのブートストラップ

ジョブの起動時、Jobのコマンドは、`sbx-server`バイナリを取得し、実行可能にし、`exec`する小さな`/bin/sh -c`スクリプトです。このバイナリは実行時依存関係がゼロの約640KBの静的[musl](https://musl.libc.org/)ビルドであるため、任意の`x86_64`Linuxイメージで実行できます。

特に注目すべきいくつかの決定事項:

- **マウントによるフォールバック付きダウンロード。** 高速パスでは、`wget` または `curl`（一般的なベースイメージにはいずれかが同梱されています）を使用して、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)でオープンソースとして公開されています。

## 認証はステートレスです

サンドボックスは、2つの独立したレイヤーによって保護されます。

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 トークンです。そのため、そのトークンを保持しているマシンであれば誰でもサンドボックストークンを再計算できます。再接続をステートレスにするのはまさにこの仕組みです。また、これはトークンが呼び出し元に対する ID 確認ではなく、あなたの認証情報から導出された*capability*であることを意味します。サーバーは呼び出し元がトークンを認識していることを検証するだけで、その人物が誰であるかを検証するわけではありません。

すべてのサンドボックスジョブには、検出用の2つの安定したラベルが付与されています — `hf-sandbox=1`（すべてに付与）と `hf-sandbox-mode=dedicated` または `hf-sandbox-mode=pool` — したがって、サーバーサイドで一覧表示やフィルタリングを行うことができます。例: `hf jobs ps --label hf-sandbox=1`。

トークンはJobシークレットを介してサーバーに渡されます。クライアントは、ラベル内の公開ノンスから必要に応じて再導出します。これにはいくつかの利点があります：

- **ステートレスな再接続。** `Sandbox.connect(id)` は、同じ HF トークンを保持する任意のマシンから動作します — ラベルから nonce を読み取り、トークンを再計算します。ローカルファイルも、コピーする状態も不要です。
- **HF トークンは、環境変数やジョブシークレットとしてサンドボックスに渡されません**（`forward_hf_token=True`でオプトインした場合を除く）。これは、認証情報がアクセス不能であることを保証するものではありません。サンドボックスポートでリッスンしているプロセスは、イメージが最初に起動するプロセスであり、信頼できないイメージがクライアントから送信されるリクエスト（`Authorization`ヘッダーを含む）を観測できる可能性があります。サンドボックスからアクセス可能な認証情報は、サンドボックスに露出している可能性があるものとして扱ってください。

### トークンスコープ

1つの **Job** ごとに1つの nonce が発行されるため、導出されたトークンは *job* の認証情報となります。dedicated モードでは、job がサンドボックスそのものなので、両者は一致します。pool モードでは、job がホストとなり、1つのホストが多数のサンドボックスを保持します。そのため、プールされたサンドボックスには、それ自身のより狭い範囲の2番目の認証情報が付与されます。

| 資格情報 | どのように導出されたか | 許可 |
| --- | --- | --- |
| **専用サンドボックストークン** | ジョブのラベルから`HMAC(hf_token, nonce)` | そのサンドボックス（つまりジョブ全体） |
| **プールホストトークン** | ホストジョブのラベルから`HMAC(hf_token, nonce)` | プール管理: そのホスト上のサンドボックスの作成、一覧表示、削除、およびトークンの復元 |
| **プールされたサンドボックストークン** | ホストサーバーが各サンドボックスに対して発行するランダムな256ビット | その1つのサンドボックス — 兄弟でもなく、プールでもなく |

クライアントは自動的に狭い方のトークンを使用します: `pool.create()` は作成応答でそれを取得し、`Sandbox.connect("<host>.<id>")` はホストトークンでそれを復元し、`proxy_headers` はホストのトークンではなくサンドボックスのトークンを返します (HF ベアラートークンは読み取り時に解決されるため、長期間保持されたハンドルが古いトークンを返すことはありません) — これらのヘッダーは通常ブラウザや WebSocket クライアントに渡されるため、1 つのサンドボックスへのアクセス権のみを付与すべきです。

リークが意味すること: **プールされたサンドボックストークン**が漏洩すると、そのサンドボックスが侵害されます。**ホストトークン**が漏洩すると、ホスト全体が侵害されます。つまり、現在のものも将来作成されるものも、そのホスト上のすべてのサンドボックス、およびその管理ルートが対象となります。そのため、プールの管理者認証情報として扱ってください。あなたのネームスペースのメンバーは別の HF トークンを持っており、あなたのトークンを導出することはできませんが、トークンが誤った場所に*提供*される可能性については、[既知の制限事項](#known-limitations)を参照してください。

クライアントはプールされた操作に対して空でないサンドボックススコープのトークンを必要とし、それを提供しないホストは拒否します。ホスト管理用の認証情報を代替として使用することはありません。このAPIを使用する前に、クライアントをアップグレードし、古いプールホストをリサイクルしてください。

## 専用サンドボックス(`Sandbox.create`)

シンプルなモデル: **サンドボックスごとに1つのJob**。Jobは実体のVMであるため、この構成は最も強力な分離（VMレベル）を提供し、GPUを含むあらゆるハードウェア構成に対応します。相互に信頼できないコードを扱う場合の適切な選択です。そのAPIルートは`/v1/*`にあり、ファイルパスはコンテナファイルシステム上の絶対パスです。`kill()`は単にJobをキャンセルします。

専用サンドボックスが提供する境界は、**Jobとそれ以外のすべて**の間です。VM内部では、あなたのコードと`sbx-server`は同じコンテナ内でrootとして実行されます。`sbx-server`は、あなたの代わりに実行するコマンドをサンドボックス化せず、HTTP経由で公開するだけです。したがって、信頼できないコードが専用サンドボックス内で実行された後は、そのVM（コントロールプレーンを含む）をそのコードが所有するものとして扱ってください。コードはサーバーを停止したり置き換えたりでき、そのサンドボックスでその後に行う操作はすべてそのコードの支配下で実行されます。信頼境界をまたいで1つのサンドボックスを再利用するのではなく、信頼できないワークロードごとに1つの専用サンドボックスを使用し、完了したら`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秒のVMコールドスタートを支払い、1台分のマシン全体が請求されます。単一のサンドボックスやGPUワークロードであれば、まさに望ましい動作です。しかし、100〜1000個の短時間のCPUタスクに対しては非効率であり、これがプールが存在する理由です。

## プール: 1つのJob(`SandboxPool`)に多数のサンドボックス

> [!WARNING]
> **プールは、1つの信頼境界内のワークロード向けです。** プール化されたサンドボックスは、共有VM内のuidとLandlockルールセットであり、専用のVMではありません。そのホストは、すべてのサンドボックスが通信する特権付きのコントロールプレーンを実行します。プールは、*ご自身の*コードを低コストで展開するために使用します。相互に信頼し合わないワークロード（他のサンドボックスのファイルを読み取らせたくないもの）には、[Sandbox.create()](/docs/huggingface_hub/v2.4.0/ja/package_reference/sandbox#huggingface_hub.Sandbox.create)を使用してください。これにより、各ワークロードに専用のVMが割り当てられます。具体的な制約については、[既知の制限事項](#known-limitations)を参照してください。

典型的なRLロールアウトやツール実行サンドボックスには、数秒間、数MBのRAMと1コアが必要です。それぞれに2 vCPUのVMと6秒のコールドスタートを支払うこと、そして1000 VMのスケジューリングバーストを引き起こすことは、誤ったトレードオフです。そのため、[SandboxPool](/docs/huggingface_hub/v2.4.0/ja/package_reference/sandbox#huggingface_hub.SandboxPool)は1つのJobをホストとして実行し、その内部で多数のサンドボックスを多重化します。

プールされたサンドボックスは、ネストされたVMやコンテナではありません。これは、クラシックなUnixのマルチユーザープリミティブです：

- **dedicated uid** (≥ 20000)
- その uid が所有する、**プライベートな `0700` のホーム**
- その uid で `exec` され、**スクラブされた環境**（`env_clear` により、ホストのシークレットが漏洩しないようにする）、`NO_NEW_PRIVS`、プロセスごとの **rlimits**、サンドボックスごとの **Landlock ルールセット** が適用されます。

したがって、サンドボックスの作成は `mkdir + chown + build ruleset` であり、サーバー側では約 1ms で完了します。2 回目の VM ブートは不要です。クライアントが認識する唯一のレイテンシは、プロキシの往復時間です。

```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 は、uid 0..65535 のみをマッピングするユーザーネームスペース内で root として実行され、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 Security Module です。まさに私たちが必要とするサンドボックスごとの境界です。各サンドボックスに対してサーバーはルールセットを構築し、exec 子プロセスはサンドボックスの uid/gid に降格し、コマンドの実行前に `NO_NEW_PRIVS`、`landlock_restrict_self`、rlimits を適用します。

個別の uid（裁量的アクセス制御）と Landlock を組み合わせることで、以下を提供するように設計・テストされています：

- ✅ プロセスAは他のプロセスの`environ`を読み取ることができないため、サンドボックス間でHFトークンやサンドボックストークンに直接アクセスすることが防止されます。
- ✅ Aは、Bのプロセスに対して`SIGKILL`を送信したり、`ptrace`を実行したり、メモリを読み取ったり、Bに`setuid`したり、Bの`0700`のホームディレクトリを読み取ったりすることはできません。
- ✅ `/tmp` および `/dev/shm` へのアクセスは拒否されます。各サンドボックスは Landlock によって自身のホームディレクトリ内に制限されており（その `TMPDIR` は `$HOME` 内を指しています）。
- ✅ A は TCP ポートを `bind` できないため、サンドボックス間のローカルホストサービスは存在しません（アウトバウンドの `connect` は引き続き許可されるため、インターネットへの接続は可能です）。
- ✅ サンドボックス間の抽象 Unix ソケットはブロックされます（`LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET`。uid の分離だけではこれらを*ブロックできません*）。

これらのコントロールはワークロードを制約します。ルートコントロールプレーンは独立した信頼境界として機能し続けます。そのホストモードのファイル操作はシンボリックリンクを拒否し、ソケットプロキシはソケットのinodeを固定してピアのuidを確認し、専用ルートはホストモードでは利用できません。残存する共有チャネルについては、[既知の制限事項](#known-limitations)に記載されています。

> [!NOTE]
> **VMの代替手段ではない理由。** Landlockとuid分離は、同一の信頼境界内のワークロードを対象としています。プールされたサンドボックスはカーネル、VM、コントロールプレーンを共有するため、サンドボックス間のすべての攻撃からの保護は保証されません。また、リソースや一部のプロセスリストのメタデータも共有され続けます。相互に信頼できないコード、またはGPUを使用する場合は、各サンドボックスに独自のVMを割り当てる[Sandbox.create()](/docs/huggingface_hub/v2.4.0/ja/package_reference/sandbox#huggingface_hub.Sandbox.create)を使用してください。

### プール内のファイルモデル

プールされたサンドボックスの書き込み可能な領域は、Landlockによって制限されたホームディレクトリ（デフォルトの作業ディレクトリでもある）のみであるため、ファイルAPIはすべてのパスをそのホームをルートとして扱います。`files.write("data/in.txt", ...)`は`$HOME/data/in.txt`に書き込み、先頭の`/`はホームからの相対パスとして解釈され、`..`ではそこから抜け出すことはできません。API経由で書き込まれたファイルは、サンドボックス自身のコードが読み取れるように、サンドボックスのuidに`chown`されます。これにより、「サンドボックスをルートとするファイルシステム」というクリーンなモデルが得られ、サンドボックス内のコードが実際にアクセスできる範囲と完全に一致します。これは、コンテナファイルシステム上でパスが絶対パスとして扱われる専用サンドボックスとは異なります。

パスは辞書順で正規化された後、シンボリックリンクの拒否を各コンポーネントに対して適用し、オープンされたホームディレクトリのディスクリプターに対して相対的に解決されます。ファイル API は、同じホーム内のリンクを含むシンボリックリンクをフォローしません。サンドボックス内のコードは、その Landlock ルールに従ってシンボリックリンクを使用できます。

### Poolsには権威あるローカル状態がありません

プールは意図的にローカル設定ファイルではありません。プールとは、`hf-sandbox-pool=<id>` ラベルを共有する実行中のホスト Job の集合です。これにより、プールはサンドボックス API の他の部分と一貫性を保ち（すべてがラベルから検出可能であり、どのマシンからも再接続可能である）、最後のホストが削除されるとプール自体が存在しなくなるという性質を持ちます。

- ホストは、プールの設定（イメージ、フレーバー、`sandboxes_per_host`、アイドルタイムアウト）をジョブの環境変数に保持します。ラベルはフィルタリングにのみ使用されます。クライアントが重複したホストを起動する必要がある場合、実行中のホストからその設定を読み戻します（`inspect_job`）。これにより、中央のレコードなしで、プール内のすべてのホストが一貫性を保ちます。
- Env と secrets はサンドボックス単位であり、作成時に渡されます。プールレベルでは設定されません。プールされたサンドボックスの環境変数は、そのサンドボックスの生存期間中、ホストサーバーのメモリに保持されます（実行するすべてのコマンドに再適用されます）。ディスクに書き込まれることはなく、Job のメタデータにも表示されません。プールされたサンドボックスには暗号化された secrets チャネルは存在しないため、`env` を使用し、その値を「ディスク上には存在しないが、暗号化もされていない」として扱ってください。暗号化ストアが必要な値には、専用サンドボックスの `secrets` を使用してください。
- Capacity はサーバーが権威的に管理します。ホストは `sandboxes_per_host` を超える作成要求を拒否し（`{"rejected": N}` を応答として返します）、クライアントはオーバーフロー分を別のホストにパックするか、重複したインスタンスを起動します。これにより、複数のプロセスが同時に同じプールに作成を行っても、パック処理が正確に保たれます。
- Idle eviction は 2 段階構成です。各サンドボックスは、`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` にあるベストエフォートのキャッシュにより、これが解消されます。create/warm の実行後、プロセスはプール設定と、ホストごとのプロキシ URL、認証ノンス、最後に確認された空きスロットを記録します。次のプロセスは、ファイルから直接ホストトランスポートを再構築し（HTTP を使用せず）、`POST` に直接アクセスします。

`<context>` は、エントリが書き込まれたエンドポイント、認証情報、およびネームスペースのダイジェストです。認証情報はフィンガープリントとして扱われ、トークン自体はキャッシュに書き込まれません。ディレクトリ名とファイルの両方にこの情報が含まれており、3つのうちいずれか1つでも一致しない読み取りは単なるキャッシュミスとして扱われます。そのため、同じマシンの別のユーザー、別のエンドポイント、または `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 ベアラートークンとサンドボックストークンを送信する先を決定することはできません。それ以外のものを指定するエントリはファイルから除外されます。また、15 分を超えると、エントリ単独では信用されなくなります。`inspect_job` が、ジョブがまだ実行中であり、まだこのプール用にラベル付けされており、まだその URL にあることを確認した後にのみ、トランスポートが構築されます。
- **その形状が信頼できないため。**型と範囲は読み取り時にチェックされるため、手動で編集されたファイル、切り詰められたファイル、または文字列型で扱われたファイルは、`create()`の途中でエラーになるのではなく、キャッシュミスとして扱われます。
- **自己修復。** 存在しなくなったキャッシュ済みホストは、最初の失敗したリクエスト時に除外され、ファイルから削除されます。create は透明にラベル検出へフォールバックします。
- **並行処理に安全です。** 書き込みはファイルロック（`job_id`をキーとする）の下でマージされ、アトミックにコミットされるため、並行して実行される`create`プロセス同士が互いのデータを上書きすることなく、読み取り側が半端に書き込まれたファイルを見ることもありません。
- **使い捨てです。**削除するとキャッシュミスが発生し、コールドパスが実行されますが、すべては引き続き正常に動作します。マシン間で共有されることはありません。

2つの点に注意してください。`$HF_HOME`は機密情報として扱ってください。エントリは再利用可能なホストURLと、そのトークンが派生するパブリックなnonceから構成されており、別の認証情報で書き込まれたファイルは無視されますが、*あなた自身*の認証情報で書き込まれたものは、上記の15分間のウィンドウ内で有効とみなされます。また、ネームスペースはキーの一部であるため、明示的な`namespace=`でキャッシュされたプールは、同じ`namespace=`が渡された場合のみ再発見されます。`hf sandbox create --pool <id>`は、プールが作成されたときと同じ`--namespace`を要求し、そうでない場合はコールドパスを経て、あなた自身のネームスペース内を検索します。

## 既知の制限

**プールは、1つの信頼境界内のワークロード向けです。信頼できないコードには、専用のサンドボックスを使用してください。** 以下のチェックは特定のリスクを軽減しますが、プールされたサンドボックスを独立したVMに変えるものではありません。

### 脅威モデルの概要

| アクター | 専用 | プール |
| --- | --- | --- |
| 匿名のインターネットユーザー | Jobsプロキシには、ネームスペースの読み取りアクセス権限を持つHFトークンが必要です。 | 同じです。 |
| 読み取りアクセス権限を持つ名前空間メンバー | プロキシとパブリックヘルスチェックには到達できますが、API操作にはサンドボックストークンが必要です。 | 同じです。 |
| ジョブを作成できるネームスペースメンバー | 他のユーザーのトークンをそのnonceから導出することはできません。 | コピープールラベルをコピーできますが、デフォルトの採用チェックではJobs APIのinitiatorを確認します。`adopt_hosts="namespace"`を指定すると、他の作成者を信頼するように設定できます。 |
| サンドボックス内で実行されるコード | VM内のサーバーとルートディレクトリを共有します。信頼境界をまたいでそのVMを再利用しないでください。 | uidとLandlockによって制限されますが、カーネル、VM、および特権付きコントロールプレーンを共有します。 |
| あなたのクライアント | HFおよび専用サンドボックスの認証情報を保持します。 | ホスト管理の認証情報と、サンドボックスごとに分離された権限を保持します。 |

### 分離とリソースのギャップ

- **ホストの検出はアテストメントではありません。** デフォルトの `adopt_hosts="own"` は、イニシエーター、イメージ、フレーバー、コマンド、公開 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` 開発フラグを使用したりすると、保証される内容が少なくなります。有効な ABI と機能については、認証済みの `/health` を確認してください。
- **プロキシ接続は一度のみ認証されます。** 最初のリクエスト以降、プロキシはEOFまでそのバックエンドへバイトをコピーします。その接続上での後続のHTTPリクエストは、独立して認証されたりルーティングされたりしません。WebSocket/SSEの互換性は、上流の接続再利用に対する安全性を確立するものではありません。

### ライフサイクルと認証情報

- **デタッチされた子プロセスは、プロセスの終了やタイムアウト後も存続する可能性があります。** シグナルはプロセスグループ全体に送信されますが、`setsid()`を呼び出した子プロセスはそのグループから離脱します。プールされたサンドボックスを削除するか、専用のJobを終了させることで、ワークロード全体を終了できます。
- **`max_hosts` はベストエフォートです。** 別々のクライアントが同じホストをカウントし、両方がより多くのリソースをプロビジョニングする可能性があります。厳密な共有上限を設けるには、バックエンドのアドミッション制御または冪等性が必要です。
- **Teardown が失敗する可能性があります。** ホストのキャンセルが失敗すると、例外が発生し、キャッシュレコードが保持されます。`close()` は、進行中の作成処理の完了を待つのには時間制限を設けています。この待機時間内に完了しない作成処理は、課金対象の Job を残す可能性があります。実行中のリソースの整合性を確認するには、Jobs API または CLI を使用してください。
- **Bearerのローテーションは、サンドボックスの機能をローテーションしません。**プロキシ認証はリクエストごとに現在のHFトークンを解決しますが、ステートレスな再接続は、元のBearer値そのものからホストトークンを導出します。この値をローテーションすると、既存のホストへの再接続ができなくなる可能性があります。
- **ローカルキャッシュは15分間、信頼できる入力として扱われます。**そのURLとセキュリティコンテキストはチェックされますが、完全性MACは付与されていません。信頼できないコードによる書き込みから`$HF_HOME`を保護してください。
- **ダイジェストピンは署名されたプロバネンスではありません。** クライアントは特定のサーバーバイナリ1つを検証します。そのピンの公開と更新はリリース手順の一部であり、ワークフローのマニフェストは暗号学的なアテストメントではありません。明示的な`SBX_ALLOW_UNVERIFIED_SERVER=1`のエスケープハッチは、両方のハッシュツールが欠落している場合のみ、検証をスキップします。

認証なしの`/health`は、稼働状態とプロトコルのみを公開します。詳細なメタデータにはホストの認証情報が必要です。Sandbox IDとトークンはカーネルの乱数ソースを必要とし、UIDの再利用は検証済みのプロセスおよびホームディレクトリのクリーンアップに従います。

## パフォーマンス

すべての数値は、`cpu-basic`上の実際のHF Jobsに対して測定されたものであり、クライアントはラップトップ上で動作し、すべてのトラフィックがJobsプロキシを介して流れます。

**専用サンドボックス:**

| メトリック                                            | value                                                           |
| ------------------------------------------------- | --------------------------------------------------------------- |
| コールドスタート(`create()`が返り、サーバーが応答する) | 中央値 約5.8秒                                                    |
| `run()`のラウンドトリップ                                | p50 ~110ms（プロキシのRTT下限は~105ms、クライアントのオーバーヘッド ≈ 0） |
| ファイル転送（並列範囲指定、>8 MiB）           | 下り約340 MiB/s、上り約441 MiB/s                                  |

**プール（共有/ホストモード）:**

| N個のサンドボックス | ホスト（50/ホスト） | プロビジョニング + すべて作成 | すべてで exec | すべて終了 | 合計     |
| ----------- | --------------- | ---------------------- | ----------- | -------- | --------- |
| 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**（`cpu-basic` 20個分）です。これに対し、サンドボックスごとに1つのJobを作成すると、約$0.06のコストと1000個のVMスケジューリングのバーストが発生します。サーバー側のcreate/exec/deleteはそれぞれ約1msであり、予算のすべてがネットワークの往復遅延に費やされます。

## 設計上の判断、まとめ

| 決定                                                              | なぜ                                                                             |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Jobsをベースに構築し、新しいサービスは作成しません                                         | 請求、ハードウェア、権限を継承し、任意のイメージで動作します                     |
| 起動時にダウンロードされる静的な Rust バイナリ                             | Python/pipなし; pipベースのブートストラップの30〜90秒に対し、コールドスタートは約6秒               |
| 手動実装のHTTP/1.1                                                  | 最小限のフレームワークはバッファリングとチャンク化された応答により、ライブストリーミングを妨げます（確認済み） |
| ステートレス HMAC 認証                                                   | どこからでも再接続可能。HF トークンそのものではなく、派生トークンを使用します（ジョブごとにスコープが設定されます — [トークンのスコープ](#token-scope)を参照）。 |
| `run()` は終了コードがゼロ以外の場合に例外を発生させます（`check=False` を指定すると無効になります）。              | 「コードを実行してエラーを確認する」ループ（E2Bスタイル）における最高の開発者体験                         |
| クライアント側のクリーンアップではなく`idle_timeout`ウォッチドッグ                | 永続的なサンドボックスは機能として提供されていますが、リークしたサンドボックスは依然として終了します                       |
| Pools = uid + Landlock、サーバー権限による容量管理、ローカル状態なし | 同じユーザーへの高速なファンアウト; 並行処理下でも正確; どこでも再接続可能        |

