このガイドでは、Cloudflare が提供するリファレンス Worker を使い、Webhook でプロビジョニングを管理します。
OpenAI Cookbook には、アプリケーションで管理する例とWebhook で管理する例があります。
- アプリケーションが Agents API のセッションを作成し、入力を送信します。
- OpenAI がセッションの Webhook を Cloudflare アカウント内の Worker に送信します。
- Worker は、
codex exec-server を実行するセッション専用のコンテナを起動するか、既存のコンテナを再接続します。エグゼキューターが OpenAI へのアウトバウンド接続を確立すると、エージェントはコマンドを実行したり、ファイルを操作したりできます。
アプリケーションは Agents API を使用し、リファレンス Worker がサンドボックスのプロビジョニングを管理します。接続時と復旧時の動作については、サンドボックスのライフサイクルを参照してください。
Cloudflare Containers を利用できる Cloudflare アカウントが必要です。アプリケーションからのリクエストには OPENAI_API_KEY を使用します。OPENAI_EXECUTOR_API_KEY に環境キーを設定し、そのキーだけを CODEX_API_KEY としてコンテナに渡します。
エージェントを作成し、その ID を OPENAI_AGENT_ID として保存します。アプリケーションとリファレンス Worker には同じエージェント ID を使用してください。
Cloudflare のリファレンス Worker には、Webhook ハンドラー、コンテナイメージ、デプロイ設定、クリーンアップ用のエンドポイントが含まれています。
次のコマンドでクリーンアップ用のエンドポイントに使用するシークレットを生成し、EXECUTOR_CLIENT_SECRET として保存します。
openssl rand -hex 32
Cloudflare アカウントに Worker をデプロイします。
Cloudflare にデプロイ
入力を求められたら、次の値を設定してください。
| 変数 |
値 |
OPENAI_API_KEY |
Worker がセッションの状態を取得するために使用するキー |
OPENAI_EXECUTOR_API_KEY |
CODEX_API_KEY としてエグゼキューターに渡す環境キー |
OPENAI_AGENT_ID |
この Worker が担当するエージェントの ID |
OPENAI_WEBHOOK_SECRET |
初回のデプロイでは pending-webhook-registration |
EXECUTOR_CLIENT_SECRET |
クリーンアップ用に生成したシークレット |
デプロイした Worker の URL を WORKER_URL として保存します。
Webhook の設定手順に従って、$WORKER_URL/webhook を OpenAI プロジェクトに登録します。Cloudflare のリファレンス実装で指定されている次のイベントを有効にしてください。
agent.session.created
agent.session.action_required
agent.session.in_progress
agent.session.idle
agent.session.failed
OPENAI_WEBHOOK_SECRET の値を OpenAI から返された署名用シークレットに置き換え、Worker の新しいバージョンをデプロイします。次に、Worker の設定を確認します。以下の例では、標準的な HTTP クライアントを使って Worker を呼び出します。
1
2
3
4
5
6
7
8// Replace the illustrative IDs and URLs below with your own resource values.
const response = await fetch(
"https://worker.example.com".replace(/\/+$/, "") + "/health",
{ method: "GET" }
);
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.text());
1
2
3
4
5
6
7# Replace the illustrative IDs and URLs below with your own resource values.
import urllib.request
url = "https://worker.example.com".rstrip("/") + "/health"
request = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(request) as response:
print(response.read().decode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// Replace the illustrative IDs and URLs below with your own resource values.
import (
"io"
"net/http"
"os"
"strings"
)
endpoint := strings.TrimRight("https://worker.example.com", "/") + "/health"
request, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
panic(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
if response.StatusCode/100 != 2 {
panic(response.Status)
}
if _, err := io.Copy(os.Stdout, response.Body); err != nil {
panic(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Replace the illustrative IDs and URLs below with your own resource values.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String endpoint = "https://worker.example.com".replaceAll("/+$", "") + "/health";
var request =
HttpRequest.newBuilder(URI.create(endpoint))
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2)
throw new IllegalStateException("Request failed: " + response.statusCode());
System.out.println(response.body());
1
2
3
4
5
6
7
8
9
10# Replace the illustrative IDs and URLs below with your own resource values.
require "uri"
require "net/http"
uri = URI("https://worker.example.com".sub(%r{/+\z}, "") + "/health")
request = Net::HTTP::Get.new(uri)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
raise "Request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
puts response.body
1curl --fail-with-body "$WORKER_URL/health"
レスポンスに "configured": true と "webhook_configured": true の両方が含まれていることを確認してください。
必須アクションとして environment_connection が届いたら、オフラインのエグゼキューターを再接続します。アイドルイベントだけでは、安全にシャットダウンできるかどうかは判断できません。ライフサイクルの動作を参照してください。
セッションの実行手順に従い、アプリケーションの OPENAI_API_KEY と、Worker に設定したものと同じ OPENAI_AGENT_ID を使用します。セルフホスト型のセッションを作成し、/workspace/hello.txt に書き込んでから内容を読み取るようエージェントに依頼してください。
Worker がセッションの Webhook を受信し、サンドボックスのエグゼキューターを接続します。アプリケーションは Agents API を通じて、エージェントの出力をストリーミングで受け取ります。
セッション ID を SESSION_ID として保存します。会話を続けるには、追加の入力を送信する前に、セッションのイベントストリームを開いてください。エグゼキューターがオフラインの場合、新しい入力を送信すると環境への接続が要求され、Worker がエグゼキューターを再接続するまで処理は待機します。再接続しただけでは、以前のコンテナにあったファイルは復元されません。
Cloudflare の基本的な Worker アプリケーションでは、@openai/agents-api TypeScript SDK を使用して、セッションの作成、初回および追加の入力の送信、リソースのクリーンアップを行います。POST /demo エンドポイントを呼び出すと、このワークフローが実行されます。
このアプリケーションでも、Webhook でプロビジョニングを管理します。アプリケーションを Worker で実行する場合でも、アプリケーションがサンドボックスを直接プロビジョニングする必要はありません。
アプリケーションでサンドボックスが不要になったら、リファレンス Worker のクリーンアップ用エンドポイントに認証付きリクエストを送信します。
1
2
3
4
5
6
7
8// Replace the illustrative IDs and URLs below with your own resource values.
const response = await fetch("https://worker.example.com/executors/sess_123", {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.EXECUTOR_CLIENT_SECRET}` },
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.text());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17# Replace the illustrative IDs and URLs below with your own resource values.
import os
from urllib.parse import quote
import urllib.request
url = (
"https://worker.example.com".rstrip("/")
+ "/executors/"
+ quote("sess_123", safe="")
)
request = urllib.request.Request(
url,
method="DELETE",
headers={"Authorization": "Bearer " + os.environ["EXECUTOR_CLIENT_SECRET"]},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// Replace the illustrative IDs and URLs below with your own resource values.
import (
"io"
"net/http"
"net/url"
"os"
"strings"
)
endpoint := strings.TrimRight("https://worker.example.com", "/") + "/executors/" + url.PathEscape("sess_123")
request, err := http.NewRequest("DELETE", endpoint, nil)
if err != nil {
panic(err)
}
request.Header.Set("Authorization", "Bearer "+os.Getenv("EXECUTOR_CLIENT_SECRET"))
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
if response.StatusCode/100 != 2 {
panic(response.Status)
}
if _, err := io.Copy(os.Stdout, response.Body); err != nil {
panic(err)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Replace the illustrative IDs and URLs below with your own resource values.
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
String endpoint =
"https://worker.example.com".replaceAll("/+$", "")
+ "/executors/"
+ URLEncoder.encode("sess_123", StandardCharsets.UTF_8).replace("+", "%20");
var request =
HttpRequest.newBuilder(URI.create(endpoint))
.header("Authorization", "Bearer " + System.getenv("EXECUTOR_CLIENT_SECRET"))
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() / 100 != 2)
throw new IllegalStateException("Request failed: " + response.statusCode());
System.out.println(response.body());
1
2
3
4
5
6
7
8
9
10
11# Replace the illustrative IDs and URLs below with your own resource values.
require "uri"
require "net/http"
uri = URI("https://worker.example.com".sub(%r{/+\z}, "") + "/executors/" + URI.encode_www_form_component("sess_123").gsub("+", "%20"))
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("EXECUTOR_CLIENT_SECRET")}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
raise "Request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
puts response.body
1
2
3
4curl --fail-with-body \
--request DELETE \
--header "Authorization: Bearer $EXECUTOR_CLIENT_SECRET" \
"$WORKER_URL/executors/$SESSION_ID"
これとは別に、Agents API のセッションを削除してください。セッションを削除しても Webhook は送信されないため、すぐにクリーンアップするには両方の操作を行います。必要なファイルは、コンテナを解放する前に取得してください。
サンドボックスのプロビジョニングを直接制御するには、アプリケーションによるライフサイクル管理とエグゼキューターの接続手順に従って、Cloudflare Sandbox SDK を使用します。プロビジョニングを管理するコントローラーは、セッションごとに 1 つだけ使用してください。