mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
committed by
GitHub
Unverified
parent
bee78806a9
commit
b1cbf622ad
@@ -875,7 +875,41 @@ def _approval_mode_model_arg_lines(*, indent: str = " ") -> list[str]
|
||||
|
||||
|
||||
def _model_arg_lines(fields: list[PublicFieldSpec], *, indent: str = " ") -> list[str]:
|
||||
return [f"{indent}{field.wire_name}={field.py_name}," for field in fields]
|
||||
lines: list[str] = []
|
||||
for field in fields:
|
||||
arg = field.py_name
|
||||
if field.wire_name == "sandbox":
|
||||
arg = "_sandbox_mode(sandbox)"
|
||||
elif field.wire_name == "sandbox_policy":
|
||||
arg = "_sandbox_policy(sandbox)"
|
||||
lines.append(f"{indent}{field.wire_name}={arg},")
|
||||
return lines
|
||||
|
||||
|
||||
def _replace_public_sandbox_field(
|
||||
fields: list[PublicFieldSpec], *, wire_name: str
|
||||
) -> list[PublicFieldSpec]:
|
||||
"""Expose stable wire sandbox settings through one public enum parameter."""
|
||||
public_fields: list[PublicFieldSpec] = []
|
||||
replaced = False
|
||||
for field in fields:
|
||||
if field.wire_name != wire_name:
|
||||
public_fields.append(field)
|
||||
continue
|
||||
if replaced:
|
||||
raise RuntimeError(f"Found more than one generated sandbox field named {wire_name}")
|
||||
public_fields.append(
|
||||
PublicFieldSpec(
|
||||
wire_name=wire_name,
|
||||
py_name="sandbox",
|
||||
annotation="Sandbox | None",
|
||||
required=False,
|
||||
)
|
||||
)
|
||||
replaced = True
|
||||
if not replaced:
|
||||
raise RuntimeError(f"Could not find generated sandbox field named {wire_name}")
|
||||
return public_fields
|
||||
|
||||
|
||||
def _replace_generated_block(source: str, block_name: str, body: str) -> str:
|
||||
@@ -1113,6 +1147,7 @@ def generate_public_api_flat_methods() -> None:
|
||||
"ThreadStartParams",
|
||||
exclude=approval_fields,
|
||||
)
|
||||
thread_start_fields = _replace_public_sandbox_field(thread_start_fields, wire_name="sandbox")
|
||||
thread_list_fields = _load_public_fields(
|
||||
"openai_codex.generated.v2_all",
|
||||
"ThreadListParams",
|
||||
@@ -1122,16 +1157,19 @@ def generate_public_api_flat_methods() -> None:
|
||||
"ThreadResumeParams",
|
||||
exclude={"thread_id", *approval_fields},
|
||||
)
|
||||
thread_resume_fields = _replace_public_sandbox_field(thread_resume_fields, wire_name="sandbox")
|
||||
thread_fork_fields = _load_public_fields(
|
||||
"openai_codex.generated.v2_all",
|
||||
"ThreadForkParams",
|
||||
exclude={"thread_id", *approval_fields},
|
||||
)
|
||||
thread_fork_fields = _replace_public_sandbox_field(thread_fork_fields, wire_name="sandbox")
|
||||
turn_start_fields = _load_public_fields(
|
||||
"openai_codex.generated.v2_all",
|
||||
"TurnStartParams",
|
||||
exclude={"thread_id", "input", *approval_fields},
|
||||
)
|
||||
turn_start_fields = _replace_public_sandbox_field(turn_start_fields, wire_name="sandbox_policy")
|
||||
|
||||
source = public_api_path.read_text()
|
||||
source = _replace_generated_block(
|
||||
|
||||
Reference in New Issue
Block a user