[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.
This commit is contained in:
Ahmed Ibrahim
2026-05-27 11:11:04 -07:00
committed by GitHub
Unverified
parent bee78806a9
commit b1cbf622ad
13 changed files with 310 additions and 74 deletions
@@ -11,6 +11,7 @@ from openai_codex.api import (
ApprovalMode,
AsyncCodex,
Codex,
Sandbox,
)
from openai_codex.generated.v2_all import TurnStartParams
from openai_codex.models import InitializeResponse
@@ -158,6 +159,40 @@ def test_unknown_approval_mode_is_rejected() -> None:
public_api_module._approval_mode_settings("allow_all") # type: ignore[arg-type]
def test_sandbox_presets_serialize_for_threads_and_turns() -> None:
"""One public sandbox enum should map to both stable wire representations."""
assert {
sandbox.name: public_api_module._sandbox_mode(sandbox).value for sandbox in Sandbox
} == {
"read_only": "read-only",
"workspace_write": "workspace-write",
"full_access": "danger-full-access",
}
assert {
sandbox.name: public_api_module._sandbox_policy(sandbox).model_dump(
by_alias=True,
mode="json",
)
for sandbox in Sandbox
} == {
"read_only": {"networkAccess": False, "type": "readOnly"},
"workspace_write": {
"excludeSlashTmp": False,
"excludeTmpdirEnvVar": False,
"networkAccess": False,
"type": "workspaceWrite",
"writableRoots": [],
},
"full_access": {"type": "dangerFullAccess"},
}
def test_raw_sandbox_strings_are_rejected() -> None:
"""Callers should use the discoverable enum rather than memorizing values."""
with pytest.raises(ValueError, match="Sandbox\\.workspace_write"):
public_api_module._sandbox_mode("workspace") # type: ignore[arg-type]
def test_retry_examples_compare_status_with_enum() -> None:
for path in (
ROOT / "examples" / "10_error_handling_and_retry" / "sync.py",