From b4bc02439f7eebcc412d409995886de3501bb66c Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 12 May 2026 00:57:44 +0300 Subject: [PATCH] [4/8] Define Python SDK public API surface (#21896) ## Why The SDK package root should be the ergonomic public client API, not a dump of every generated app-server schema type. Generated models still need a supported import path, but callers should be able to tell which names are high-level SDK entrypoints and which names are protocol value models. ## What - Define a curated root `__all__` for clients, handles, input helpers, retry helpers, config, and public errors. - Add a `types` module as the supported home for generated app-server response, event, enum, and helper models. - Update docs and examples to import protocol/value models from the type module. - Add tests that lock root exports, type-module exports, star-import behavior, and example import hygiene. ## 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. This PR `[4/8]` Define Python SDK public API surface 5. #21905 `[5/8]` Rename Python SDK package to `openai-codex` 6. #21910 `[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 public API signature tests for root exports, `types` exports, and example imports. --------- Co-authored-by: Codex --- sdk/python/README.md | 4 +- sdk/python/docs/api-reference.md | 22 +-- sdk/python/docs/faq.md | 4 +- sdk/python/docs/getting-started.md | 7 +- .../10_error_handling_and_retry/async.py | 2 +- .../10_error_handling_and_retry/sync.py | 2 +- sdk/python/examples/11_cli_mini_app/async.py | 2 + sdk/python/examples/11_cli_mini_app/sync.py | 2 + .../12_turn_params_kitchen_sink/async.py | 6 +- .../12_turn_params_kitchen_sink/sync.py | 6 +- .../13_model_select_and_turn_params/async.py | 6 +- .../13_model_select_and_turn_params/sync.py | 6 +- sdk/python/examples/README.md | 3 +- sdk/python/src/codex_app_server/__init__.py | 49 +----- sdk/python/src/codex_app_server/types.py | 69 ++++++++ .../tests/test_public_api_signatures.py | 162 +++++++++++++++++- 16 files changed, 274 insertions(+), 78 deletions(-) create mode 100644 sdk/python/src/codex_app_server/types.py diff --git a/sdk/python/README.md b/sdk/python/README.md index 0c5a8bd3a6..e93cc44236 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -5,6 +5,8 @@ Experimental Python SDK for `codex app-server` JSON-RPC v2 over stdio, with a sm The generated wire-model layer is sourced from the pinned `openai-codex-cli-bin` runtime package and exposed as Pydantic models with snake_case Python fields that serialize back to the app-server’s camelCase wire format. +The package root exports the ergonomic client API; public app-server value and +event types live in `codex_app_server.types`. ## Install @@ -110,4 +112,4 @@ This supports the CI release flow: - Use context managers (`with Codex() as codex:`) to ensure shutdown. - Prefer `thread.run("...")` for the common case. Use `thread.turn(...)` when you need streaming, steering, or interrupt control. -- For transient overload, use `codex_app_server.retry.retry_on_overload`. +- For transient overload, use `retry_on_overload` from the package root. diff --git a/sdk/python/docs/api-reference.md b/sdk/python/docs/api-reference.md index c7a763498d..e0d2ca5c3d 100644 --- a/sdk/python/docs/api-reference.md +++ b/sdk/python/docs/api-reference.md @@ -15,7 +15,6 @@ from codex_app_server import ( AsyncThread, TurnHandle, AsyncTurnHandle, - InitializeResponse, Input, InputItem, TextInput, @@ -23,14 +22,18 @@ from codex_app_server import ( LocalImageInput, SkillInput, MentionInput, +) +from codex_app_server.types import ( + InitializeResponse, + ThreadItem, + ThreadTokenUsage, TurnStatus, ) -from codex_app_server.generated.v2_all import ThreadItem, ThreadTokenUsage ``` - Version: `codex_app_server.__version__` - Requires Python >= 3.10 -- Canonical generated app-server models live in `codex_app_server.generated.v2_all` +- Public app-server value and event types live in `codex_app_server.types` ## Codex (sync) @@ -124,7 +127,7 @@ object with: phase-less assistant message item. Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`, -`interrupt()`) or the canonical generated `Turn` from `TurnHandle.run()`. +`interrupt()`) or the public `Turn` model from `TurnHandle.run()`. ## TurnHandle / AsyncTurnHandle @@ -133,7 +136,7 @@ Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`, - `steer(input: Input) -> TurnSteerResponse` - `interrupt() -> TurnInterruptResponse` - `stream() -> Iterator[Notification]` -- `run() -> codex_app_server.generated.v2_all.Turn` +- `run() -> codex_app_server.types.Turn` Behavior notes: @@ -145,7 +148,7 @@ Behavior notes: - `steer(input: Input) -> Awaitable[TurnSteerResponse]` - `interrupt() -> Awaitable[TurnInterruptResponse]` - `stream() -> AsyncIterator[Notification]` -- `run() -> Awaitable[codex_app_server.generated.v2_all.Turn]` +- `run() -> Awaitable[codex_app_server.types.Turn]` Behavior notes: @@ -165,16 +168,15 @@ InputItem = TextInput | ImageInput | LocalImageInput | SkillInput | MentionInput Input = list[InputItem] | InputItem ``` -## Generated Models +## Public Types -The SDK wrappers return and accept canonical generated app-server models wherever possible: +The SDK wrappers return and accept public app-server models wherever possible: ```python -from codex_app_server.generated.v2_all import ( +from codex_app_server.types import ( AskForApproval, ThreadReadResponse, Turn, - TurnStartParams, TurnStatus, ) ``` diff --git a/sdk/python/docs/faq.md b/sdk/python/docs/faq.md index bc5cec6e32..9446b2f40c 100644 --- a/sdk/python/docs/faq.md +++ b/sdk/python/docs/faq.md @@ -8,7 +8,7 @@ ## `run()` vs `stream()` -- `TurnHandle.run()` / `AsyncTurnHandle.run()` is the easiest path. It consumes events until completion and returns the canonical generated app-server `Turn` model. +- `TurnHandle.run()` / `AsyncTurnHandle.run()` is the easiest path. It consumes events until completion and returns the public app-server `Turn` model from `codex_app_server.types`. - `TurnHandle.stream()` / `AsyncTurnHandle.stream()` yields raw notifications (`Notification`) so you can react event-by-event. Choose `run()` for most apps. Choose `stream()` for progress UIs, custom timeout logic, or custom parsing. @@ -99,5 +99,5 @@ Do not blindly retry all errors. For `InvalidParamsError` or `MethodNotFoundErro - Starting a new thread for every prompt when you wanted continuity. - Forgetting to `close()` (or not using context managers). -- Assuming `run()` returns extra SDK-only fields instead of the generated `Turn` model. +- Assuming `run()` returns extra SDK-only fields instead of the public `Turn` model. - Mixing SDK input classes with raw dicts incorrectly. diff --git a/sdk/python/docs/getting-started.md b/sdk/python/docs/getting-started.md index 1794d39f70..74dee3aa45 100644 --- a/sdk/python/docs/getting-started.md +++ b/sdk/python/docs/getting-started.md @@ -95,12 +95,13 @@ with Codex() as codex: print(result.final_response) ``` -## 6) Generated models +## 6) Public app-server types -The convenience wrappers live at the package root, but the canonical app-server models live under: +The convenience wrappers live at the package root. Public app-server value and +event types live under: ```python -from codex_app_server.generated.v2_all import Turn, TurnStatus, ThreadReadResponse +from codex_app_server.types import ThreadReadResponse, Turn, TurnStatus ``` ## 7) Next stops diff --git a/sdk/python/examples/10_error_handling_and_retry/async.py b/sdk/python/examples/10_error_handling_and_retry/async.py index c23ee00847..feb48f8a0b 100644 --- a/sdk/python/examples/10_error_handling_and_retry/async.py +++ b/sdk/python/examples/10_error_handling_and_retry/async.py @@ -24,9 +24,9 @@ from codex_app_server import ( JsonRpcError, ServerBusyError, TextInput, - TurnStatus, is_retryable_error, ) +from codex_app_server.types import TurnStatus ResultT = TypeVar("ResultT") diff --git a/sdk/python/examples/10_error_handling_and_retry/sync.py b/sdk/python/examples/10_error_handling_and_retry/sync.py index 585f24a9d2..2d30b193d4 100644 --- a/sdk/python/examples/10_error_handling_and_retry/sync.py +++ b/sdk/python/examples/10_error_handling_and_retry/sync.py @@ -19,9 +19,9 @@ from codex_app_server import ( JsonRpcError, ServerBusyError, TextInput, - TurnStatus, retry_on_overload, ) +from codex_app_server.types import TurnStatus with Codex(config=runtime_config()) as codex: thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"}) diff --git a/sdk/python/examples/11_cli_mini_app/async.py b/sdk/python/examples/11_cli_mini_app/async.py index 4216cf7820..f8ae75398f 100644 --- a/sdk/python/examples/11_cli_mini_app/async.py +++ b/sdk/python/examples/11_cli_mini_app/async.py @@ -14,6 +14,8 @@ import asyncio from codex_app_server import ( AsyncCodex, TextInput, +) +from codex_app_server.types import ( ThreadTokenUsageUpdatedNotification, TurnCompletedNotification, ) diff --git a/sdk/python/examples/11_cli_mini_app/sync.py b/sdk/python/examples/11_cli_mini_app/sync.py index e961cfbcc3..f0613943ee 100644 --- a/sdk/python/examples/11_cli_mini_app/sync.py +++ b/sdk/python/examples/11_cli_mini_app/sync.py @@ -12,6 +12,8 @@ ensure_local_sdk_src() from codex_app_server import ( Codex, TextInput, +) +from codex_app_server.types import ( ThreadTokenUsageUpdatedNotification, TurnCompletedNotification, ) diff --git a/sdk/python/examples/12_turn_params_kitchen_sink/async.py b/sdk/python/examples/12_turn_params_kitchen_sink/async.py index 88a24535c2..ee94690578 100644 --- a/sdk/python/examples/12_turn_params_kitchen_sink/async.py +++ b/sdk/python/examples/12_turn_params_kitchen_sink/async.py @@ -18,11 +18,13 @@ ensure_local_sdk_src() import asyncio from codex_app_server import ( - AskForApproval, AsyncCodex, + TextInput, +) +from codex_app_server.types import ( + AskForApproval, Personality, ReasoningSummary, - TextInput, ) OUTPUT_SCHEMA = { diff --git a/sdk/python/examples/12_turn_params_kitchen_sink/sync.py b/sdk/python/examples/12_turn_params_kitchen_sink/sync.py index e4095c8ec9..9e8bbf57fa 100644 --- a/sdk/python/examples/12_turn_params_kitchen_sink/sync.py +++ b/sdk/python/examples/12_turn_params_kitchen_sink/sync.py @@ -16,11 +16,13 @@ from _bootstrap import ( ensure_local_sdk_src() from codex_app_server import ( - AskForApproval, Codex, + TextInput, +) +from codex_app_server.types import ( + AskForApproval, Personality, ReasoningSummary, - TextInput, ) OUTPUT_SCHEMA = { diff --git a/sdk/python/examples/13_model_select_and_turn_params/async.py b/sdk/python/examples/13_model_select_and_turn_params/async.py index cbbcff462b..5cef2c70cc 100644 --- a/sdk/python/examples/13_model_select_and_turn_params/async.py +++ b/sdk/python/examples/13_model_select_and_turn_params/async.py @@ -12,13 +12,15 @@ ensure_local_sdk_src() import asyncio from codex_app_server import ( - AskForApproval, AsyncCodex, + TextInput, +) +from codex_app_server.types import ( + AskForApproval, Personality, ReasoningEffort, ReasoningSummary, SandboxPolicy, - TextInput, ) REASONING_RANK = { diff --git a/sdk/python/examples/13_model_select_and_turn_params/sync.py b/sdk/python/examples/13_model_select_and_turn_params/sync.py index e02d99cf75..a6be24104e 100644 --- a/sdk/python/examples/13_model_select_and_turn_params/sync.py +++ b/sdk/python/examples/13_model_select_and_turn_params/sync.py @@ -10,13 +10,15 @@ from _bootstrap import assistant_text_from_turn, ensure_local_sdk_src, find_turn ensure_local_sdk_src() from codex_app_server import ( - AskForApproval, Codex, + TextInput, +) +from codex_app_server.types import ( + AskForApproval, Personality, ReasoningEffort, ReasoningSummary, SandboxPolicy, - TextInput, ) REASONING_RANK = { diff --git a/sdk/python/examples/README.md b/sdk/python/examples/README.md index 3a93c28a0c..0fdda6da41 100644 --- a/sdk/python/examples/README.md +++ b/sdk/python/examples/README.md @@ -5,7 +5,8 @@ Each example folder contains runnable versions: - `sync.py` (public sync surface: `Codex`) - `async.py` (public async surface: `AsyncCodex`) -All examples intentionally use only public SDK exports from `codex_app_server`. +All examples intentionally use only public SDK exports from `codex_app_server` +and `codex_app_server.types`. ## Prerequisites diff --git a/sdk/python/src/codex_app_server/__init__.py b/sdk/python/src/codex_app_server/__init__.py index 281e9a472d..745e8f91e1 100644 --- a/sdk/python/src/codex_app_server/__init__.py +++ b/sdk/python/src/codex_app_server/__init__.py @@ -1,5 +1,4 @@ -from .async_client import AsyncAppServerClient -from .client import AppServerClient, AppServerConfig +from .client import AppServerConfig from .errors import ( AppServerError, AppServerRpcError, @@ -14,29 +13,6 @@ from .errors import ( TransportClosedError, is_retryable_error, ) -from .generated.v2_all import ( - AskForApproval, - Personality, - PlanType, - ReasoningEffort, - ReasoningSummary, - SandboxMode, - SandboxPolicy, - ThreadItem, - ThreadForkParams, - ThreadListParams, - ThreadResumeParams, - ThreadSortKey, - ThreadSource, - ThreadSourceKind, - ThreadStartParams, - ThreadTokenUsageUpdatedNotification, - TurnCompletedNotification, - TurnStartParams, - TurnStatus, - TurnSteerParams, -) -from .models import InitializeResponse from .api import ( AsyncCodex, AsyncThread, @@ -58,8 +34,6 @@ from ._version import __version__ __all__ = [ "__version__", - "AppServerClient", - "AsyncAppServerClient", "AppServerConfig", "Codex", "AsyncCodex", @@ -67,7 +41,6 @@ __all__ = [ "AsyncThread", "TurnHandle", "AsyncTurnHandle", - "InitializeResponse", "RunResult", "Input", "InputItem", @@ -76,26 +49,6 @@ __all__ = [ "LocalImageInput", "SkillInput", "MentionInput", - "ThreadItem", - "ThreadTokenUsageUpdatedNotification", - "TurnCompletedNotification", - "AskForApproval", - "Personality", - "PlanType", - "ReasoningEffort", - "ReasoningSummary", - "SandboxMode", - "SandboxPolicy", - "ThreadStartParams", - "ThreadResumeParams", - "ThreadListParams", - "ThreadSortKey", - "ThreadSource", - "ThreadSourceKind", - "ThreadForkParams", - "TurnStatus", - "TurnStartParams", - "TurnSteerParams", "retry_on_overload", "AppServerError", "TransportClosedError", diff --git a/sdk/python/src/codex_app_server/types.py b/sdk/python/src/codex_app_server/types.py new file mode 100644 index 0000000000..bda240afeb --- /dev/null +++ b/sdk/python/src/codex_app_server/types.py @@ -0,0 +1,69 @@ +"""Public generated app-server model exports for type annotations and matching.""" + +from __future__ import annotations + +from .generated.v2_all import ( + ApprovalsReviewer, + AskForApproval, + ModelListResponse, + Personality, + PlanType, + ReasoningEffort, + ReasoningSummary, + SandboxMode, + SandboxPolicy, + SortDirection, + ThreadArchiveResponse, + ThreadCompactStartResponse, + ThreadItem, + ThreadListCwdFilter, + ThreadListResponse, + ThreadReadResponse, + ThreadSetNameResponse, + ThreadSortKey, + ThreadSource, + ThreadSourceKind, + ThreadStartSource, + ThreadTokenUsage, + ThreadTokenUsageUpdatedNotification, + Turn, + TurnCompletedNotification, + TurnInterruptResponse, + TurnStatus, + TurnSteerResponse, +) +from .models import InitializeResponse, JsonObject, Notification + +__all__ = [ + "ApprovalsReviewer", + "AskForApproval", + "InitializeResponse", + "JsonObject", + "ModelListResponse", + "Notification", + "Personality", + "PlanType", + "ReasoningEffort", + "ReasoningSummary", + "SandboxMode", + "SandboxPolicy", + "SortDirection", + "ThreadArchiveResponse", + "ThreadCompactStartResponse", + "ThreadItem", + "ThreadListCwdFilter", + "ThreadListResponse", + "ThreadReadResponse", + "ThreadSetNameResponse", + "ThreadSortKey", + "ThreadSource", + "ThreadSourceKind", + "ThreadStartSource", + "ThreadTokenUsage", + "ThreadTokenUsageUpdatedNotification", + "Turn", + "TurnCompletedNotification", + "TurnInterruptResponse", + "TurnStatus", + "TurnSteerResponse", +] diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index ddbcba0675..6168c3fbe5 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -7,12 +7,86 @@ from pathlib import Path from typing import Any import codex_app_server -from codex_app_server import AppServerConfig, RunResult -from codex_app_server.models import InitializeResponse -from codex_app_server.api import AsyncCodex, AsyncThread, Codex, Thread +import codex_app_server.types as public_types +from codex_app_server import ( + AppServerConfig, + AsyncCodex, + AsyncThread, + Codex, + RunResult, + Thread, +) +from codex_app_server.types import InitializeResponse + +EXPECTED_ROOT_EXPORTS = [ + "__version__", + "AppServerConfig", + "Codex", + "AsyncCodex", + "Thread", + "AsyncThread", + "TurnHandle", + "AsyncTurnHandle", + "RunResult", + "Input", + "InputItem", + "TextInput", + "ImageInput", + "LocalImageInput", + "SkillInput", + "MentionInput", + "retry_on_overload", + "AppServerError", + "TransportClosedError", + "JsonRpcError", + "AppServerRpcError", + "ParseError", + "InvalidRequestError", + "MethodNotFoundError", + "InvalidParamsError", + "InternalRpcError", + "ServerBusyError", + "RetryLimitExceededError", + "is_retryable_error", +] + +EXPECTED_TYPES_EXPORTS = [ + "ApprovalsReviewer", + "AskForApproval", + "InitializeResponse", + "JsonObject", + "ModelListResponse", + "Notification", + "Personality", + "PlanType", + "ReasoningEffort", + "ReasoningSummary", + "SandboxMode", + "SandboxPolicy", + "SortDirection", + "ThreadArchiveResponse", + "ThreadCompactStartResponse", + "ThreadItem", + "ThreadListCwdFilter", + "ThreadListResponse", + "ThreadReadResponse", + "ThreadSetNameResponse", + "ThreadSortKey", + "ThreadSource", + "ThreadSourceKind", + "ThreadStartSource", + "ThreadTokenUsage", + "ThreadTokenUsageUpdatedNotification", + "Turn", + "TurnCompletedNotification", + "TurnInterruptResponse", + "TurnStatus", + "TurnSteerResponse", +] def _keyword_only_names(fn: object) -> list[str]: + """Return only user-facing keyword-only parameter names for a public method.""" signature = inspect.signature(fn) return [ param.name @@ -22,6 +96,7 @@ def _keyword_only_names(fn: object) -> list[str]: def _assert_no_any_annotations(fn: object) -> None: + """Reject loose annotations on public wrapper methods.""" signature = inspect.signature(fn) for param in signature.parameters.values(): if param.annotation is Any: @@ -33,14 +108,17 @@ def _assert_no_any_annotations(fn: object) -> None: def test_root_exports_app_server_config() -> None: + """The root package should expose the process configuration object.""" assert AppServerConfig.__name__ == "AppServerConfig" def test_root_exports_run_result() -> None: + """The root package should expose the common-case run result wrapper.""" assert RunResult.__name__ == "RunResult" 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" pyproject = tomllib.loads(pyproject_path.read_text()) @@ -49,10 +127,85 @@ def test_package_and_default_client_versions_follow_project_version() -> None: def test_package_includes_py_typed_marker() -> None: + """The wheel should advertise that inline type information is available.""" marker = resources.files("codex_app_server").joinpath("py.typed") assert marker.is_file() +def test_package_root_exports_only_public_api() -> None: + """The package root should expose the supported SDK surface, not internals.""" + assert codex_app_server.__all__ == EXPECTED_ROOT_EXPORTS + assert { + name: hasattr(codex_app_server, name) for name in EXPECTED_ROOT_EXPORTS + } == {name: True for name in EXPECTED_ROOT_EXPORTS} + assert { + "AppServerClient": hasattr(codex_app_server, "AppServerClient"), + "AsyncAppServerClient": hasattr(codex_app_server, "AsyncAppServerClient"), + "InitializeResponse": hasattr(codex_app_server, "InitializeResponse"), + "ThreadStartParams": hasattr(codex_app_server, "ThreadStartParams"), + "TurnStartParams": hasattr(codex_app_server, "TurnStartParams"), + "TurnCompletedNotification": hasattr( + codex_app_server, "TurnCompletedNotification" + ), + "TurnStatus": hasattr(codex_app_server, "TurnStatus"), + } == { + "AppServerClient": False, + "AsyncAppServerClient": False, + "InitializeResponse": False, + "ThreadStartParams": False, + "TurnStartParams": False, + "TurnCompletedNotification": False, + "TurnStatus": False, + } + + +def test_package_star_import_matches_public_api() -> None: + """Star imports should follow the same explicit public API list.""" + namespace: dict[str, object] = {} + exec("from codex_app_server import *", namespace) + + exported = set(namespace) - {"__builtins__"} + assert exported == set(EXPECTED_ROOT_EXPORTS) + + +def test_types_module_exports_curated_public_types() -> None: + """The public type module should be the supported place for app-server models.""" + assert public_types.__all__ == EXPECTED_TYPES_EXPORTS + assert {name: hasattr(public_types, name) for name in EXPECTED_TYPES_EXPORTS} == { + name: True for name in EXPECTED_TYPES_EXPORTS + } + + +def test_types_star_import_matches_public_types() -> None: + """Star imports from the type module should match its explicit export list.""" + namespace: dict[str, object] = {} + exec("from codex_app_server.types import *", namespace) + + exported = set(namespace) - {"__builtins__"} + assert exported == set(EXPECTED_TYPES_EXPORTS) + + +def test_examples_use_public_import_surfaces() -> None: + """Examples should teach users the public root and type-module imports only.""" + examples_root = Path(__file__).resolve().parents[1] / "examples" + private_import_markers = [ + "codex_app_server.api", + "codex_app_server.client", + "codex_app_server.generated", + "codex_app_server.models", + "codex_app_server.retry", + ] + + offenders = { + str(path.relative_to(examples_root)): marker + for path in examples_root.rglob("*.py") + for marker in private_import_markers + if marker in path.read_text() + } + + assert offenders == {} + + def test_generated_public_signatures_are_snake_case_and_typed() -> None: """Generated convenience methods should expose typed Pythonic keyword names.""" expected = { @@ -228,6 +381,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: def test_lifecycle_methods_are_codex_scoped() -> None: + """Lifecycle operations should hang off the client rather than thread objects.""" assert hasattr(Codex, "thread_resume") assert hasattr(Codex, "thread_fork") assert hasattr(Codex, "thread_archive") @@ -258,6 +412,7 @@ def test_lifecycle_methods_are_codex_scoped() -> None: def test_initialize_metadata_parses_user_agent_shape() -> None: + """Initialize metadata should accept the legacy user-agent-only payload shape.""" payload = InitializeResponse.model_validate({"userAgent": "codex-cli/1.2.3"}) parsed = Codex._validate_initialize(payload) assert parsed is payload @@ -268,6 +423,7 @@ def test_initialize_metadata_parses_user_agent_shape() -> None: def test_initialize_metadata_requires_non_empty_information() -> None: + """Initialize metadata should fail when the runtime gives no identity signal.""" try: Codex._validate_initialize(InitializeResponse.model_validate({})) except RuntimeError as exc: