Files
codex/sdk/python/docs/faq.md
T
Ahmed Ibrahim b1cbf622ad [codex] Add friendly Python SDK sandbox presets (#24772)
## 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.
2026-05-27 11:11:04 -07:00

4.0 KiB

FAQ

Thread vs turn

  • A Thread is conversation state.
  • A Turn is one model execution inside that thread.
  • Multi-turn chat means multiple turns on the same Thread.

run() vs stream()

  • Thread.run(...) starts a turn and returns TurnResult.
  • TurnHandle.run() / AsyncTurnHandle.run() consumes events for an existing turn handle and returns the same TurnResult shape.
  • TurnHandle.stream() / AsyncTurnHandle.stream() yields raw notifications (Notification) so you can react event-by-event.

Choose run() for most apps. Choose stream() for progress UIs, custom timeout logic, or custom parsing.

Sync vs async clients

  • Codex is the sync public API.
  • AsyncCodex is an async replica of the same public API shape.
  • Prefer async with AsyncCodex() for async code. It is the standard path for explicit startup/shutdown, and AsyncCodex initializes lazily on context entry or first awaited API use.

If your app is not already async, stay with Codex.

How do I log in?

  • login_api_key(...) authenticates immediately with an API key.
  • login_chatgpt() starts browser login and returns a handle with auth_url.
  • login_chatgpt_device_code() starts device-code login and returns a handle with verification_url and user_code.
  • Interactive handles expose wait() for the matching account/login/completed notification and cancel() to stop that attempt.
  • account() reads the current account state, and logout() clears it.

Public kwargs are snake_case

Public API keyword names are snake_case. The SDK still maps them to wire camelCase under the hood.

If you are migrating older code, update these names:

  • approvalPolicy -> approval_policy
  • baseInstructions -> base_instructions
  • developerInstructions -> developer_instructions
  • modelProvider -> model_provider
  • modelProviders -> model_providers
  • sortKey -> sort_key
  • sourceKinds -> source_kinds
  • outputSchema -> output_schema

How do I choose sandbox access?

Use the same sandbox= keyword for threads and turns:

from openai_codex import Sandbox

thread = codex.thread_start(sandbox=Sandbox.workspace_write)
result = thread.run("Review only.", sandbox=Sandbox.read_only)

The presets are:

  • 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 sandbox override applies to that turn and subsequent turns.

Why only thread_start(...) and thread_resume(...)?

The public API keeps only explicit lifecycle calls:

  • thread_start(...) to create new threads
  • thread_resume(thread_id, ...) to continue existing threads

This avoids duplicate ways to do the same operation and keeps behavior explicit.

Why does constructor fail?

Codex() is eager: it starts transport and calls initialize in __init__.

Common causes:

  • published runtime package (openai-codex-cli-bin) is not installed
  • local codex_bin override points to a missing file
  • app-server version older than the SDK schema

Why does a turn "hang"?

A turn is complete only when turn/completed arrives for that turn ID.

  • run() waits for this automatically.
  • With stream(), keep consuming notifications until completion.

How do I retry safely?

Use retry_on_overload(...) for transient overload failures (ServerBusyError).

Do not blindly retry all errors. For InvalidParamsError or MethodNotFoundError, fix inputs or update the runtime/schema version instead.

Common pitfalls

  • Starting a new thread for every prompt when you wanted continuity.
  • Forgetting to close() (or not using context managers).
  • Reading Turn.items from live start/completed payloads instead of using TurnResult.items.
  • Mixing SDK input classes with raw dicts incorrectly.