[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

@@ -10,6 +10,7 @@ import openai_codex
import openai_codex.types as public_types
from openai_codex import (
AppServerConfig,
ApprovalMode,
AsyncCodex,
AsyncThread,
Codex,
@@ -23,6 +24,7 @@ EXPECTED_ROOT_EXPORTS = [
"AppServerConfig",
"Codex",
"AsyncCodex",
"ApprovalMode",
"Thread",
"AsyncThread",
"TurnHandle",
@@ -95,6 +97,11 @@ def _keyword_only_names(fn: object) -> list[str]:
]
def _keyword_default(fn: object, name: str) -> object:
"""Return the default value for one keyword parameter on a public method."""
return inspect.signature(fn).parameters[name].default
def _assert_no_any_annotations(fn: object) -> None:
"""Reject loose annotations on public wrapper methods."""
signature = inspect.signature(fn)
@@ -117,6 +124,14 @@ def test_root_exports_run_result() -> None:
assert RunResult.__name__ == "RunResult"
def test_root_exports_approval_mode() -> None:
"""The root package should expose the high-level approval mode enum."""
assert [(mode.name, mode.value) for mode in ApprovalMode] == [
("deny_all", "deny_all"),
("auto_review", "auto_review"),
]
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"
@@ -135,18 +150,16 @@ def test_package_includes_py_typed_marker() -> None:
def test_package_root_exports_only_public_api() -> None:
"""The package root should expose the supported SDK surface, not internals."""
assert openai_codex.__all__ == EXPECTED_ROOT_EXPORTS
assert {
name: hasattr(openai_codex, name) for name in EXPECTED_ROOT_EXPORTS
} == {name: True for name in EXPECTED_ROOT_EXPORTS}
assert {name: hasattr(openai_codex, name) for name in EXPECTED_ROOT_EXPORTS} == {
name: True for name in EXPECTED_ROOT_EXPORTS
}
assert {
"AppServerClient": hasattr(openai_codex, "AppServerClient"),
"AsyncAppServerClient": hasattr(openai_codex, "AsyncAppServerClient"),
"InitializeResponse": hasattr(openai_codex, "InitializeResponse"),
"ThreadStartParams": hasattr(openai_codex, "ThreadStartParams"),
"TurnStartParams": hasattr(openai_codex, "TurnStartParams"),
"TurnCompletedNotification": hasattr(
openai_codex, "TurnCompletedNotification"
),
"TurnCompletedNotification": hasattr(openai_codex, "TurnCompletedNotification"),
"TurnStatus": hasattr(openai_codex, "TurnStatus"),
} == {
"AppServerClient": False,
@@ -210,8 +223,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"""Generated convenience methods should expose typed Pythonic keyword names."""
expected = {
Codex.thread_start: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"base_instructions",
"config",
"cwd",
@@ -239,8 +251,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"use_state_db_only",
],
Codex.thread_resume: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"base_instructions",
"config",
"cwd",
@@ -252,8 +263,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"service_tier",
],
Codex.thread_fork: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"base_instructions",
"config",
"cwd",
@@ -266,8 +276,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"thread_source",
],
Thread.turn: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"cwd",
"effort",
"model",
@@ -278,8 +287,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"summary",
],
Thread.run: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"cwd",
"effort",
"model",
@@ -290,8 +298,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"summary",
],
AsyncCodex.thread_start: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"base_instructions",
"config",
"cwd",
@@ -319,8 +326,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"use_state_db_only",
],
AsyncCodex.thread_resume: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"base_instructions",
"config",
"cwd",
@@ -332,8 +338,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"service_tier",
],
AsyncCodex.thread_fork: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"base_instructions",
"config",
"cwd",
@@ -346,8 +351,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"thread_source",
],
AsyncThread.turn: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"cwd",
"effort",
"model",
@@ -358,8 +362,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
"summary",
],
AsyncThread.run: [
"approval_policy",
"approvals_reviewer",
"approval_mode",
"cwd",
"effort",
"model",
@@ -380,6 +383,36 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
_assert_no_any_annotations(fn)
def test_new_thread_methods_default_to_auto_review() -> None:
"""New threads should start with auto-review unless callers opt out."""
funcs = [
Codex.thread_start,
AsyncCodex.thread_start,
]
assert {fn: _keyword_default(fn, "approval_mode") for fn in funcs} == {
fn: ApprovalMode.auto_review for fn in funcs
}
def test_existing_thread_methods_default_to_preserving_approval_settings() -> None:
"""Existing thread operations should not serialize approval overrides by default."""
funcs = [
Codex.thread_resume,
Codex.thread_fork,
Thread.turn,
Thread.run,
AsyncCodex.thread_resume,
AsyncCodex.thread_fork,
AsyncThread.turn,
AsyncThread.run,
]
assert {fn: _keyword_default(fn, "approval_mode") for fn in funcs} == {
fn: None for fn in funcs
}
def test_lifecycle_methods_are_codex_scoped() -> None:
"""Lifecycle operations should hang off the client rather than thread objects."""
assert hasattr(Codex, "thread_resume")