## Why
The Python SDK currently exposes sandbox selection differently depending
on where it is used: thread lifecycle methods accept `SandboxMode`,
while turns accept the lower-level `SandboxPolicy` shape. For the common
case of choosing an access level, that leaks app-server wire details
into otherwise straightforward SDK usage.
This makes the common path explicit and discoverable: callers choose a
named sandbox preset once, using the same keyword on threads and turns.
The preset name `workspace_write` also makes the granted capability
clear at the callsite.
## What changed
- Added a root-level `Sandbox` enum with documented presets:
- `Sandbox.read_only`: read files without allowing writes.
- `Sandbox.workspace_write`: the normal default for projects with a
recorded trust decision; read files and write inside the workspace and
configured writable roots.
- `Sandbox.full_access`: run without filesystem access restrictions.
- Documented that omitting `sandbox=` delegates to app-server's
configured default, while explicit turn overrides remain sticky for
subsequent turns.
- Updated sync and async thread lifecycle and turn APIs to consistently
accept `sandbox=Sandbox...`, translating to the existing app-server
thread and turn representations internally.
- Updated the public API artifact generator so regenerated SDK wrappers
retain the friendly enum shape.
- Replaced low-level policy construction in Python docs, examples, and
the walkthrough notebook with the preset API.
- Added focused coverage for root exports, method signatures,
preset-to-wire mapping, and rejection of raw string sandbox inputs.
## API impact
High-level turn calls now use `sandbox=` instead of `sandbox_policy=`:
```python
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
result = thread.run("Review the diff only.", sandbox=Sandbox.read_only)
```
`thread_start(...)` already defaults to `ApprovalMode.auto_review`, so
normal writable usage is concise:
```python
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
thread.run("Update the files in this workspace.")
```
With that combination, edits inside `cwd` and configured writable roots
run within the workspace-write sandbox. Operations that require
approval, such as edits outside those roots, are routed through auto
review. When `sandbox=` is omitted, app-server resolves its configured
default. A sandbox supplied to `run(...)` or `turn(...)` applies to that
turn and subsequent turns.
## Test coverage
- `sdk/python/tests/test_public_api_signatures.py` covers the public
export and parameter names, including the default approval mode.
- `sdk/python/tests/test_public_api_runtime_behavior.py` covers preset
mappings to the existing wire types and raw string rejection.
4.8 KiB
Getting Started
This is the fastest path from install to a multi-turn thread using the public SDK surface.
The SDK is experimental, so the public API and runtime requirements may keep evolving before the first public release.
1) Install
From repo root:
cd sdk/python
uv sync
source .venv/bin/activate
Requirements:
- Python
>=3.10 - uv
- installed
openai-codex-cli-binruntime package, or an explicitcodex_binoverride
2) Authenticate when needed
Existing Codex auth state is reused automatically. To authenticate from the SDK, use the flow that fits your app:
from openai_codex import Codex, Sandbox
with Codex() as codex:
codex.login_api_key("sk-...")
account = codex.account()
print(account.account)
Interactive ChatGPT browser login returns a handle that carries the URL and the matching completion event:
with Codex() as codex:
login = codex.login_chatgpt()
print(login.auth_url)
completed = login.wait()
print(completed.success)
Device-code login works the same way with
login_chatgpt_device_code(), which exposes verification_url, user_code,
and wait().
3) Run your first turn (sync)
from openai_codex import Codex
with Codex() as codex:
server = codex.metadata.serverInfo
print("Server:", None if server is None else server.name, None if server is None else server.version)
thread = codex.thread_start(
model="gpt-5.4",
config={"model_reasoning_effort": "high"},
sandbox=Sandbox.workspace_write,
)
result = thread.run("Say hello in one sentence.")
print("Thread:", thread.id)
print("Text:", result.final_response)
print("Items:", len(result.items))
What happened:
Codex()started and initializedcodex app-server.thread_start(...)created a thread.thread.run("...")started a turn, consumed events until completion, and returnedTurnResultwith turn metadata, final assistant response, collected items, and usage.result.final_responseisNonewhen no final-answer or phase-less assistant message item completes for the turn.- plain strings are accepted anywhere a turn input is accepted; typed inputs are still available for multimodal and structured cases
- use
thread.turn(...)when you need aTurnHandlefor streaming, steering, or interrupting before collectingTurnResult - one client can consume multiple active turns concurrently; turn streams are routed by turn ID
4) Change sandbox access
Use one enum for the initial sandbox and for later turn overrides:
from openai_codex import Codex, Sandbox
with Codex() as codex:
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
thread.run("Make the requested changes.")
review = thread.run("Review the diff only.", sandbox=Sandbox.read_only)
Available presets:
Sandbox.read_only: read files without allowing writes.Sandbox.workspace_write: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots.Sandbox.full_access: run without filesystem access restrictions.
When sandbox= is omitted, app-server uses its configured default. A turn
override also becomes the sandbox for subsequent turns on that thread.
5) Continue the same thread (multi-turn)
from openai_codex import Codex
with Codex() as codex:
thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
first = thread.run("Summarize Rust ownership in 2 bullets.")
second = thread.run("Now explain it to a Python developer.")
print("first:", first.final_response)
print("second:", second.final_response)
6) Async parity
Use async with AsyncCodex() as the normal async entrypoint. AsyncCodex
initializes lazily, and context entry makes startup/shutdown explicit.
import asyncio
from openai_codex import AsyncCodex
async def main() -> None:
async with AsyncCodex() as codex:
thread = await codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"})
result = await thread.run("Continue where we left off.")
print(result.final_response)
asyncio.run(main())
7) Resume an existing thread
from openai_codex import Codex
THREAD_ID = "thr_123" # replace with a real id
with Codex() as codex:
thread = codex.thread_resume(THREAD_ID)
result = thread.run("Continue where we left off.")
print(result.final_response)
8) Public app-server types
The convenience wrappers live at the package root. Public app-server value and event types live under:
from openai_codex.types import ThreadReadResponse, Turn, TurnStatus
9) Next stops
- API surface and signatures:
docs/api-reference.md - Common decisions/pitfalls:
docs/faq.md - End-to-end runnable examples:
examples/README.md