GenHTTP Lambda

Come funziona

Scrivi uno snippet di C#. Quello che restituisce va online a un indirizzo pubblico, in HTTPS, in pochi secondi. Qui trovi tutto, nell’ordine in cui lo incontrerai.

Cos’è una lambda

Una lambda è uno snippet che restituisce un handler GenHTTP. La piattaforma lo compila, lo carica e monta quello che ha restituito sotto il tuo indirizzo. Niente progetto, niente file di build, niente istruzioni using: tutti i moduli GenHTTP sono già importati.

return Content.From(Resource.FromString("hello"));

Questa è una lambda completa. Dopo il deploy su /lambda/your-key/, risponde a ogni richiesta con la parola «hello».

Lo snippet è fatto di istruzioni, non di una classe. L’ultima cosa che fa è restituire qualcosa in grado di servire le richieste: un handler, o un builder che ne crea uno.

La tua prima lambda

  1. 1
    Premi Crea la mia lambda. Ricevi un indirizzo pubblico e una chiave di modifica. La chiave è l’unico modo per rientrare, quindi conservala. Nessuno può recuperarla per te.
  2. 2
    Arrivi nel suo pannello di controllo, con un piccolo servizio REST già scritto come prima versione. È solo un punto di partenza.
  3. 3
    Passa la chiave di modifica a un agente e digli cosa creare: scrive nuove versioni tramite MCP. Oppure apri Codice e scrivi tu il codice: Verifica compila senza salvare niente e ti mostra cosa dice il compilatore, con file e riga.
  4. 4
    Premi Deploy. Ora è online. Prima non è raggiungibile, e ogni nuovo deploy allunga il tempo in cui resta online.

Il pannello di controllo

Il link di modifica apre un pannello di controllo, non un semplice editor di testo: qui gran parte del codice la scrivono gli agenti, quindi la prima cosa che vedi è come sta andando la tua lambda. La barra laterale mostra la lambda (se è online, il suo indirizzo e un pulsante quando una versione più recente aspetta di andare online) e le sue sezioni. Le azioni che servono di rado, come cambiare l’indirizzo o eliminarla, sono nel menu ⋯.

Panoramica
Cos’è l’app, se è online, quante richieste ha ricevuto oggi e quante sono fallite, l’ultima modifica e quanto spazio resta.
Documentazione
Cos’è l’app, per chi è e perché, e perché è costruita così: la scrivono gli agenti e resta con ogni versione.
Modifica
Scrivi cosa deve cambiare e l’agente di questo server lo fa sotto i tuoi occhi. Prova la modifica su una bozza, una copia con un indirizzo tutto suo, e la mette online quando funziona. Disattiva Metti online a lavoro finito per provare prima tu la bozza. Lavora solo sulla tua app: una richiesta che non la riguarda, o che serve a fare danni, viene rifiutata, e ti dice perché.
Bozze
Modifiche provate prima di andare online, ognuna a un indirizzo tutto suo e su dati di prova tutti suoi. Una volta aperta, una bozza ha il suo codice, i suoi dati di prova e i suoi log. La sezione compare appena c’è una bozza.
File
I file di una versione: il codice e gli asset, cioè il programma vero e proprio. Un lucchetto o un globo indica se sono pubblici.
Dati
Quello che la lambda conserva mentre gira, condiviso da tutte le versioni: il database, il workspace e le chiavi e password, ognuno con la sua scheda. Guarda le tabelle e i file, carica file, imposta chiavi e password o attiva e disattiva un tipo. La vista semplice lo mostra appena l’app conserva qualcosa.
Versioni
Cosa ha cambiato ogni versione, cosa era stato chiesto e le differenze rispetto alla precedente. Da qui fai il deploy o torni indietro, oppure avvii una bozza da una qualsiasi di esse.
Deployment
Cosa è stato online e quando, e cosa l’ha fermato.
Statistiche
Richieste, errori, tempi di risposta e i percorsi più richiesti, nell’ultima ora o nelle ultime 24 ore.
Log
Le richieste, cosa ha stampato e lo stack trace di ogni errore, in tempo reale.
Codice
Per scriverlo a mano. Verifica compila, Salva crea una versione, Deploy la mette online. In una bozza, Salva lo tiene nella bozza e lo mostra all’indirizzo della bozza. Ctrl-S salva; F12 va alla dichiarazione.
Test
Come viene testata automaticamente l’app, con gli script e i dati di test che servono. Solo nella vista completa.

Ogni sezione funziona allo stesso modo: il titolo, il pulsante ⓘ che la spiega, le azioni a destra e, dove ci sono più viste, una fila di schede sotto. Nel codice, le schede sono i file. La vista completa raccoglie le sezioni in gruppi: come la gente la trova, dove si fa una modifica, il programma e i suoi dati, e come gira.

Traffico e log restano in memoria: servono per tenere d’occhio le cose, non per conservarle. Un riavvio del server li azzera. Le versioni e la cronologia dei deployment invece vengono salvate.

Spiegare il perché

Una versione è il codice più, se vuoi, due note: la specifica, cioè cosa vuole l’utente e perché, se possibile con parole sue, e la modifica, una riga su cosa fa la versione. Compaiono accanto al diff nella cronologia delle versioni, così il perché resta vicino al cosa: per te e per il prossimo agente che leggerà la cronologia prima di cambiare qualcosa.

POST /api/v1/lambdas/{editorKey}/versions
{
  "files": [ { "name": "lambda.cs", "code": "..." } ],
  "specification": "Un guestbook che la gente può firmare; le voci devono sopravvivere a un riavvio",
  "change": "Salva le voci nel database così sopravvivono a un riavvio"
}

Gli agenti passano gli stessi due campi a write_code. In Codice, quando salvi ti viene chiesta la modifica. Sono entrambi facoltativi: una specifica lunga viene tagliata a 4000 caratteri e una modifica a 500, invece di essere rifiutata. Una bozza ha le sue due note, e la versione in cui viene integrata le riprende.

Documentazione e test

Ogni versione conserva, accanto al suo programma, quello che è scritto su di lei: la sua documentazione (cos’è l’app, per chi è e perché, e perché è costruita così) e i suoi test: come verificare automaticamente che funziona, con gli script e i dati di test che servono. Gli agenti li scrivono insieme a una nuova lambda e li tengono aggiornati a ogni modifica. Il prossimo agente che modifica la lambda li legge per prima cosa, così sa a cosa serve l’app e cosa deve continuare a funzionare: cose che il codice da solo non dice.

.lambda/docs/product.md
cos’è l’app, per chi è, cosa ci fa la gente e perché
.lambda/docs/decisions.md
le decisioni tecniche, e perché sono state prese
.lambda/tests/README.md
come viene testata automaticamente l’app, e come eseguire i test
.lambda/tests/…
gli script e i dati di test usati dai test

Sono file della versione come tutti gli altri, nella cartella .lambda: la cronologia mostra cosa ci ha cambiato una versione, tornando indietro torna la documentazione che valeva per quella versione, e una bozza ne ha una copia sua che va online insieme a lei. Non vengono mai compilati né serviti, e contano nello spazio a disposizione per gli asset di una versione.

Nel pannello di controllo, Documentazione mostra le pagine da leggere, e Test come viene testata l’app e i file che ci sono accanto; la versione si sceglie come per i suoi file. Lì si può anche modificare una pagina, e così si salva la versione successiva. La vista semplice chiama la documentazione Informazioni e mostra solo a cosa serve l’app: per correggerla, dillo all’agente.

Sono scritti nella lingua che usi con l’agente, per chi modificherà l’app dopo, che sia una persona o un agente. Non sono una copia del codice: dicono a cosa serve l’app, e perché.

Cambiarla senza rischi

Una versione, una volta salvata, non cambia più, ed è per questo che vale la pena conservarle tutte: ognuna si può confrontare e rimettere online esattamente com’era. Per cambiare una lambda che la gente usa, prova prima la modifica in una bozza.

  1. 1
    Avviala da una versione qualsiasi in Versioni, oppure lascia che lo faccia l’agente. È una copia del codice, degli asset, della documentazione e dei test di quella versione, e dei dati della lambda.
  2. 2
    Modificala tutte le volte che serve, in Codice o chiedendolo all’agente. La sua anteprima risponde a un indirizzo tutto suo, /features/…/, con dati di prova tutti suoi. I visitatori della lambda non ne vedono niente, e niente di quello che scrive arriva ai dati della lambda.
  3. 3
    Quando è a posto, premi Metti online: diventa la prossima versione, con le sue note, e va online. La bozza sparisce insieme a lei, con la sua anteprima e i suoi dati di prova.
POST /api/v1/lambdas/{editorKey}/features
{ "name": "Classifica" }

PUT  /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true
POST /api/v1/lambdas/{editorKey}/features/{feature}/merge
{ "deploy": true }

Si può lavorare a più bozze insieme. Solo una bozza aggiornata alla versione più recente può andare online, così non annulla mai una versione salvata dopo il suo avvio. Se prima ne è andata online un’altra, porta dentro le sue modifiche (o chiedilo all’agente) e segna la bozza come aggiornata. Niente va online da solo, ed è voluto. L’API chiama una bozza feature, e metterla online merge.

Più di un file

I tipi non devono per forza stare sotto il codice che li usa. In Codice, premi + accanto ai file: il nuovo file viene compilato insieme allo snippet, nello stesso namespace, quindi non devi importare niente per usarlo. Un nome senza estensione viene considerato C#.

lambda.cs
var shelf = new Shelf();

return Inline.Create()
             .Get(() => shelf.All())
             .Post((Book book) => shelf.Add(book));
Shelf.cs
public sealed class Shelf
{
    private readonly List<Book> _books = [];

    public IEnumerable<Book> All() => _books;

    public Book Add(Book book)
    {
        _books.Add(book);
        return book;
    }
}

public record Book(string Title, string Author);

Servire una pagina

Ci sono due modi per servire una pagina, più uno per quello che la gente carica accanto.

Una pagina, scritta nel codice

Va bene per le cose piccole. La pagina fa parte dello snippet.

var page = Resource.FromString("""
                              <!doctype html>
                              <title>Mine</title>
                              <h1>It works</h1>
                              """)
                   .Type(new ContentType("text/html; charset=utf-8"));

return Content.From(page);

Una cartella di file veri

Quello che ti serve per qualsiasi cosa con un foglio di stile e uno script. I file si aggiungono come un file C# e vengono serviti esattamente come li hai scritti. Niente li compila.

return Layout.Create()
             .Add("api", api)
             .Add(Assets.App("site"));

File caricati, dai dati

Per quello che la gente carica o che la lambda crea (foto, documenti), servito accanto all’app. Non per le pagine dell’app stessa: quelle vanno in una cartella di file, dove seguono le versioni insieme al codice che le usa.

return Layout.Create()
             .Add("api", api)
             .Add("uploads", Workspace.Files("uploads"))
             .Add(Assets.App("site"));

Un front-end, passo per passo

Il secondo modo, per intero. Ogni demo serve la sua pagina così, da una cartella chiamata web: apri demo-crud per vederne una. Le demo sono in sola lettura, e la loro chiave di modifica è il loro nome.

  1. 1
    In Codice, premi + accanto ai file e scrivi site/index.html. Un nome con una barra mette il file in una cartella; un nome con un’estensione viene trattato come il tipo di file che indica.
  2. 2
    Aggiungi site/app.css e site/app.js allo stesso modo. La pagina li richiama per nome, come in href="app.css", perché la cartella è la radice di ciò che viene servito, non una parte dell’indirizzo.
  3. 3
    Per tutto ciò che non è testo, come un’immagine o un font, apri un file in site e premi il pulsante di caricamento accanto ai file: finisce nella stessa cartella. Un PNG non si può scrivere in un editor di testo, quindi si passa da lì.
  4. 4
    In lambda.cs, servi la cartella:
    return Layout.Create().Add(Assets.App("site"));
  5. 5
    Premi Deploy. site/index.html risponde su /, site/app.css su /app.css, e qualsiasi indirizzo che non corrisponde a un file riceve la pagina: così un front-end con un suo routing funziona anche quando qualcuno ricarica la pagina su un deep link.
  6. 6
    Aggiungi un’API accanto e la pagina avrà qualcosa con cui parlare:
    var api = Inline.Create().Get("notes", () => notes);
    
    return Layout.Create()
                 .Add("api", api)
                 .Add(Assets.App("site"));

I due posti dove stanno i file

Una lambda tiene i file in due posti, e l’editor li mostra separati: File contiene i file di una versione (il programma) e Dati contiene il workspace (quello che il programma conserva). La differenza è di chi sono. I file di una versione appartengono a quella versione; i dati appartengono alla lambda, e tutte le versioni li condividono.

In una versioneNei dati
cosa contieneil codice e gli asset: il programma, front-end compreso, con la sua documentazione e i suoi testquello che scrive la lambda o che carica qualcuno
quando cambiamai: una modifica è una nuova versioneappena ci viene scritto qualcosa
un deploymette online esattamente questi filenon li tocca mai
tornare indietroriporta i vecchi filenessun effetto: tutte le versioni li condividono
una bozzaparte da una copia di questi filelavora su una copia dei dati
quando spariscecon le versioni vecchie, oltre il limitecon la lambda, o quando disattivi il workspace
dal codice si raggiunge conAssetsWorkspace

Non possono stare in un unico posto. Se ci stessero, un deploy cancellerebbe tutto ciò che la lambda ha scritto nel frattempo, oppure non si potrebbe mai togliere niente da ciò che pubblica. Un gioco con una classifica vuole la seconda cosa; la pagina che serve vuole la prima. Quindi la pagina va nella versione, e la classifica nei dati.

Salvare le voci

Le voci (messaggi, account, ordini, voti) vanno nel database: un database SQLite tutto della lambda, da attivare in Dati. Il codice apre una connessione con Database.GetConnection() e legge e scrive i dati tramite Entity Framework Core, con un contesto tutto suo che mappa le tabelle:

// migrations/V1__Create_notes.sql:
//   CREATE TABLE notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL);

using (var connection = Database.GetConnection())
{
    new Evolve(connection) { Locations = [Assets.Root + "migrations"] }.Migrate();
}

return Inline.Create()
             .Get("notes", () =>
             {
                 using var db = new Notes(Database.GetConnection());

                 return db.Entries.OrderBy(n => n.Id).Select(n => n.Text).ToList();
             })
             .Post("notes", (NoteInput input) =>
             {
                 using var db = new Notes(Database.GetConnection());

                 db.Entries.Add(new Note { Text = input.Text });

                 return db.SaveChanges();
             });

record NoteInput(string Text);

class Note
{
    public long Id { get; set; }

    public string Text { get; set; }
}

// maps the table the migration made, on the connection it is handed
class Notes(SqliteConnection connection) : DbContext
{
    public DbSet<Note> Entries => Set<Note>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite(connection, contextOwnsConnection: true);

    protected override void OnModelCreating(ModelBuilder model)
        => model.Entity<Note>().ToTable("notes");
}

Le sue tabelle le creano le migrazioni: file SQL che arrivano con la versione in migrations/, applicati in ordine da Evolve all’avvio della lambda, ognuno una volta sola, così una nuova versione esegue solo quello che è nuovo. Non modificare mai una migrazione già applicata: una modifica a una tabella è il file successivo.

Come tutti i dati, il database è condiviso da tutte le versioni, deploy e ripristini non lo toccano, e una bozza lavora su una sua copia. In Dati vedi le sue tabelle e le righe che contengono, che la vista semplice chiama voci. Scarica come progetto .NET lo porta con sé come semplice file SQLite.

Crea un contesto dove ti serve e poi rilascialo, e usalo in modo sincrono: ToList e SaveChanges, non ToListAsync e SaveChangesAsync. Le tabelle le creano le migrazioni, mai Entity Framework. La demo demo-crud fa tutto questo.

Salvare i file

Workspace è una cartella privata che la tua lambda può leggere e scrivere: il posto per i file, come le foto caricate da qualcuno, un documento che crea, un modello che legge. Le voci vanno nel database, e anche quello che si sa di un file (chi l’ha caricato, quando) è una voce.

// uploads go into a folder of their own, served as they are
Workspace.CreateFolder("photos");

return Layout.Create()
             .Add("photos", Workspace.Files("photos"))
             .Add("upload", Inline.Create().Post(async (Stream body) =>
             {
                 using var content = new MemoryStream();

                 await body.CopyToAsync(content);

                 Workspace.WriteBytes($"photos/{Guid.NewGuid():N}.jpg", content.ToArray());
             }));

Ci sono anche ReadBytes, WriteBytes, Delete, List, CreateFolder, e Tree/Files/App per servirlo. Il resto del file system non è raggiungibile.

Chiavi e password

Una chiave API, una password o un token va nei secret, non nel codice, dove l’avrebbero ogni versione, ogni download e chiunque legga la cronologia. Il codice legge un secret per nome:

var weather = new System.Net.Http.HttpClient();

weather.DefaultRequestHeaders.Add("X-Api-Key", Secret.Read("WEATHER_API_KEY"));

return Inline.Create()
             .Get("today", async () => await weather.GetStringAsync("https://weather.example/today"));

Attiva i secret in Dati e imposta lì il valore. Una volta salvato, non viene più mostrato, né a te né a un agente: puoi solo sostituirlo. L’elenco dice quali nomi legge il codice senza che ci sia ancora un valore, e la panoramica li chiede. Secret.Exists dice se un secret è impostato, per il codice che funziona anche senza. Come tutti i dati, i secret sono condivisi da tutte le versioni, e una bozza lavora su una copia.

Sono salvati cifrati, con una chiave che non sta nel database. In un progetto scaricato, Secret.Read("NAME") legge la variabile d’ambiente NAME: i valori restano qui.

WebSocket

Supportati sul serio, non aggiunti all’ultimo momento. La demo demo-game abbina i giocatori e gestisce ogni partita sul server. La forma più semplice sono tre callback:

var room = new ConcurrentDictionary<IReactiveConnection, string>();

var socket = Websocket.Functional()
                      .OnOpen(c => { room[c] = "someone"; return ValueTask.CompletedTask; })
                      .OnMessage(async (c, text) =>
                      {
                          foreach (var other in room.Keys)
                          {
                              await other.WritePayloadAsync(text);
                          }
                      })
                      .OnClose((c, _) => { room.TryRemove(c, out _); return ValueTask.CompletedTask; });

return Layout.Create().Add("chat", socket);

Quando la pagina deve solo ascoltare (un contatore, un feed, una classifica), gli eventi inviati dal server sono più semplici: una sola risposta lunga su cui il server continua a scrivere e che il browser riapre da solo. La demo demo-live invia così ogni voto a tutti quelli che stanno guardando. In entrambi i casi è il server a inviare ciò che è cambiato. Una pagina che chiede di nuovo ogni pochi secondi manda una richiesta ogni volta, che sia cambiato qualcosa o no, ed è comunque in ritardo.

C’è un tranello in cui cadono tutti: un browser non può impostare header nell’handshake di un websocket. Passa quello che serve all’handler nella query, dove lo legge da connection.Request.Header.Query, oppure manda i dati segreti come primo messaggio.

Cosa non puoi fare

Il tuo codice gira su un server condiviso, quindi una parte di C# viene rifiutata prima ancora della compilazione: avviare processi, aprire socket propri, caricare assembly, accedere al file system fuori dal workspace e usare la reflection per aggirare uno di questi limiti. Lo stesso vale per attendere un task con .Result o .Wait() invece di usare await: le richieste girano su un thread per core, e il task dovrebbe terminare proprio sul thread che lo sta aspettando.

Tutto il resto c’è, compresa l’intera API dei moduli GenHTTP. Se qualcosa viene rifiutato, vedi quale riga e perché, non solo che non ha funzionato.

Portarla via

Con Scarica come progetto .NET, nell’editor, ti porti a casa tutto: una solution da aprire, avviare con dotnet run e tenere. Le basta il pacchetto GenHTTP, e include un Dockerfile per compilarla ed eseguirla come container.

Il tuo snippet diventa Project.cs, e Program.cs serve ciò che restituisce. Gli altri file arrivano esattamente come li hai scritti. Workspace e Assets diventano due cartelle accanto al programma, con gli stessi metodi, a parte in una cartella Platform, quindi nel tuo codice non devi cambiare niente. Secret lì legge le variabili d’ambiente con lo stesso nome; i valori restano qui. Anche la documentazione e i test vengono con te, in docs e tests. Database apre database/database.db, che il download porta con sé insieme alle voci che la tua app ha conservato.

Meglio saperlo prima di creare qualsiasi cosa qui: quello che scrivi è tuo e te lo porti via intero. Farlo girare su questo server non ti lega a questo server.

Pubblicare il codice

Se quello che hai creato può servire a qualcun altro, pubblicane il codice: apri Open source nel pannello di controllo, scegli una licenza (MIT, se non ne vuoi un’altra) e attiva l’opzione. Il codice ottiene una pagina tutta sua tra le app open source, dove chiunque può leggerlo, dargli una stella e scaricare qualsiasi versione come lo stesso progetto che ti dà Scarica come progetto .NET, con accanto la licenza.

Viene pubblicata ogni versione, anche quelle precedenti, con la sua documentazione, i suoi test e la modifica che ha fatto. Quello che l’app conserva non viene mai pubblicato (le sue voci, i file che ha salvato, i valori delle sue chiavi e password), e nemmeno quello che hai chiesto con le tue parole o chi usa l’app. Se disattivi l’opzione, la pagina sparisce; le sue stelle restano, per quando pubblicherai di nuovo il codice.

Tutto quello che c’è nel codice diventa pubblico, comprese le versioni precedenti. Una chiave o una password va tra le chiavi e password in Dati, mai nel codice, che sia pubblicato o no.

Lasciar fare a un agente

C’è un endpoint MCP su /mcp. Collegaci un agente e potrà fare tutto quello che fa l’editor: leggere la guida, leggere una demo per intero, scrivere file, compilarli e fare il deploy. Sotto c’è la stessa API.

Mentre lavora spiega il perché (write_code riceve la specifica e la modifica) e può controllare quello che ha pubblicato: read_logs restituisce le richieste recenti della lambda, cosa ha stampato e lo stack trace di ogni eccezione. È così che un agente scopre che il suo codice funziona, invece di darlo per scontato. Tu vedi le stesse cose nel pannello di controllo. Man mano scrive la documentazione e i test, li legge prima di cambiare qualcosa ed esegue i test sull’indirizzo di una bozza prima di metterla online. A una pagina pensata per essere trovata dà un titolo, una descrizione, un’icona e un’anteprima per quando qualcuno ne condivide il link. In fondo alle pagine che costruisce aggiunge una piccola riga che dice che sono state fatte con GenHTTP Lambda: diglielo se preferisci non averla, e la toglie.

Scopri di più →