Hub Python Library documentation
Sandbox
Sandbox
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 : 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).
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.
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 (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() — dédié | 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 (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
>>> 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)
42Choisissez n’importe quelle image et quelle que soit la variante de matériel :
>>> 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() exécute une commande et attend qu’elle se termine. Passez une chaîne de shell ou une liste argv :
>>> 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" :
>>> 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 au lieu de lever une exception :
>>> result = sbx.run("test -f /tmp/missing", check=False)
>>> result.exit_code
1Processus 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 au lieu d’un SandboxCommandResult :
>>> 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é :
>>> 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
>>> 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() — 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) :
>>> 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()) : associer un port TCP normal sur
127.0.0.1:<port>. - Pool / partagé (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 exempleuvicorn 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 :
>>> sbx = Sandbox.create()
>>> sbx.id
'687f911eaea852de79c4a50a'
# Later, from anywhere:
>>> sbx = Sandbox.connect("687f911eaea852de79c4a50a")
>>> sbx.kill() # terminate nowidle_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 passerNonepour 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=Trueexpose explicitement votre jeton HF au code exécuté dans le bac à sable (sous la formeHF_TOKEN). Même avecforward_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 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.
>>> 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
hiChaque create() renvoie un 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 :
>>> 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 :
>>> sbx = pool.create(env={"SEED": "42"}, idle_timeout="5m", forward_hf_token=True)Un bac à sable mutualisé est un 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() accepte donc un ensemble d’arguments plus restreint que 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 :
>>> 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() — 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 :
>>> pool = SandboxPool.connect("pool-ae9f7efe0bc7") # from anywhere, no config needed
>>> sbx = pool.create()Un pool connect()é ne possède pas les hôtes partagés (d’autres clients peuvent les utiliser), de sorte que — comme pour Sandbox.connect() — la sortie de son bloc with (ou l’appel de 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>.
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’isolement contre toute attaque inter-sandboxes n’est pas garanti. Pour du code mutuellement non fiable, ou pour le GPU, utilisez Sandbox.create() (une VM distincte par sandbox). Les compromis sont détaillés dans le guide conceptuel : comment fonctionne l’isolement et — plus important si vous devez choisir entre les deux modes — la liste complète des limitations connues.
Depuis l'interface en ligne de commande
La commande hf sandbox reproduit l’API Python. Un bac à sable dédié :
>>> 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 687f911eaea852de79c4a50ahf 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 :
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>)) :
>>> 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 1234Pour de nombreux sandboxes partagés bon marché, préchauffez un pool une fois, puis créez des instances à la demande :
# 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é).