Compare commits

...

5 Commits

Author SHA1 Message Date
Ahmed Ibrahim
a1ef35b7b0 Format Python SDK dangerous bypass updates 2026-05-16 09:49:22 -07:00
Ahmed Ibrahim
d8ea048b5e Fix Python SDK approvals import order 2026-05-16 09:49:22 -07:00
Ahmed Ibrahim
79a9adbf44 Fix dangerous bypass integration coverage 2026-05-16 09:49:22 -07:00
Ahmed Ibrahim
3cc8e7b30f Format dangerous bypass SDK updates 2026-05-16 09:49:22 -07:00
Ahmed Ibrahim
2eb61fa741 Add Python SDK dangerous bypass mode 2026-05-16 09:49:21 -07:00
8 changed files with 358 additions and 5 deletions

View File

@@ -3,7 +3,7 @@
Public surface of `openai_codex` for app-server v2.
This SDK surface is experimental. Turn streams are routed by turn ID so one client can consume multiple active turns concurrently.
Thread and turn starts expose `approval_mode`. `ApprovalMode.auto_review` is the default; use `ApprovalMode.deny_all` to deny escalated permissions.
Thread and turn starts expose `approval_mode`. `ApprovalMode.auto_review` is the default; use `ApprovalMode.deny_all` to deny escalated permissions. `ApprovalMode.dangerously_bypass_approvals_and_sandbox` disables approvals and requests full sandbox bypass. That explicit bypass mode cannot be combined with `sandbox` on thread APIs or `sandbox_policy` on turn APIs.
## Package Entry

View File

@@ -867,6 +867,21 @@ def _approval_mode_model_arg_lines(*, indent: str = " ") -> list[str]
]
def _approval_mode_thread_sandbox_line(*, indent: str = " ") -> str:
"""Return the approval-mode sandbox preset for thread operations."""
return f"{indent}sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)"
def _approval_mode_turn_sandbox_policy_lines(*, indent: str = " ") -> list[str]:
"""Return the approval-mode sandbox preset for turn operations."""
return [
f"{indent}sandbox_policy = _turn_sandbox_policy_for_approval_mode(",
f"{indent} approval_mode,",
f"{indent} sandbox_policy,",
f"{indent})",
]
def _model_arg_lines(fields: list[PublicFieldSpec], *, indent: str = " ") -> list[str]:
return [f"{indent}{field.wire_name}={field.py_name}," for field in fields]
@@ -896,6 +911,7 @@ def _render_codex_block(
*_kw_signature_lines(thread_start_fields),
" ) -> Thread:",
_approval_mode_assignment_line("_approval_mode_settings"),
_approval_mode_thread_sandbox_line(),
" params = ThreadStartParams(",
*_approval_mode_model_arg_lines(),
*_model_arg_lines(thread_start_fields),
@@ -921,6 +937,7 @@ def _render_codex_block(
*_kw_signature_lines(resume_fields),
" ) -> Thread:",
_approval_mode_assignment_line("_approval_mode_override_settings"),
_approval_mode_thread_sandbox_line(),
" params = ThreadResumeParams(",
" thread_id=thread_id,",
*_approval_mode_model_arg_lines(),
@@ -937,6 +954,7 @@ def _render_codex_block(
*_kw_signature_lines(fork_fields),
" ) -> Thread:",
_approval_mode_assignment_line("_approval_mode_override_settings"),
_approval_mode_thread_sandbox_line(),
" params = ThreadForkParams(",
" thread_id=thread_id,",
*_approval_mode_model_arg_lines(),
@@ -970,6 +988,7 @@ def _render_async_codex_block(
" ) -> AsyncThread:",
" await self._ensure_initialized()",
_approval_mode_assignment_line("_approval_mode_settings"),
_approval_mode_thread_sandbox_line(),
" params = ThreadStartParams(",
*_approval_mode_model_arg_lines(),
*_model_arg_lines(thread_start_fields),
@@ -997,6 +1016,7 @@ def _render_async_codex_block(
" ) -> AsyncThread:",
" await self._ensure_initialized()",
_approval_mode_assignment_line("_approval_mode_override_settings"),
_approval_mode_thread_sandbox_line(),
" params = ThreadResumeParams(",
" thread_id=thread_id,",
*_approval_mode_model_arg_lines(),
@@ -1014,6 +1034,7 @@ def _render_async_codex_block(
" ) -> AsyncThread:",
" await self._ensure_initialized()",
_approval_mode_assignment_line("_approval_mode_override_settings"),
_approval_mode_thread_sandbox_line(),
" params = ThreadForkParams(",
" thread_id=thread_id,",
*_approval_mode_model_arg_lines(),
@@ -1047,6 +1068,7 @@ def _render_thread_block(
" ) -> TurnHandle:",
" wire_input = _to_wire_input(input)",
_approval_mode_assignment_line("_approval_mode_override_settings"),
*_approval_mode_turn_sandbox_policy_lines(),
" params = TurnStartParams(",
" thread_id=self.id,",
" input=wire_input,",
@@ -1073,6 +1095,7 @@ def _render_async_thread_block(
" await self._codex._ensure_initialized()",
" wire_input = _to_wire_input(input)",
_approval_mode_assignment_line("_approval_mode_override_settings"),
*_approval_mode_turn_sandbox_policy_lines(),
" params = TurnStartParams(",
" thread_id=self.id,",
" input=wire_input,",

View File

@@ -15,6 +15,7 @@ class ApprovalMode(str, Enum):
deny_all = "deny_all"
auto_review = "auto_review"
dangerously_bypass_approvals_and_sandbox = "dangerously_bypass_approvals_and_sandbox"
def _approval_mode_settings(
@@ -33,6 +34,8 @@ def _approval_mode_settings(
)
case ApprovalMode.deny_all:
return AskForApproval(root=AskForApprovalValue.never), None
case ApprovalMode.dangerously_bypass_approvals_and_sandbox:
return AskForApproval(root=AskForApprovalValue.never), None
case _:
return _assert_never_approval_mode(approval_mode)

View File

@@ -30,6 +30,7 @@ from ._run import (
from .async_client import AsyncAppServerClient
from .client import AppServerClient, AppServerConfig
from .generated.v2_all import (
DangerFullAccessSandboxPolicy,
ModelListResponse,
Personality,
ReasoningEffort,
@@ -60,6 +61,30 @@ from .generated.v2_all import (
from .models import InitializeResponse, JsonObject, Notification
def _thread_sandbox_for_approval_mode(
approval_mode: ApprovalMode | None,
sandbox: SandboxMode | None,
) -> SandboxMode | None:
"""Apply approval-mode sandbox presets for thread operations."""
if approval_mode is not ApprovalMode.dangerously_bypass_approvals_and_sandbox:
return sandbox
if sandbox is not None:
raise ValueError("dangerous bypass approval_mode cannot be combined with sandbox")
return SandboxMode.danger_full_access
def _turn_sandbox_policy_for_approval_mode(
approval_mode: ApprovalMode | None,
sandbox_policy: SandboxPolicy | None,
) -> SandboxPolicy | None:
"""Apply approval-mode sandbox presets for turn operations."""
if approval_mode is not ApprovalMode.dangerously_bypass_approvals_and_sandbox:
return sandbox_policy
if sandbox_policy is not None:
raise ValueError("dangerous bypass approval_mode cannot be combined with sandbox_policy")
return SandboxPolicy(root=DangerFullAccessSandboxPolicy(type="dangerFullAccess"))
class Codex:
"""Typed Python client for app-server v2 workflows."""
@@ -105,6 +130,7 @@ class Codex:
thread_source: ThreadSource | None = None,
) -> Thread:
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)
params = ThreadStartParams(
approval_policy=approval_policy,
approvals_reviewer=approvals_reviewer,
@@ -169,6 +195,7 @@ class Codex:
service_tier: str | None = None,
) -> Thread:
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)
params = ThreadResumeParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -203,6 +230,7 @@ class Codex:
thread_source: ThreadSource | None = None,
) -> Thread:
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)
params = ThreadForkParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -307,6 +335,7 @@ class AsyncCodex:
) -> AsyncThread:
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)
params = ThreadStartParams(
approval_policy=approval_policy,
approvals_reviewer=approvals_reviewer,
@@ -373,6 +402,7 @@ class AsyncCodex:
) -> AsyncThread:
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)
params = ThreadResumeParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -408,6 +438,7 @@ class AsyncCodex:
) -> AsyncThread:
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
sandbox = _thread_sandbox_for_approval_mode(approval_mode, sandbox)
params = ThreadForkParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -496,6 +527,10 @@ class Thread:
) -> TurnHandle:
wire_input = _to_wire_input(input)
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
sandbox_policy = _turn_sandbox_policy_for_approval_mode(
approval_mode,
sandbox_policy,
)
params = TurnStartParams(
thread_id=self.id,
input=wire_input,
@@ -580,6 +615,10 @@ class AsyncThread:
await self._codex._ensure_initialized()
wire_input = _to_wire_input(input)
approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode)
sandbox_policy = _turn_sandbox_policy_for_approval_mode(
approval_mode,
sandbox_policy,
)
params = TurnStartParams(
thread_id=self.id,
input=wire_input,

View File

@@ -98,6 +98,11 @@ def response_approval_policy(response: Any) -> str:
return response.model_dump(by_alias=True, mode="json")["approvalPolicy"]
def response_sandbox_type(response: Any) -> str:
"""Return serialized sandbox policy type from a generated thread response."""
return response.model_dump(by_alias=True, mode="json")["sandbox"]["type"]
def agent_message_texts(events: list[Notification]) -> list[str]:
"""Extract completed agent-message text from SDK notifications."""
texts: list[str] = []

View File

@@ -1,12 +1,30 @@
from __future__ import annotations
import asyncio
import json
import shlex
from app_server_harness import AppServerHarness
from app_server_helpers import response_approval_policy
import pytest
from app_server_harness import (
AppServerHarness,
ev_completed,
ev_function_call,
ev_response_created,
sse,
)
from app_server_helpers import response_approval_policy, response_sandbox_type
from openai_codex import ApprovalMode, AsyncCodex, Codex
from openai_codex.generated.v2_all import AskForApprovalValue, ThreadResumeParams
from openai_codex.generated.v2_all import (
AskForApprovalValue,
DangerFullAccessSandboxPolicy,
ReadOnlySandboxPolicy,
SandboxMode,
SandboxPolicy,
ThreadResumeParams,
)
DANGER_FULL_ACCESS_SANDBOX_POLICY_TYPE = DangerFullAccessSandboxPolicy(type="dangerFullAccess").type
def test_thread_resume_inherits_deny_all_approval_mode(tmp_path) -> None:
@@ -86,6 +104,261 @@ def test_thread_fork_can_override_approval_mode(tmp_path) -> None:
}
def test_dangerous_bypass_thread_lifecycle_persists_thread_settings(
tmp_path,
) -> None:
"""Thread lifecycle operations should preserve the explicit bypass preset."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message(
"bypass seeded",
response_id="bypass-thread",
)
with Codex(config=harness.app_server_config()) as codex:
source = codex.thread_start(
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
)
result = source.run("seed the bypass thread")
started_state = codex._client.thread_resume( # noqa: SLF001
source.id,
ThreadResumeParams(thread_id=source.id),
)
resumed = codex.thread_resume(
source.id,
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
)
resumed_state = codex._client.thread_resume( # noqa: SLF001
resumed.id,
ThreadResumeParams(thread_id=resumed.id),
)
forked = codex.thread_fork(
source.id,
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
)
forked_state = codex._client.thread_resume( # noqa: SLF001
forked.id,
ThreadResumeParams(thread_id=forked.id),
)
assert {
"final_response": result.final_response,
"forked_is_distinct": forked.id != source.id,
"started": (
response_approval_policy(started_state),
response_sandbox_type(started_state),
),
"resumed": (
response_approval_policy(resumed_state),
response_sandbox_type(resumed_state),
),
"forked": (
response_approval_policy(forked_state),
response_sandbox_type(forked_state),
),
} == {
"final_response": "bypass seeded",
"forked_is_distinct": True,
"started": (
AskForApprovalValue.never.value,
DANGER_FULL_ACCESS_SANDBOX_POLICY_TYPE,
),
"resumed": (
AskForApprovalValue.never.value,
DANGER_FULL_ACCESS_SANDBOX_POLICY_TYPE,
),
"forked": (
AskForApprovalValue.never.value,
DANGER_FULL_ACCESS_SANDBOX_POLICY_TYPE,
),
}
def test_turn_dangerous_bypass_persists_thread_settings(tmp_path) -> None:
"""Turn-level bypass should persist approvals disabled and sandbox bypassed."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message(
"turn bypass",
response_id="bypass-turn",
)
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start(approval_mode=ApprovalMode.auto_review)
result = thread.run(
"bypass this turn",
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
)
after_turn = codex._client.thread_resume( # noqa: SLF001
thread.id,
ThreadResumeParams(thread_id=thread.id),
)
assert {
"final_response": result.final_response,
"thread_settings": (
response_approval_policy(after_turn),
response_sandbox_type(after_turn),
),
} == {
"final_response": "turn bypass",
"thread_settings": (
AskForApprovalValue.never.value,
DANGER_FULL_ACCESS_SANDBOX_POLICY_TYPE,
),
}
def test_async_turn_dangerous_bypass_persists_thread_settings(tmp_path) -> None:
"""Async turn-level bypass should persist the same app-server settings."""
async def scenario() -> None:
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message(
"async turn bypass",
response_id="async-bypass-turn",
)
async with AsyncCodex(config=harness.app_server_config()) as codex:
thread = await codex.thread_start(approval_mode=ApprovalMode.auto_review)
result = await thread.run(
"bypass this async turn",
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
)
after_turn = await codex._client.thread_resume( # noqa: SLF001
thread.id,
ThreadResumeParams(thread_id=thread.id),
)
assert {
"final_response": result.final_response,
"thread_settings": (
response_approval_policy(after_turn),
response_sandbox_type(after_turn),
),
} == {
"final_response": "async turn bypass",
"thread_settings": (
AskForApprovalValue.never.value,
DANGER_FULL_ACCESS_SANDBOX_POLICY_TYPE,
),
}
asyncio.run(scenario())
def test_outside_workspace_write_rejected_for_deny_all_and_allowed_for_bypass(
tmp_path,
) -> None:
"""Dangerous bypass should be the mode that permits outside-workspace writes."""
rejected_path = tmp_path / "deny-all-outside-write.txt"
allowed_path = tmp_path / "dangerous-outside-write.txt"
with AppServerHarness(tmp_path) as harness:
rejected_args = json.dumps(
{
"command": (f"printf %s rejected > {shlex.quote(str(rejected_path))}"),
"login": False,
"timeout_ms": 1_000,
}
)
dangerous_args = json.dumps(
{
"command": (f"printf %s dangerous > {shlex.quote(str(allowed_path))}"),
"login": False,
"timeout_ms": 1_000,
}
)
harness.responses.enqueue_sse(
sse(
[
ev_response_created("deny-all-write"),
ev_function_call(
"deny-all-outside-write",
"shell_command",
rejected_args,
),
ev_completed("deny-all-write"),
]
)
)
harness.responses.enqueue_assistant_message(
"deny-all shell completed",
response_id="deny-all-final",
)
harness.responses.enqueue_sse(
sse(
[
ev_response_created("dangerous-write"),
ev_function_call(
"dangerous-outside-write",
"shell_command",
dangerous_args,
),
ev_completed("dangerous-write"),
]
)
)
harness.responses.enqueue_assistant_message(
"dangerous shell completed",
response_id="dangerous-final",
)
with Codex(config=harness.app_server_config()) as codex:
denied_thread = codex.thread_start(approval_mode=ApprovalMode.deny_all)
denied_result = denied_thread.run("write outside the workspace")
bypass_thread = codex.thread_start(
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
)
bypass_result = bypass_thread.run("write outside the workspace")
assert {
"denied_final_response": denied_result.final_response,
"denied_path_exists": rejected_path.exists(),
"bypass_final_response": bypass_result.final_response,
"bypass_file_contents": allowed_path.read_text(),
} == {
"denied_final_response": "deny-all shell completed",
"denied_path_exists": False,
"bypass_final_response": "dangerous shell completed",
"bypass_file_contents": "dangerous",
}
def test_dangerous_bypass_rejects_explicit_sandbox_conflicts_before_state_changes(
tmp_path,
) -> None:
"""Conflicting bypass presets should fail before mutating app-server state."""
with AppServerHarness(tmp_path) as harness:
with Codex(config=harness.app_server_config()) as codex:
with pytest.raises(ValueError, match="combined with sandbox"):
codex.thread_start(
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
sandbox=SandboxMode.read_only,
)
threads_after_invalid_start = codex.thread_list(archived=False)
thread = codex.thread_start()
with pytest.raises(ValueError, match="combined with sandbox_policy"):
thread.run(
"this should never reach app-server",
approval_mode=ApprovalMode.dangerously_bypass_approvals_and_sandbox,
sandbox_policy=SandboxPolicy(root=ReadOnlySandboxPolicy(type="readOnly")),
)
thread_state = thread.read()
assert {
"threads_after_invalid_start": [
existing.id for existing in threads_after_invalid_start.data
],
"turns_after_invalid_run": thread_state.thread.turns,
} == {
"threads_after_invalid_start": [],
"turns_after_invalid_run": [],
}
def test_turn_approval_mode_persists_until_next_turn(tmp_path) -> None:
"""A turn-level approval override should apply to later omitted-arg turns."""
with AppServerHarness(tmp_path) as harness:

View File

@@ -149,12 +149,18 @@ def test_approval_modes_serialize_to_expected_start_params() -> None:
"approvalPolicy": "on-request",
"approvalsReviewer": "auto_review",
},
"dangerously_bypass_approvals_and_sandbox": {
"approvalPolicy": "never",
},
}
def test_unknown_approval_mode_is_rejected() -> None:
"""Invalid approval modes should fail before params are constructed."""
with pytest.raises(ValueError, match="deny_all, auto_review"):
with pytest.raises(
ValueError,
match="deny_all, auto_review, dangerously_bypass_approvals_and_sandbox",
):
public_api_module._approval_mode_settings("allow_all") # type: ignore[arg-type]

View File

@@ -129,6 +129,10 @@ def test_root_exports_approval_mode() -> None:
assert [(mode.name, mode.value) for mode in ApprovalMode] == [
("deny_all", "deny_all"),
("auto_review", "auto_review"),
(
"dangerously_bypass_approvals_and_sandbox",
"dangerously_bypass_approvals_and_sandbox",
),
]