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
+25
-2
@@ -23,12 +23,12 @@ when you intentionally want to run against a specific local app-server binary.
|
||||
## Quickstart
|
||||
|
||||
```python
|
||||
from openai_codex import Codex
|
||||
from openai_codex import Codex, Sandbox
|
||||
|
||||
with Codex() as codex:
|
||||
# Call login_api_key(...) first when this app-server session is not
|
||||
# already authenticated.
|
||||
thread = codex.thread_start(model="gpt-5")
|
||||
thread = codex.thread_start(model="gpt-5", sandbox=Sandbox.workspace_write)
|
||||
result = thread.run("Say hello in one sentence.")
|
||||
print(result.final_response)
|
||||
print(len(result.items))
|
||||
@@ -38,6 +38,29 @@ with Codex() as codex:
|
||||
`final_response` is `None` when the turn completes without a final-answer or
|
||||
phase-less assistant message item.
|
||||
|
||||
## Sandbox
|
||||
|
||||
Use the same enum when creating a thread or changing its sandbox for a turn:
|
||||
|
||||
```python
|
||||
from openai_codex import Codex, Sandbox
|
||||
|
||||
with Codex() as codex:
|
||||
thread = codex.thread_start(sandbox=Sandbox.workspace_write)
|
||||
thread.run("Make the requested change.")
|
||||
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 sandbox
|
||||
passed to `run(...)` or `turn(...)` applies to that turn and subsequent turns
|
||||
on the thread.
|
||||
|
||||
## Login
|
||||
|
||||
Use the auth helper that matches your app:
|
||||
|
||||
@@ -12,6 +12,7 @@ from openai_codex import (
|
||||
Codex,
|
||||
AsyncCodex,
|
||||
ApprovalMode,
|
||||
Sandbox,
|
||||
ChatgptLoginHandle,
|
||||
DeviceCodeLoginHandle,
|
||||
AsyncChatgptLoginHandle,
|
||||
@@ -63,10 +64,10 @@ Properties/methods:
|
||||
- `login_chatgpt_device_code() -> DeviceCodeLoginHandle`
|
||||
- `account(*, refresh_token: bool = False) -> GetAccountResponse`
|
||||
- `logout() -> None`
|
||||
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox=None) -> Thread`
|
||||
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread`
|
||||
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> ThreadListResponse`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox=None) -> Thread`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox=None) -> Thread`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox: Sandbox | None = None) -> Thread`
|
||||
- `thread_archive(thread_id: str) -> ThreadArchiveResponse`
|
||||
- `thread_unarchive(thread_id: str) -> Thread`
|
||||
- `models(*, include_hidden: bool = False) -> ModelListResponse`
|
||||
@@ -103,10 +104,10 @@ Properties/methods:
|
||||
- `login_chatgpt_device_code() -> Awaitable[AsyncDeviceCodeLoginHandle]`
|
||||
- `account(*, refresh_token: bool = False) -> Awaitable[GetAccountResponse]`
|
||||
- `logout() -> Awaitable[None]`
|
||||
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox=None) -> Awaitable[AsyncThread]`
|
||||
- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
|
||||
- `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> Awaitable[ThreadListResponse]`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox=None) -> Awaitable[AsyncThread]`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, sandbox=None) -> Awaitable[AsyncThread]`
|
||||
- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
|
||||
- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]`
|
||||
- `thread_archive(thread_id: str) -> Awaitable[ThreadArchiveResponse]`
|
||||
- `thread_unarchive(thread_id: str) -> Awaitable[AsyncThread]`
|
||||
- `models(*, include_hidden: bool = False) -> Awaitable[ModelListResponse]`
|
||||
@@ -148,16 +149,16 @@ attempt. API-key login completes synchronously and does not return a handle.
|
||||
|
||||
### Thread
|
||||
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> TurnResult`
|
||||
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> TurnHandle`
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnResult`
|
||||
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnHandle`
|
||||
- `read(*, include_turns: bool = False) -> ThreadReadResponse`
|
||||
- `set_name(name: str) -> ThreadSetNameResponse`
|
||||
- `compact() -> ThreadCompactStartResponse`
|
||||
|
||||
### AsyncThread
|
||||
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[TurnResult]`
|
||||
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]`
|
||||
- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[TurnResult]`
|
||||
- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]`
|
||||
- `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]`
|
||||
- `set_name(name: str) -> Awaitable[ThreadSetNameResponse]`
|
||||
- `compact() -> Awaitable[ThreadCompactStartResponse]`
|
||||
@@ -182,6 +183,27 @@ phase-less assistant message item.
|
||||
Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`,
|
||||
`interrupt()`) before collecting the turn result.
|
||||
|
||||
## Sandbox
|
||||
|
||||
Use `sandbox=` consistently on thread lifecycle methods and turns:
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
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 sandbox
|
||||
passed to `run(...)` or `turn(...)` applies to that turn and subsequent turns.
|
||||
|
||||
## TurnHandle / AsyncTurnHandle
|
||||
|
||||
### TurnHandle
|
||||
|
||||
+20
-1
@@ -48,7 +48,26 @@ If you are migrating older code, update these names:
|
||||
- `sortKey` -> `sort_key`
|
||||
- `sourceKinds` -> `source_kinds`
|
||||
- `outputSchema` -> `output_schema`
|
||||
- `sandboxPolicy` -> `sandbox_policy`
|
||||
|
||||
## How do I choose sandbox access?
|
||||
|
||||
Use the same `sandbox=` keyword for threads and turns:
|
||||
|
||||
```python
|
||||
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(...)`?
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Existing Codex auth state is reused automatically. To authenticate from the SDK,
|
||||
use the flow that fits your app:
|
||||
|
||||
```python
|
||||
from openai_codex import Codex
|
||||
from openai_codex import Codex, Sandbox
|
||||
|
||||
with Codex() as codex:
|
||||
codex.login_api_key("sk-...")
|
||||
@@ -58,7 +58,11 @@ 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"})
|
||||
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)
|
||||
@@ -76,7 +80,29 @@ What happened:
|
||||
- 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) Continue the same thread (multi-turn)
|
||||
## 4) Change sandbox access
|
||||
|
||||
Use one enum for the initial sandbox and for later turn overrides:
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
```python
|
||||
from openai_codex import Codex
|
||||
@@ -91,7 +117,7 @@ with Codex() as codex:
|
||||
print("second:", second.final_response)
|
||||
```
|
||||
|
||||
## 5) Async parity
|
||||
## 6) Async parity
|
||||
|
||||
Use `async with AsyncCodex()` as the normal async entrypoint. `AsyncCodex`
|
||||
initializes lazily, and context entry makes startup/shutdown explicit.
|
||||
@@ -111,7 +137,7 @@ async def main() -> None:
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## 6) Resume an existing thread
|
||||
## 7) Resume an existing thread
|
||||
|
||||
```python
|
||||
from openai_codex import Codex
|
||||
@@ -124,7 +150,7 @@ with Codex() as codex:
|
||||
print(result.final_response)
|
||||
```
|
||||
|
||||
## 7) Public app-server types
|
||||
## 8) Public app-server types
|
||||
|
||||
The convenience wrappers live at the package root. Public app-server value and
|
||||
event types live under:
|
||||
@@ -133,7 +159,7 @@ event types live under:
|
||||
from openai_codex.types import ThreadReadResponse, Turn, TurnStatus
|
||||
```
|
||||
|
||||
## 8) Next stops
|
||||
## 9) Next stops
|
||||
|
||||
- API surface and signatures: `docs/api-reference.md`
|
||||
- Common decisions/pitfalls: `docs/faq.md`
|
||||
|
||||
@@ -13,12 +13,12 @@ import asyncio
|
||||
|
||||
from openai_codex import (
|
||||
AsyncCodex,
|
||||
Sandbox,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
Personality,
|
||||
ReasoningEffort,
|
||||
ReasoningSummary,
|
||||
SandboxPolicy,
|
||||
)
|
||||
|
||||
REASONING_RANK = {
|
||||
@@ -67,13 +67,6 @@ OUTPUT_SCHEMA = {
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
SANDBOX_POLICY = SandboxPolicy.model_validate(
|
||||
{
|
||||
"type": "readOnly",
|
||||
"access": {"type": "fullAccess"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with AsyncCodex(config=runtime_config()) as codex:
|
||||
@@ -106,7 +99,7 @@ async def main() -> None:
|
||||
model=selected_model.model,
|
||||
output_schema=OUTPUT_SCHEMA,
|
||||
personality=Personality.pragmatic,
|
||||
sandbox_policy=SANDBOX_POLICY,
|
||||
sandbox=Sandbox.read_only,
|
||||
summary=ReasoningSummary.model_validate("concise"),
|
||||
)
|
||||
second = await second_turn.run()
|
||||
|
||||
@@ -11,12 +11,12 @@ ensure_local_sdk_src()
|
||||
|
||||
from openai_codex import (
|
||||
Codex,
|
||||
Sandbox,
|
||||
)
|
||||
from openai_codex.types import (
|
||||
Personality,
|
||||
ReasoningEffort,
|
||||
ReasoningSummary,
|
||||
SandboxPolicy,
|
||||
)
|
||||
|
||||
REASONING_RANK = {
|
||||
@@ -65,14 +65,6 @@ OUTPUT_SCHEMA = {
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
SANDBOX_POLICY = SandboxPolicy.model_validate(
|
||||
{
|
||||
"type": "readOnly",
|
||||
"access": {"type": "fullAccess"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
with Codex(config=runtime_config()) as codex:
|
||||
models = codex.models(include_hidden=True)
|
||||
selected_model = _pick_highest_model(models.data)
|
||||
@@ -102,7 +94,7 @@ with Codex(config=runtime_config()) as codex:
|
||||
model=selected_model.model,
|
||||
output_schema=OUTPUT_SCHEMA,
|
||||
personality=Personality.pragmatic,
|
||||
sandbox_policy=SANDBOX_POLICY,
|
||||
sandbox=Sandbox.read_only,
|
||||
summary=ReasoningSummary.model_validate("concise"),
|
||||
).run()
|
||||
|
||||
|
||||
@@ -220,11 +220,11 @@
|
||||
"source": [
|
||||
"# Cell 5b: one turn with most optional turn params\n",
|
||||
"from pathlib import Path\n",
|
||||
"from openai_codex import (\n",
|
||||
"from openai_codex import Sandbox\n",
|
||||
"from openai_codex.types import (\n",
|
||||
" Personality,\n",
|
||||
" ReasoningEffort,\n",
|
||||
" ReasoningSummary,\n",
|
||||
" SandboxPolicy,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"output_schema = {\n",
|
||||
@@ -237,7 +237,6 @@
|
||||
" 'additionalProperties': False,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"sandbox_policy = SandboxPolicy.model_validate({'type': 'readOnly', 'access': {'type': 'fullAccess'}})\n",
|
||||
"summary = ReasoningSummary.model_validate('concise')\n",
|
||||
"\n",
|
||||
"with Codex() as codex:\n",
|
||||
@@ -249,7 +248,7 @@
|
||||
" model='gpt-5.4',\n",
|
||||
" output_schema=output_schema,\n",
|
||||
" personality=Personality.pragmatic,\n",
|
||||
" sandbox_policy=sandbox_policy,\n",
|
||||
" sandbox=Sandbox.read_only,\n",
|
||||
" summary=summary,\n",
|
||||
" )\n",
|
||||
" result = turn.run()\n",
|
||||
@@ -266,11 +265,11 @@
|
||||
"source": [
|
||||
"# Cell 5c: choose highest model + highest supported reasoning, then run turns\n",
|
||||
"from pathlib import Path\n",
|
||||
"from openai_codex import (\n",
|
||||
"from openai_codex import Sandbox\n",
|
||||
"from openai_codex.types import (\n",
|
||||
" Personality,\n",
|
||||
" ReasoningEffort,\n",
|
||||
" ReasoningSummary,\n",
|
||||
" SandboxPolicy,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"reasoning_rank = {\n",
|
||||
@@ -310,7 +309,6 @@
|
||||
" 'required': ['summary', 'actions'],\n",
|
||||
" 'additionalProperties': False,\n",
|
||||
"}\n",
|
||||
"sandbox_policy = SandboxPolicy.model_validate({'type': 'readOnly', 'access': {'type': 'fullAccess'}})\n",
|
||||
"\n",
|
||||
"with Codex() as codex:\n",
|
||||
" models = codex.models(include_hidden=True)\n",
|
||||
@@ -337,7 +335,7 @@
|
||||
" model=selected_model.model,\n",
|
||||
" output_schema=output_schema,\n",
|
||||
" personality=Personality.pragmatic,\n",
|
||||
" sandbox_policy=sandbox_policy,\n",
|
||||
" sandbox=Sandbox.read_only,\n",
|
||||
" summary=ReasoningSummary.model_validate('concise'),\n",
|
||||
" ).run()\n",
|
||||
" print('agent.message.params:', second.final_response)\n",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -15,6 +15,7 @@ from .api import (
|
||||
LocalImageInput,
|
||||
MentionInput,
|
||||
RunInput,
|
||||
Sandbox,
|
||||
SkillInput,
|
||||
TextInput,
|
||||
Thread,
|
||||
@@ -44,6 +45,7 @@ __all__ = [
|
||||
"Codex",
|
||||
"AsyncCodex",
|
||||
"ApprovalMode",
|
||||
"Sandbox",
|
||||
"ChatgptLoginHandle",
|
||||
"DeviceCodeLoginHandle",
|
||||
"AsyncChatgptLoginHandle",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import NoReturn
|
||||
|
||||
from .generated.v2_all import (
|
||||
DangerFullAccessSandboxPolicy,
|
||||
ReadOnlySandboxPolicy,
|
||||
SandboxMode,
|
||||
SandboxPolicy,
|
||||
WorkspaceWriteSandboxPolicy,
|
||||
)
|
||||
|
||||
|
||||
class Sandbox(str, Enum):
|
||||
"""Preset filesystem access levels for threads and turns.
|
||||
|
||||
`read_only` allows file reads without writes. `workspace_write` is the
|
||||
normal default for projects with a recorded trust decision and allows
|
||||
writes inside the workspace and configured writable roots. `full_access`
|
||||
removes filesystem access restrictions.
|
||||
"""
|
||||
|
||||
read_only = "read-only"
|
||||
workspace_write = "workspace-write"
|
||||
full_access = "full-access"
|
||||
|
||||
|
||||
def _require_sandbox(sandbox: Sandbox) -> None:
|
||||
if isinstance(sandbox, Sandbox):
|
||||
return
|
||||
options = ", ".join(f"Sandbox.{value.name}" for value in Sandbox)
|
||||
raise ValueError(f"sandbox must be one of: {options}")
|
||||
|
||||
|
||||
def _sandbox_mode(sandbox: Sandbox | None) -> SandboxMode | None:
|
||||
"""Translate a public preset to the thread lifecycle wire mode."""
|
||||
if sandbox is None:
|
||||
return None
|
||||
_require_sandbox(sandbox)
|
||||
|
||||
match sandbox:
|
||||
case Sandbox.read_only:
|
||||
return SandboxMode.read_only
|
||||
case Sandbox.workspace_write:
|
||||
return SandboxMode.workspace_write
|
||||
case Sandbox.full_access:
|
||||
return SandboxMode.danger_full_access
|
||||
case _:
|
||||
return _assert_never_sandbox(sandbox)
|
||||
|
||||
|
||||
def _sandbox_policy(sandbox: Sandbox | None) -> SandboxPolicy | None:
|
||||
"""Translate a public preset to the turn override wire policy."""
|
||||
if sandbox is None:
|
||||
return None
|
||||
_require_sandbox(sandbox)
|
||||
|
||||
match sandbox:
|
||||
case Sandbox.read_only:
|
||||
return SandboxPolicy(
|
||||
root=ReadOnlySandboxPolicy(type="readOnly"),
|
||||
)
|
||||
case Sandbox.workspace_write:
|
||||
return SandboxPolicy(
|
||||
root=WorkspaceWriteSandboxPolicy(type="workspaceWrite"),
|
||||
)
|
||||
case Sandbox.full_access:
|
||||
return SandboxPolicy(
|
||||
root=DangerFullAccessSandboxPolicy(type="dangerFullAccess"),
|
||||
)
|
||||
case _:
|
||||
return _assert_never_sandbox(sandbox)
|
||||
|
||||
|
||||
def _assert_never_sandbox(sandbox: NoReturn) -> NoReturn:
|
||||
"""Make sandbox mapping exhaustive for static type checkers."""
|
||||
raise AssertionError(f"Unhandled sandbox: {sandbox!r}")
|
||||
@@ -37,6 +37,7 @@ from ._run import (
|
||||
_collect_async_turn_result,
|
||||
_collect_turn_result,
|
||||
)
|
||||
from ._sandbox import Sandbox as Sandbox, _sandbox_mode, _sandbox_policy
|
||||
from .async_client import AsyncAppServerClient
|
||||
from .client import AppServerClient, AppServerConfig
|
||||
from .generated.v2_all import (
|
||||
@@ -48,8 +49,6 @@ from .generated.v2_all import (
|
||||
Personality,
|
||||
ReasoningEffort,
|
||||
ReasoningSummary,
|
||||
SandboxMode,
|
||||
SandboxPolicy,
|
||||
SortDirection,
|
||||
ThreadArchiveResponse,
|
||||
ThreadCompactStartResponse,
|
||||
@@ -138,7 +137,7 @@ class Codex:
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_name: str | None = None,
|
||||
service_tier: str | None = None,
|
||||
session_start_source: ThreadStartSource | None = None,
|
||||
@@ -156,7 +155,7 @@ class Codex:
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
service_name=service_name,
|
||||
service_tier=service_tier,
|
||||
session_start_source=session_start_source,
|
||||
@@ -205,7 +204,7 @@ class Codex:
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
) -> Thread:
|
||||
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
|
||||
@@ -220,7 +219,7 @@ class Codex:
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
service_tier=service_tier,
|
||||
)
|
||||
resumed = self._client.thread_resume(thread_id, params)
|
||||
@@ -238,7 +237,7 @@ class Codex:
|
||||
ephemeral: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> Thread:
|
||||
@@ -254,7 +253,7 @@ class Codex:
|
||||
ephemeral=ephemeral,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
sandbox=sandbox,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
service_tier=service_tier,
|
||||
thread_source=thread_source,
|
||||
)
|
||||
@@ -371,7 +370,7 @@ class AsyncCodex:
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_name: str | None = None,
|
||||
service_tier: str | None = None,
|
||||
session_start_source: ThreadStartSource | None = None,
|
||||
@@ -390,7 +389,7 @@ class AsyncCodex:
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
service_name=service_name,
|
||||
service_tier=service_tier,
|
||||
session_start_source=session_start_source,
|
||||
@@ -440,7 +439,7 @@ class AsyncCodex:
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
) -> AsyncThread:
|
||||
await self._ensure_initialized()
|
||||
@@ -456,7 +455,7 @@ class AsyncCodex:
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
personality=personality,
|
||||
sandbox=sandbox,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
service_tier=service_tier,
|
||||
)
|
||||
resumed = await self._client.thread_resume(thread_id, params)
|
||||
@@ -474,7 +473,7 @@ class AsyncCodex:
|
||||
ephemeral: bool | None = None,
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> AsyncThread:
|
||||
@@ -491,7 +490,7 @@ class AsyncCodex:
|
||||
ephemeral=ephemeral,
|
||||
model=model,
|
||||
model_provider=model_provider,
|
||||
sandbox=sandbox,
|
||||
sandbox=_sandbox_mode(sandbox),
|
||||
service_tier=service_tier,
|
||||
thread_source=thread_source,
|
||||
)
|
||||
@@ -529,7 +528,7 @@ class Thread:
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnResult:
|
||||
@@ -541,7 +540,7 @@ class Thread:
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
summary=summary,
|
||||
)
|
||||
@@ -562,7 +561,7 @@ class Thread:
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnHandle:
|
||||
@@ -578,7 +577,7 @@ class Thread:
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
sandbox_policy=_sandbox_policy(sandbox),
|
||||
service_tier=service_tier,
|
||||
summary=summary,
|
||||
)
|
||||
@@ -612,7 +611,7 @@ class AsyncThread:
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnResult:
|
||||
@@ -624,7 +623,7 @@ class AsyncThread:
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
summary=summary,
|
||||
)
|
||||
@@ -645,7 +644,7 @@ class AsyncThread:
|
||||
model: str | None = None,
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
sandbox: Sandbox | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> AsyncTurnHandle:
|
||||
@@ -662,7 +661,7 @@ class AsyncThread:
|
||||
model=model,
|
||||
output_schema=output_schema,
|
||||
personality=personality,
|
||||
sandbox_policy=sandbox_policy,
|
||||
sandbox_policy=_sandbox_policy(sandbox),
|
||||
service_tier=service_tier,
|
||||
summary=summary,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -16,6 +16,7 @@ from openai_codex import (
|
||||
AsyncThread,
|
||||
AsyncTurnHandle,
|
||||
Codex,
|
||||
Sandbox,
|
||||
Thread,
|
||||
TurnHandle,
|
||||
TurnResult,
|
||||
@@ -29,6 +30,7 @@ EXPECTED_ROOT_EXPORTS = [
|
||||
"Codex",
|
||||
"AsyncCodex",
|
||||
"ApprovalMode",
|
||||
"Sandbox",
|
||||
"ChatgptLoginHandle",
|
||||
"DeviceCodeLoginHandle",
|
||||
"AsyncChatgptLoginHandle",
|
||||
@@ -191,6 +193,15 @@ def test_root_exports_approval_mode() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_root_exports_sandbox_presets() -> None:
|
||||
"""The friendly sandbox API should expose only obvious named presets."""
|
||||
assert [(sandbox.name, sandbox.value) for sandbox in Sandbox] == [
|
||||
("read_only", "read-only"),
|
||||
("workspace_write", "workspace-write"),
|
||||
("full_access", "full-access"),
|
||||
]
|
||||
|
||||
|
||||
def test_package_and_default_client_versions_follow_project_version() -> None:
|
||||
"""The importable package version should stay aligned with pyproject metadata."""
|
||||
pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml"
|
||||
@@ -341,7 +352,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"model",
|
||||
"output_schema",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"summary",
|
||||
],
|
||||
@@ -352,7 +363,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"model",
|
||||
"output_schema",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"summary",
|
||||
],
|
||||
@@ -416,7 +427,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"model",
|
||||
"output_schema",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"summary",
|
||||
],
|
||||
@@ -427,7 +438,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"model",
|
||||
"output_schema",
|
||||
"personality",
|
||||
"sandbox_policy",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"summary",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user