AI
Sandbox Agents OpenAI Agents SDK: Manifest, Docker, dan E2B
Oktober 202612 menit baca

SandboxAgent adalah Agent biasa yang dilengkapi workspace. Ia tetap punya tools, handoffs, guardrails, dan API Runner yang biasa, lalu menambahkan Manifest default, user run_as, dan capabilities seperti shell dan edit file. Tempat workspace benar-benar berjalan dipilih per run lewat SandboxRunConfig.
Tidak. Di Linux ia menjalankan command sebagai proses host tanpa confinement level OS, dan di macOS client Python membatasi filesystem lewat sandbox-exec tetapi tidak memberikan isolasi jaringan. Ia juga mewarisi seluruh environment host kecuali Anda mengatur inherit_host_environment=False. Dokumentasi menyarankan Docker atau provider hosted untuk apa pun yang dipengaruhi input tidak tepercaya.
Install openai-agents[docker], lalu berikan DockerSandboxClient dengan DockerSandboxClientOptions(image=..., network_mode="none") di SandboxRunConfig. none adalah satu-satunya network mode eksplisit yang diterima SDK, dan tidak bisa digabung dengan exposed ports. Image harus sudah berisi semua dependency, karena tidak ada yang bisa diunduh di dalam container.
Snapshot memulai sandbox session baru dari file workspace yang disimpan, sedangkan session_state menyambung kembali ke session backend tertentu yang sudah Anda serialisasi. Snapshot hanya mencakup root workspace, tidak termasuk mount atau extra path grants. State yang diserialisasi membuang host path dan kredensial mount, jadi Anda harus resume dengan manifest tepercaya terkini.
Di Windows, dokumentasi menyarankan Docker atau sandbox client hosted, karena Unix-local hanya mendukung macOS dan Linux. Sandbox agents diluncurkan untuk Python pada April 2026, dan kini SDK TypeScript juga mendokumentasikannya, dengan SandboxAgent dan Manifest diekspor dari @openai/agents/sandbox serta membutuhkan Node.js 22 atau lebih baru.

Ringkasan Utama
Sandbox agents di OpenAI Agents SDK memasangkan agent biasa dengan sebuah workspace: SandboxAgent mendeklarasikan Manifest berisi file, repo, dan mount, capabilities menambahkan tool shell dan edit file, lalu SandboxRunConfig memilih backend-nya. Unix-local menjalankan command sebagai proses host tanpa isolasi jaringan, jadi pekerjaan yang tidak tepercaya harus berjalan di Docker atau provider hosted seperti E2B.
Agent pertama yang ingin saya beri akses shell sebenarnya sederhana: membaca bug report tentang pembulatan pajak pada credit note di modul invoicing ERP, mereproduksinya lewat test yang gagal, membuat patch, menjalankan ulang test, lalu menulis laporan singkat. Bagian kodenya mudah. Yang sulit adalah pertanyaan yang selalu muncul ketika agent punya terminal: filesystem siapa yang sedang ia ketik, dan apa lagi yang bisa ia jangkau dari sana?
OpenAI menjawabnya pada 15 April 2026 lewat eksekusi sandbox native di Agents SDK, yang dirilis lebih dulu untuk Python dengan TypeScript menyusul, dan kini panduan TypeScript sudah mendokumentasikan model yang sama. Tulisan ini membahas desain openai agents sdk sandbox agents berdasarkan dokumentasi resmi Python: SandboxAgent, Manifest yang mendeskripsikan workspace, capabilities dan skills yang dimuat secara lazy, client Unix-local, Docker, dan hosted, snapshot dan resume, serta peringatan di dokumentasi yang menentukan backend mana yang benar-benar bisa Anda pakai.
SandboxAgent tetaplah sebuah Agent. Ia tetap punya instructions, tools, handoffs, MCP server, guardrails, dan hooks, serta tetap dijalankan lewat Runner.run, run_sync, dan run_streamed. Yang berubah adalah batas eksekusinya, dan dokumentasi membagi batas itu menjadi lima bagian dengan tugas yang sengaja dipisahkan:
Pemisahan ini penting karena definisi agent tidak pernah menyebut backend. SandboxAgent yang sama berjalan di laptop saat development dan di container atau VM milik provider di production, dan yang berubah hanya run config. Dokumentasi juga menegaskan satu batas yang mudah terlewat: runtime luar tetap memegang approval, tracing, dan handoff, sementara sandbox session hanya memegang command dan perubahan file. Sandbox tidak menggantikan kebijakan approval Anda; ia membatasi kerusakan ketika kebijakan itu kebobolan.
Manifest mendeskripsikan apa yang harus ada di sandbox baru sebelum panggilan model pertama. Input sintetis kecil dan folder output memakai File dan Dir, materi dari host yang perlu disalin memakai LocalFile dan LocalDir, repository memakai GitRepo, dan storage eksternal memakai mount seperti S3Mount, GCSMount, R2Mount, AzureBlobMount, dan BoxMount. Berikut manifest untuk agent invoicing tadi, dengan salinan repo yang bisa ditulis, fixtures read-only, dan direktori output, semuanya dimiliki oleh user sandbox khusus:
from agents.sandbox import FileMode, Manifest, Permissions, SandboxAgent, User
from agents.sandbox.entries import Dir, LocalDir
# The identity every model-facing tool runs as: shell commands,
# file reads, apply_patch. Declaring it in the manifest is what
# lets the entries below grant it exactly what it needs.
engineer = User(name="engineer")
WRITABLE = Permissions(
owner=FileMode.ALL, group=FileMode.ALL, other=FileMode.NONE, directory=True
)
READ_ONLY = Permissions(
owner=FileMode.READ | FileMode.EXEC,
group=FileMode.READ | FileMode.EXEC,
other=FileMode.NONE,
)
manifest = Manifest(
users=[engineer],
entries={
# COPIED into the sandbox. The agent edits the copy; your
# working tree is untouched until you take a diff out.
# src resolves against the SDK process cwd and must stay
# under it unless extra_path_grants says otherwise.
"repo": LocalDir(src="./erp-invoicing", group=engineer, permissions=WRITABLE),
# Reference data the agent may read but never "fix".
"fixtures": LocalDir(src="./fixtures/tax-2026", group=engineer, permissions=READ_ONLY),
# Dir is synthetic: created inside the sandbox, read from
# nowhere on the host. This is where the report must land.
"output": Dir(group=engineer, permissions=WRITABLE),
},
)
agent = SandboxAgent(
name="Invoice fixer",
model="gpt-5.6-sol",
instructions=(
"Read repo/task.md before editing. Make the smallest correct change, "
"run pytest tests/test_tax_rounding.py, and write a summary to output/report.md."
),
default_manifest=manifest,
run_as=engineer,
)
# Rejected before anything runs: entry paths are workspace-relative.
# "/etc/erp.conf": LocalFile(...) absolute path
# "../secrets": LocalDir(...) escapes the workspaceAda tiga aturan di dokumentasi yang membentuk setiap manifest. Path entry bersifat relatif terhadap workspace dan tidak boleh absolut atau naik keluar dengan dua titik, sehingga satu manifest tetap portable antara client lokal, Docker, dan hosted. Source LocalDir di-resolve terhadap working directory proses SDK dan harus tetap berada di bawahnya kecuali Anda menambahkan extra_path_grants. Lalu Dir sama sekali tidak membaca dari host: ia dibuat di dalam sandbox, persis yang Anda butuhkan untuk folder output yang harus ditulis agent.
Permissions di sini adalah mode bit file pada entry yang dimaterialisasi, bukan izin model atau kredensial API. Secara default sebuah entry bisa dibaca dan dieksekusi semua orang dan hanya bisa ditulis pemiliknya. Mendeklarasikan User dan mengisi run_as itulah yang mengubah bit tersebut menjadi aturan nyata: command shell, pembacaan file, dan patch dieksekusi sebagai identitas itu, sehingga folder fixtures tanpa write bit tetap utuh seyakin apa pun model bahwa fixture itu salah. Perlakukan extra_path_grants sebagai konfigurasi tepercaya juga, dan jangan pernah membangunnya dari output model.
Capabilities adalah cara tool native sandbox sampai ke model. Shell menambahkan exec_command, plus write_stdin bila backend mendukung PTY. Filesystem menambahkan apply_patch dan view_image. Skills mengindeks folder skill dan mematerialisasinya saat dibutuhkan. Memory menyaring pelajaran dari run sebelumnya ke file di workspace, dan Compaction memangkas context pada run yang panjang. Default-nya, Capabilities.default(), berisi Filesystem, Shell, dan Compaction, dan default inilah sumber kesalahan yang paling sering terjadi:
from agents.sandbox.capabilities import Capabilities, LocalDirLazySkillSource, Skills
from agents.sandbox.entries import GitRepo, LocalDir
# Wrong: a list REPLACES Capabilities.default(). This agent can
# load skills, but it has lost exec_command, apply_patch and
# compaction. It will read the task and then have no way to act.
capabilities = [
Skills(lazy_from=LocalDirLazySkillSource(source=LocalDir(src="./skills"))),
]
# Right: extend the default set (Filesystem + Shell + Compaction).
capabilities = Capabilities.default() + [
Skills(
lazy_from=LocalDirLazySkillSource(
# A HOST path, read by the SDK process. Only the skills the
# model asks for are copied into the sandbox (".agents").
source=LocalDir(src="./skills"),
)
),
]
# Skills with their own release cadence: pull them from a repo.
capabilities = Capabilities.default() + [
Skills(from_=GitRepo(repo="acme/erp-agent-skills", ref="v3")),
]Lazy skills adalah default yang tepat untuk direktori skill yang besar: model melihat indeksnya dulu dan memanggil load_skill hanya untuk yang ia perlukan, sehingga selusin playbook ERP tidak memenuhi context window sampai salah satunya relevan. Source path-nya dibaca oleh proses SDK di host, jadi arahkan ke direktori aslinya, bukan path yang hanya ada di dalam image sandbox. Gunakan Skills dengan from_ dan LocalDir untuk bundle kecil yang ingin di-stage di awal, atau GitRepo bila skill tersebut punya siklus rilis sendiri.
Satu turn tetaplah satu langkah model, bukan satu command shell. Dokumentasi menegaskan tidak ada pemetaan satu-satu antara operasi sandbox dan turn: turn baru hanya terpakai ketika runtime membutuhkan respons model berikutnya. Atur max_turns berdasarkan seberapa sering agent harus berpikir, bukan berapa banyak command yang akan dijalankan test suite.
Client adalah keputusan keamanan yang sesungguhnya, dan dokumentasinya terus terang soal ini. Di Linux, UnixLocalSandboxClient menjalankan command sebagai proses host biasa tanpa confinement di level OS: sebuah command bisa menjangkau file atau resource jaringan apa pun yang bisa dijangkau proses host, dan direktori workspace, HOME, maupun cwd tidak membatasi apa-apa. Di macOS, client Python menerapkan pembatasan filesystem lewat sandbox-exec tetapi tetap tanpa isolasi jaringan. Panduan TypeScript menyebut client Unix-local miliknya tidak menambahkan isolasi filesystem maupun jaringan di kedua platform. Saya bekerja di Windows, dan di sana kedua SDK sama sekali tidak mendukung Unix-local, jadi dokumentasi langsung mengarahkan saya ke Docker atau provider hosted.
Apa yang diberikan setiap client yang terdokumentasi, menurut dokumentasi SDK dan source client-nya:
| Client | Instalasi | Batas isolasi | Cocok untuk |
|---|---|---|---|
| UnixLocalSandboxClient | Tidak perlu, sudah termasuk | Proses host. Linux: tanpa confinement. macOS Python: aturan filesystem sandbox-exec, tanpa isolasi jaringan | Development lokal yang tepercaya, atau di dalam isolasi yang sudah Anda sediakan |
| DockerSandboxClient | openai-agents[docker] | Container dari image pilihan Anda; network_mode none menghapus akses jaringan | Command tidak tepercaya di mesin sendiri atau CI, kesamaan image dengan production |
| E2BSandboxClient | openai-agents[e2b] ditambah E2B_API_KEY | Sandbox yang dikelola provider; akses internet aktif kecuali Anda mematikannya | Eksekusi hosted tanpa harus menjalankan infrastruktur container sendiri |
| Blaxel, Cloudflare, Daytona, Modal, Runloop, Vercel | Extra yang sesuai, misalnya openai-agents[modal] | Dikelola provider; dukungan mount berbeda per provider | Isolasi bergaya production di infrastruktur yang sudah Anda bayar |
| Client buatan sendiri | Implementasikan interface sandbox client | Apa pun yang ditegakkan platform Anda | Membawa provider sandbox internal ke kode agent yang sama |
Secara default, Unix-local juga meneruskan seluruh environment host ke agent. Setiap command yang dijalankan model dimulai dengan variabel milik proses yang meluncurkannya, yang di mesin developer biasanya mencakup OPENAI_API_KEY, kredensial cloud, dan URL database. Kalau Anda tetap memakai Unix-local, buat client dengan inherit_host_environment=False, yang hanya menyisakan allowlist pendek seperti PATH, LANG, TZ, dan variabel CA bundle, dan ingat bahwa opsi ini hanya menyaring variabel. Ia tidak menambahkan confinement.
Mount layak dicurigai dengan cara yang sama. Mount helper yang berjalan di dalam sandbox yang dikendalikan model bisa melihat kredensial yang dipakainya, sehingga SDK menolak in-container mount yang membutuhkan otoritas terlindungi sampai kode tepercaya meng-acknowledge eksposur itu untuk path mount yang persis. Dokumentasi menyarankan strategi native provider bila tersedia, dan selain itu kredensial yang scoped ke sandbox, berumur pendek, dan least-privilege.
Pindah dari Unix-local ke batas yang sungguhan hanya mengubah run config, tidak lebih. Client Docker menerima client docker-py dan sebuah image; client E2B berasal dari agents.extensions.sandbox dan membaca E2B_API_KEY. Dua opsi yang layak diatur sejak hari pertama adalah saklar jaringannya, karena kedua client membiarkan jaringan tetap aktif kecuali diperintahkan lain:
from docker import from_env as docker_from_env
from agents import Runner
from agents.run import RunConfig
from agents.sandbox import SandboxRunConfig
from agents.sandbox.sandboxes.docker import DockerSandboxClient, DockerSandboxClientOptions
from agents.extensions.sandbox import E2BSandboxClient, E2BSandboxClientOptions, E2BSandboxType
# pip install "openai-agents[docker]"
docker_run = RunConfig(
sandbox=SandboxRunConfig(
client=DockerSandboxClient(docker_from_env()),
options=DockerSandboxClientOptions(
image="python:3.14-slim",
# "none" is the only explicit mode the SDK accepts. Omit it
# and the container keeps Docker's default networking.
# It cannot be combined with exposed_ports.
network_mode="none",
labels={"com.example.owner": "invoice-fixer"},
),
),
)
# pip install "openai-agents[e2b]" and export E2B_API_KEY
e2b_run = RunConfig(
sandbox=SandboxRunConfig(
client=E2BSandboxClient(),
options=E2BSandboxClientOptions(
sandbox_type=E2BSandboxType.E2B,
# The options default this to True. Turn it off unless the
# task really has to install packages from the internet.
allow_internet_access=False,
workspace_persistence="snapshot", # default is "tar"
),
),
)
# Same SandboxAgent, different boundary. Nothing on the agent changes.
result = await Runner.run(agent, "Fix the bug described in repo/task.md.", run_config=docker_run)Untuk agent invoicing, network_mode none adalah pilihan yang jelas: repo dan fixtures sudah ada di manifest dan test suite tidak butuh apa pun dari luar. Konsekuensinya, image harus sudah berisi semua dependency, karena pip install akan gagal di dalam container. Label adalah asuransi operasional yang murah, dan SDK memverifikasinya ketika tersambung kembali ke container yang sudah ada, dengan melempar ValueError bila tidak cocok alih-alih diam-diam menempel ke container yang salah. Di E2B, pause_on_exit dan workspace_persistence menentukan apa yang bertahan antar-run, dan mode persistence default-nya adalah arsip tar dari workspace.
Ada dua mode lifecycle. Berikan client dan runner memegang semuanya: membuat sandbox, menjalankan agent, menyimpan snapshot, dan membersihkan. Berikan session live dan Anda yang memegangnya, yang memang Anda butuhkan untuk job multi-langkah, untuk memeriksa file setelah run, atau untuk streaming di atas sandbox yang Anda jalankan sendiri:
from pathlib import Path
from agents.sandbox import LocalSnapshotSpec
client = DockerSandboxClient(docker_from_env())
# Developer-owned lifecycle: you create it, you decide when it dies.
sandbox = await client.create(
manifest=agent.default_manifest,
snapshot=LocalSnapshotSpec(base_path=Path("/var/lib/agents/snapshots/invoice-fixer")),
options=DockerSandboxClientOptions(image="python:3.14-slim", network_mode="none"),
)
try:
await sandbox.start()
run_config = RunConfig(sandbox=SandboxRunConfig(session=sandbox))
await Runner.run(agent, "Reproduce the failing test. Do not fix it yet.", run_config=run_config)
# A checkpoint, not a shutdown: persists the snapshot-backed
# workspace and leaves the sandbox running for the next run.
await sandbox.stop()
await Runner.run(agent, "Now fix it and rerun the test.", run_config=run_config)
finally:
# The full cleanup path: pre-stop hooks, stop(), resource shutdown.
await sandbox.aclose()
# Later, in another process: reconnect from state you stored yourself.
# Serialized state is stripped of host_path values and mount
# credentials, so pass the CURRENT trusted manifest back in.
restored = client.deserialize_session_state(load_saved_payload())
resume_run = RunConfig(
sandbox=SandboxRunConfig(client=client, session_state=restored, manifest=manifest),
)Nama-nama di blok itu mudah tertukar. stop menyimpan isi workspace yang berbasis snapshot dan tidak meruntuhkan sandbox. aclose adalah jalur cleanup penuh. Snapshot memulai session baru dari file yang disimpan, sedangkan session_state menyambung kembali ke session backend tertentu yang sudah Anda serialisasi sebelumnya. Hanya root workspace yang masuk ke snapshot: path yang di-mount dan extra path grants adalah akses runtime, bukan state yang tahan lama, jadi bucket yang Anda mount tidak ikut disalin ke arsip.
Jalur resume juga menjadi tempat SDK melindungi Anda dari storage Anda sendiri. State yang diserialisasi membuang nilai host_path, kredensial cloud mount, dan acknowledgement eksposur kredensial, dan resume gagal sebelum sandbox dimulai kecuali Anda memberikan manifest tepercaya terkini dengan layout mount yang sama. State yang diserialisasi tidak pernah memberikan otoritas dengan sendirinya, dan itulah default yang tepat untuk apa pun yang Anda simpan di job queue atau database. Ada satu pengecualian provider yang terdokumentasi: Vercel tidak bisa me-resume session yang punya mount, jadi Anda harus memulai session baru.
Sandbox agents bisa dikomposisikan dengan handoff dan dengan agent.as_tool, dan sandbox menambahkan satu keputusan: apakah sub-agent mendapat workspace sendiri atau memakai ulang milik parent? Berbagi session live itu cepat, karena tidak ada yang dibuat, di-hydrate, atau di-snapshot dua kali, dan run_as ditambah mode file menjaga reviewer tetap read-only di filesystem yang sama:
# For a SHARED session, the manifest must already declare
# users=[engineer, reviewer_user], and repo/ and output/ need
# other=FileMode.READ | FileMode.EXEC: the reviewer is "other",
# so it can read the fix but holds no write bit to change it.
# An injected live session cannot add users after the fact.
reviewer_user = User(name="reviewer")
reviewer = SandboxAgent(
name="Diff reviewer",
instructions="Read repo/ and output/report.md. Report risks. Do not edit files.",
run_as=reviewer_user,
)
sandbox = await client.create(manifest=manifest)
async with sandbox:
shared = RunConfig(sandbox=SandboxRunConfig(session=sandbox))
fixer = SandboxAgent(
name="Invoice fixer",
instructions="Fix the bug, then call review_fix before you answer.",
run_as=engineer,
tools=[
reviewer.as_tool(
tool_name="review_fix",
tool_description="Review the change in repo/ for regressions.",
run_config=shared, # same live workspace, no second hydrate
max_turns=2, # nested run, its own turn budget
)
],
)
result = await Runner.run(fixer, "Fix repo/task.md and get it reviewed.", run_config=shared)Kedua gaya komposisi menghitung turn secara berbeda. Handoff mempertahankan satu run tingkat atas, dan sandbox agent cukup mengambil turn berikutnya. Panggilan as_tool memulai nested run dengan turn loop, max_turns, approval, dan biasanya run config sendiri, dan tidak satu pun turn-nya menambah hitungan milik parent. Approval yang muncul di dalam nested sandbox run tetap tampil di run luar dan melanjutkan nested run itu setelah Anda menyetujuinya. Bila sub-agent harus mengedit secara independen atau membutuhkan image berbeda, berikan panggilan as_tool-nya SandboxRunConfig sendiri dengan client Docker.
Untuk job yang harus bertahan saat worker crash, Temporal merilis integrasi bersamaan dengan peluncuran ini, dipublikasikan pada 16 April 2026, dengan contoh yang bisa dijalankan di openai-agents-python pada examples/sandbox/extensions/temporal. temporal_sandbox_client membungkus sandbox client apa pun sehingga panggilan model, lifecycle sandbox, dan command berjalan sebagai activity Temporal yang durable, dan sebuah session bisa di-fork ke backend lain, mulai di Docker lalu dilanjutkan di Daytona, dengan workspace dibawa lewat snapshot.
Inilah urutan yang akan saya pakai untuk memasukkan agent repo ERP yang di-sandbox ke dalam job CI, dengan setiap langkah menutup celah yang ditinggalkan langkah sebelumnya:
Model mental yang berguna adalah: sandbox merupakan properti dari run, bukan dari agent. Tulis SandboxAgent dan Manifest sekali, jaga tetap sempit, lalu pilih batasnya per environment: Unix-local hanya untuk kode yang sudah Anda percaya, Docker atau provider hosted dengan jaringan mati untuk semua hal yang bisa ditulis orang asing. Agent-nya sama di kedua tempat. Yang bisa ia jangkau tidak sama.
Sumber