Hub Python Library documentation

Sandboxes

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v2.4.0).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Sandboxes

Sandboxes são um recurso experimental. A API, os padrões e o comportamento podem mudar sem aviso prévio. Sandboxes compartilhados são destinados a cargas de trabalho dentro do mesmo limite de confiança; seu isolamento não garante proteção contra todos os ataques entre sandboxes. Use sandboxes dedicados para cargas de trabalho que não confiam umas nas outras e evite disponibilizar credenciais de longa duração ou com escopo amplo para cargas de trabalho em sandboxes.

Um sandbox é uma máquina em nuvem isolada que você pode iniciar em segundos, executar comandos com saída transmitida ao vivo e mover arquivos para dentro e para fora — tudo a partir do Python ou da CLI. Os sandboxes são construídos sobre os Jobs: por baixo, um sandbox é apenas um Job executando um servidor pequeno que expõe a execução de comandos e a transferência de arquivos via HTTP.

Eles são uma boa opção sempre que você precisar executar código em outro local que não seja sua própria máquina:

  • Executando código não confiável ou gerado por IA — permita que um agente execute código arbitrário sem conceder a ele acesso ao seu sistema de arquivos. Use um sandbox dedicado para isso: ele é uma VM própria.
  • Compilações e experimentos reproduzíveis — execute em uma imagem limpa e bem definida, em CPU ou GPU.
  • Distribuir o trabalho — inicie centenas de ambientes paralelos (rollouts de RL, avaliação, execução em lote de ferramentas) de forma econômica.

Qualquer imagem Docker com /bin/sh funciona — não é necessário ter Python, pip ou agente pré-instalados (um pequeno binário de servidor estático é injetado na inicialização).

Curioso sobre como isso funciona por baixo dos panos — o servidor dentro do Job, a autenticação stateless ou como um único Job hospeda muitos sandboxes isolados? Veja o guia conceitual de Sandboxes.

Os dois tipos de sandbox

Existem duas formas de obter um sandbox. Ambas entregam o mesmo objeto Sandbox (mesmos run, files, connect, kill); elas diferem apenas em como a máquina subjacente é alocada:

Sandbox.create() — dedicadoSandboxPool — compartilhado / pool
mapeamentoum Job = um sandbox (uma VM inteira)um Job = muitos sandboxes (uma VM, empacotada)
isolamentoVM completouid + Landlock (confiança do mesmo usuário)
início a frio~6s por sandbox~6s para o primeiro host, depois ~1 ida e volta cada
custouma VM por sandboxuma VM por host, amortizada entre muitos sandboxes
GPU✅❌ (somente CPU)
melhor paraum único sandbox, cargas de trabalho em GPU, código não confiávelmuitos sandboxes de CPU baratos (rollouts de RL, fan-out)

Regra prática: precisa de uma GPU ou de executar código de fontes não confiáveis entre si → dedicado. Precisa de centenas de sandboxes de CPU baratas → um pool.

Início rápido

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

Escolha qualquer imagem e sabor de hardware:

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

Executando comandos

Sandbox.run() executa um comando e aguarda por ele. Passe uma string de shell ou uma lista 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)

Por padrão, o modo é inferido a partir do tipo (uma string é executada por meio de /bin/sh -c, enquanto uma lista é executada diretamente como argv). Passe shell= para tornar isso explícito — útil para evitar o clássico erro em que uma lista de um único elemento, como ["echo hi"], é executada como um único programa chamado "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 requer uma string e shell=False requer uma lista; passar o tipo errado levanta um ValueError.

Um comando que termina com um código de saída diferente de zero levanta SandboxCommandError (com stdout, stderr e exit_code anexados). Passe check=False para receber o SandboxCommandResult de volta em vez de levantar a exceção:

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

Processos em segundo plano

Pass background=True para iniciar um processo de longa duração (um servidor, um watcher, uma execução de treinamento) sem esperar por ele. run retorna um SandboxProcess imediatamente em vez de um 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)

Liste os processos de um sandbox e pare um deles quando terminar. Os processos concluídos permanecem listados (com running=False e seu exit_code) até que o sandbox seja excluído, permitindo verificar se um processo ainda está ativo ou já foi encerrado:

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

As opções de streaming/apenas espera (timeout, stdin, on_stdout, on_stderr, check) não se aplicam no modo em segundo plano — apenas env, cwd e shell são respeitados.

Arquivos

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

Outros auxiliares: stat, exists, mkdir, delete.

Acessando um servidor dentro de um sandbox

Inicie um servidor no sandbox (em segundo plano) e, em seguida, acesse-o de fora com Sandbox.proxy_url_for() — a requisição é encaminhada pelo servidor de sandbox do job para o seu servidor interno, de modo que não há necessidade de expor uma porta pública adicional. Funciona para HTTP simples, Server-Sent Events e WebSocket. Combine a URL com Sandbox.proxy_headers para autenticação (seu cliente WebSocket/HTTP deve enviá-los):

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

Ao usar proxy_headers, desative os redirecionamentos no seu cliente HTTP (follow_redirects=False com httpx, ou allow_redirects=False com requests). Um aplicativo pode redirecionar para outra origem, e esses cabeçalhos de autenticação personalizados não devem ser encaminhados para lá.

Sandeboxes em pool exigem um servidor que retorne tokens de capacidade por sandbox. Tokens ausentes são um erro; atualize e recicle o host em vez de usar seu token de gerenciamento.

Como o servidor interno deve escutar depende do tipo de sandbox:

  • Dedicado (Sandbox.create()): vincule uma porta TCP normal em 127.0.0.1:<port>.
  • Pool / compartilhado (SandboxPool): sandboxes em pool não podem vincular uma porta TCP (Landlock), então escute em um socket unix em $SBX_PROXY_DIR/<port>.sock (essa variável de ambiente é definida em cada sandbox), por exemplo, uvicorn app:app --uds $SBX_PROXY_DIR/8000.sock. O lado do cliente (proxy_url_for / proxy_headers) é idêntico em qualquer caso.

Ciclo de vida

Um sandbox sobrevive ao processo que o criou — você pode criá-lo agora e se reconectar depois, de qualquer máquina que possua o mesmo token do HF, sem precisar copiar nenhum estado:

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

# Later, from anywhere:
>>> sbx = Sandbox.connect("687f911eaea852de79c4a50a")
>>> sbx.kill()           # terminate now
  • idle_timeout (padrão 10 minutos) é o verdadeiro guardião: ele desliga o sandbox quando nenhuma chamada de API é feita e nenhum processo está em execução, para que sandboxes abandonados parem de gerar cobrança. Defina-o no momento da criação (Sandbox.create(idle_timeout="30m")) ou passe None para desativar.
  • O trabalho também possui uma vida útil máxima fixa de 24 horas como limite rígido (não configurável).
  • forward_hf_token=True expõe explicitamente seu token do HF para o código executado no sandbox (como HF_TOKEN). Mesmo com forward_hf_token=False, não trate o sandbox como uma fronteira rígida para suas credenciais — veja o aviso no topo desta página.

Muitos sandboxes ao mesmo tempo: SandboxPool

Quando você precisa de muitos sandboxes (rollouts de RL paralelos, avaliação em leque, execução em lote de ferramentas), usar um Job por sandbox é desperdício: cada um paga o custo de um cold start completo de VM e ocupa uma máquina inteira para uma carga de trabalho que precisa de poucos MB de RAM. O SandboxPool agrupa muitos sandboxes leves em alguns Jobs de host compartilhados — uma única VM cobrada atende a dezenas de sandboxes, de modo que o custo por sandbox diminui nesse fator e o cold start por sandbox é de aproximadamente um round-trip de rede.

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

Cada create() retorna um Sandbox completo; chame-o repetidamente para distribuir as tarefas. O pool inicia Jobs no host conforme necessário, empacota sandboxes_per_host sandboxes por host e encerra tudo no close() (ou quando um host fica ocioso, como uma medida de segurança para faturamento). O padrão típico de distribuição:

>>> 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 e idle_timeout são por sandbox (pertencem a create(), não ao pool), então os sandboxes em um pool podem ter ambientes diferentes:

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

Um sandbox agrupado é um Sandbox completo, mas alguns inputs são fixados pelo host em vez de serem específicos de cada sandbox — portanto, SandboxPool.create() aceita um conjunto menor de argumentos do que Sandbox.create():

EntradaSandbox.create (dedicado)SandboxPool.create (com pool)
image, flavorpor sandboxfixado pelo pool (definido uma vez no host)
volumespor sandboxindisponível (montado na inicialização do host)
envpor sandboxpor sandbox
idle_timeoutpor sandboxpor sandbox
forward_hf_tokenpor sandboxpor sandbox

Os sandboxes agrupados compartilham um host job de longa duração, portanto não há um canal de Job-secrets criptografados como os sandboxes dedicados recebem. Mas o env de um sandbox agrupado é entregue ao servidor host na criação do sandbox (não no início do job), portanto nunca é armazenado nos metadados de nenhum job — passe os segredos pretendidos como env simples.

Crescimento sob demanda e pré-aquecimento

pool.create() cria um sandbox por vez, reutilizando um host que ainda tem capacidade livre antes de iniciar um novo — assim, você pode criar sandboxes conforme o trabalho chega e eles se acomodam em hosts já aquecidos:

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

Para evitar o início frio do host nas primeiras chamadas, provisione os hosts com antecedência usando warm_up=N (iniciado no primeiro create()) ou chamando pool.warm(N) previamente.

Reutilizando pools entre processos e máquinas

Hosts quentes são descobertos por meio de rótulos de job, de modo que a reutilização funciona entre processos: um SandboxPool totalmente novo com o mesmo image/flavor/name se conecta aos hosts que uma execução anterior deixou em execução, em vez de iniciar os próprios. Passe um name= para evitar que pools distintos compartilhem hosts.

Para reanexar de outra máquina sem estado local, reconecte pelo id do pool com SandboxPool.connect() — ele encontra um host em execução, reconstrói a configuração do pool (imagem, flavor, densidade de empacotamento) a partir do job desse host e fica pronto para create() mais:

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

Um pool com connect() não é proprietário dos hosts compartilhados (outros clientes podem estar usando-os), portanto — assim como Sandbox.connect() — sair do bloco with (ou chamar close()) apenas libera os clientes HTTP locais e deixa os hosts em execução. Encerre os hosts de um pool explicitamente com pool delete / hf sandbox pool delete <id>.

Os sandboxes dentro de um host são separados por uids distintos e conjuntos de regras Landlock por sandbox. Sandboxes compartilhados são destinados a cargas de trabalho paralelas do próprio usuário, e o isolamento de todos os ataques entre sandboxes não é garantido. Para código mutuamente não confiável, ou para GPU, use Sandbox.create() (uma VM separada por sandbox). Os trade-offs são detalhados no guia conceitual: como o isolamento funciona e — mais importante se você estiver escolhendo entre os dois modos — a lista completa de limitações conhecidas.

Da CLI

O comando hf sandbox espelha a API em Python. Um sandbox dedicado:

>>> 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 transmite a saída em tempo real e encerra com o código de saída do comando, o que permite sua composição em scripts:

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

Inicie um processo de longa duração em segundo plano com hf sandbox spawn (imprime seu pid), depois liste ou pare processos. A listagem mostra o status de cada processo (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 1234

Para muitos sandboxes compartilhados de baixo custo, aqueça um pool uma vez e, em seguida, crie instâncias nele sob demanda:

# 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 produces a shared sandbox; its id looks like <host_job_id>.<local_id> and works everywhere a dedicated id does (exec, cp, kill). A pool has no local state — it is just its running host VMs, found by the pool id — so it works from any machine and stops existing once all its hosts are gone (killed or idle-timed-out).

Update on GitHub