Vercel Labsのvercel-openclawで知る、Vercelサービスの組み合わせ事例

Vercel Labsのvercel-openclawで知る、Vercelサービスの組み合わせ事例

2026.05.07

こんにちは、豊島です。
Vercel Labsからvercel-labs/vercel-openclawというリポジトリが公開されていたので、ソースコードとドキュメントを読み込んでアーキテクチャを整理してみました。

このリポジトリは表向きではOpenClawというLLMエージェントをVercel上にデプロイするためのリファレンス実装ですが、中身を読んでいくと、Vercelプラットフォームの主要機能(Sandbox、AI Gateway、Workflow、Queues、Cron、OIDC、Deployment Protection、Marketplace連携)が一通り使われており、Vercelの機能群を組み合わせると実際どんなシステムが組めるのか、を確認できる構成になっています。

私はOpenClawを会社環境とは切り離した検証環境でのPoCを通じて触れていて一通り把握しているので、本記事ではOpenClawの中身そのものよりも、この「Vercel機能の組み合わせ事例」としての側面を中心に読み解きます。
Vercelの個別機能を触る機会はあっても、これだけ多くの機能が一気に組み合わさったコードを読む機会は少なかったため、自分用の整理を兼ねて記事にしました。

前提: OpenClawとは何か

OpenClawは自己ホスト型のAIエージェントフレームワークです。
LLMを軸として、shell・ファイルシステム・ブラウザにアクセスする権限を与え、SlackやTelegram・Discordなどのメッセージングアプリから呼び出して自律的にタスクを実行します。
ただし、これだけの権限を持つエージェントをローカルで動かすのは取り扱いに気を使います。OpenClawを安全に運用するためのインフラをどう設計するか は、それ自体が独立した課題となっている中、vercel-openclawはその回答のひとつで、Vercelプラットフォームの機能を組み合わせるとこういうかたちで作れる、という提案にもなっていると感じました。

このリポジトリの全体像

vercel-openclawは、OpenClawをVercel Sandbox上で動かすための単一インスタンスのNext.jsコントロールプレーンです。

公式READMEに記載のある機能は次のとおりです。

  • /gatewayパスでOpenClaw UIを認証付きでプロキシ配信
  • スナップショット&リストア(実験後にロールバック可能)
  • SlackやTelegramへの永続的なメッセージ送信
  • Egressファイアウォール(エージェントが通信するドメインを学習し、後でロックダウンする)
  • Auto-wake(OpenClawのスケジュールジョブが来たらサンドボックスを起こすcron)

これらを実現するために、Vercelプラットフォームの主要機能が一通り使われています。
本記事を読み解く前提として、各機能がこのアプリ内で何の役割を担っているかをまず整理しておきます。

  • Next.js
    • コントロールプレーン本体として、管理UI(/gateway)、APIエンドポイント、各チャネルのwebhookハンドラを提供する
  • Vercel Sandbox v2 beta
    • Vercel上でユーザーコードを隔離実行できる環境(2026年1月GA、v2はbeta)
    • OpenClaw本体をこのMicroVM内で動かす。v2のpersistent機能で、停止時の自動スナップショットと次回起動時の自動復元を利用している
  • Vercel AI Gateway
    • 複数LLMプロバイダーを統一APIで扱えるVercelのゲートウェイ機能。モデル切り替え、フォールバック、コスト計測などを担う
    • サンドボックスからAI Gatewayへ向かう送信リクエストに、ファイアウォール層がAuthorizationヘッダを動的に付与する。サンドボックス内にAPIキーを置かない仕組みの中核
  • Vercel Workflow
    • 関数の再起動やタイムアウトを越えて実行を継続できる耐久ワークフロー基盤(Workflow DevKit)
    • 停止中のサンドボックスへ来たSlack/Telegramメッセージを、関数の再起動を生き延びる形で確実に送り届ける役
  • Vercel Queues
    • Vercel上で動作する耐久イベントキュー(public beta)
    • 起動確認用のテストリクエスト(Launch Verification)を流すために使われている
  • Vercel Cron
    • Vercelプロジェクトに定義する定期実行スケジュール。指定したパスを指定時刻に呼び出す
    • 定期的にサンドボックスを点検し、実行時刻に達したジョブがあればサンドボックスを起こす
  • Vercel OIDC
    • Vercelの関数に短命のOIDCトークンを発行する仕組み。シークレットを環境変数に置かずに別サービスへ認証できる
    • サンドボックスからAI Gatewayへ認証付きで抜けるためのトークン発行に使われる
  • Vercel Deployment Protection
    • デプロイされたVercelプロジェクトのURLを、Vercelアカウントでログインしていないユーザーから保護する機能。
    • 管理画面は保護したまま、外部からのwebhookだけbypass secret付きURLで通す形で使われる(これがないとSlackやTelegramからのwebhookがブロックされます)
  • Upstash Redis
    • Vercel Marketplace経由で利用できるサーバーレスRedis(マネージド)
    • メタデータ・状態・チャネル設定の永続ストアとして使う

このようにWorkflow、Queues、Cron、OIDC、Deployment Protectionといった機能が一通り組み合わさっており、それぞれを点として知っているだけでは見えにくい機能同士の組み合わせ方が、コードと設計ドキュメントとして一気に追える教材になっています。

アーキテクチャの核: 2プレーン構造

このアプリの設計でまず押さえるべきは、コントロールプレーンとエンフォースメントプレーンの分離という発想です。全体像を図示すると次のような構造になっています。

上段のコントロールプレーンはステートレスなVercel FunctionsとUpstash Redisで構成され、サンドボックスのライフサイクルを操作します。
下段のエンフォースメントプレーンが実際にOpenClawが動くVercel Sandboxで、内部のネットワークポリシーが認証情報の付与とegress制御を担当します。境界をまたぐ通信はコントロール→サンドボックスのライフサイクル操作と、サンドボックス→AI Gatewayのoutboundだけに絞られています。

コントロールプレーン

コントロールプレーンは、Redis上に保持される単一のメタデータレコードとして実装されています(実体はsrc/shared/types.tsSingleMeta型)1レコードに次のような情報がまとめて格納されています。

  • サンドボックスIDとスナップショットID
  • ライフサイクル状態(running / stopped / errorなど)
  • 構成ハッシュ(スナップショット時点と現在の希望状態の差分判定用)
  • Gateway認証トークンとその有効期限
  • 次回起動時の準備状態(Resume-Prepared State)
  • ファイアウォール状態(learn / enforceモード、学習済みドメイン)
  • 接続済みチャネル(Slack / Telegram / Discord / WhatsApp)の認証情報とwebhook状態
  • resume履歴・メトリクスと、restorePreparedStatus/restoreOracleなどで構成される自動resumeの準備状態

設計上の重要な制約として、シングルインスタンス前提が明示的に選択されており、docs/architecture.mdは次のように記載されています。

vercel-openclaw is a single-instance Next.js control plane for one OpenClaw sandbox on Vercel.
(訳: vercel-openclawは、Vercel上の1つのOpenClawサンドボックスのためのシングルインスタンスのNext.jsコントロールプレーンである)

個人インスタンスというユースケースに振り切ることで、認可モデルもメタデータ構造も劇的にシンプルに保たれており、レコードがid: "single"を1個持つだけで済んでいます。

またRedisが必須(チャネル、cron wake、永続状態がすべて永続ストレージに依存するため)、ローカル開発ではin-memoryストアにフォールバックできる、という構造になっています。

エンフォースメントプレーン

エンフォースメントプレーンは、実際にOpenClawが動くVercel Sandbox本体とそのネットワークポリシーです。
アプリは@vercel/sandbox v2 beta SDK経由で、サンドボックスの作成・resume・停止・ネットワークポリシー更新を行います。
ここで重要なのが、v2 SDKのpersistent sandbox(永続サンドボックス)機能です。

  • 停止時に自動スナップショットが取る
  • 起動時にスナップショットから自動的に復元される
  • マニュアルでsnapshot()を呼ぶ必要がない

つまり、停止しても状態は失われず、シャットダウンというより一時停止に近い挙動になります。
初回はフルブートストラップが走りますが、ドキュメントには停止状態からの復元は10秒程度で済むと記載がありました。

設計のキー: Credential Brokering

認証情報の扱い方も、このアプリの構造を読み解くうえで押さえておきたい部分です。

一般的なアプローチ

エージェントを動かすにはLLMプロバイダーのAPIキーをエージェント実行環境に渡す必要があります(環境変数や設定ファイル経由で渡すのが一般的かと思います)

ただし、AIエージェントにはプロンプト経由で意図しない指示を実行させられるリスクが構造的にあります。
APIキーをサンドボックスの内側に置いた場合、不正なプロンプトを起点にAuthorizationヘッダの値を外部URLへ送り出すような経路が理論上開いてしまいます。エージェントがシェルとネットワークアクセスを持つ以上、内側のコードでこれを完全に守りきるのは設計上手間がかかるため、サンドボックスの外で認証情報を管理する設計が意味を持ちます。

vercel-openclawの実装

このアプリは、サンドボックスのなかにAPIキーを一切置かない設計を採用しています。
実装の核心となっているのはネットワークポリシーレベルでのtransformルールです。docs/lifecycle-and-restore.mdから引用します。

The AI Gateway API key is not written as a file or env var inside the sandbox. Instead, it is injected at the firewall layer via network policy transform rules that add an Authorization: Bearer <token> header to outbound requests to ai-gateway.vercel.sh.
(訳: AI Gateway APIキーはサンドボックス内のファイルや環境変数として書き込まれない。代わりにファイアウォール層でネットワークポリシーのtransformルール経由で、ai-gateway.vercel.shへの送信リクエストにAuthorization: Bearer <token>ヘッダを付与するかたちで注入される)

サンドボックスからai-gateway.vercel.shに向かう送信リクエストに対して、ファイアウォール層が動的にAuthorization: Bearer <token>ヘッダを注入します。
サンドボックス内のプロセスはOPENAI_BASE_URLだけを知っていて、認証ヘッダは外側で透過的に付与される構造です。

この設計が生む効果は3つあります。

  1. トークンローテーションがネットワークポリシー更新だけで完結する。ファイル書き換えもゲートウェイ再起動も不要で、sandbox.update({ networkPolicy })の1コールで終わる
  2. プロンプトインジェクションによる資格情報漏洩が原理的に発生しない。サンドボックス内に資格情報が存在しないため、何を吐き出させても本物のキーは出てこない
  3. エージェントから見たコードは何も変わらない。標準のOpenAI互換クライアントがそのまま動く

これはVercel Sandboxのネットワークポリシー機能があって実現できる設計です。「サンドボックスごとに専用のリバースプロキシを立てて認証を挟む」のと同等のことを、追加のインフラなしにネットワークポリシーだけで実現しています。

例えばエンタープライズ案件でAIエージェントを動かす提案をすると、セキュリティ部門から「APIキーの管理はどうなっているか」を確認されることが多いです。本構成では、認証情報がそもそもサンドボックスの外側で管理される設計になっているため、その問いに対する答えがアプリ実装ではなくネットワーク設定として説明できる、という違いがあります。

ライフサイクル状態管理の精度

docs/lifecycle-and-restore.mdを読むと、サンドボックスのライフサイクル管理が想像以上に細かく作り込まれていることがわかります。

ライフサイクル状態

サンドボックスは次の状態を遷移します(SingleMeta.status

状態 意味
uninitialized サンドボックス未作成
creating サンドボックス作成中(新規または停止からの再開)
setup ブートストラップが設定ファイルを書き込み、OpenClawをインストール中
booting ゲートウェイ起動中
running 健全に稼働中
snapshotting スナップショット取得中
stopped 停止中(自動スナップショット済み、sandboxIdは保持)
restoring スナップショットから復元中
error エラー(復旧可能な場合あり)

Ensure Runningの3パス

ensureRunningは、サンドボックスをrunning状態にするために必要最小限の処理を選ぶ設計です。
重い処理(新規作成)はできるだけ避け、軽い処理(既存サンドボックスの再開、何もしない)を優先します。状況に応じて次のいずれかを実行します。

  • サンドボックスが存在しない → 新規作成({ name: "oc-xxx", persistent: true }の指定でフルブートストラップが走る)
  • 停止中 → Sandbox.create()を同名で呼び出してresumeする。同名サンドボックスとの名前衝突(HTTP 400または409)が返るため、SDKはそれを検知して内部でSandbox.get()に切り替え、停止中のサンドボックスをそのままresumeする
  • すでにrunningでhealthy → 何もしない

これらの判定と実行は、Next.jsのafter()でスケジュールされます。
リクエストには即座に「待機中」のレスポンスを返し、ブラウザ側はサンドボックスの準備完了をポーリングで検知する、という非同期処理です。

静的アセットと動的アセット

サンドボックスを停止状態から再開(resume)するときは、コントロールプレーン側からサンドボックス内に必要なファイル(OpenClawの設定、Skill定義、スタートアップスクリプトなど)を書き込み直します。この書き込みは毎回フルで行うのではなく、アセットの種類によって扱いが分かれています。

  • 静的レジュームアセット
    • スタートアップスクリプト、force-pairスクリプト、Skill markdown、Skillスクリプト、組み込み画像生成オーバーライド。これらはアプリバージョンが変わったときだけ書き直される(assetSha256ハッシュで判定)
  • 動的レジュームアセット
    • 主にopenclaw.json。常にconfigハッシュ比較で現在の希望状態と照合される

つまり、アプリのバージョンが変わっていなければ静的ファイルの書き込みは丸ごとスキップという最適化が入っており、レジューム時間を短くしています。

Resume-Prepared State

サンドボックスが停止すると自動でスナップショットが取られ、次に起動するときはそこから復元します。

ここで興味深いのが、サンドボックスがいま稼働しているか(running / stopped / error)とは別に、「次回起動時にそのまま再利用していいか」を独立した軸で追跡している点です。たとえば現在runningでも、その間にアプリ側のデプロイで設定や同梱アセットが更新されていると、そのまま停止→次回再開すると古い状態で立ち上がってしまいます。これを防ぐための準備がResume-Prepared Stateです。

ステータス 意味
unknown まだ判定されていない
dirty サンドボックスの中身が、最新のアプリ設定や同梱アセットと一致していない
preparing 整合性を取るための処理が進行中
ready 検証済み。停止しても次回起動時にそのまま再利用できる
failed 準備処理が失敗した

dirtyになる主な理由は次のように分類されており、

  • snapshot-missing(スナップショットが見つからない)
  • dynamic-config-changedopenclaw.jsonなどの設定が変わった)
  • static-assets-changed(同梱アセットがアプリバージョン更新で差し替わった)
  • deployment-changed(Vercel側で別デプロイメントに切り替わった)

といったラベルが付きます。

なぜここまでするのか、ドキュメントには次のような記述があります。

A stale sandbox that would boot with the wrong config is worse than a fresh create, because the sandbox would come up in a misconfigured state that is hard to diagnose.
(訳: 間違ったconfigで起動する古いサンドボックスは、新規作成よりも悪い。なぜなら診断困難な不整合状態で立ち上がるからだ)

「古いまま起動するくらいなら、いっそ作り直したほうがマシ」という割り切りです。
runningのまま放置されたサンドボックスを盲目的に再利用しないために、稼働状態と再利用可否を別軸で持っているわけです。

/gatewayへのリクエストフロー

このアプリで一番ユーザーが触るパスが/gatewayです。OpenClawの本来のUIが、認証つきプロキシ越しに配信されます。

5つのステップ

  1. ブラウザが/gatewayをリクエスト
  2. アプリが認証(管理者シークレットCookieもしくはVercel OAuthセッション)
  3. サンドボックスがrunningでなければ、after()でcreate / resumeを非同期スケジュールし、待機ページを返す。ブラウザはポーリング
  4. サンドボックスがrunningかつゲートウェイがhealthyになったら、サンドボックス内のポートにプロキシ
  5. HTMLレスポンスを書き換え、WebSocket接続をアプリ経由にルーティング、ゲートウェイトークンをクライアント側認証用に注入

HTML書き換えで認証境界を一本化する

ステップ5のHTML書き換えが、このプロキシ実装の中心です。

OpenClawのUIはターミナルやブラウザのリアルタイム表示があり、WebSocketでゲートウェイと双方向通信します。素のOpenClawは、UIが直接ゲートウェイにWebSocket接続する前提です。

このアプリでは認証はコントロールプレーン(Next.js側)に集約したいので、WebSocket接続もコントロールプレーン経由にしないと認証境界が崩れます。そこでHTMLレスポンスを書き換え、WebSocketの接続先をアプリのURLに向け、ゲートウェイトークンをクライアント側JavaScriptに注入する形を取っています。

結果として、管理者シークレット1つで全機能を守る割り切った認可モデルになっています。OAuthスコープのような細粒度の管理は持たず、ログインできれば全機能が使えるという作りで、シングルインスタンス前提と整合する設計です。

チャネル統合: 確実にメッセージ送信をするためのWorkflow / Queues

SlackやTelegramからのメッセージ受信は、サンドボックスの状態によってフローが分岐します。

Fast Path: サンドボックスがrunningの場合

  • Slack
    • 検証済みペイロードをゲートウェイの/slack/eventsに直接転送
  • Telegram
    • ネイティブTelegramハンドラ(ポート8787)にraw updateを直接転送する。これによりスラッシュコマンド、メディア、インラインキーボードなどのTelegramネイティブ機能がフルに使える

Durable Path: サンドボックスが停止中の場合

Vercel Workflowにて、次のような流れで処理します。

  1. Telegram / WhatsAppの場合のみ、まずユーザーに「Waking up… one moment.」というbootメッセージを送る(Slackは送らない)
  2. Workflowステップでサンドボックスをresume
  3. メッセージをゲートウェイにPOST /v1/chat/completionsで送信
  4. レスポンスを発信元チャネルへ送信
  5. Telegram / WhatsAppの場合、送信完了後にbootメッセージを削除

Auto-Wakeの仕組み

OpenClawはネイティブでcronスケジューラを持ち、ジョブを~/.openclaw/cron/jobs.jsonに永続化します。ただし、サンドボックスが寝ている間はこのスケジューラ自体も動きません。アプリはこのギャップを次のように埋めています。

  1. 停止前
    • サンドボックスからjobs.jsonを読み出し、最早の次回実行時刻と全ジョブペイロードを永続ストアに保存する
  2. ハートビート時(OpenClawから定期的に届く生存確認のタイミング)
    • 同じデータをストアでリフレッシュする(明示的な停止なしに自然タイムアウトしても生存させるため)
  3. 監視cron実行時
    • 保存されたwake時刻が過ぎていてサンドボックスが停止中なら、監視cronがサンドボックスをresumeする
    • OpenClawのネイティブcronが引き継ぐ
  4. 再開後
    • jobs.jsonが空でストアにコピーがある場合、ストアから書き戻してゲートウェイを再起動する(cronモジュールのリロードのため)
  5. wake後
    • cron復元成功が確認されてからwake keyをクリア
    • 失敗時は次回の監視cronでリトライできるよう保持

この監視cronはチャット完了を実行したり、メッセージを送信したり、チャネルと相互作用したりしません。サンドボックスを起こすことだけが責務として明確に分離されています。

設定検証と動作検証は別もの

このリポジトリのドキュメントが繰り返し強調する区別が、「設定が正しい」と「実際に動く」は別問題、というものです。vercel.jsonが完璧でデプロイそのものが通っても、Slackのwebhookが届くか、サンドボックスがブートできるか、AI Gatewayに疎通するかはランタイムにならないと分かりません。

そこで検証は3段階に分かれています。Preflightは設定だけ確認するconfig-only、Safe Launch Verificationはサンドボックスを起動してチャット完了まで通すモード、Destructive Launch VerificationはさらにstopとWake-from-sleepも通すモードで、channelReadiness.readyをtrueにできるのはここだけです。

設定だけ通っているのに本番で躓く事故を、3段階の検証で段階的に切り分けるという発想です。

なぜこの構成なのか: 設計上の制約と必然性

ここまで見てきたアーキテクチャの選択を、「なぜそうなったのか」から眺めてみます。

LLMエージェントを実用的に動かすには、ステートフルが前提です。

  • ファイルシステム上の作業ファイル
  • 過去の会話履歴
  • 学習されたユーザー固有のパターン
  • スケジュール済みのジョブ

一方、Vercelのようなサーバーレス環境はステートレスです。
これをこのアプリの設計課題とした上で、以下のようなアプローチをとることでサーバーレス上でステートフルなエージェントをVercelのサービスで実現しています。

  • エージェント本体はVercel Sandbox(MicroVM)で動かす
    • 「サーバーレス的に管理できるが、内部はステートフル」な実行環境を実現するため
  • ライフサイクルはコントロールプレーン(Next.js + Redis)で管理する
    • ステートレス関数群がステートフルなサンドボックスをふるまうため
  • 永続化はRedisに集約する
    • メタデータ、cronジョブ、チャネル設定、すべてRedis経由にするため
  • 関数の寿命を超える処理はVercel Workflowで動かす
    • 関数の寿命を超えるロジックはここで処理するため

まとめ

ここまでで見えてきた点を整理します。

設計面の発想

  • シングルインスタンス前提に振り切ることで、認可モデルもメタデータ構造もid: "single"を1個持つだけで済む構造になっている
  • Credential BrokeringでAPIキーをサンドボックスの外に置き、エージェント自身は認証ヘッダにも触れない構造を取っている
  • 稼働状態と「次回起動時にそのまま使えるか」を別軸で追跡しており、古い設定のまま立ち上がる状況を避けている

運用設計

  • 設定検証と動作検証を3段階に分けるアプローチは、サーバーレス運用一般でも流用できる
  • cronのAuto-Wakeで、サーバーレス上のステートフルなプロセスを寝かせて起こす運用が成立している
  • Fast pathとDurable pathの使い分けで、平常時の応答速度と停止中のメッセージ受け取りを両立させている

Vercelプラットフォームの組み合わせとして

  • Sandbox v2、AI Gateway、Workflow、Queues、Cron、OIDC、Deployment Protection、Marketplace Redisのほぼ全部を1リポジトリの中で組み合わせている
  • ネットワークポリシーので、認証情報をアプリ層ではなくネットワーク層に分離している

このリポジトリはリサーチプレビュー扱いですが、コードとドキュメントから「Vercelの主要機能を組み合わせるとこういうシステムが組める」が具体的に追えます。エージェント基盤の設計を考えている方や、AIセキュリティの実務的な観点を探している方に参考になる題材になると思います。

この記事をシェアする

関連記事