이 가이드에서는 Cloudflare의 참조 Worker로 웹훅 관리형 프로비저닝을 구성합니다.
OpenAI Cookbook에서 애플리케이션 관리형 및 웹훅 관리형 예제를 확인하세요.
- 애플리케이션이 Agents API 세션을 만들고 입력을 보냅니다.
- OpenAI가 사용자의 Cloudflare 계정에 있는 Worker로 세션 웹훅을 보냅니다.
- Worker가
codex exec-server를 실행하는 세션 전용 컨테이너를 시작하거나 다시 연결합니다. 실행기가 OpenAI로 아웃바운드 연결을 설정하면 에이전트가 명령어를 실행하고 파일을 다룰 수 있습니다.
애플리케이션은 Agents API를 사용하고 참조 Worker는 샌드박스 프로비저닝을 관리합니다. 연결 및 복구 방식은 샌드박스 수명 주기를 참고하세요.
Containers를 사용할 수 있는 Cloudflare 계정이 필요합니다. 애플리케이션 요청에는 OPENAI_API_KEY를 사용하세요. OPENAI_EXECUTOR_API_KEY를 환경 키로 설정하고, 컨테이너에는 이 키만 CODEX_API_KEY로 전달하세요.
에이전트를 만들고 해당 ID를 OPENAI_AGENT_ID로 저장하세요. 애플리케이션과 참조 Worker에서 같은 에이전트 ID를 사용하세요.
Cloudflare의 참조 Worker에는 웹훅 핸들러, 컨테이너 이미지, 배포 설정, 정리용 엔드포인트가 포함되어 있습니다.
다음 명령어로 정리용 엔드포인트의 비밀 키를 생성하고 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로 저장하세요.
웹훅 설정 절차에 따라 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를 사용하고, OPENAI_AGENT_ID에는 Worker에 설정한 것과 같은 값을 사용하세요. 자체 호스팅 세션을 만든 다음 에이전트에게 /workspace/hello.txt에 내용을 쓰고 다시 읽어 달라고 요청하세요.
Worker는 세션 웹훅을 수신하고 샌드박스 실행기를 연결합니다. 애플리케이션은 Agents API를 통해 에이전트의 출력을 스트리밍합니다.
세션 ID를 SESSION_ID로 저장하세요. 대화를 이어가려면 후속 입력을 보내기 전에 세션 이벤트 스트림을 여세요. 실행기가 오프라인인 상태에서 새 입력을 보내면 환경 연결 요청이 발생합니다. Worker가 실행기를 다시 연결할 때까지 해당 입력은 대기 상태로 남습니다. 다시 연결하는 것만으로는 이전 컨테이너의 파일이 복원되지 않습니다.
Cloudflare의 기본 Worker 애플리케이션은 @openai/agents-api TypeScript SDK를 사용해 세션을 만들고, 최초 입력과 후속 입력을 보내고, 리소스를 정리합니다. 이 애플리케이션의 POST /demo 엔드포인트를 호출하면 워크플로우가 실행됩니다.
이 애플리케이션도 웹훅 관리형 프로비저닝을 사용합니다. 애플리케이션을 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 세션 삭제는 별도로 진행하세요. 세션을 삭제해도 웹훅이 전송되지 않으므로 리소스를 즉시 정리하려면 두 작업을 모두 수행하세요. 컨테이너를 해제하기 전에 필요한 파일을 가져오세요.
샌드박스 프로비저닝을 직접 제어하려면 Cloudflare Sandbox SDK를 사용하세요. 애플리케이션 관리형 수명 주기와 실행기 연결 지침을 따르세요. 세션마다 프로비저닝 컨트롤러를 하나만 사용하세요.