仕組み
C#のスニペットを書くと、それが返すものが数秒で公開URLにHTTPSでホストされます。ここでは、実際に使う順に全体を説明します。
lambdaとは
lambdaは、GenHTTPのハンドラーを返すスニペットです。プラットフォームがそれをコンパイルして読み込み、返されたものを専用のURLの下にマウントします。プロジェクトもビルドファイルもusingディレクティブも要りません。GenHTTPのモジュールはすべてインポート済みです。
return Content.From(Resource.FromString("hello"));
これで完全なlambdaです。/lambda/your-key/にデプロイすると、どのリクエストにも「hello」と返します。
スニペットはクラスではなく、ステートメントの並びです。最後に、リクエストを処理できるもの(ハンドラーか、そのビルダー)を返します。
最初のlambda
- 1lambdaを作成を押します。公開URLと編集用キーが発行されます。キーはlambdaに戻る唯一の方法なので、必ず保管してください。誰にも復元できません。
- 2管理画面が開きます。最初のバージョンとして、小さなRESTサービスがすでに書かれています。これはあくまで出発点です。
- 3編集用キーをエージェントに渡して、作りたいものを伝えます。エージェントはMCP経由で新しいバージョンを書きます。またはコードを開いて自分で書きます。チェックは何も保存せずにコンパイルし、コンパイラーの指摘をファイル名と行番号付きで表示します。
- 4デプロイを押すと、オンラインになります。それまでは外からアクセスできません。もう一度デプロイすると、オンラインでいられる期間が延びます。
管理画面
編集用リンクで開くのは、テキストボックスではなく管理画面です。ここではコードの多くをエージェントが書くので、最初に表示されるのはlambdaの状態です。サイドバーには、lambdaの情報(オンラインかどうか、URL、オンラインになるのを待っている新しいバージョンがあるときのボタン)と、各セクションが並びます。URLの変更や削除など、めったに使わない操作は、そこにある⋯メニューの中にあります。
- 概要
- オンラインかどうか、今日のリクエスト数と失敗数、最新の変更、残りの容量。
- 変更依頼
- 変えたいところを書くと、このサーバーのエージェントが目の前で対応します。下書きの中で作業してそこで試し、うまく動いたら確定して次のバージョンにします。先に自分で下書きを試したいときは、完了したらオンラインにするをオフにしてください。
- 下書き
- lambdaとは別に進める変更。それぞれ専用のURLで試し、うまくいったら確定して次のバージョンにします。下書きを開くと、専用のコード、データ、ログがあります。
- ファイル
- バージョンのファイル(コードとアセット、つまりプログラムそのもの)。鍵か地球のアイコンで、一般公開されているかどうかがわかります。
- データ
- lambdaが実行中に保存するもの、つまりすべてのバージョンで共有するワークスペース。中身を見る、ファイルをアップロード・削除する、オフにする、といったことができます。
- バージョン
- 各バージョンの変更点と依頼内容、前のバージョンとの差分。ここからデプロイやロールバックができ、どのバージョンからでも下書きを作れます。
- デプロイ履歴
- いつ何がオンラインだったか、何が原因で止まったか。
- 統計
- 直近1時間または24時間のリクエスト数、失敗数、応答時間、よくアクセスされるパス。
- ログ
- リクエスト、出力された内容、エラーのスタックトレースをリアルタイムで。
- コード
- 手で書くときに使います。チェックでコンパイル、保存でバージョンを作成、デプロイでオンラインに。下書きの中では、保存で下書きに保存し、プレビューをデプロイで下書きのURLでオンラインにします。
Ctrl-Sで保存、F12で宣言へ移動します。
どのセクションも作りは同じです。タイトル、説明を開くⓘ、右側の操作ボタン、そして表示が複数あるときは、その下に切り替えボタンが並びます。コードでは、この切り替えボタンがファイルのタブです。
トラフィックとログはメモリ上にあり、保存するためではなく、様子を見るためのものです。サーバーが再起動するとリセットされます。バージョンとデプロイ履歴は保存されます。
「なぜ」を残す
バージョンは、コードと、任意の2つのメモでできています。仕様(ユーザーが何を、なぜ望んでいるか。できればユーザー自身の言葉で)と、変更内容(そのバージョンで何をするかを1行で)です。どちらもバージョン履歴で差分の横に表示されるので、何を変えたかの隣になぜが残ります。自分のためにも、履歴を読んでから手を加える次のエージェントのためにもなります。
POST /api/v1/lambdas/{editorKey}/versions { "files": [ { "name": "lambda.cs", "code": "..." } ], "specification": "誰でも記帳できるゲストブック。再起動しても書き込みが消えないこと", "change": "書き込みをワークスペースに保存し、再起動しても消えないようにする" }
エージェントも、同じ2つの項目をwrite_codeに渡します。コードでは、保存するときに変更内容を聞かれます。どちらも任意です。長すぎても拒否はせず、仕様は4000文字、変更内容は500文字で切り詰めます。下書きも自分の2つのメモを持ち、確定してできたバージョンがそれを引き継ぎます。
安全に変更する
バージョンは、一度保存すると変わりません。だからこそ、どのバージョンも残しておく価値があります。どれとでも比較でき、そのままの形でオンラインに戻せるからです。使われているlambdaを変えるときは、代わりに下書きを作りましょう。
- 1下書きのセクションから、またはどのバージョンからでも作れます。中身は、そのバージョンのコードとアセット、そしてlambdaのデータのコピーです。
- 2納得がいくまで何度でも変更します。コードで直接でも、エージェントに頼んでもかまいません。プレビューをデプロイを押すと、専用のURL(
/features/…/)で、下書き用のデータのコピーを使ってオンラインになります。lambdaの訪問者には何も見えず、下書きが書き込んだものがlambdaのデータに届くこともありません。 - 3うまくいったら確定します。メモ付きで次のバージョンになり、望めばすぐにオンラインにもなります。下書き(プレビューとデータのコピー)はなくなります。
POST /api/v1/lambdas/{editorKey}/features { "name": "ランキング" } PUT /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true POST /api/v1/lambdas/{editorKey}/features/{feature}/merge { "deploy": true }
複数の下書きを同時に進められます。ただし確定できるのは、最新のバージョンをもとにした下書きだけです。下書きを作ったあとに保存されたバージョンが、確定で元に戻ってしまわないようにするためです。別の下書きが先に確定されたときは、その変更を取り込んでから(エージェントに頼んでもかまいません)、最新のバージョンをもとにするよう変えてください。勝手に確定されることはありません。これは意図したものです。
複数のファイル
型は、それを使うコードの下に書かなくてもかまいません。コードでファイルの横の+を押すと、新しいファイルがスニペットと同じ名前空間で一緒にコンパイルされるので、インポートしなくても参照できます。拡張子のない名前はC#として扱われます。
var shelf = new Shelf(); return Inline.Create() .Get(() => shelf.All()) .Post((Book book) => shelf.Add(book));
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);
ページを配信する
ページを配信する方法は2つです。それとは別に、利用者がアップロードしたものを一緒に配信する方法がもう1つあります。
ページを1つ、インラインで書く
小さなものならこれで十分です。ページはスニペットの一部になります。
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);
フォルダーに実際のファイルを置く
スタイルシートやスクリプトがあるなら、これがおすすめです。ファイルはC#のファイルと同じ方法で追加し、書いたとおりに配信されます。コンパイルはされません。
return Layout.Create() .Add("api", api) .Add(Assets.App("site"));
アップロードされたファイルを、データから
利用者がアップロードしたものや、lambdaが作ったもの(画像、文書など)を、アプリと一緒に配信するときに。アプリ自体のページには使いません。ページはフォルダーにファイルとして置けば、それを使うコードと一緒にバージョンで管理されます。
return Layout.Create() .Add("api", api) .Add("uploads", Workspace.Files("uploads")) .Add(Assets.App("site"));
フロントエンドを一歩ずつ
2つ目の方法を、最初から最後まで説明します。どのデモも、webというフォルダーからこの方法でページを配信しています。demo-crudを開くと実例が読めます。デモは読み取り専用で、編集用キーはデモの名前です。
- 1コードでファイルの横の+を押し、
site/index.htmlと入力します。スラッシュを含む名前ならファイルはフォルダーに入り、拡張子のある名前ならその種類のファイルとして扱われます。 - 2同じように
site/app.cssとsite/app.jsを追加します。フォルダーはURLの一部ではなく、配信のルートになります。そのため、ページからはhref="app.css"のように名前だけで参照します。 - 3画像やフォントなど、テキスト以外のものは、
site内のファイルを開いてから、ファイルの横のアップロードボタンを押します。同じフォルダーに入ります。PNGはテキストエディターでは入力できないので、この方法で追加します。 - 4
lambda.csでフォルダーを配信します:return Layout.Create().Add(Assets.App("site"));
- 5デプロイを押します。
site/index.htmlは/で、site/app.cssは/app.cssで応答します。どのファイルにも一致しないURLにはページを返すので、自前でルーティングするフロントエンドでも、ディープリンクで再読み込みしたときにちゃんと動きます。 - 6横にAPIを追加すれば、ページから呼び出せます:
var api = Inline.Create().Get("notes", () => notes); return Layout.Create() .Add("api", api) .Add(Assets.App("site"));
ファイルの2つの置き場所
lambdaはファイルを2か所に置いていて、エディターでも分けて表示します。ファイルにはバージョンのファイル(プログラム)が、データにはワークスペース(プログラムが保存しておくもの)があります。違いは誰のものかです。バージョンのファイルはそのバージョンのものです。データはlambdaのもので、すべてのバージョンが共有します。
| バージョンの中 | データの中 | |
|---|---|---|
| 中身 | コードとアセット(フロントエンドを含むプログラム) | lambdaが書き込んだもの、誰かがアップロードしたもの |
| 変わるタイミング | 変わらない(変更は新しいバージョンになる) | 何かが書き込まれた瞬間 |
| デプロイすると | まさにこのファイルがオンラインになる | 一切変わらない |
| ロールバックすると | 古いファイルに戻る | 影響なし(すべてのバージョンで共有) |
| 下書きを作ると | このファイルのコピーから始まる | データのコピーを使う |
| 消えるとき | 上限を超えたら、古いバージョンと一緒に | lambdaと一緒に、またはオフにしたとき |
| コードからの参照名 | Assets | Workspace |
1か所にまとめることはできません。もしそうなら、デプロイのたびにlambdaが書き込んだものがすべて消えるか、配信するファイルを二度と削除できなくなるかのどちらかです。ランキングを保存するゲームには後者が必要で、そのゲームが配信するページには前者が必要です。だから、ページはバージョンに、ランキングはデータに置きます。
データを保存する
Workspaceは、lambdaが読み書きできる非公開のディレクトリです。リクエストやデプロイのあとも残したいものは、ここに置きます。
var notes = new List<string>(); if (Workspace.Exists("notes.json")) { notes.AddRange(JsonSerializer.Deserialize<List<string>>(Workspace.ReadText("notes.json")) ?? []); } return Inline.Create() .Get("notes", () => notes) .Post("notes", (string text) => { notes.Add(text); Workspace.WriteText("notes.json", JsonSerializer.Serialize(notes)); return notes.Count; });
ほかにもReadBytes、WriteBytes、Delete、List、CreateFolder、配信用のTree/Files/Appがあります。ファイルシステムのそれ以外の場所にはアクセスできません。
WebSocket
対応しています。しかも後付けではありません。demo-gameのデモでは、プレイヤーをマッチングして、すべての対戦をサーバーで動かしています。いちばんシンプルな形は、3つのコールバックです:
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);
誰もが一度はつまずくポイントがあります。ブラウザーは、WebSocketのハンドシェイクにヘッダーを設定できません。ハンドラーに必要な情報はクエリで渡してconnection.Request.Header.Queryから読むか、秘密の情報なら最初のメッセージで送ってください。
できないこと
コードは共有サーバーで動くため、C#の一部の機能はコンパイル前に拒否されます。プロセスの起動、独自のソケットを開くこと、アセンブリの読み込み、ワークスペース外のファイルシステムへのアクセス、そしてこれらを回避するためのリフレクションです。
それ以外はすべて使えます。GenHTTPのモジュールAPIもまるごと使えます。拒否されたときは、単に失敗したとだけではなく、どの行がなぜ拒否されたかが表示されます。
まるごと持ち出す
エディターの.NETプロジェクトとしてダウンロードを使えば、すべてをまとめて持ち出せます。そのまま開けるソリューションなので、dotnet runで実行でき、手元に残しておけます。パッケージ参照は1つだけで、このプラットフォームの痕跡はどこにもありません。
スニペットはProgram.csの本体になり、返したものを配信するホストで包まれます。ほかのファイルは、書いたとおりにそのまま移ります。WorkspaceとAssetsはコードの隣の2つのフォルダーになり、メソッドも同じなので、コードを変える必要はありません。
ここで何かを作る前に知っておいてほしいこと:書いたものはあなたのもので、まるごと持ち出せます。このサーバーで動かしているからといって、このサーバーに縛られることはありません。
エージェントに任せる
/mcpにMCPエンドポイントがあります。エージェントを接続すれば、エディターでできることはすべてできます。ガイドを読む、デモをまるごと読む、ファイルを書く、コンパイルする、デプロイする。中で使っているのは同じAPIです。
エージェントは作業しながら「なぜ」を残します(write_codeは仕様と変更内容を受け取ります)。デプロイしたものの様子も確認できます。read_logsは、lambdaの最近のリクエスト、出力された内容、発生した例外のスタックトレースを返します。エージェントはこれで、コードが動くと思い込むのではなく、実際に動くことを確かめます。同じものは管理画面でも見られます。