Files
codex/sdk/python/docs/getting-started.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.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-bin runtime package, or an explicit codex_bin override

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 initialized codex app-server.
  • thread_start(...) created a thread.
  • thread.run("...") started a turn, consumed events until completion, and returned TurnResult with turn metadata, final assistant response, collected items, and usage.
  • result.final_response is None when 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 a TurnHandle for streaming, steering, or interrupting before collecting TurnResult
  • 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