このガイドでは、Cloudflare の参照実装の Worker で Webhook 管理のプロビジョニング を使用します。
OpenAI Cookbook のアプリケーション管理とWebhook 管理の例を参照してください。
- アプリケーションが Agents API セッションを作成し、入力を送信します。
- OpenAI が、Cloudflare アカウント内の Worker にセッションの Webhook を送信します。
- Worker が、
codex exec-server を実行するセッション専用の Container を起動または再接続します。エグゼキューターが OpenAI にアウトバウンド接続することで、エージェントがコマンドを実行し、ファイルを操作できるようになります。
アプリケーションは Agents API を使用し、参照実装の Worker がサンドボックスのプロビジョニングを管理します。接続と復旧の動作については、サンドボックスのライフサイクルを参照してください。
Containers を利用できる Cloudflare アカウントが必要です。アプリケーションからのリクエストには OPENAI_API_KEY を使用してください。OPENAI_EXECUTOR_API_KEY に環境キーを設定し、そのキーだけを CODEX_API_KEY として Container に渡してください。
エージェントを作成し、その ID を OPENAI_AGENT_ID として保存します。アプリケーションと参照実装の Worker で同じエージェント ID を使用してください。
Cloudflare の参照実装の Workerには、Webhook ハンドラー、Container イメージ、デプロイ構成、クリーンアップ用エンドポイントが含まれています。
クリーンアップ用エンドポイントのシークレットを生成し、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 のセットアップの手順に従い、OpenAI プロジェクトに $WORKER_URL/webhook を登録します。Cloudflare の参照実装のインテグレーションに記載されている、次のイベントを有効にします。
agent.session.created
agent.session.action_required
agent.session.in_progress
agent.session.idle
agent.session.failed
OPENAI_WEBHOOK_SECRET を OpenAI から返された署名用シークレットに置き換え、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 による再接続を待ちます。再接続するだけでは、以前の Container のファイルは復元されません。
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 は送信されないため、すぐにクリーンアップするには両方の操作を実行します。Container を解放する前に、必要なファイルを取得してください。
サンドボックスのプロビジョニングを直接制御するには、アプリケーション管理のライフサイクルとエグゼキューターの接続手順に従って Cloudflare Sandbox SDK を使用します。プロビジョニングコントローラーは、セッションごとに 1 つ使用してください。