Hub Python Library documentation
Sandboxing por debajo
Sandboxing por debajo
Esta guía explica cómo funcionan internamente los Sandboxes, por qué están diseñados de esa manera y cuáles son sus limitaciones. Si solo quieres usar sandboxes, la guía de Sandboxes es suficiente; continúa leyendo si deseas comprender el mecanismo, evaluar el modelo de confianza o depurar algún problema.
La API de Sandbox es experimental. Su API y comportamiento pueden cambiar sin previo aviso.
No existe un “servicio de sandbox”
Lo primero que hay que entender es que no existe un backend de sandbox dedicado. Un sandbox es simplemente un HF Job (una VM) que ejecuta un único binario estático pequeño, sbx-server, que habla HTTP. El cliente se comunica con ese servidor a través del proxy de Jobs (la URL *.hf.jobs que expone el Job). Todo lo demás: autenticación, descubrimiento y empaquetar muchos sandboxes en un solo Job, se construye a partir de primitivas existentes de Jobs: etiquetas, variables de entorno y secretos.
Este diseño de “sin infraestructura nueva” es la razón por la que un sandbox funciona en cualquier imagen de Docker y hereda la facturación, los sabores de hardware y los permisos de namespace de Jobs sin costo adicional.
Iniciando el servidor
Al iniciar el trabajo, el comando del Job es un pequeño script /bin/sh -c que descarga el binario sbx-server, lo hace ejecutable y lo ejecuta con exec. El binario es una compilación estática de musl de unos 640 KB, sin dependencias en tiempo de ejecución, por lo que funciona en cualquier imagen de Linux x86_64.
Algunas decisiones que merecen destacarse:
- Descarga, con un montaje como alternativa. La ruta rápida descarga el binario del CDN de HF con
wgetocurl(cualquier imagen base común incluye uno), lo cual es rápido y gratuito. Como red de seguridad, el repositorio del Hub del servidor también se monta como volumen en cada trabajo: si la imagen no tiene niwgetnicurl, el script copia el binario desde ese montaje. El montaje es transparente: no tiene ningún coste a menos que se lea realmente, pero leerlo añade unos 2-3 s al arranque en frío, por lo que no es la opción predeterminada. El único requisito estricto para la imagen es/bin/sh. - Un servidor HTTP/1.1 hecho a mano, sin framework. La transmisión de salida en tiempo real requiere vaciar cada fragmento a medida que se genera. Los servidores HTTP mínimos de Rust comunes (p. ej.
tiny_http) almacenan en búfer las respuestas por fragmentos hasta que la respuesta se completa, lo que rompe la transmisión. Por lo tanto, el servidor implementa HTTP/1.1 a mano: flujos de eventos NDJSON paraexec, cuerpos crudos para archivos y un vaciado explícito por fragmento. - Puerto 49983. El servidor escucha en un puerto deliberadamente poco común para que los puertos de desarrollo habituales (3000, 8000, 8080, …) queden libres para tu propio código.
El servidor es de código abierto en github.com/huggingface/sandbox-server.
La autenticación es sin estado
Dos capas independientes protegen un sandbox:
- La puerta de acceso del proxy. El proxy de Jobs solo reenvía las solicitudes que incluyen un token de HF con acceso de lectura al espacio de nombres del trabajo. Un miembro aleatorio de internet no puede acceder a la URL.
- La puerta de la aplicación.
sbx-servercomprueba además un encabezadoX-Sandbox-Tokenen cada solicitud, excepto en/health. Esto es una defensa en profundidad: un miembro del espacio de nombres de solo lectura que pueda acceder al proxy aún no puede ejecutar comandos.
El token de sandbox se deriva, no se almacena:
nonce = random 128-bit hex # stored in the job label "hf-sandbox-nonce" token = HMAC-SHA256(key=your_hf_token, msg="hf-sandbox:" + nonce)
El nonce es público y la clave HMAC es tu token de HF, por lo que cualquier máquina que posea ese token puede recalcular el token de sandbox; eso es exactamente lo que hace que la reconexión sea sin estado. También significa que el token es una capacidad derivada de tu credencial, no una verificación de identidad del llamador: el servidor verifica que el llamador conoce el token, no quién es.
Cada trabajo de sandbox también incluye dos etiquetas estables para su detección: hf-sandbox=1 (en todos ellos) y hf-sandbox-mode=dedicated o hf-sandbox-mode=pool, lo que permite listarlos o filtrarlos en el servidor, por ejemplo, con hf jobs ps --label hf-sandbox=1.
El token se entrega al servidor a través de un secreto de Job. El cliente lo rederiva bajo demanda a partir del nonce público en la etiqueta. Esto tiene algunas consecuencias positivas:
- Reconexión sin estado.
Sandbox.connect(id)funciona desde cualquier máquina que tenga el mismo token de HF: lee el nonce de la etiqueta y recalcula el token. No hay archivos locales ni estado que copiar. - El token de HF no se pasa a la sandbox como variable de entorno ni como secreto del trabajo (a menos que lo actives con
forward_hf_token=True). Esto no es una garantía absoluta de que tus credenciales queden fuera de alcance: el proceso que escucha en el puerto de la sandbox es el que la imagen inicia primero, por lo que una imagen no confiable podría observar las solicitudes que envía el cliente, incluido su encabezadoAuthorization. Trata las credenciales accesibles desde una sandbox como potencialmente expuestas a ella.
Alcance del token
Se emite un nonce por Job, por lo que el token derivado es una credencial de job. En el modo dedicado, el job es el sandbox, por lo que ambos coinciden. En el modo pool, el job es el host, y un host contiene muchos sandboxes; por lo tanto, un sandbox en pool obtiene una segunda credencial propia, más restringida:
| credencial | se derivó cómo | autoriza |
|---|---|---|
| token de sandbox dedicado | HMAC(hf_token, nonce) de la etiqueta del trabajo | esa sandbox (que es todo el trabajo) |
| token del host del pool | HMAC(hf_token, nonce) de la etiqueta del trabajo del host | gestión del pool: crear, listar y eliminar sandboxes en ese host, y recuperar sus tokens |
| token de sandbox agrupado | 256 bits aleatorios, generados por el servidor host para cada sandbox | esa arena — no una hermana, no el grupo |
El cliente utiliza automáticamente el estrecho: pool.create() lo recibe en la respuesta de creación, Sandbox.connect("<host>.<id>") lo recupera con el token del host y proxy_headers entrega el token del sandbox en lugar del del host (resolviendo el bearer de HF en el momento en que se lee, de modo que un handle de larga duración no entregue uno obsoleto); esas cabeceras suelen terminar en un navegador o cliente WebSocket, por lo que deberían otorgar acceso a un solo sandbox y nada más.
Lo que esto implica para una fuga: un token de sandbox agrupado compromete esa sandbox. Un token de host compromete el host: todas las sandboxes en él, actuales y futuras, además de sus rutas de gestión, por lo que debe tratarse como la credencial de administrador del pool. Los miembros de su namespace poseen un token de HF diferente y no pueden derivar el suyo, pero consulte Limitaciones conocidas para ver cómo un token puede ser entregado en el lugar equivocado.
El cliente requiere un token no vacío con ámbito de sandbox para las operaciones agrupadas y rechaza los hosts que no lo proporcionan. Nunca sustituye la credencial de gestión del host. Actualice el cliente y recicle los hosts de pool anteriores antes de usar esta API.
Sandbox dedicado ( Sandbox.create )
El modelo directo: un Job por sandbox. Un Job es una VM real, por lo que ofrece el aislamiento más fuerte (a nivel de VM), admite cualquier tipo de hardware, incluidas las GPUs, y es la opción adecuada para código que no se confía mutuamente. Sus rutas de API están en /v1/* y las rutas de archivo son absolutas en el sistema de archivos del contenedor. kill() simplemente cancela el Job.
El límite que te ofrece un sandbox dedicado es entre el Job y todo lo que está fuera de él. Dentro de la VM, tu código y sbx-server se ejecutan ambos como root en el mismo contenedor: sbx-server no aísla el comando que ejecuta por ti, solo lo expone a través de HTTP. Por lo tanto, una vez que código en el que no confías se ha ejecutado en un sandbox dedicado, considera que esa VM —incluido su control plane— pertenece a ese código: puede matar o reemplazar el servidor, y cualquier cosa que hagas en ese sandbox después se ejecuta a su merced. Usa un sandbox dedicado por cada carga de trabajo no confiable y kill() cuando termines, en lugar de reutilizar uno a través de límites de confianza.
El costo está justo ahí en el diagrama: cada sandbox paga un arranque en frío de VM de ~6s completos y factura una máquina entera. Para un solo sandbox o una carga de trabajo de GPU, es exactamente lo que se desea. Para 100–1000 tareas CPU cortas, es un desperdicio, y para eso están los pools.
Pools: muchos sandbox en un solo Job (SandboxPool)
Los pools están destinados a cargas de trabajo dentro de un mismo límite de confianza. Un sandbox en pool es un uid + un conjunto de reglas de Landlock dentro de una VM compartida, no una VM propia, y su host ejecuta un plano de control privilegiado con el que se comunican todos sus sandboxes. Usa pools para desplegar de forma económica tu propio código. Para cargas de trabajo que se desconfían mutuamente —cualquier cosa que no permitirías que leyera los archivos de tus otros sandboxes—, usa Sandbox.create(), que asigna a cada carga de trabajo su propia VM. Consulta Limitaciones conocidas para conocer las carencias específicas.
Un despliegue típico de RL o un sandbox de ejecución de herramientas necesita unos pocos MB de RAM y un núcleo durante unos segundos. Pagar por una VM de 2 vCPU y un arranque en frío de 6 s en cada caso, y provocar un pico de programación de 1000 VM, es un intercambio incorrecto. Por lo tanto, SandboxPool ejecuta un Job como host y multiplexa muchos sandboxes dentro de él.
Un sandbox agrupado no es una VM ni un contenedor anidado. Es el primitivo clásico de multiusuario de Unix:
- un uid dedicado (≥ 20000),
- una carpeta
0700privada propiedad de ese uid, - comandos
exec‘d como ese uid con un entorno depurado (env_clear, para que los secretos del host nunca se filtren),NO_NEW_PRIVS, rlimits por proceso y un conjunto de reglas de Landlock por sandbox.
Crear un sandbox es, por lo tanto, mkdir + chown + build ruleset ≈ 1 ms en el servidor: no hay que iniciar una segunda VM. La única latencia visible para el cliente es el viaje de ida y vuelta del proxy.
El id público de un sandbox agrupado es <host_job_id>.<local_id>, por lo que connect/exec/kill funcionan de forma sin estado, igual que los dedicados. kill() en un sandbox agrupado envía un DELETE a su host (liberando un slot); el host sigue ejecutándose.
Aislamiento en un pool: uid + Landlock
Este es el punto clave del diseño del pool, por lo que vale la pena ser preciso sobre qué está y qué no está aislado.
Un Job estándar se ejecuta como root dentro de un espacio de nombres de usuario que solo mapea los uids 0..65535, con un filtro seccomp activado y sin CAP_SYS_ADMIN / CAP_NET_ADMIN / CAP_NET_RAW. Esto descarta las herramientas de aislamiento pesadas habituales: no hay espacios de nombres anidados, no hay montajes nuevos y no hay delegación de cgroups (unshare, mount y la escritura en /sys/fs/cgroup/... fallan). Lo que el kernel sí ofrece es Landlock (ABI 6), un módulo de seguridad de Linux que permite a cualquier proceso no privilegiado restringirse a sí mismo y a sus hijos: exactamente el límite por sandbox que necesitamos. Para cada sandbox, el servidor construye un conjunto de reglas; el hijo de exec baja al uid/gid del sandbox y aplica NO_NEW_PRIVS, landlock_restrict_self y rlimits antes de ejecutar el comando.
La combinación de uids distintos (control de acceso discrecional) con Landlock está diseñada y probada para proporcionar:
- ✅ A no puede leer el
environde otro proceso, lo que impide el acceso directo a los tokens de HF y de sandbox entre sandboxes. - ✅ A no puede
SIGKILL/ptrace/ leer la memoria de los procesos de B,setuiden B, ni leer el directorio0700de B. - ✅ El acceso a
/tmpy/dev/shmestá denegado: cada sandbox está confinado por Landlock a su propio directorio de inicio (suTMPDIRapunta dentro de$HOME). - ✅ A no puede
bindun puerto TCP, por lo que no hay un servicio de localhost entre sandbox (la conexión salienteconnectsigue permitida, por lo que internet funciona). - ✅ Los sockets abstractos de unix entre sandbox están bloqueados (
LANDLOCK_SCOPED_ABSTRACT_UNIX_SOCKET; el aislamiento por uid por sí solo no bloquea estos).
Estos controles limitan la carga de trabajo. El plano de control raíz sigue siendo un límite de confianza separado: sus operaciones de archivos en modo host rechazan los enlaces simbólicos, su proxy de sockets fija el inode del socket y comprueba el uid del par, y las rutas dedicadas no están disponibles en modo host. Los canales compartidos residuales se describen en Limitaciones conocidas.
Por qué esto no es un sustituto de una VM. El aislamiento de Landlock y uid está diseñado para cargas de trabajo dentro del mismo límite de confianza. Dado que los sandbox agrupados comparten un kernel, una VM y un plano de control, no se garantiza la protección contra todos los ataques entre sandbox; los recursos y algunos metadatos de la lista de procesos también permanecen compartidos. Para código mutuamente no confiable, o para GPU, use Sandbox.create(), que proporciona a cada sandbox su propia VM.
El archivo de modelo en un pool
Dado que el único área escribible de un sandbox agrupado es su directorio de inicio confinado por Landlock (que también es su directorio de trabajo predeterminado), la API de archivos toma ese directorio de inicio como raíz para todas las rutas: files.write("data/in.txt", ...) escribe en $HOME/data/in.txt, un / inicial se interpreta como relativo al directorio de inicio, y .. no puede salir de él. Los archivos escritos a través de la API se asignan con chown al uid del sandbox para que el propio código del sandbox pueda leerlos. Esto proporciona un modelo limpio de “sistema de archivos con raíz en el sandbox” que coincide exactamente con lo que el código dentro del sandbox puede tocar, y difiere de los sandboxes dedicados, donde las rutas son absolutas en el sistema de archivos del contenedor.
Las rutas se normalizan léxicamente y luego se resuelven de forma relativa a un descriptor de directorio de inicio abierto, con rechazo de enlaces simbólicos en cada componente. La API de archivos no sigue enlaces simbólicos, incluidos los enlaces dentro del mismo directorio de inicio; el código del sandbox aún puede usarlos bajo sus reglas de Landlock.
Los pools no tienen un estado local autoritativo
Un pool no es deliberadamente un archivo de configuración local. Un pool es su conjunto de Jobs de host en ejecución, todos los cuales comparten la etiqueta hf-sandbox-pool=<id>. Esto mantiene a los pools consistentes con el resto de la API de sandbox (todo es descubrible a partir de etiquetas y reasociable desde cualquier máquina) y significa que un pool simplemente deja de existir una vez que su último host desaparece.
- Un host lleva la configuración del pool (imagen, flavor,
sandboxes_per_host, timeout de inactividad) en las variables de entorno de su trabajo; las etiquetas se usan solo para filtrar. Cuando un cliente debe iniciar un host duplicado, lee esa configuración desde un host en ejecución (inspect_job), de modo que todos los hosts de un pool se mantienen consistentes sin un registro central. - Env y secrets son por sandbox y se pasan en el momento de la creación, nunca a nivel de pool. El entorno de un sandbox en pool se mantiene en la memoria del servidor host durante la vida útil del sandbox (se vuelve a aplicar a cada comando que ejecutes); nunca se escribe en disco y nunca aparece en los metadatos de ningún Job. No hay un canal de secrets cifrados para sandboxes en pool: usa
envy trata esos valores como “no en reposo, pero tampoco cifrados”. Para valores que requieran el almacén cifrado, usa lossecretsde un sandbox dedicado. - La capacidad es autoritativa del servidor. Un host rechaza las creaciones que superan
sandboxes_per_host(respondiendo con{"rejected": N}); el cliente agrupa el excedente en otro host o inicia un duplicado. Esto mantiene el agrupamiento exacto incluso cuando varios procesos crean en el mismo pool de forma concurrente. - La expulsión por inactividad es de dos niveles. Cada sandbox se expulsa tras su propio
idle_timeoutde inactividad (a menos que aún tenga un proceso en ejecución); una vez que un host no ha tenido sandboxes durante el tiempo de inactividad del host, se apaga a sí mismo, lo que actúa como una medida de respaldo de facturación incluso si todos los clientes desaparecen.
Una caché de mejor esfuerzo mantiene create --pool rápido
No tener un estado local autoritativo es excelente para la corrección, pero tiene un coste en latencia. Un hf sandbox create --pool <id> en frío (un proceso CLI nuevo) tendría que redescubrir todo a través de la red antes de poder crear un sandbox: list_jobs para escanear el espacio de nombres → inspect_job de cada host para reconstruir su URL y nonce → GET /v1/sandboxes para ver qué tan lleno está cada uno → y finalmente POST para crearlo. Varios viajes de ida y vuelta de sobrecarga pura, en cada llamada.
Una caché de mejor esfuerzo en $HF_HOME/sandbox/pools/<context>/<pool-id>.json elimina eso. Después de cualquier creación o calentamiento, un proceso registra la configuración del pool y, por cada host, su URL de proxy, su nonce de autenticación y los últimos slots libres vistos. El siguiente proceso reconstruye el transporte del host directamente desde el archivo (sin HTTP) y va directamente al POST.
<context> es un resumen del endpoint, la credencial y el namespace para los que se escribió la entrada: la credencial como huella digital; su token en sí nunca se escribe en la caché. Tanto el nombre del directorio como el archivo lo contienen, y una lectura que no coincide en los tres es un simple fallo de caché, por lo que otro usuario de la máquina, otro endpoint o una connect(pool, namespace="other-org") obtiene la ruta en frío en lugar de los hosts que se habían cacheado para otra cosa. Los archivos se crean con 0600 y los directorios con 0700.
Para qué se confía y para qué no se confía la caché:
- No es autoritativo en cuanto a capacidad. El servidor dentro del trabajo sigue siendo autoritativo, por lo que un recuento
livedesactualizado solo implica una solicitud desperdiciada, nunca un host mal empaquetado. - No se confía en la ubicación del host. Una URL en caché solo se utiliza si es la URL del proxy de trabajos HTTPS del trabajo que la misma entrada nombra, por lo que un archivo de caché no puede elegir el destino al que se envían su token de portador de HF y su token de sandbox; una entrada que nombre cualquier otra cosa se descarta del archivo. Pasados 15 minutos, una entrada tampoco se acredita por sí sola:
inspect_jobdebe confirmar que el trabajo sigue en ejecución, sigue etiquetado para este pool y sigue en esa URL antes de que se construya el transporte. - No confiable por su forma. Los tipos y rangos se comprueban al leer, por lo que un archivo editado a mano, truncado o con tipado por cadena es un fallo de caché en lugar de un error a mitad de
create(). - Autoreparación. Un host en caché que ya no existe se descarta en la primera solicitud fallida y se elimina del archivo; la creación recurre de forma transparente a la detección por etiquetas.
- Seguro para concurrencia. Las escrituras se fusionan bajo un bloqueo de archivo (identificado por
job_id) y se confirman de forma atómica, por lo que los procesos paralelos decreateno se sobrescriben entre sí y los lectores nunca ven un archivo parcialmente escrito. - Descartable. Si lo eliminas, se produce un fallo de caché: se ejecuta la ruta en frío y todo sigue funcionando. Nunca se comparte entre máquinas.
Dos cosas que conviene tener en cuenta. Trate $HF_HOME como sensible: una entrada sigue siendo una URL de host reutilizable más el nonce público del que deriva su token, y aunque se ignora un archivo escrito con una credencial diferente, uno escrito con la suya se acredita para la ventana de 15 minutos mencionada anteriormente. Y dado que el espacio de nombres forma parte de la clave, un pool cacheado con un namespace= explícito solo se vuelve a encontrar cuando se pasa el mismo namespace=: hf sandbox create --pool <id> requiere el mismo --namespace con el que se creó el pool, o bien toma el camino en frío y busca en su propio espacio de nombres.
Limitaciones conocidas
Los pools están destinados a cargas de trabajo dentro de un mismo límite de confianza. Utilice sandboxes dedicados para el código que no confía. Las comprobaciones siguientes reducen riesgos específicos; no convierten los sandboxes en pool en VMs separadas.
Modelo de amenazas de un vistazo
| Actor | Dedicado | Pool |
|---|---|---|
| Usuario anónimo de internet | El proxy de Jobs requiere un token de HF con acceso de lectura al espacio de nombres. | Igual. |
| Miembro del espacio de nombres con acceso de lectura | Puede alcanzar el proxy y la comprobación de salud pública, pero necesita el token de sandbox para las operaciones de la API. | Igual. |
| Miembro del espacio de nombres que puede crear Jobs | No se puede derivar el token de otro usuario a partir de su nonce. | Se pueden copiar las etiquetas del pool, pero la adopción predeterminada verifica el iniciador de la API de Jobs. adopt_hosts="namespace" opta por confiar en otros creadores. |
| Código que se ejecuta en el sandbox | Comparte la raíz con el servidor dentro de la VM; no reutilice esa VM entre límites de confianza. | Limitado por uid y Landlock, pero comparte el kernel, la VM y el plano de control con privilegios. |
| Tu cliente | Contiene las credenciales de HF y del sandbox dedicado. | Contiene la credencial de gestión del host y capacidades separadas por sandbox. |
Brechas de aislamiento y recursos
- La detección de hosts no es una atestación. El valor predeterminado
adopt_hosts="own"comprueba el iniciador, la imagen, el sabor, el comando y la URL expuesta. No atesta el binario en ejecución ni hace que una imagen mutable sea de confianza. Utiliceadopt_hosts="namespace"solo con miembros del espacio de nombres en los que confíe. - Las credenciales del host siguen siendo potentes. Gestionan el pool y recuperan el token de cada sandbox. Este cliente nunca las sustituye por tokens con ámbito ausentes. Los hosts iniciados por esta versión ejecutan sbx-server 0.7.0, que las rechaza en las rutas con ámbito; los hosts iniciados por clientes anteriores conservan su servidor (y su compatibilidad con el token del host) hasta que se reciclen.
- Los canales compartidos permanecen. El TCP saliente, el acceso loopback al servidor de control, UDP, la IPC del kernel y los metadatos legibles de la lista de procesos no están aislados. Los directorios del sistema y los nodos de dispositivo seleccionados siguen siendo accesibles. El aislamiento del pool de GPU no está probado; use el modo dedicado para GPU.
- Los límites no son cuotas agregadas. Los rlimits por proceso y las colas de salida acotadas reducen la amplificación de recursos. No particionan las cuotas de CPU, la memoria total, el espacio en disco, los inodos ni la capacidad de red entre los sandbox. La paginación de directorios acota las respuestas, pero aún así materializa el directorio en el servidor. Las escrituras de archivos a través de la API no son una cuota de disco, y las escrituras en modo dedicado a archivos especiales pueden bloquearse.
- El aislamiento puede debilitarse de forma explícita. El modo host requiere ABI 6 de Landlock de forma predeterminada y rechaza un conjunto de reglas fallido. Reducir
SBX_MIN_LANDLOCK_ABIo usar la bandera de desarrollo--allow-unconfineddel servidor acepta menos garantías; inspeccione/healthautenticado para conocer la ABI y las funciones efectivas. - Las conexiones por proxy se autentican solo una vez. Después de la primera solicitud, el proxy copia bytes hacia ese backend hasta el EOF. Las solicitudes HTTP posteriores en esa conexión no se autentican ni se enrutan de forma independiente. La compatibilidad con WebSocket/SSE no establece seguridad frente a la reutilización de conexiones aguas arriba.
Ciclo de vida y credenciales
- Los descendientes desacoplados pueden sobrevivir a la terminación del proceso o al tiempo de espera. Ambas señales se envían al grupo de procesos; un descendiente que llama a
setsid()escapa de ese grupo. Elimine el sandbox de la piscina o termine el Job dedicado para finalizar toda la carga de trabajo. max_hostses de mejor esfuerzo. Clientes separados pueden contar los mismos hosts y ambos aprovisionar más. Se necesita admisión del backend o idempotencia para un límite compartido estricto.- El desmontaje puede fallar. Las cancelaciones de host fallidas lanzan una excepción y conservan los registros de la caché.
close()limita su espera para las creaciones en curso; una creación que exceda ese tiempo de espera puede dejar un Job facturable. Use la API de Jobs o la CLI para conciliar los recursos en ejecución. - La rotación de Bearer no rota las capacidades del sandbox. La autenticación del proxy resuelve el token HF actual por solicitud, pero la reconexión sin estado deriva un token de host a partir del valor exacto del bearer original. Rotar ese valor puede impedir la reconexión a hosts existentes.
- La caché local se mantiene como entrada de confianza durante 15 minutos. Se verifica su URL y su contexto de seguridad, pero no tiene un MAC de integridad; proteja
$HF_HOMEde escrituras por parte de código no confiable. - Un pin de digest no es una procedencia firmada. El cliente verifica un binario de servidor específico. Publicar y actualizar ese pin siguen siendo pasos de la publicación; el manifiesto del flujo de trabajo no es una atestación criptográfica. La vía de escape explícita
SBX_ALLOW_UNVERIFIED_SERVER=1omite la verificación solo cuando faltan ambas herramientas de hash.
Unauthenticated /health expone solo la disponibilidad y el protocolo. Los metadatos detallados requieren la credencial del host. Los ids de sandbox y los tokens requieren la fuente aleatoria del kernel; la reutilización de UIDs sigue la limpieza verificada del proceso y del directorio de inicio.
Rendimiento
Todos los números se miden en relación con trabajos reales de HF Jobs en cpu-basic, con el cliente en un portátil y todo el tráfico fluyendo a través del proxy de Jobs.
Área de trabajo aislada:
| métrica | value |
|---|---|
arranque en frío (create() devuelve, el servidor responde) | ~5,8 s mediana |
run() de ida y vuelta | p50 ~110ms (el límite inferior del RTT del proxy es ~105ms; sobrecarga del cliente ≈ 0) |
| transferencia de archivos (rangos paralelos, >8 MiB) | ~340 MiB/s de bajada, ~441 MiB/s de subida |
Pool (modo compartido/anfitrión):
| N sandbox | hosts (50/host) | provisionar + crear todo | exec en todo | kill all | total |
|---|---|---|---|---|---|
| 100 | 2 | 6.1s | 1.5s | 0.6s | 8.2s |
| 1000 | 20 | 7.4s | 4.2s | 4.2s | 15.8s |
Se crearon, ejecutaron y eliminaron 1000 sandboxes en ~16s, con un coste aproximado de un arranque en frío de un host (~6s) amortizado entre todos ellos: unos $0.0009 en total (20 × cpu-basic), frente a ~$0.06 y un pico de programación de 1000 VMs si se usara un Job por sandbox. Las operaciones de creación/ejecución/eliminación en el servidor tardan ~1ms cada una; el presupuesto total lo consume el viaje de ida y vuelta por la red.
Decisiones de diseño, resumen
| decisión | por qué |
|---|---|
| Construir sobre Jobs, sin un nuevo servicio | hereda la facturación, el hardware y los permisos; funciona en cualquier imagen |
| Binario estático de Rust, descargado al inicio | sin Python/pip; arranque en frío de ~6 s frente a 30–90 s para un arranque basado en pip |
| HTTP/1.1 hecho a mano | los frameworks mínimos almacenan en búfer las respuestas por lotes y rompen la transmisión en vivo (verificado) |
| Autenticación HMAC sin estado | reconectar desde cualquier lugar; un token derivado en lugar del propio token de HF (con alcance por trabajo, consulte Alcance del token) |
run() lanza una excepción si el código de salida no es cero (check=False desactiva esta comprobación) | mejor experiencia de desarrollo para los ciclos de “ejecutar código, ver el error” (estilo E2B) |
idle_timeout watchdog en lugar de la limpieza en el lado del cliente | Las arenas persistentes son una característica; las que se filtran igualmente se cierran |
| Pools = uid + Landlock, capacidad autorizada por el servidor, sin estado local | difusión rápida a usuarios con la misma cuenta; correcto bajo concurrencia; reinsertable en cualquier lugar |