[6/8] Add high-level Python SDK approval mode (#21910)

## Why

The high-level SDK should expose the approval behavior it actually
supports instead of leaking generated app-server routing fields. New
work should have two clear choices: default auto review, or explicitly
deny escalated permission requests. Existing threads and subsequent
turns should preserve their current approval behavior unless the caller
passes an override.

## What

- Add the public `ApprovalMode` enum with `auto_review` and `deny_all`.
- Default new thread creation to `ApprovalMode.auto_review`.
- Preserve existing approval settings by default for resume, fork, run,
and turn helpers.
- Remove raw `approval_policy` / `approvals_reviewer` kwargs from
high-level SDK wrappers.
- Update generated wrapper output, docs, examples, notebooks, and tests
for the high-level approval mode API.

## Stack

1. #21891 `[1/8]` Pin Python SDK runtime dependency
2. #21893 `[2/8]` Generate Python SDK types from pinned runtime
3. #21895 `[3/8]` Run Python SDK tests in CI
4. #21896 `[4/8]` Define Python SDK public API surface
5. #21905 `[5/8]` Rename Python SDK package to `openai-codex`
6. This PR `[6/8]` Add high-level Python SDK approval mode
7. #22014 `[7/8]` Add Python SDK app-server integration harness
8. #22021 `[8/8]` Add Python SDK Ruff formatting

## Verification

- Added approval-mode mapping/default tests for new threads, existing
threads, forks, resumes, and subsequent turns.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-05-12 01:02:43 +03:00
committed by GitHub
parent f1b84fac63
commit 2b90c37069
11 changed files with 403 additions and 94 deletions

View File

@@ -14,6 +14,7 @@ from .errors import (
is_retryable_error,
)
from .api import (
ApprovalMode,
AsyncCodex,
AsyncThread,
AsyncTurnHandle,
@@ -37,6 +38,7 @@ __all__ = [
"AppServerConfig",
"Codex",
"AsyncCodex",
"ApprovalMode",
"Thread",
"AsyncThread",
"TurnHandle",

View File

@@ -2,13 +2,15 @@ from __future__ import annotations
import asyncio
from dataclasses import dataclass
from typing import AsyncIterator, Iterator
from enum import Enum
from typing import AsyncIterator, Iterator, NoReturn
from .async_client import AsyncAppServerClient
from .client import AppServerClient, AppServerConfig
from .generated.v2_all import (
ApprovalsReviewer,
AskForApproval,
AskForApprovalValue,
ModelListResponse,
Personality,
ReasoningEffort,
@@ -69,6 +71,47 @@ def _split_user_agent(user_agent: str) -> tuple[str | None, str | None]:
return raw, None
class ApprovalMode(str, Enum):
"""High-level approval behavior for escalated permission requests."""
deny_all = "deny_all"
auto_review = "auto_review"
def _approval_mode_settings(
approval_mode: ApprovalMode,
) -> tuple[AskForApproval, ApprovalsReviewer | None]:
"""Map the public approval mode to generated app-server start params."""
if not isinstance(approval_mode, ApprovalMode):
supported = ", ".join(mode.value for mode in ApprovalMode)
raise ValueError(f"approval_mode must be one of: {supported}")
match approval_mode:
case ApprovalMode.auto_review:
return (
AskForApproval(root=AskForApprovalValue.on_request),
ApprovalsReviewer.auto_review,
)
case ApprovalMode.deny_all:
return AskForApproval(root=AskForApprovalValue.never), None
case _:
return _assert_never_approval_mode(approval_mode)
def _assert_never_approval_mode(approval_mode: NoReturn) -> NoReturn:
"""Make approval mode mapping exhaustive for static type checkers."""
raise AssertionError(f"Unhandled approval mode: {approval_mode!r}")
def _approval_mode_override_settings(
approval_mode: ApprovalMode | None,
) -> tuple[AskForApproval | None, ApprovalsReviewer | None]:
"""Map an optional public approval mode to app-server override params."""
if approval_mode is None:
return None, None
return _approval_mode_settings(approval_mode)
class Codex:
"""Minimal typed SDK surface for app-server v2."""
@@ -140,8 +183,7 @@ class Codex:
def thread_start(
self,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode = ApprovalMode.auto_review,
base_instructions: str | None = None,
config: JsonObject | None = None,
cwd: str | None = None,
@@ -156,6 +198,7 @@ class Codex:
session_start_source: ThreadStartSource | None = None,
thread_source: ThreadSource | None = None,
) -> Thread:
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
params = ThreadStartParams(
approval_policy=approval_policy,
approvals_reviewer=approvals_reviewer,
@@ -208,8 +251,7 @@ class Codex:
self,
thread_id: str,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
base_instructions: str | None = None,
config: JsonObject | None = None,
cwd: str | None = None,
@@ -220,6 +262,9 @@ class Codex:
sandbox: SandboxMode | None = None,
service_tier: str | None = None,
) -> Thread:
approval_policy, approvals_reviewer = _approval_mode_override_settings(
approval_mode
)
params = ThreadResumeParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -241,8 +286,7 @@ class Codex:
self,
thread_id: str,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
base_instructions: str | None = None,
config: JsonObject | None = None,
cwd: str | None = None,
@@ -254,6 +298,9 @@ class Codex:
service_tier: str | None = None,
thread_source: ThreadSource | None = None,
) -> Thread:
approval_policy, approvals_reviewer = _approval_mode_override_settings(
approval_mode
)
params = ThreadForkParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -341,8 +388,7 @@ class AsyncCodex:
async def thread_start(
self,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode = ApprovalMode.auto_review,
base_instructions: str | None = None,
config: JsonObject | None = None,
cwd: str | None = None,
@@ -358,6 +404,7 @@ class AsyncCodex:
thread_source: ThreadSource | None = None,
) -> AsyncThread:
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_settings(approval_mode)
params = ThreadStartParams(
approval_policy=approval_policy,
approvals_reviewer=approvals_reviewer,
@@ -411,8 +458,7 @@ class AsyncCodex:
self,
thread_id: str,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
base_instructions: str | None = None,
config: JsonObject | None = None,
cwd: str | None = None,
@@ -424,6 +470,9 @@ class AsyncCodex:
service_tier: str | None = None,
) -> AsyncThread:
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(
approval_mode
)
params = ThreadResumeParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -445,8 +494,7 @@ class AsyncCodex:
self,
thread_id: str,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
base_instructions: str | None = None,
config: JsonObject | None = None,
cwd: str | None = None,
@@ -459,6 +507,9 @@ class AsyncCodex:
thread_source: ThreadSource | None = None,
) -> AsyncThread:
await self._ensure_initialized()
approval_policy, approvals_reviewer = _approval_mode_override_settings(
approval_mode
)
params = ThreadForkParams(
thread_id=thread_id,
approval_policy=approval_policy,
@@ -502,8 +553,7 @@ class Thread:
self,
input: RunInput,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
cwd: str | None = None,
effort: ReasoningEffort | None = None,
model: str | None = None,
@@ -515,8 +565,7 @@ class Thread:
) -> RunResult:
turn = self.turn(
_normalize_run_input(input),
approval_policy=approval_policy,
approvals_reviewer=approvals_reviewer,
approval_mode=approval_mode,
cwd=cwd,
effort=effort,
model=model,
@@ -537,8 +586,7 @@ class Thread:
self,
input: Input,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
cwd: str | None = None,
effort: ReasoningEffort | None = None,
model: str | None = None,
@@ -549,6 +597,9 @@ class Thread:
summary: ReasoningSummary | None = None,
) -> TurnHandle:
wire_input = _to_wire_input(input)
approval_policy, approvals_reviewer = _approval_mode_override_settings(
approval_mode
)
params = TurnStartParams(
thread_id=self.id,
input=wire_input,
@@ -587,8 +638,7 @@ class AsyncThread:
self,
input: RunInput,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
cwd: str | None = None,
effort: ReasoningEffort | None = None,
model: str | None = None,
@@ -600,8 +650,7 @@ class AsyncThread:
) -> RunResult:
turn = await self.turn(
_normalize_run_input(input),
approval_policy=approval_policy,
approvals_reviewer=approvals_reviewer,
approval_mode=approval_mode,
cwd=cwd,
effort=effort,
model=model,
@@ -622,8 +671,7 @@ class AsyncThread:
self,
input: Input,
*,
approval_policy: AskForApproval | None = None,
approvals_reviewer: ApprovalsReviewer | None = None,
approval_mode: ApprovalMode | None = None,
cwd: str | None = None,
effort: ReasoningEffort | None = None,
model: str | None = None,
@@ -635,6 +683,9 @@ class AsyncThread:
) -> AsyncTurnHandle:
await self._codex._ensure_initialized()
wire_input = _to_wire_input(input)
approval_policy, approvals_reviewer = _approval_mode_override_settings(
approval_mode
)
params = TurnStartParams(
thread_id=self.id,
input=wire_input,