Hub Python Library documentation
Sandboxes por baixo dos panos
Sandboxes por baixo dos panos
Este guia explica como os Sandboxes funcionam internamente, por que são construídos da forma em que são e quais são suas limitações. Se você apenas deseja usar sandboxes, o guia de Sandboxes é suficiente; continue lendo se quiser entender o mecanismo, avaliar o modelo de confiança ou depurar algo.
A API do Sandbox é experimental. Sua API e comportamento podem mudar sem aviso prévio.
Não existe um “serviço de sandbox”
A primeira coisa a entender é que não existe um backend de sandbox dedicado. Uma sandbox é apenas um HF Job (uma VM) executando um único binário estático pequeno sbx-server que fala HTTP. O cliente se comunica com esse servidor através do proxy de Jobs (a URL *.hf.jobs que o Job expõe). Tudo o mais — autenticação, descoberta, empacotamento de muitas sandboxes em um único Job — é construído a partir de primitivas existentes de Jobs: rótulos, variáveis de ambiente e segredos.
Esse design de “sem nova infraestrutura” é a razão pela qual um sandbox funciona em qualquer imagem Docker e herda gratuitamente a cobrança, os sabores de hardware e as permissões de namespace dos Jobs.
Inicializando o servidor
At job startup, the Job’s command is a small /bin/sh -c script that fetches the sbx-server binary, makes it executable, and execs it. The binary is a ~640KB static musl build with zero runtime dependencies, so it runs in any x86_64 Linux image.
Algumas decisões que valem a pena destacar:
- Download, com fallback para mount. O caminho rápido baixa o binário do CDN do HF com
wgetoucurl(toda imagem base comum inclui um deles), o que é rápido e gratuito. Como rede de segurança, o repositório Hub do servidor também é montado como volume em cada job: se a imagem não tiver nemwgetnemcurl, o script copia o binário a partir dessa montagem. A montagem é transparente — não custa nada a menos que seja efetivamente lida —, mas lê-la adiciona ~2-3s ao cold start, motivo pelo qual não é o padrão. O único requisito rígido para a imagem é/bin/sh. - Um servidor HTTP/1.1 feito à mão, sem framework. O streaming de saída ao vivo exige que cada chunk seja enviado (flush) assim que for produzido. Servidores HTTP mínimos em Rust (por exemplo,
tiny_http) fazem buffer de respostas em chunks até a conclusão da resposta, o que quebra o streaming. Portanto, o servidor implementa o HTTP/1.1 manualmente: streams de eventos NDJSON paraexec, corpos brutos para arquivos e um flush explícito por chunk. - Porta 49983. O servidor escuta em uma porta intencionalmente incomum para que as portas de desenvolvimento comuns (3000, 8000, 8080, …) fiquem livres para o seu próprio código.
O servidor é de código aberto em github.com/huggingface/sandbox-server.
A autenticação é sem estado
Duas camadas independentes protegem um sandbox:
- O gate do proxy. O proxy de Jobs apenas encaminha solicitações que carregam um token do HF com acesso de leitura ao namespace do job. Um membro aleatório da internet não consegue acessar a URL.
- O portão da aplicação.
sbx-serververifica adicionalmente um cabeçalhoX-Sandbox-Tokenem todas as solicitações, exceto/health. Isso é defesa em profundidade: um membro de namespace somente leitura que consegue acessar o proxy ainda não pode executar comandos.
O token do sandbox é derivado, não armazenado:
nonce = random 128-bit hex # stored in the job label "hf-sandbox-nonce" token = HMAC-SHA256(key=your_hf_token, msg="hf-sandbox:" + nonce)
O nonce é público e a chave HMAC é seu token do HF, portanto qualquer máquina que possua esse token pode recalcular o token do sandbox — é exatamente isso que torna a reconexão sem estado. Isso também significa que o token é uma capacidade derivada da sua credencial, e não uma verificação de identidade do chamador: o servidor verifica que o chamador conhece o token, e não quem ele é.
Cada job de sandbox também carrega dois rótulos estáveis para descoberta — hf-sandbox=1 (em todos eles) e hf-sandbox-mode=dedicated ou hf-sandbox-mode=pool — para que você possa listá-los ou filtrá-los no servidor, por exemplo, hf jobs ps --label hf-sandbox=1.
O token é enviado ao servidor por meio de um segredo de Job. O cliente o re-deriva sob demanda a partir do nonce público no rótulo. Isso tem algumas consequências positivas:
- Reconexão sem estado.
Sandbox.connect(id)funciona a partir de qualquer máquina que possua o mesmo token do HF — leia o nonce do rótulo e recalcule o token. Sem arquivos locais, sem estado para copiar. - O token do HF não é passado para o sandbox como variável de ambiente ou segredo de job (a menos que você opte por isso com
forward_hf_token=True). Isso não é uma garantia absoluta de que suas credenciais fiquem fora de alcance: o processo que escuta na porta do sandbox é o que a imagem inicia primeiro, portanto, uma imagem não confiável pode ser capaz de observar as requisições enviadas pelo cliente — incluindo seu cabeçalhoAuthorization. Trate credenciais acessíveis a partir de um sandbox como potencialmente expostas a ele.
Escopo do token
Um nonce é emitido por Job, portanto o token derivado é uma credencial de job. No modo dedicado, o job é o sandbox, de modo que os dois coincidem. No modo pool, o job é o host, e um host abriga muitos sandboxes — assim, um sandbox em pool recebe uma segunda credencial própria, mais restrita:
| credencial | como foi derivado | autoriza |
|---|---|---|
| token de sandbox dedicado | HMAC(hf_token, nonce) do rótulo do job | aquele sandbox (que é o trabalho inteiro) |
| token do host do pool | HMAC(hf_token, nonce) do rótulo do trabalho do host | gerenciamento de pool: criar, listar e excluir sandboxes naquele host, e recuperar seus tokens |
| token de sandbox agrupado | 256 bits aleatórios, gerados pelo servidor host para cada sandbox | aquele sandbox — não um irmão, não o pool |
O cliente usa o estreito automaticamente: pool.create() o recebe na resposta de criação, Sandbox.connect("<host>.<id>") o recupera com o token do host e proxy_headers fornece o token do sandbox em vez do do host (resolvendo o bearer do HF no momento em que você o lê, para que um handle de longa duração não forneça um obsoleto) — esses cabeçalhos geralmente acabam em um navegador ou cliente WebSocket, então devem conceder acesso a um único sandbox e nada mais.
O que isso significa para uma falha de segurança: um token de sandbox em pool compromete aquela sandbox. Um token do host compromete o host — todas as sandboxes nele, atuais e futuras, além de suas rotas de gerenciamento —, portanto, trate-o como a credencial de administrador da pool. Os membros do seu namespace possuem um token HF diferente e não podem derivar o seu, mas consulte Limitações conhecidas para saber como um token pode ser entregue ao local errado.
O cliente requer um token não vazio com escopo de sandbox para operações em pool e recusa hosts que não forneçam um. Ele nunca substitui a credencial de gerenciamento do host. Atualize o cliente e recicle hosts de pool mais antigos antes de usar esta API.
Sandboxes dedicados ( Sandbox.create )
O modelo direto: um Job por sandbox. Como um Job é uma VM real, isso oferece o isolamento mais forte (nível de VM), suporta qualquer tipo de hardware, incluindo GPUs, e é a escolha certa para código mutuamente não confiável. As rotas da API ficam em /v1/* e os caminhos de arquivo são absolutos no sistema de arquivos do container. kill() simplesmente cancela o Job.
O limite que um sandbox dedicado oferece é entre o Job e tudo o que está fora dele. Dentro da VM, seu código e o sbx-server executam ambos como root no mesmo container: o sbx-server não isola o comando que executa para você, ele apenas o expõe via HTTP. Portanto, uma vez que código não confiável tenha sido executado em um sandbox dedicado, trate aquela VM — incluindo seu plano de controle — como pertencente a esse código: ele pode matar ou substituir o servidor, e qualquer coisa que você fizer naquele sandbox depois disso será executada sob sua mercê. Use um sandbox dedicado por carga de trabalho não confiável e kill() quando terminar, em vez de reutilizá-lo entre fronteiras de confiança.
O custo está ali no diagrama: cada sandbox paga um cold start de VM de ~6s e cobra uma máquina inteira. Para uma única sandbox ou uma carga de trabalho em GPU, é exatamente o que você quer. Para 100–1000 tarefas CPU curtas, é desperdício — e é para isso que existem os pools.
Pools: muitos sandboxes em um único Job ( SandboxPool )
Pools são para workloads dentro de uma única fronteira de confiança. Um sandbox em pool é um uid + um conjunto de regras Landlock dentro de uma VM compartilhada, não uma VM própria, e seu host executa um plano de controle privilegiado com o qual todos os seus sandboxes se comunicam. Use pools para distribuir seu próprio código de forma econômica. Para workloads que se desconfiam mutuamente — qualquer coisa que você não permitiria ler os arquivos dos seus outros sandboxes — use Sandbox.create(), que dá a cada workload sua própria VM. Veja Limitações conhecidas para as lacunas específicas.
Um rollout típico de RL ou um sandbox de execução de ferramentas requer alguns MB de RAM e um núcleo por alguns segundos. Pagar por uma VM de 2 vCPUs e um cold start de 6s para cada uma — e disparar um burst de agendamento de 1000 VMs — é o trade-off errado. Portanto, o SandboxPool executa um Job como host e multiplexa muitos sandboxes dentro dele.
Uma sandbox agrupada não é uma VM ou um container aninhado. É o primitivo multiusuário clássico do Unix:
- um uid dedicado (≥ 20000),
- um diretório
0700privado pertencente a esse uid, - commands
exec‘d como esse uid com um ambiente limpo (env_clear, para que os segredos do host nunca vazem),NO_NEW_PRIVS, rlimits por processo e um conjunto de regras Landlock por sandbox.
Portanto, a criação de um sandbox é mkdir + chown + build ruleset ≈ 1ms no servidor — sem a inicialização de uma segunda VM. A única latência visível para o cliente é o tempo de ida e volta do proxy.
A pooled sandbox’s public id is <host_job_id>.<local_id>, so connect/exec/kill work statelessly just like dedicated ones. kill() on a pooled sandbox sends a DELETE to its host (freeing a slot); the host keeps running.
Isolamento em um pool: uid + Landlock
Este é o ponto central do design do pool, portanto, vale a pena ser preciso sobre o que é e o que não é isolado.
Um Job padrão é executado como root dentro de um namespace de usuário que mapeia apenas os uids 0..65535, com um filtro seccomp ativado e sem CAP_SYS_ADMIN / CAP_NET_ADMIN / CAP_NET_RAW. Isso elimina as ferramentas de isolamento pesadas habituais: sem namespaces aninhados, sem novos mounts, sem delegação de cgroup (unshare, mount, escrita em /sys/fs/cgroup/... falham). O que o kernel oferece é o Landlock (ABI 6), um Linux Security Module que permite a qualquer processo não privilegiado restringir a si mesmo e aos seus filhos — exatamente o limite por sandbox de que precisamos. Para cada sandbox, o servidor constrói um ruleset; o filho do exec rebaixa-se para o uid/gid da sandbox e aplica NO_NEW_PRIVS, landlock_restrict_self e rlimits antes de executar o comando.
A combinação de uids distintos (controle de acesso discricionário) com Landlock é projetada e testada para fornecer:
- ✅ A não pode ler o
environde outro processo, impedindo o acesso direto a tokens de HF e de sandbox entre sandboxes. - ✅ A não pode
SIGKILL/ptrace/ ler a memória dos processos de B,setuidpara B, ou ler o home0700de B. - ✅ O acesso a
/tmpe/dev/shmé negado — cada sandbox é confinada pelo Landlock ao seu próprio diretório home (oTMPDIRdela aponta para dentro de$HOME). - ✅ A não pode
binduma porta TCP, portanto não há serviço de localhost entre sandboxes (oconnectde saída continua permitido, então a internet funciona). - ✅ Sockets unix abstratos entre sandboxes são bloqueados (
LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET; o isolamento por uid sozinho não bloqueia esses sockets).
Esses controles restringem a carga de trabalho. O plano de controle raiz permanece como uma fronteira de confiança separada: suas operações de arquivo em modo de host recusam links simbólicos, seu proxy de socket fixa o inode do socket e verifica o uid do par, e as rotas dedicadas não estão disponíveis no modo de host. Os canais compartilhados residuais são descritos em Limitações conhecidas.
Por que isso não substitui uma VM. O isolamento por Landlock e uid é destinado a cargas de trabalho dentro do mesmo limite de confiança. Como os sandboxes em pool compartilham um kernel, uma VM e um plano de controle, a proteção contra todos os ataques entre sandboxes não é garantida; recursos e alguns metadados da lista de processos também permanecem compartilhados. Para código mutuamente não confiável, ou para GPU, use Sandbox.create(), que fornece a cada sandbox sua própria VM.
O arquivo de modelo em um pool
Because a pooled sandbox’s only writable area is its Landlock-confined home (which is also its default working directory), the file API roots every path at that home: files.write("data/in.txt", ...) writes to $HOME/data/in.txt, a leading / is taken relative to the home, and .. cannot escape it. Files written through the API are chowned to the sandbox’s uid so the sandbox’s own code can read them. This gives a clean “filesystem rooted at the sandbox” model that matches exactly what code inside the sandbox can touch — and differs from dedicated sandboxes, where paths are absolute on the container filesystem.
Os caminhos são normalizados lexicograficamente e, em seguida, resolvidos em relação a um descritor de diretório home aberto, com recusa de symlinks em cada componente. A API de arquivos não segue symlinks, incluindo links dentro do mesmo home; o código do sandbox ainda pode usá-los sob suas regras Landlock.
Pools não possuem estado local autoritativo
Uma pool não é, de propósito, um arquivo de configuração local. Uma pool é o conjunto de seus Jobs em execução nos hosts, todos compartilhando o rótulo hf-sandbox-pool=<id>. Isso mantém as pools consistentes com o restante da API de sandbox (tudo é descobrível por meio de rótulos e pode ser reanexado de qualquer máquina) e significa que uma pool simplesmente deixa de existir assim que seu último host é removido.
- Um host carrega a configuração do pool (imagem, flavor,
sandboxes_per_host, tempo limite de inatividade) em suas variáveis de ambiente de job — os rótulos são usados apenas para filtragem. Quando um cliente precisa iniciar um host duplicado, ele lê essa configuração de volta de um host em execução (inspect_job), de modo que todos os hosts em um pool permaneçam consistentes sem um registro central. - Env e secrets são por sandbox, passados no momento da criação — nunca em nível de pool. O ambiente de uma sandbox em pool é mantido na memória do servidor host durante o ciclo de vida da sandbox (ele é reaplicado a cada comando que você executa); ele nunca é gravado em disco e nunca aparece nos metadados de nenhum Job. Não há um canal de secrets criptografados para sandboxes em pool — use
enve trate esses valores como “não em repouso, mas também não criptografados”. Para valores que precisam do armazenamento criptografado, use osecretsde uma sandbox dedicada. - A capacidade é autoritativa no servidor. Um host recusa criações que excedam
sandboxes_per_host(respondendo com{"rejected": N}); o cliente empacota o excedente em outro host ou inicia um duplicado. Isso mantém o empacotamento exato mesmo quando vários processos criam simultaneamente no mesmo pool. - A evicção por inatividade é de dois níveis. Cada sandbox é evictada após seu próprio
idle_timeoutde inatividade (a menos que ainda tenha um processo em execução); uma vez que um host não tenha sandboxes pelo tempo de inatividade do host, ele desliga a si mesmo — uma trava de faturamento mesmo que todos os clientes desapareçam.
Um cache de melhor esforço mantém o create --pool rápido
Não ter um estado local autoritativo é ótimo para a correção, mas custa latência. Um hf sandbox create --pool <id> frio (um novo processo de CLI) teria que redescobrir tudo pela rede antes de poder criar um sandbox: list_jobs para varrer o namespace → inspect_job em cada host para reconstruir sua URL e nonce → GET /v1/sandboxes para ver o quão cheio cada um está → finalmente POST para criar. Vários round-trips de puro overhead, em cada chamada.
Um cache de melhor esforço em $HF_HOME/sandbox/pools/<context>/<pool-id>.json elimina isso. Após qualquer criação/aquecimento, um processo registra a configuração da pool e, por host, sua URL de proxy, nonce de autenticação e os slots livres vistos pela última vez. O próximo processo reconstrói o transporte do host diretamente do arquivo (sem HTTP) e vai direto para o POST.
<context> é um resumo do endpoint, da credencial e do namespace para os quais a entrada foi gravada — a credencial como uma impressão digital; o próprio token nunca é gravado no cache. Tanto o nome do diretório quanto o arquivo o contêm, e uma leitura que não corresponder a todos os três é um simples cache miss, de modo que outro usuário da máquina, outro endpoint ou um connect(pool, namespace="other-org") obtém o caminho frio em vez de hosts que foram cacheados para outra coisa. Os arquivos são criados com 0600 e os diretórios com 0700.
O que a cache é e não é confiável para:
- Não é autoritativo em relação à capacidade. O servidor dentro do job permanece autoritativo, portanto, uma contagem
livedesatualizada apenas custa uma solicitação desperdiçada, nunca um host mal empacotado. - Não confiável quanto à localização do host. Uma URL em cache só é usada se for a URL do jobs-proxy HTTPS do mesmo job nomeado na entrada correspondente, de modo que um arquivo de cache não pode escolher o destino para o qual seu bearer HF e o token de sandbox são enviados; uma entrada que nomeie qualquer outra coisa é descartada do arquivo. Após 15 minutos, uma entrada também não é mais creditada por si só:
inspect_jobprecisa confirmar que o job ainda está em execução, ainda rotulado para este pool e ainda naquela URL antes que o transporte seja construído. - Não confiável em relação à sua forma. Os tipos e intervalos são verificados na leitura, de modo que um arquivo editado manualmente, truncado ou com tipagem baseada em strings resulta em um cache miss, em vez de um erro no meio de
create(). - Auto-recuperação. Um host em cache que não existe mais é descartado na primeira solicitação com falha e removido do arquivo; a criação recorre de forma transparente à descoberta por rótulo.
- Seguro para concorrência. As gravações são mescladas sob um lock de arquivo (chaveado por
job_id) e commitadas atomicamente, de modo que processos paralelos decreatenão sobrescrevam uns aos outros e os leitores nunca vejam um arquivo parcialmente escrito. - Descartável. Exclua-o e você terá um cache miss: o caminho frio é executado e tudo continua funcionando. Ele nunca é compartilhado entre máquinas.
Duas coisas a ter em mente. Trate $HF_HOME como sensível: uma entrada ainda é uma URL de host reutilizável mais o nonce público do qual seu token deriva, e embora um arquivo escrito sob uma credencial diferente seja ignorado, um escrito sob a sua é creditado para a janela de 15 minutos acima. E porque o namespace faz parte da chave, um pool em cache sob um namespace= explícito só é encontrado novamente quando o mesmo namespace= é passado — hf sandbox create --pool <id> exige o mesmo --namespace com o qual o pool foi criado, ou segue o caminho frio e procura no seu próprio namespace.
Limitações conhecidas
Os pools são para cargas de trabalho dentro de uma única fronteira de confiança. Use sandboxes dedicados para código que você não confia. As verificações abaixo reduzem riscos específicos; elas não transformam sandboxes em pool em VMs separadas.
Visão geral do modelo de ameaças
| Ator | Dedicado | Pool |
|---|---|---|
| Usuário anônimo da internet | O proxy de Jobs requer um token do HF com acesso de leitura ao namespace. | Igual. |
| Membro do namespace com acesso de leitura | É possível acessar o proxy e a verificação de saúde pública, mas é necessário o token do sandbox para as operações de API. | Igual. |
| Membro do namespace que pode criar Jobs | Não é possível derivar o token de outro usuário a partir de seu nonce. | É possível copiar rótulos de pool, mas a adoção padrão verifica o iniciador da API de Jobs. adopt_hosts="namespace" opta por confiar em outros criadores. |
| Código executado no sandbox | Compartilha a raiz com o servidor dentro da VM; não reutilize essa VM entre fronteiras de confiança. | Confinado por uid e Landlock, mas compartilha o kernel, a VM e o plano de controle privilegiado. |
| Seu cliente | Armazena as credenciais da HF e da sandbox dedicada. | Armazena a credencial de gerenciamento do host e as capacidades separadas por sandbox. |
Isolamento e lacunas de recursos
- A descoberta de hosts não é atestação. O padrão
adopt_hosts="own"verifica o iniciador, a imagem, o sabor, o comando e a URL exposta. Ele não atesta o binário em execução nem torna uma imagem mutável confiável. Useadopt_hosts="namespace"apenas com membros de namespace nos quais você confia. - As credenciais do host permanecem poderosas. Elas gerenciam o pool e recuperam o token de cada sandbox. Elas nunca são substituídas por tokens escopados ausentes por este cliente. Hosts inicializados por esta versão executam o sbx-server 0.7.0, que as recusa em rotas escopadas; hosts iniciados por clientes mais antigos mantêm seu servidor (e a compatibilidade com o token do host) até serem reciclados.
- Os canais compartilhados permanecem. O TCP de saída, o acesso loopback ao servidor de controle, o UDP, o IPC do kernel e os metadados legíveis da lista de processos não são isolados. Os diretórios do sistema e os nós de dispositivo selecionados permanecem acessíveis. O isolamento do pool de GPUs não foi testado; use o modo dedicado para GPUs.
- Os limites não são cotas agregadas. Os rlimits por processo e as filas de saída limitadas reduzem a amplificação de recursos. Eles não particionam as fatias de CPU, a memória total, o espaço em disco, os inodes ou a capacidade de rede entre os sandboxes. A paginação de diretórios limita as respostas, mas ainda materializa o diretório no servidor. As gravações de arquivos via API não são uma cota de disco, e as gravações em modo dedicado em arquivos especiais podem bloquear.
- O confinamento pode ser enfraquecido explicitamente. O modo host requer o ABI 6 do Landlock por padrão e recusa um conjunto de regras com falha. Reduzir
SBX_MIN_LANDLOCK_ABIou usar a flag de desenvolvimento--allow-unconfineddo servidor aceita menos garantias; inspecione o/healthautenticado para o ABI e os recursos efetivos. - Conexões de proxy são autenticadas apenas uma vez. Após a primeira solicitação, o proxy copia os bytes para esse backend até o EOF. Solicitações HTTP subsequentes nessa conexão não são autenticadas ou roteadas de forma independente. A compatibilidade com WebSocket/SSE não estabelece segurança contra a reutilização de conexões upstream.
Ciclo de vida e credenciais
- Descendentes desconectados podem sobreviver à terminação do processo ou ao tempo limite. Ambos sinalizam o grupo de processos; um descendente que chama
setsid()foge desse grupo. Exclua o sandbox em pool ou encerre o Job dedicado para encerrar toda a carga de trabalho. max_hostsé uma tentativa de melhor esforço. Clientes separados podem contar os mesmos hosts e ambos provisionarem mais. É necessária admissão do backend ou idempotência para um limite compartilhado rígido.- A desmontagem pode falhar. Cancelamentos de host com falha lançam exceções e retêm registros de cache.
close()limita a espera por criações em andamento; uma criação que exceda esse tempo de espera pode deixar um Job faturável. Use a API de Jobs ou a CLI para conciliar os recursos em execução. - A rotação de Bearer não rotaciona as capacidades do sandbox. A autenticação do proxy resolve o token HF atual por solicitação, mas a reconexão sem estado deriva um token de host a partir do valor exato do bearer original. Rotacionar esse valor pode impedir a reconexão a hosts existentes.
- O cache local permanece como entrada confiável por 15 minutos. Sua URL e contexto de segurança são verificados, mas ele não possui MAC de integridade; proteja
$HF_HOMEcontra gravações por código não confiável. - Um pin de digest não é uma proveniência assinada. O cliente verifica um binário específico do servidor. A publicação e a atualização desse pin continuam sendo etapas de liberação; o manifesto do fluxo de trabalho não é uma atestação criptográfica. A saída de emergência explícita
SBX_ALLOW_UNVERIFIED_SERVER=1ignora a verificação apenas quando ambas as ferramentas de hash estão ausentes.
Não autenticado /health expõe apenas liveness e protocolo. Metadados detalhados exigem a credencial do host. IDs de sandbox e tokens exigem a fonte aleatória do kernel; a reutilização de UID segue a limpeza verificada do processo e do home.
Desempenho
Todos os números foram medidos em relação a Jobs reais da HF em cpu-basic, com o cliente em um laptop e todo o tráfego passando pelo proxy dos Jobs.
Sandbox dedicado:
| métrica | value |
|---|---|
início a frio (create() retorna, servidor respondendo) | ~5,8s mediana |
run() ida e volta | p50 ~110ms (o piso do RTT do proxy é ~105ms; sobrecarga do cliente ≈ 0) |
| transferência de arquivo (intervalos paralelos, >8 MiB) | ~340 MiB/s de download, ~441 MiB/s de upload |
Pool (modo compartilhado/host):
| N sandboxes | hosts (50/host) | provisionar + criar tudo | exec em todos | kill all | total |
|---|---|---|---|---|---|
| 100 | 2 | 6.1s | 1.5s | 0.6s | 8.2s |
| 1000 | 20 | 7.4s | 4.2s | 4.2s | 15.8s |
1000 sandboxes criados, executados e encerrados em ~16s custam aproximadamente um cold start de um host (~6s) amortizado entre todos eles — cerca de $0,0009 no total (20 × cpu-basic), em comparação com ~$0,06 e um pico de agendamento de 1000 VMs para um Job por sandbox. As operações de criação/execução/exclusão no servidor levam ~1ms cada; o orçamento é inteiramente o round-trip de rede.
Decisões de design, resumo
| decisão | por quê |
|---|---|
| Construir em Jobs, sem novo serviço | herda faturamento, hardware, permissões; funciona em qualquer imagem |
| Binário estático em Rust, baixado na inicialização | no Python/pip; ~6s de inicialização a frio vs 30–90s para um bootstrap baseado em pip |
| HTTP/1.1 feito à mão | frameworks mínimos bufferizam respostas em blocos e quebram o streaming ao vivo (verificado) |
| Autenticação HMAC sem estado | reconectar de qualquer lugar; um token derivado em vez do próprio token do HF (com escopo por job — veja Escopo do token) |
run() levanta uma exceção em caso de saída não nula (check=False desativa esse comportamento) | melhor DX para loops de “executar código, ver o erro” (estilo E2B) |
idle_timeout watchdog em vez de limpeza no lado do cliente | sandboxes persistentes são um recurso; as vazadas ainda assim são encerradas |
| Pools = uid + Landlock, capacidade autoritativa no servidor, sem estado local | fan-out rápido para o mesmo usuário; correto sob concorrência; pode ser reanexado em qualquer lugar |