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
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user