# Sandbox

> [!NOTE]
> Les sandboxes sont une fonctionnalité **expérimentale**. L'API, les valeurs par défaut et le comportement peuvent changer sans préavis. Les sandboxes partagées sont destinées aux charges de travail au sein de la même frontière de confiance ; leur isolation ne garantit pas la protection contre toutes les attaques inter-sandboxes. Utilisez des sandboxes dédiées pour les charges de travail qui ne se font pas confiance mutuellement, et évitez de rendre disponibles des identifiants à longue durée de vie ou à large portée pour les charges de travail des sandboxes.

Un sandbox est une machine cloud isolée que vous pouvez démarrer en quelques secondes, dans laquelle vous pouvez exécuter des commandes avec un affichage en direct de la sortie, et vers laquelle vous pouvez transférer des fichiers — le tout depuis Python ou l’interface en ligne de commande. Les sandboxes sont construits sur la base de [Jobs](./jobs) : en interne, un sandbox n’est qu’un Job exécutant un petit serveur qui expose l’exécution de commandes et le transfert de fichiers via HTTP.

Ils sont adaptés chaque fois que vous devez exécuter du code ailleurs que sur votre propre machine :

- **Exécution de code non fiable ou généré par IA** — laissez un agent exécuter du code arbitraire sans lui accorder d'accès à votre système de fichiers. Utilisez un bac à sable *dédié* à cet effet : il s'agit d'une machine virtuelle autonome.
- **Constructions et expériences reproductibles** — exécutées sur une image propre et bien définie, sur CPU ou GPU.
- **Répartition du travail** — lancer des centaines d'environnements parallèles (roulements RL, évaluation, exécution d'outils par lots) à moindre coût.

Toute image Docker avec `/bin/sh` fonctionne — il n'est pas nécessaire d'avoir Python, pip ou un agent préinstallés (un petit binaire de serveur statique est injecté au démarrage).

> [!TIP]
> Envie de comprendre le fonctionnement interne — le serveur dans le Job, l’authentification sans état, ou la manière dont un seul Job héberge de nombreux sandboxes isolés ? Consultez le [guide conceptuel des Sandboxes](../concepts/sandbox).

## Les deux types de bac à sable

Il existe deux façons d’obtenir un bac à sable. Les deux vous remettent le même objet [Sandbox](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox) (mêmes `run`, `files`, `connect`, `kill`) ; elles ne diffèrent que par la manière dont la machine sous-jacente est allouée :

|            | [Sandbox.create()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.create) — **dédié**              | [SandboxPool](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxPool) — **partagé / pool**                                                     |
| ---------- | ----------------------------------------------- | --------------------------------------------------------------------------------------- |
| mapping    | un Job = **une sandbox** (une VM entière)          | un Job = **de nombreuses sandbox** (une VM, groupée)                                           |
| isolation  | VM complète                                         | uid + [Landlock](https://docs.kernel.org/userspace-api/landlock.html) (confiance même utilisateur) |
| démarrage à froid | ~6 s par bac à sable                                 | ~6 s pour l'hôte principal, puis ~1 aller-retour chacun                                         |
| coût       | une VM par bac à sable                              | une VM par **hôte**, amortie sur de nombreux sandboxes                                    |
| GPU        | ✅                                               | ❌ (CPU uniquement)                                                                            |
| idéal pour   | un seul bac à sable, charges de travail GPU, code non fiable | de nombreux sandboxes CPU peu coûteux (roulements RL, fan-out)                                         |

Règle générale : besoin d'un GPU ou d'exécuter du code mutuellement non fiable → dédié. Besoin de centaines de sandbox CPU peu coûteuses → un pool.

## Démarrage rapide

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

Choisissez n’importe quelle image et quelle que soit la [variante](./jobs#select-the-hardware) de matériel :

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

## Exécution des commandes

[Sandbox.run()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.run) exécute une commande et attend qu’elle se termine. Passez une chaîne de shell ou une liste 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)
```

Par défaut, le mode est déduit du type (une chaîne est exécutée via `/bin/sh -c`, une liste est exécutée directement en tant qu'argv). Passez `shell=` pour l'indiquer explicitement — pratique pour éviter le piège classique où une liste à un seul élément comme `["echo hi"]` est exécutée comme un seul programme nommé `"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` nécessite une chaîne de caractères et `shell=False` nécessite une liste ; passer le mauvais type provoque une `ValueError`.

Une commande qui se termine avec un code non nul lève `SandboxCommandError` (avec `stdout`, `stderr` et `exit_code` joints). Passez `check=False` pour récupérer le [SandboxCommandResult](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxCommandResult) au lieu de lever une exception :

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

### Processus en arrière-plan

Passez `background=True` pour démarrer un processus de longue durée (un serveur, un watcher, une exécution d’entraînement) sans attendre. `run` renvoie immédiatement un [SandboxProcess](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxProcess) au lieu d’un [SandboxCommandResult](/docs/huggingface_hub/main/fr/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)
```

Listez les processus d’un bac à sable et arrêtez-en un lorsque vous avez terminé. Les processus terminés restent listés (avec `running=False` et leur `exit_code`) jusqu’à la suppression du bac à sable, ce qui vous permet de savoir si un processus est toujours actif ou s’il a déjà terminé :

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

Les options de streaming/attente uniquement (`timeout`, `stdin`, `on_stdout`, `on_stderr`, `check`) ne s'appliquent pas en mode arrière-plan — seules `env`, `cwd` et `shell` sont prises en compte.

## Fichiers

```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, ...)]
```

Autres utilitaires : `stat`, `exists`, `mkdir`, `delete`.

## Accéder à un serveur dans un bac à sable

Démarrez un serveur dans le bac à sable (en arrière-plan), puis accédez-y depuis l’extérieur à l’aide de [Sandbox.proxy_url_for()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.proxy_url_for) — la requête est transférée par le serveur de bac à sable du job vers votre serveur interne, il n’y a donc pas de port public supplémentaire à exposer. Cela fonctionne pour le HTTP simple, les Server-Sent Events et les WebSocket. Associez l’URL à `Sandbox.proxy_headers` pour l’authentification (votre client WebSocket/HTTP doit les envoyer) :

```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://")
```

Lors de l’utilisation de `proxy_headers`, désactivez les redirections dans votre client HTTP (`follow_redirects=False` avec httpx, ou `allow_redirects=False` avec requests). Une application peut rediriger vers une autre origine, et ces en-têtes d’authentification personnalisés ne doivent pas y être transmis.

Les sandboxs mutualisés nécessitent un serveur qui renvoie des jetons de capacité par sandbox. L'absence de jetons constitue une erreur ; mettez à niveau et recyclez l'hôte plutôt que d'utiliser son jeton de gestion.

La manière dont le serveur interne doit écouter dépend du type de bac à sable :

- **Dédié** ([Sandbox.create()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.create)) : associer un port TCP normal sur `127.0.0.1:<port>`.
- **Pool / partagé** ([SandboxPool](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxPool)) : les sandboxes mutualisées ne peuvent pas lier un port TCP (Landlock), il faut donc écouter sur un **socket unix** situé à `$SBX_PROXY_DIR/<port>.sock` (cette variable d’environnement est définie dans chaque sandbox), par exemple `uvicorn app:app --uds $SBX_PROXY_DIR/8000.sock`. Le côté client (`proxy_url_for` / `proxy_headers`) est identique dans les deux cas.

## Cycle de vie

Un sandbox survit au processus qui l’a créé — vous pouvez le créer maintenant et vous y reconnecter plus tard, depuis n’importe quelle machine qui détient le même jeton HF, sans avoir à copier d’état :

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

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

- `idle_timeout` (10 minutes par défaut) est l’option clé : elle arrête le sandbox dès qu’aucun appel API n’est effectué et qu’aucun processus n’est en cours d’exécution, ce qui met fin à la facturation des sandboxes abandonnés. Vous pouvez le définir à la création (`Sandbox.create(idle_timeout="30m")`) ou passer `None` pour le désactiver.
- Le job a également une durée de vie maximale fixe de 24 h en tant que filet de sécurité (non configurable).
- `forward_hf_token=True` expose explicitement votre jeton HF au code exécuté dans le bac à sable (sous la forme `HF_TOKEN`). Même avec `forward_hf_token=False`, ne considérez pas le bac à sable comme une frontière stricte pour vos identifiants — consultez l'avertissement en haut de cette page.

## Plusieurs sandboxes à la fois : SandboxPool

Lorsque vous avez besoin de nombreuses sandbox (roulages RL parallèles, évaluation en éventail, exécution par lots d’outils), un Job par sandbox est gaspilleur : chacun subit un démarrage à froid complet de la VM et occupe une machine entière pour une charge de travail qui n’a besoin que de quelques Mo de RAM. [SandboxPool](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxPool) regroupe de nombreuses sandbox légères dans quelques Jobs hôtes partagés — une seule VM facturée sert des dizaines de sandbox, ce qui réduit le coût par sandbox de ce facteur et ramène le démarrage à froid par sandbox à environ un aller-retour réseau.

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

Chaque `create()` renvoie un [Sandbox](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox) complet ; appelez-le de manière répétée pour effectuer un fan-out. Le pool démarre les Jobs hôte au besoin, regroupe `sandboxes_per_host` sandboxes par hôte et met tout à fin sur `close()` (ou lorsqu'un hôte est inactif, en tant que filet de sécurité de facturation). Le motif de fan-out typique :

```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 et `idle_timeout` sont propres à chaque bac à sable (ils appartiennent à `create()`, et non au pool), de sorte que les bacs à sable d’un même pool peuvent avoir des environnements différents :

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

Un bac à sable mutualisé est un [Sandbox](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox) complet, mais certains paramètres d’entrée sont fixés par l’hôte au lieu d’être propres à chaque bac à sable — [SandboxPool.create()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxPool.create) accepte donc un ensemble d’arguments plus restreint que [Sandbox.create()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.create) :

| Entrée              | `Sandbox.create` (dédié) | `SandboxPool.create` (avec mise en pool)            |
| ------------------ | ---------------------------- | ---------------------------------------- |
| `image`, `flavor`  | par bac à sable                  | fixé par le pool (défini une seule fois sur l'hôte) |
| `volumes`          | par bac à sable                  | **non disponible** (monté au démarrage de l'hôte) |
| `env`              | par bac à sable                  | par bac à sable                              |
| `idle_timeout`     | par bac à sable                  | par bac à sable                              |
| `forward_hf_token` | par bac à sable                  | par bac à sable                              |

Les sandbox mutualisées partagent un job hôte à durée de vie longue, il n'existe donc pas de canal de secrets de Job chiffrés comme celui des sandbox dédiées. En revanche, l'`env` d'une sandbox mutualisée est transmis au serveur hôte lors de la création de la sandbox (et non au démarrage du job), il n'est donc jamais stocké dans les métadonnées d'un job — passez les secrets potentiels en tant que `env` simple.

### Croissance à la demande et préchauffage

`pool.create()` crée un sandbox à la fois, en réutilisant un hôte qui dispose encore de capacité libre avant d’en démarrer un nouveau — vous pouvez ainsi générer des sandboxes au fur et à mesure que le travail arrive, et ils se répartissent automatiquement sur les hôtes déjà chauds :

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

Pour éviter le démarrage à froid de l'hôte lors des premiers appels, pré-provisionnez les hôtes avec `warm_up=N` (démarrés lors du premier `create()`) ou en appelant `pool.warm(N)` à l'avance.

### Réutilisation des pools entre processus et machines

Les hôtes chauds sont découverts via les étiquettes des tâches, ce qui permet la réutilisation entre les processus : un `SandboxPool` tout neuf avec la même `image`/`flavor`/`name` se connecte aux hôtes laissés en cours d’exécution par une exécution antérieure, au lieu d’en démarrer de nouveaux. Passez un `name=` pour empêcher des pools distincts de partager les mêmes hôtes.

Pour se reconnecter depuis une autre machine sans état local, reconnectez-vous par identifiant de pool avec [SandboxPool.connect()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.SandboxPool.connect) — il trouve un hôte en cours d’exécution, reconstruit la configuration du pool (image, saveur, densité d’empilement) à partir de ce travail d’hôte, et est prêt à `create()` davantage :

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

Un pool connecté via `connect()` ne possède pas les hôtes partagés (d'autres clients peuvent les utiliser), de sorte que — comme pour [Sandbox.connect()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.connect) — la sortie de son bloc `with` (ou l'appel à `close()`) ne libère que les clients HTTP locaux et laisse les hôtes en cours d'exécution. Mettez fin aux hôtes d'un pool explicitement avec `pool delete` / `hf sandbox pool delete <id>`.

> [!NOTE]
> Les sandboxes au sein d'un hôte sont isolées par des uids distincts et des ensembles de règles Landlock spécifiques à chaque sandbox. Les sandboxes partagées sont destinées aux charges de travail parallèles *d'un même utilisateur*, et l'isolation contre toute attaque inter-sandboxes n'est pas garantie. Pour du code mutuellement non fiable, ou pour le GPU, utilisez [Sandbox.create()](/docs/huggingface_hub/main/fr/package_reference/sandbox#huggingface_hub.Sandbox.create) (une VM distincte par sandbox). Les compromis sont détaillés dans le guide conceptuel : [comment fonctionne l'isolation](../concepts/sandbox#isolation-in-a-pool-uid--landlock) et — plus important si vous devez choisir entre les deux modes — la liste complète des [limitations connues](../concepts/sandbox#known-limitations).

## Depuis l'interface en ligne de commande

La commande `hf sandbox` reproduit l’API Python. Un bac à sable dédié :

```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` transmet la sortie en direct et se termine avec le code de sortie de la commande, ce qui permet de le composer dans des scripts :

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

Lancez un processus de longue durée en arrière-plan avec `hf sandbox spawn` (affiche son pid), puis listez ou arrêtez les processus. La liste affiche l'état de chaque processus (`running` ou `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
```

Pour de nombreux sandboxes partagés bon marché, préchauffez un pool une fois, puis créez des instances à la demande :

```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` crée un bac à sable partagé ; son identifiant ressemble à `<host_job_id>.<local_id>` et fonctionne partout où un identifiant dédié est accepté (`exec`, `cp`, `kill`). Un pool n'a pas d'état local — il ne s'agit que de ses machines virtuelles hôte en cours d'exécution, identifiées par l'identifiant du pool — il fonctionne donc depuis n'importe quelle machine et cesse d'exister dès que toutes ses hôtes ont disparu (tuées ou expirées par inactivité).

