Cloudflare
Cloudflare
Cloudflare를 자체 호스팅 샌드박스 제공자로 사용하는 방법을 안내하는 가이드예요. 이 가이드는 Cloudflare의 레퍼런스 Worker로 **웹훅 관리 프로비저닝(webhook-managed provisioning)**을 사용해요.
출처: 문서
본문
이 가이드는 Cloudflare의 레퍼런스 Worker로 웹훅 관리 프로비저닝을 사용해요.
OpenAI Cookbook의 application-managed 및 webhook-managed 예시를 참고하세요.
동작 방식
- 여러분의 애플리케이션이 Agents API 세션을 만들고 입력을 보내요.
- OpenAI가 사용자 Cloudflare 계정의 Worker로 세션 웹훅을 보내요.
- Worker가
codex exec-server를 실행하는 세션별 Container를 시작하거나 다시 연결해요. executor는 OpenAI로 아웃바운드 연결하므로 에이전트가 명령을 실행하고 파일을 다룰 수 있어요.
여러분의 애플리케이션은 Agents API를 사용하고, 레퍼런스 Worker가 샌드박스 프로비저닝을 관리해요. 연결 및 복구 동작은 Sandbox lifecycle을 참고하세요.
시작하기 전에
Containers 접근이 있는 Cloudflare 계정이 필요해요. 애플리케이션 요청에는 OPENAI_API_KEY를 사용하세요. OPENAI_EXECUTOR_API_KEY를 environment key로 설정하고, 그 키만 CODEX_API_KEY로 Container에 전달하세요.
에이전트를 만들고 그 ID를 OPENAI_AGENT_ID로 저장하세요. 애플리케이션과 레퍼런스 Worker에서 동일한 에이전트 ID를 사용하세요.
레퍼런스 Worker 배포하기
Cloudflare의 reference Worker에는 웹훅 핸들러, Container 이미지, 배포 설정, 정리 엔드포인트가 포함돼요.
정리 엔드포인트용 시크릿을 생성해 EXECUTOR_CLIENT_SECRET으로 저장하세요:
openssl rand -hex 32
Cloudflare 계정에 Worker를 배포하세요:
Deploy to Cloudflare
프롬프트가 나타나면 이 값을 입력하세요:
| 변수 | 값 |
|---|---|
OPENAI_API_KEY |
Worker가 세션 상태를 검색하는 데 사용하는 키 |
OPENAI_EXECUTOR_API_KEY |
CODEX_API_KEY로 executor에 전달되는 환경 키 |
OPENAI_AGENT_ID |
이 Worker가 서비스하는 에이전트 ID |
OPENAI_WEBHOOK_SECRET |
첫 배포의 경우 pending-webhook-registration |
EXECUTOR_CLIENT_SECRET |
정리를 위해 생성한 시크릿 |
배포된 Worker URL을 WORKER_URL로 저장하세요.
웹훅 등록하기
OpenAI 프로젝트에서 $WORKER_URL/webhook을 등록하려면 webhook setup을 따르세요. Cloudflare의 레퍼런스 통합이 나열한 이벤트를 활성화하세요:
agent.session.createdagent.session.action_requiredagent.session.in_progressagent.session.idleagent.session.failed
OPENAI_WEBHOOK_SECRET을 OpenAI가 반환한 서명 시크릿으로 교체한 뒤 새 Worker 버전을 배포하세요. 그 설정을 확인하세요. 이 예시들은 표준 HTTP 클라이언트로 Worker를 호출해요:
Worker 상태 확인하기
// 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());
# 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())
// 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)
}
// 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());
# 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
curl --fail-with-body "$WORKER_URL/health"
응답에는 "configured": true와 "webhook_configured": true가 모두 포함되어야 해요.
environment_connection required action은 오프라인 executor를 다시 연결하라는 신호예요. 유휴 이벤트만으로는 안전한 종료 신호가 아니에요. lifecycle behavior를 참고하세요.
세션 실행하기
애플리케이션의 OPENAI_API_KEY와 Worker에 설정된 동일한 OPENAI_AGENT_ID로 session steps을 따르세요. 자체 호스팅 세션을 만들고 에이전트에게 /workspace/hello.txt를 쓰고 읽도록 요청하세요.
Worker가 세션 웹훅을 받아 샌드박스 executor를 연결해요. 여러분의 애플리케이션은 Agents API를 통해 에이전트의 출력을 스트리밍해요.
세션 ID를 SESSION_ID로 저장하세요. 대화를 계속하려면 후속 입력을 보내기 전에 세션 이벤트 스트림을 열어 두세요. executor가 오프라인이면 새 입력이 환경 연결을 요청하고 Worker가 다시 연결할 때까지 기다려요. 재연결만으로는 이전 Container의 파일이 자동으로 복원되지 않아요.
애플리케이션을 Worker에서 실행하기
Cloudflare의 basic Worker application은 @openai/agents-api TypeScript SDK를 사용해 세션을 만들고, 초기 및 후속 입력을 보내고, 리소스를 정리해요. 그 POST /demo 엔드포인트가 워크플로우를 실행해요.
이 애플리케이션도 웹훅 관리 프로비저닝을 사용해요. 애플리케이션을 Worker에서 실행한다고 해서 샌드박스를 직접 프로비저닝해야 한다는 뜻은 아니에요.
정리
애플리케이션이 더 이상 샌드박스가 필요하지 않으면 레퍼런스 Worker의 인증된 정리 엔드포인트를 호출하세요:
Worker 샌드박스 정리하기
// 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: *** ${process.env.EXECUTOR_CLIENT_SECRET}` },
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.text());
# 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())
// 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)
}
// 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());
# 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
curl --fail-with-body \
--request DELETE \
--header "Authorization: Bearer $EXECU...CRET" \
"$WORKER_URL/executors/$SESSION_ID"
Agents API 세션은 별도로 삭제하세요. 세션 삭제는 웹훅을 발생시키지 않으므로, 즉시 정리를 위해 두 작업을 모두 수행하세요. Container를 해제하기 전에 필요한 파일을 가져오세요.
고급: 애플리케이션 관리 프로비저닝
샌드박스 프로비저닝을 직접 제어하려면 Cloudflare Sandbox SDK를 application-managed lifecycle 및 executor connection instructions과 함께 사용하세요. 세션당 하나의 프로비저닝 컨트롤러를 사용하세요.
참고 자료
- 설정, 수명 주기 동작, 스냅샷, 이미지 커스터마이즈는 Use Cloudflare Containers with OpenAI Agents API 읽기
- Cloudflare Sandbox 문서 읽기
- Cloudflare Sandbox TypeScript SDK reference 읽기
더 알아보기 (Learn more)
관련 문서: 자체 호스팅 샌드박스와 샌드박스 수명 주기를 참고하세요.