Abbiamo scommesso fin da subito sul dare all'LLM la potenza necessaria per eseguire codice arbitrario. Questo articolo parla del perché abbiamo fatto questa scelta e di cosa significhi gestire migliaia di queste sandbox contemporaneamente, avviandole e chiudendole con la stessa rapidità con cui gli utenti iniziano e terminano le conversazioni con l'agente.
Ogni conversazione che un utente ha con l'agente Adapt è supportata dal proprio computer dedicato. Non si tratta solo di un container limitato su un server condiviso, ma di una VM isolata con cui il modello può fare ciò che vuole: installare software, scrivere ed eseguire programmi, navigare sul web, interagire con le API. Le chiamiamo sandbox e sono uno dei pilastri fondamentali su cui è costruito Adapt.
Controllo totale
Gli LLM sono geni della programmazione e il mio lavoro è consistito principalmente nel costruire l'ambiente di sviluppo perfetto in cui possano operare.
Il modo abituale di connettere un'IA al mondo esterno consiste nel creare integrazioni su misura: un connettore per GitHub, un altro per HubSpot, un altro per Stripe, oppure attendere che ogni servizio rilasci un server MCP. Questo approccio non è scalabile e non ho molta voglia di scrivere codice di integrazione tutto il giorno.
Quindi, invece di fare quel lavoro noi stessi, lasciamo che sia il modello a farlo. Qualsiasi servizio che esponga un'API può essere accessibile da Adapt, perché forniamo all'LLM tutto ciò di cui ha bisogno per scrivere lo script o il programma che comunica con quell'API. È una parte importante di ciò che intendiamo quando definiamo Adapt una "intelligenza orizzontale": non è collegata a un elenco fisso di strumenti, ma può costruire lo strumento di cui ha bisogno sul momento.
Fondamentale in questo processo è dare all'LLM pieno accesso alla sandbox. Invece di fornire al modello un set statico di linguaggi e strumenti CLI con accesso limitato al file system, gli diamo accesso completo a tutto. Viene eseguito come root. E sebbene le nostre sandbox siano dotate di runtime comuni come Node e Python, cosa succede se il miglior SDK per l'API di un servizio è scritto in Go? Il modello può semplicemente procedere a installarlo ed eseguirlo.

L'LLM deve scrivere un programma in Go? Può installare Go ed eseguirlo.
Quindi, se permettiamo al modello di installare ciò che vuole ed eseguire codice che nessun essere umano ha verificato, come lo mettiamo in sicurezza? Fortunatamente, non siamo i primi ad aver avuto bisogno di eseguire codice non attendibile. Esistono due runtime sicuri molto popolari proprio per questo scopo: gVisor e Firecracker. Il nostro percorso finora ci ha resi molto esperti con entrambi.
Da gVisor a Firecracker
La nostra prima incursione nelle sandbox sicure per LLM è stata l'approccio "semplice": eseguire ogni sandbox con gVisor su GKE (Google Kubernetes Engine), utilizzando GKE Sandbox. Eseguiamo già tutti i nostri altri servizi su GKE, quindi questo è stato il passo naturale per noi.
gVisor si posiziona tra un container e il kernel host. Invece di consentire a un programma di effettuare chiamate di sistema direttamente al kernel Linux reale — cosa che non vuoi assolutamente che un codice non attendibile faccia — gVisor intercetta tali chiamate in un proprio kernel in user-space e le gestisce autonomamente. Ottieni gran parte della comodità di un container normale con una superficie di attacco molto più ridotta. E GKE Sandbox racchiude tutto questo. Distribuisci i Pod (container) e questi vengono eseguiti in modo trasparente sotto gVisor, senza che noi dobbiamo fare molta configurazione dell'infrastruttura.
E all'inizio ha funzionato molto bene. Abbiamo definito la sandbox "base" come un'immagine Docker e abbiamo lasciato che GKE la scalasse al numero di sandbox di cui avevamo bisogno in qualsiasi momento. Gli aggiornamenti al software contenuto nelle sandbox erano semplici aggiornamenti del Dockerfile e un incremento di versione in un manifest.

Centinaia di Pod sandbox in esecuzione sotto GKE Sandbox.
Ma la stessa astrazione che rendeva gVisor semplice è quella contro cui abbiamo continuato a scontrarci. Poiché gVisor reimplementa la superficie delle syscall di Linux nello spazio utente, non tutto si comporta esattamente come farebbe su un kernel reale, e i carichi di lavoro che il nostro modello immagina sono quanto di più imprevedibile possa esistere. L'intercettazione che ti garantisce sicurezza ti costa cara in termini di lavoro pesante su syscall e I/O. E fare affidamento su GKE per l'intero ciclo di vita significava che le parti che volevamo controllare di più — tempo di avvio, densità di pacchettizzazione, networking e aggressività nel riciclo delle macchine — erano quelle su cui avevamo meno controllo. Il Pod "OutOfcpu" che si vede sopra è il tipo di problema che inizi a riscontrare quando spingi lo scheduler di qualcun altro oltre i suoi limiti.
È questo che ci ha spinti verso Firecracker.
Le microVM Firecracker sono vere macchine virtuali, ognuna con il proprio kernel guest, eseguite con virtualizzazione hardware, ma ridotte all'essenziale per avviarsi in una frazione di secondo con solo pochi megabyte di overhead. È la stessa tecnologia che AWS ha costruito per stipare un numero enorme di carichi di lavoro Lambda e Fargate su hardware condiviso. Ci offre un confine di isolamento più forte rispetto a un kernel condiviso, si avvia abbastanza velocemente da sembrare istantaneo ed è abbastanza piccola da poterne inserire molte su un singolo host.
Il compromesso è che Firecracker ti fornisce una VM e poco altro. Non c'è uno strato in stile GKE che gestisce lo scheduling, il networking e l'orchestrazione del ciclo di vita. Quindi ne abbiamo costruito uno noi, e lo chiamiamo orc.
Il rootfs è solo un'immagine
Una cosa a cui non volevamo rinunciare nel passaggio dai container era definire una sandbox come un semplice Dockerfile. I container rendono la cosa banale; le VM tradizionalmente no, poiché una microVM avvia un file system root, non un'immagine OCI.
Quindi orc fa da ponte tra i due. Quando gli viene chiesto di creare una VM, prende una normale immagine Docker/OCI e genera il file system root della VM al volo, memorizzando il risultato nella cache in modo che i successivi avvii della stessa immagine siano rapidi. La nostra sandbox base è ancora solo un Dockerfile, e orc lo trasforma in un rootfs avviabile al momento della richiesta.
Questo mantiene il nostro flusso di lavoro identico ai tempi di GKE: modifichi un Dockerfile, distribuisci una nuova sandbox, il tutto eseguendo sotto vere VM. E apre una porta che stiamo appena iniziando ad attraversare. Poiché qualsiasi immagine OCI può diventare una microVM, possiamo avviare sandbox da immagini diverse da quella predefinita. Vuoi una VM che abbia già Postgres e pgvector integrati? Punta orc a quell'immagine e la otterrai come macchina isolata. La sandbox smette di essere un singolo ambiente fisso e diventa "qualsiasi immagine di cui il lavoro ha bisogno, avviata come una propria VM".
Esecuzione su scala
Ed ecco cosa rende questo problema genuinamente difficile: ogni chat ottiene la propria sandbox. Una macchina per conversazione. In ogni dato momento ne abbiamo migliaia attive, e quel numero non è mai fermo. Ogni volta che qualcuno apre una chat, deve apparire una sandbox; ogni volta che una chat diventa inattiva, una deve scomparire per non doverla pagare. Avviamo e chiudiamo sandbox continuamente.
Due numeri dominano tutto: quanto velocemente possiamo preparare una sandbox e quante possiamo inserirne su un host.
Latenza di avvio. Una microVM Firecracker si avvia in poche centinaia di millisecondi. È abbastanza veloce da non dover mantenere affatto un pool "caldo", che è una delle vittorie più silenziose di questo passaggio. Sotto GKE avremmo dovuto mantenere capacità di riserva per nascondere il tempo di avvio. Con orc una nuova sandbox è pronta prima che tu te ne accorga, quindi ne creiamo una su richiesta quando inizia una chat e la eliminiamo quando la chat è finita. Niente più pool inattivo da monitorare o pagare.
Densità. Poiché ogni microVM è minuscola, possiamo inserirne molte su un singolo host fisico. Dimensioniamo la CPU e la memoria di ogni sandbox in base a ciò di cui ha effettivamente bisogno, invece di sovradimensionare, il che è ciò che ci permette di eseguirne migliaia in modo economico.
orc stesso è deliberatamente piccolo. È un piano di controllo che parla una semplice API: crea una VM con N vCPU e M megabyte di memoria da una data immagine, invia comandi al suo interno, leggi e scrivi file al suo interno, etichettala con tag in modo da poterla ritrovare in seguito ed eliminala quando abbiamo finito. Ogni guest esegue un minuscolo processo init come PID 1 e ottiene la propria rete isolata. Tutto qui. La magia non è in un singolo trucco ingegnoso, ma nel fatto che queste primitive sono noiose e abbastanza veloci da gestire un'intera flotta.
Il vantaggio di tutto questo lavoro di infrastruttura è ciò da cui siamo partiti: un modello in grado di installare qualsiasi cosa, scrivere un programma, interrogare un'API e restituirti una risposta, il tutto su un vero computer.





