From e3cbf227d550d1330859050520fc0681667aec06 Mon Sep 17 00:00:00 2001 From: sdcoffey Date: Tue, 21 Apr 2026 10:03:50 -0700 Subject: [PATCH] build(python-sdk): pin package version to codex runtime --- sdk/python-runtime/README.md | 2 +- sdk/python/README.md | 34 +- sdk/python/_runtime_setup.py | 94 +- sdk/python/docs/faq.md | 15 +- sdk/python/examples/README.md | 2 +- sdk/python/pyproject.toml | 4 +- sdk/python/scripts/update_sdk_artifacts.py | 48 +- sdk/python/src/codex_app_server/__init__.py | 3 +- sdk/python/src/codex_app_server/_version.py | 37 + sdk/python/src/codex_app_server/api.py | 40 +- sdk/python/src/codex_app_server/client.py | 3 +- .../generated/notification_registry.py | 8 + .../src/codex_app_server/generated/v2_all.py | 1460 +++++++++++++---- .../test_artifact_workflow_and_binaries.py | 127 +- .../tests/test_public_api_signatures.py | 27 + sdk/python/uv.lock | 50 +- 16 files changed, 1478 insertions(+), 476 deletions(-) create mode 100644 sdk/python/src/codex_app_server/_version.py diff --git a/sdk/python-runtime/README.md b/sdk/python-runtime/README.md index 22c59ef156c3..27623b28aa30 100644 --- a/sdk/python-runtime/README.md +++ b/sdk/python-runtime/README.md @@ -1,6 +1,6 @@ # Codex CLI Runtime for Python SDK -Platform-specific runtime package consumed by the published `codex-app-server-sdk`. +Platform-specific runtime package consumed by the published `openai-codex-app-server-sdk`. This package is staged during release so the SDK can pin an exact Codex CLI version without checking platform binaries into the repo. diff --git a/sdk/python/README.md b/sdk/python/README.md index 7d69e23357fe..149420ad9567 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -12,10 +12,11 @@ uv sync source .venv/bin/activate ``` -Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency. For local -repo development, either pass `AppServerConfig(codex_bin=...)` to point at a -local build explicitly, or use the repo examples/notebook bootstrap which -installs the pinned runtime package automatically. +Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency +with the same version as the SDK. For local repo development, either pass +`AppServerConfig(codex_bin=...)` to point at a local build explicitly, or use +the repo examples/notebook bootstrap which installs the pinned runtime package +automatically. ## Quickstart @@ -54,9 +55,9 @@ python examples/01_quickstart_constructor/async.py The repo no longer checks `codex` binaries into `sdk/python`. -Published SDK builds are pinned to an exact `openai-codex-cli-bin` package version, -and that runtime package carries the platform-specific binary for the target -wheel. +Published SDK builds are pinned to an exact `openai-codex-cli-bin` package +version, and that runtime package carries the platform-specific binary for the +target wheel. The SDK package version and runtime package version must match. For local repo development, the checked-in `sdk/python-runtime` package is only a template for staged release artifacts. Editable installs should use an @@ -70,30 +71,35 @@ cd sdk/python python scripts/update_sdk_artifacts.py generate-types python scripts/update_sdk_artifacts.py \ stage-sdk \ - /tmp/codex-python-release/codex-app-server-sdk \ - --runtime-version 1.2.3 + /tmp/codex-python-release/openai-codex-app-server-sdk \ + --codex-version python scripts/update_sdk_artifacts.py \ stage-runtime \ /tmp/codex-python-release/openai-codex-cli-bin \ /path/to/codex \ - --runtime-version 1.2.3 + --codex-version ``` +Pass `--platform-tag ...` to `stage-runtime` when the wheel should be tagged for +a Rust target that differs from the Python build host. The intended one-off +matrix is `macosx_11_0_arm64`, `macosx_10_9_x86_64`, +`musllinux_1_1_aarch64`, `musllinux_1_1_x86_64`, `win_arm64`, and +`win_amd64`. + This supports the CI release flow: - run `generate-types` before packaging -- stage `codex-app-server-sdk` once with an exact `openai-codex-cli-bin==...` dependency +- stage `openai-codex-app-server-sdk` once with an exact `openai-codex-cli-bin==...` dependency - stage `openai-codex-cli-bin` on each supported platform runner with the same pinned runtime version - build and publish `openai-codex-cli-bin` as platform wheels only; do not publish an sdist ## Compatibility and versioning -- Package: `codex-app-server-sdk` +- Package: `openai-codex-app-server-sdk` - Runtime package: `openai-codex-cli-bin` -- Current SDK version in this repo: `0.2.0` - Python: `>=3.10` - Target protocol: Codex `app-server` JSON-RPC v2 -- Recommendation: keep SDK and `codex` CLI reasonably up to date together +- Versioning rule: the SDK package version is the underlying Codex runtime version ## Notes diff --git a/sdk/python/_runtime_setup.py b/sdk/python/_runtime_setup.py index 6c4cf457add8..8d33da6c880e 100644 --- a/sdk/python/_runtime_setup.py +++ b/sdk/python/_runtime_setup.py @@ -1,10 +1,12 @@ from __future__ import annotations import importlib +import importlib.metadata import importlib.util import json import os import platform +import re import shutil import subprocess import sys @@ -16,7 +18,7 @@ from pathlib import Path PACKAGE_NAME = "openai-codex-cli-bin" -PINNED_RUNTIME_VERSION = "0.116.0-alpha.1" +SDK_PACKAGE_NAME = "openai-codex-app-server-sdk" REPO_SLUG = "openai/codex" @@ -25,7 +27,16 @@ class RuntimeSetupError(RuntimeError): def pinned_runtime_version() -> str: - return PINNED_RUNTIME_VERSION + source_version = _source_tree_project_version() + if source_version is not None: + return _normalized_package_version(source_version) + + try: + return _normalized_package_version(importlib.metadata.version(SDK_PACKAGE_NAME)) + except importlib.metadata.PackageNotFoundError as exc: + raise RuntimeSetupError( + f"Unable to resolve {SDK_PACKAGE_NAME} version for runtime pinning." + ) from exc def ensure_runtime_package_installed( @@ -39,7 +50,10 @@ def ensure_runtime_package_installed( installed_version = _installed_runtime_version(python_executable) normalized_requested = _normalized_package_version(requested_version) - if installed_version is not None and _normalized_package_version(installed_version) == normalized_requested: + if ( + installed_version is not None + and _normalized_package_version(installed_version) == normalized_requested + ): return requested_version with tempfile.TemporaryDirectory(prefix="codex-python-runtime-") as temp_root_str: @@ -61,7 +75,10 @@ def ensure_runtime_package_installed( importlib.invalidate_caches() installed_version = _installed_runtime_version(python_executable) - if installed_version is None or _normalized_package_version(installed_version) != normalized_requested: + if ( + installed_version is None + or _normalized_package_version(installed_version) != normalized_requested + ): raise RuntimeSetupError( f"Expected {PACKAGE_NAME} {requested_version} in {python_executable}, " f"but found {installed_version!r} after installation." @@ -121,7 +138,8 @@ def _installed_runtime_version(python_executable: str | Path) -> str | None: def _release_metadata(version: str) -> dict[str, object]: - url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/rust-v{version}" + release_tag = _release_tag(version) + url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/{release_tag}" token = _github_token() attempts = [True, False] if token is not None else [False] last_error: urllib.error.HTTPError | None = None @@ -146,7 +164,7 @@ def _release_metadata(version: str) -> dict[str, object]: assert last_error is not None raise RuntimeSetupError( - f"Failed to resolve release metadata for rust-v{version} from {REPO_SLUG}: " + f"Failed to resolve release metadata for {release_tag} from {REPO_SLUG}: " f"{last_error.code} {last_error.reason}" ) from last_error @@ -154,9 +172,10 @@ def _release_metadata(version: str) -> dict[str, object]: def _download_release_archive(version: str, temp_root: Path) -> Path: asset_name = platform_asset_name() archive_path = temp_root / asset_name + release_tag = _release_tag(version) browser_download_url = ( - f"https://github.com/{REPO_SLUG}/releases/download/rust-v{version}/{asset_name}" + f"https://github.com/{REPO_SLUG}/releases/download/{release_tag}/{asset_name}" ) request = urllib.request.Request( browser_download_url, @@ -172,7 +191,9 @@ def _download_release_archive(version: str, temp_root: Path) -> Path: metadata = _release_metadata(version) assets = metadata.get("assets") if not isinstance(assets, list): - raise RuntimeSetupError(f"Release rust-v{version} returned malformed assets metadata.") + raise RuntimeSetupError( + f"Release {release_tag} returned malformed assets metadata." + ) asset = next( ( item @@ -183,7 +204,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path: ) if asset is None: raise RuntimeSetupError( - f"Release rust-v{version} does not contain asset {asset_name} for this platform." + f"Release {release_tag} does not contain asset {asset_name} for this platform." ) api_url = asset.get("url") @@ -198,7 +219,10 @@ def _download_release_archive(version: str, temp_root: Path) -> Path: headers=_github_api_headers("application/octet-stream"), ) try: - with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh: + with ( + urllib.request.urlopen(request) as response, + archive_path.open("wb") as fh, + ): shutil.copyfileobj(response, fh) return archive_path except urllib.error.HTTPError: @@ -216,7 +240,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path: "gh", "release", "download", - f"rust-v{version}", + release_tag, "--repo", REPO_SLUG, "--pattern", @@ -230,7 +254,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path: ) except subprocess.CalledProcessError as exc: raise RuntimeSetupError( - f"gh release download failed for rust-v{version} asset {asset_name}.\n" + f"gh release download failed for {release_tag} asset {asset_name}.\n" f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}" ) from exc return archive_path @@ -249,7 +273,9 @@ def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path: with zipfile.ZipFile(archive_path) as zip_file: zip_file.extractall(extract_dir) else: - raise RuntimeSetupError(f"Unsupported release archive format: {archive_path.name}") + raise RuntimeSetupError( + f"Unsupported release archive format: {archive_path.name}" + ) binary_name = runtime_binary_name() archive_stem = archive_path.name.removesuffix(".tar.gz").removesuffix(".zip") @@ -346,12 +372,50 @@ def _github_token() -> str | None: def _normalized_package_version(version: str) -> str: - return version.strip().replace("-alpha.", "a").replace("-beta.", "b") + normalized = version.strip() + if normalized.startswith("rust-v"): + normalized = normalized.removeprefix("rust-v") + elif normalized.startswith("v"): + normalized = normalized.removeprefix("v") + + normalized = re.sub(r"-alpha\.?([0-9]+)$", r"a\1", normalized) + normalized = re.sub(r"-beta\.?([0-9]+)$", r"b\1", normalized) + normalized = re.sub(r"-rc\.?([0-9]+)$", r"rc\1", normalized) + return normalized + + +def _codex_release_version(version: str) -> str: + normalized = _normalized_package_version(version) + match = re.fullmatch(r"([0-9]+(?:\.[0-9]+)*)(a|b|rc)([0-9]+)", normalized) + if match is None: + return normalized + + base, prerelease, number = match.groups() + prerelease_name = {"a": "alpha", "b": "beta", "rc": "rc"}[prerelease] + return f"{base}-{prerelease_name}.{number}" + + +def _release_tag(version: str) -> str: + return f"rust-v{_codex_release_version(version)}" + + +def _source_tree_project_version() -> str | None: + pyproject_path = Path(__file__).resolve().parent / "pyproject.toml" + if not pyproject_path.exists(): + return None + + match = re.search( + r'(?m)^version = "([^"]+)"$', + pyproject_path.read_text(encoding="utf-8"), + ) + if match is None: + return None + return match.group(1) __all__ = [ "PACKAGE_NAME", - "PINNED_RUNTIME_VERSION", + "SDK_PACKAGE_NAME", "RuntimeSetupError", "ensure_runtime_package_installed", "pinned_runtime_version", diff --git a/sdk/python/docs/faq.md b/sdk/python/docs/faq.md index af688a3a1963..bc5cec6e329e 100644 --- a/sdk/python/docs/faq.md +++ b/sdk/python/docs/faq.md @@ -60,23 +60,28 @@ Common causes: - incompatible/old app-server Maintainers stage releases by building the SDK once and the runtime once per -platform with the same pinned runtime version. Publish `openai-codex-cli-bin` as -platform wheels only; do not publish an sdist: +platform with the same pinned runtime version. Publish `openai-codex-cli-bin` +as platform wheels only; do not publish an sdist: ```bash cd sdk/python python scripts/update_sdk_artifacts.py generate-types python scripts/update_sdk_artifacts.py \ stage-sdk \ - /tmp/codex-python-release/codex-app-server-sdk \ - --runtime-version 1.2.3 + /tmp/codex-python-release/openai-codex-app-server-sdk \ + --codex-version python scripts/update_sdk_artifacts.py \ stage-runtime \ /tmp/codex-python-release/openai-codex-cli-bin \ /path/to/codex \ - --runtime-version 1.2.3 + --codex-version ``` +If you are packaging a binary for a different target than the Python build +host, pass `--platform-tag ...` to `stage-runtime`. The intended one-off matrix +is `macosx_11_0_arm64`, `macosx_10_9_x86_64`, `musllinux_1_1_aarch64`, +`musllinux_1_1_x86_64`, `win_arm64`, and `win_amd64`. + ## Why does a turn "hang"? A turn is complete only when `turn/completed` arrives for that turn ID. diff --git a/sdk/python/examples/README.md b/sdk/python/examples/README.md index 99ea0a31f5a3..59428ba32246 100644 --- a/sdk/python/examples/README.md +++ b/sdk/python/examples/README.md @@ -28,7 +28,7 @@ will download the matching GitHub release artifact, stage a temporary local `openai-codex-cli-bin` package, install it into your active interpreter, and clean up the temporary files afterward. -Current pinned runtime version: `0.116.0-alpha.1` +The pinned runtime version comes from the SDK package version. ## Run examples diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index d67cb54c2848..f54838bfa838 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -3,8 +3,8 @@ requires = ["hatchling>=1.24.0"] build-backend = "hatchling.build" [project] -name = "codex-app-server-sdk" -version = "0.2.0" +name = "openai-codex-app-server-sdk" +version = "0.116.0a1" description = "Python SDK for Codex app-server v2" readme = "README.md" requires-python = ">=3.10" diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index 42c1ec091f34..0d0e739c7897 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -17,6 +17,7 @@ from pathlib import Path from typing import Any, Callable, Sequence, get_args, get_origin +SDK_DISTRIBUTION_NAME = "openai-codex-app-server-sdk" RUNTIME_DISTRIBUTION_NAME = "openai-codex-cli-bin" @@ -178,15 +179,19 @@ def _rewrite_sdk_runtime_dependency(pyproject_text: str, runtime_version: str) - ) raw_items = [item.strip() for item in match.group(1).split(",") if item.strip()] - raw_items = [item for item in raw_items if "codex-cli-bin" not in item] + raw_items = [ + item + for item in raw_items + if RUNTIME_DISTRIBUTION_NAME.removeprefix("openai-") not in item + and RUNTIME_DISTRIBUTION_NAME not in item + ] raw_items.append(f'"{RUNTIME_DISTRIBUTION_NAME}=={runtime_version}"') replacement = "dependencies = [\n " + ",\n ".join(raw_items) + ",\n]" return pyproject_text[: match.start()] + replacement + pyproject_text[match.end() :] -def stage_python_sdk_package( - staging_dir: Path, sdk_version: str, runtime_version: str -) -> Path: +def stage_python_sdk_package(staging_dir: Path, codex_version: str) -> Path: + package_version = normalize_codex_version(codex_version) _copy_package_tree(sdk_root(), staging_dir) sdk_bin_dir = staging_dir / "src" / "codex_app_server" / "bin" if sdk_bin_dir.exists(): @@ -194,8 +199,9 @@ def stage_python_sdk_package( pyproject_path = staging_dir / "pyproject.toml" pyproject_text = pyproject_path.read_text() - pyproject_text = _rewrite_project_version(pyproject_text, sdk_version) - pyproject_text = _rewrite_sdk_runtime_dependency(pyproject_text, runtime_version) + pyproject_text = _rewrite_project_name(pyproject_text, SDK_DISTRIBUTION_NAME) + pyproject_text = _rewrite_project_version(pyproject_text, package_version) + pyproject_text = _rewrite_sdk_runtime_dependency(pyproject_text, package_version) pyproject_path.write_text(pyproject_text) return staging_dir @@ -625,7 +631,7 @@ class PublicFieldSpec: @dataclass(frozen=True) class CliOps: generate_types: Callable[[], None] - stage_python_sdk_package: Callable[[Path, str, str], Path] + stage_python_sdk_package: Callable[[Path, str], Path] stage_python_runtime_package: Callable[[Path, str, Path, str | None], Path] current_sdk_version: Callable[[], str] @@ -992,14 +998,21 @@ def build_parser() -> argparse.ArgumentParser: type=Path, help="Output directory for the staged SDK package", ) + stage_sdk_parser.add_argument( + "--codex-version", + help=( + "Codex release version to write into the staged SDK package and exact " + f"{RUNTIME_DISTRIBUTION_NAME} dependency. Accepts PEP 440 versions " + "or release tags such as rust-v0.116.0-alpha.1." + ), + ) stage_sdk_parser.add_argument( "--runtime-version", - required=True, - help="Pinned openai-codex-cli-bin version for the staged SDK package", + help=argparse.SUPPRESS, ) stage_sdk_parser.add_argument( "--sdk-version", - help="Version to write into the staged SDK package (defaults to sdk/python current version)", + help=argparse.SUPPRESS, ) stage_runtime_parser = subparsers.add_parser( @@ -1050,22 +1063,23 @@ def default_cli_ops() -> CliOps: ) -def _resolve_runtime_version(args: argparse.Namespace) -> str: +def _resolve_codex_version(args: argparse.Namespace) -> str: versions = [ value for value in ( getattr(args, "codex_version", None), getattr(args, "runtime_version", None), + getattr(args, "sdk_version", None), ) if value is not None ] if not versions: - raise RuntimeError("Pass --codex-version to stage the Python runtime package") + raise RuntimeError("Pass --codex-version to stage Python release artifacts") normalized_versions = [normalize_codex_version(version) for version in versions] if len(set(normalized_versions)) != 1: raise RuntimeError( - "Runtime package versions must match; pass one --codex-version" + "SDK and runtime package versions must match; pass one --codex-version" ) return normalized_versions[0] @@ -1074,17 +1088,17 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None: if args.command == "generate-types": ops.generate_types() elif args.command == "stage-sdk": + codex_version = _resolve_codex_version(args) ops.generate_types() ops.stage_python_sdk_package( args.staging_dir, - args.sdk_version or ops.current_sdk_version(), - args.runtime_version, + codex_version, ) elif args.command == "stage-runtime": - runtime_version = _resolve_runtime_version(args) + codex_version = _resolve_codex_version(args) ops.stage_python_runtime_package( args.staging_dir, - runtime_version, + codex_version, args.runtime_binary.resolve(), args.platform_tag, ) diff --git a/sdk/python/src/codex_app_server/__init__.py b/sdk/python/src/codex_app_server/__init__.py index c35ce0ebe584..33f9e628d952 100644 --- a/sdk/python/src/codex_app_server/__init__.py +++ b/sdk/python/src/codex_app_server/__init__.py @@ -54,8 +54,7 @@ TurnHandle, ) from .retry import retry_on_overload - -__version__ = "0.2.0" +from ._version import __version__ __all__ = [ "__version__", diff --git a/sdk/python/src/codex_app_server/_version.py b/sdk/python/src/codex_app_server/_version.py new file mode 100644 index 000000000000..b4b724e3844b --- /dev/null +++ b/sdk/python/src/codex_app_server/_version.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import re +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as distribution_version +from pathlib import Path + +DISTRIBUTION_NAME = "openai-codex-app-server-sdk" +UNKNOWN_VERSION = "0+unknown" + + +def package_version() -> str: + source_version = _source_tree_project_version() + if source_version is not None: + return source_version + + try: + return distribution_version(DISTRIBUTION_NAME) + except PackageNotFoundError: + return UNKNOWN_VERSION + + +def _source_tree_project_version() -> str | None: + pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml" + if not pyproject_path.exists(): + return None + + match = re.search( + r'(?m)^version = "([^"]+)"$', + pyproject_path.read_text(encoding="utf-8"), + ) + if match is None: + return None + return match.group(1) + + +__version__ = package_version() diff --git a/sdk/python/src/codex_app_server/api.py b/sdk/python/src/codex_app_server/api.py index c330e3f743c9..ed0535db802d 100644 --- a/sdk/python/src/codex_app_server/api.py +++ b/sdk/python/src/codex_app_server/api.py @@ -10,15 +10,18 @@ ApprovalsReviewer, AskForApproval, ModelListResponse, + PermissionProfile, Personality, ReasoningEffort, ReasoningSummary, SandboxMode, SandboxPolicy, ServiceTier, + SortDirection, ThreadArchiveResponse, ThreadCompactStartResponse, ThreadForkParams, + ThreadListCwdFilter, ThreadListParams, ThreadListResponse, ThreadReadResponse, @@ -26,6 +29,7 @@ ThreadSetNameResponse, ThreadSortKey, ThreadSourceKind, + ThreadStartSource, ThreadStartParams, Turn as AppServerTurn, TurnCompletedNotification, @@ -146,6 +150,7 @@ def thread_start( ephemeral: bool | None = None, model: str | None = None, model_provider: str | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox: SandboxMode | None = None, service_name: str | None = None, @@ -162,6 +167,7 @@ def thread_start( ephemeral=ephemeral, model=model, model_provider=model_provider, + permission_profile=permission_profile, personality=personality, sandbox=sandbox, service_name=service_name, @@ -176,13 +182,14 @@ def thread_list( *, archived: bool | None = None, cursor: str | None = None, - cwd: str | None = None, + cwd: ThreadListCwdFilter | None = None, limit: int | None = None, model_providers: list[str] | None = None, search_term: str | None = None, sort_direction: SortDirection | None = None, sort_key: ThreadSortKey | None = None, source_kinds: list[ThreadSourceKind] | None = None, + use_state_db_only: bool | None = None, ) -> ThreadListResponse: params = ThreadListParams( archived=archived, @@ -194,6 +201,7 @@ def thread_list( sort_direction=sort_direction, sort_key=sort_key, source_kinds=source_kinds, + use_state_db_only=use_state_db_only, ) return self._client.thread_list(params) @@ -207,8 +215,10 @@ def thread_resume( config: JsonObject | None = None, cwd: str | None = None, developer_instructions: str | None = None, + exclude_turns: bool | None = None, model: str | None = None, model_provider: str | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox: SandboxMode | None = None, service_tier: ServiceTier | None = None, @@ -221,8 +231,10 @@ def thread_resume( config=config, cwd=cwd, developer_instructions=developer_instructions, + exclude_turns=exclude_turns, model=model, model_provider=model_provider, + permission_profile=permission_profile, personality=personality, sandbox=sandbox, service_tier=service_tier, @@ -241,8 +253,10 @@ def thread_fork( cwd: str | None = None, developer_instructions: str | None = None, ephemeral: bool | None = None, + exclude_turns: bool | None = None, model: str | None = None, model_provider: str | None = None, + permission_profile: PermissionProfile | None = None, sandbox: SandboxMode | None = None, service_tier: ServiceTier | None = None, ) -> Thread: @@ -255,8 +269,10 @@ def thread_fork( cwd=cwd, developer_instructions=developer_instructions, ephemeral=ephemeral, + exclude_turns=exclude_turns, model=model, model_provider=model_provider, + permission_profile=permission_profile, sandbox=sandbox, service_tier=service_tier, ) @@ -340,6 +356,7 @@ async def thread_start( ephemeral: bool | None = None, model: str | None = None, model_provider: str | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox: SandboxMode | None = None, service_name: str | None = None, @@ -357,6 +374,7 @@ async def thread_start( ephemeral=ephemeral, model=model, model_provider=model_provider, + permission_profile=permission_profile, personality=personality, sandbox=sandbox, service_name=service_name, @@ -371,13 +389,14 @@ async def thread_list( *, archived: bool | None = None, cursor: str | None = None, - cwd: str | None = None, + cwd: ThreadListCwdFilter | None = None, limit: int | None = None, model_providers: list[str] | None = None, search_term: str | None = None, sort_direction: SortDirection | None = None, sort_key: ThreadSortKey | None = None, source_kinds: list[ThreadSourceKind] | None = None, + use_state_db_only: bool | None = None, ) -> ThreadListResponse: await self._ensure_initialized() params = ThreadListParams( @@ -390,6 +409,7 @@ async def thread_list( sort_direction=sort_direction, sort_key=sort_key, source_kinds=source_kinds, + use_state_db_only=use_state_db_only, ) return await self._client.thread_list(params) @@ -403,8 +423,10 @@ async def thread_resume( config: JsonObject | None = None, cwd: str | None = None, developer_instructions: str | None = None, + exclude_turns: bool | None = None, model: str | None = None, model_provider: str | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox: SandboxMode | None = None, service_tier: ServiceTier | None = None, @@ -418,8 +440,10 @@ async def thread_resume( config=config, cwd=cwd, developer_instructions=developer_instructions, + exclude_turns=exclude_turns, model=model, model_provider=model_provider, + permission_profile=permission_profile, personality=personality, sandbox=sandbox, service_tier=service_tier, @@ -438,8 +462,10 @@ async def thread_fork( cwd: str | None = None, developer_instructions: str | None = None, ephemeral: bool | None = None, + exclude_turns: bool | None = None, model: str | None = None, model_provider: str | None = None, + permission_profile: PermissionProfile | None = None, sandbox: SandboxMode | None = None, service_tier: ServiceTier | None = None, ) -> AsyncThread: @@ -453,8 +479,10 @@ async def thread_fork( cwd=cwd, developer_instructions=developer_instructions, ephemeral=ephemeral, + exclude_turns=exclude_turns, model=model, model_provider=model_provider, + permission_profile=permission_profile, sandbox=sandbox, service_tier=service_tier, ) @@ -491,6 +519,7 @@ def run( effort: ReasoningEffort | None = None, model: str | None = None, output_schema: JsonObject | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, service_tier: ServiceTier | None = None, @@ -504,6 +533,7 @@ def run( effort=effort, model=model, output_schema=output_schema, + permission_profile=permission_profile, personality=personality, sandbox_policy=sandbox_policy, service_tier=service_tier, @@ -526,6 +556,7 @@ def turn( effort: ReasoningEffort | None = None, model: str | None = None, output_schema: JsonObject | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, service_tier: ServiceTier | None = None, @@ -541,6 +572,7 @@ def turn( effort=effort, model=model, output_schema=output_schema, + permission_profile=permission_profile, personality=personality, sandbox_policy=sandbox_policy, service_tier=service_tier, @@ -575,6 +607,7 @@ async def run( effort: ReasoningEffort | None = None, model: str | None = None, output_schema: JsonObject | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, service_tier: ServiceTier | None = None, @@ -588,6 +621,7 @@ async def run( effort=effort, model=model, output_schema=output_schema, + permission_profile=permission_profile, personality=personality, sandbox_policy=sandbox_policy, service_tier=service_tier, @@ -610,6 +644,7 @@ async def turn( effort: ReasoningEffort | None = None, model: str | None = None, output_schema: JsonObject | None = None, + permission_profile: PermissionProfile | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, service_tier: ServiceTier | None = None, @@ -626,6 +661,7 @@ async def turn( effort=effort, model=model, output_schema=output_schema, + permission_profile=permission_profile, personality=personality, sandbox_policy=sandbox_policy, service_tier=service_tier, diff --git a/sdk/python/src/codex_app_server/client.py b/sdk/python/src/codex_app_server/client.py index db7cf77cd5d7..665e1c672514 100644 --- a/sdk/python/src/codex_app_server/client.py +++ b/sdk/python/src/codex_app_server/client.py @@ -44,6 +44,7 @@ UnknownNotification, ) from .retry import retry_on_overload +from ._version import __version__ as SDK_VERSION ModelT = TypeVar("ModelT", bound=BaseModel) ApprovalHandler = Callable[[str, JsonObject | None], JsonObject] @@ -129,7 +130,7 @@ class AppServerConfig: env: dict[str, str] | None = None client_name: str = "codex_python_sdk" client_title: str = "Codex Python SDK" - client_version: str = "0.2.0" + client_version: str = SDK_VERSION experimental_api: bool = True diff --git a/sdk/python/src/codex_app_server/generated/notification_registry.py b/sdk/python/src/codex_app_server/generated/notification_registry.py index ab6f87f11a32..5b54207b5d16 100644 --- a/sdk/python/src/codex_app_server/generated/notification_registry.py +++ b/sdk/python/src/codex_app_server/generated/notification_registry.py @@ -22,6 +22,7 @@ from .v2_all import FsChangedNotification from .v2_all import FuzzyFileSearchSessionCompletedNotification from .v2_all import FuzzyFileSearchSessionUpdatedNotification +from .v2_all import GuardianWarningNotification from .v2_all import HookCompletedNotification from .v2_all import HookStartedNotification from .v2_all import ItemCompletedNotification @@ -32,6 +33,7 @@ from .v2_all import McpServerStatusUpdatedNotification from .v2_all import McpToolCallProgressNotification from .v2_all import ModelReroutedNotification +from .v2_all import ModelVerificationNotification from .v2_all import PlanDeltaNotification from .v2_all import ReasoningSummaryPartAddedNotification from .v2_all import ReasoningSummaryTextDeltaNotification @@ -41,6 +43,8 @@ from .v2_all import TerminalInteractionNotification from .v2_all import ThreadArchivedNotification from .v2_all import ThreadClosedNotification +from .v2_all import ThreadGoalClearedNotification +from .v2_all import ThreadGoalUpdatedNotification from .v2_all import ThreadNameUpdatedNotification from .v2_all import ThreadRealtimeClosedNotification from .v2_all import ThreadRealtimeErrorNotification @@ -75,6 +79,7 @@ "fs/changed": FsChangedNotification, "fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification, "fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification, + "guardianWarning": GuardianWarningNotification, "hook/completed": HookCompletedNotification, "hook/started": HookStartedNotification, "item/agentMessage/delta": AgentMessageDeltaNotification, @@ -94,11 +99,14 @@ "mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification, "mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification, "model/rerouted": ModelReroutedNotification, + "model/verification": ModelVerificationNotification, "serverRequest/resolved": ServerRequestResolvedNotification, "skills/changed": SkillsChangedNotification, "thread/archived": ThreadArchivedNotification, "thread/closed": ThreadClosedNotification, "thread/compacted": ContextCompactedNotification, + "thread/goal/cleared": ThreadGoalClearedNotification, + "thread/goal/updated": ThreadGoalUpdatedNotification, "thread/name/updated": ThreadNameUpdatedNotification, "thread/realtime/closed": ThreadRealtimeClosedNotification, "thread/realtime/error": ThreadRealtimeErrorNotification, diff --git a/sdk/python/src/codex_app_server/generated/v2_all.py b/sdk/python/src/codex_app_server/generated/v2_all.py index fac98223d77c..70c700928602 100644 --- a/sdk/python/src/codex_app_server/generated/v2_all.py +++ b/sdk/python/src/codex_app_server/generated/v2_all.py @@ -33,6 +33,13 @@ class ApiKeyAccount(BaseModel): type: Annotated[Literal["apiKey"], Field(title="ApiKeyAccountType")] +class AmazonBedrockAccount(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[Literal["amazonBedrock"], Field(title="AmazonBedrockAccountType")] + + class AccountLoginCompletedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -227,6 +234,7 @@ class AuthMode(Enum): apikey = "apikey" chatgpt = "chatgpt" chatgpt_auth_tokens = "chatgptAuthTokens" + agent_identity = "agentIdentity" class AutoReviewDecisionSource(RootModel[Literal["agent"]]): @@ -274,6 +282,7 @@ class CodexErrorInfoValue(Enum): context_window_exceeded = "contextWindowExceeded" usage_limit_exceeded = "usageLimitExceeded" server_overloaded = "serverOverloaded" + cyber_policy = "cyberPolicy" internal_server_error = "internalServerError" unauthorized = "unauthorized" bad_request = "badRequest" @@ -658,6 +667,56 @@ class ConfigReadParams(BaseModel): include_layers: Annotated[bool | None, Field(alias="includeLayers")] = False +class CommandConfiguredHookHandler(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + async_: Annotated[bool, Field(alias="async")] + command: str + status_message: Annotated[str | None, Field(alias="statusMessage")] = None + timeout_sec: Annotated[int | None, Field(alias="timeoutSec", ge=0)] = None + type: Annotated[Literal["command"], Field(title="CommandConfiguredHookHandlerType")] + + +class PromptConfiguredHookHandler(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[Literal["prompt"], Field(title="PromptConfiguredHookHandlerType")] + + +class AgentConfiguredHookHandler(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[Literal["agent"], Field(title="AgentConfiguredHookHandlerType")] + + +class ConfiguredHookHandler( + RootModel[ + CommandConfiguredHookHandler + | PromptConfiguredHookHandler + | AgentConfiguredHookHandler + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + CommandConfiguredHookHandler + | PromptConfiguredHookHandler + | AgentConfiguredHookHandler + ) + + +class ConfiguredHookMatcherGroup(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + hooks: list[ConfiguredHookHandler] + matcher: str | None = None + + class InputTextContentItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -704,6 +763,75 @@ class DeprecationNoticeNotification(BaseModel): summary: Annotated[str, Field(description="Concise summary of what is deprecated.")] +class DeviceKeyAlgorithm(RootModel[Literal["ecdsa_p256_sha256"]]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + Literal["ecdsa_p256_sha256"], + Field( + description="Device-key algorithm reported at enrollment and signing boundaries." + ), + ] + + +class DeviceKeyProtectionClass(Enum): + hardware_secure_enclave = "hardware_secure_enclave" + hardware_tpm = "hardware_tpm" + os_protected_nonextractable = "os_protected_nonextractable" + + +class DeviceKeyProtectionPolicy(Enum): + hardware_only = "hardware_only" + allow_os_protected_nonextractable = "allow_os_protected_nonextractable" + + +class DeviceKeyPublicParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + key_id: Annotated[str, Field(alias="keyId")] + + +class DeviceKeyPublicResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + algorithm: DeviceKeyAlgorithm + key_id: Annotated[str, Field(alias="keyId")] + protection_class: Annotated[ + DeviceKeyProtectionClass, Field(alias="protectionClass") + ] + public_key_spki_der_base64: Annotated[ + str, + Field( + alias="publicKeySpkiDerBase64", + description="SubjectPublicKeyInfo DER encoded as base64.", + ), + ] + + +class DeviceKeySignResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + algorithm: DeviceKeyAlgorithm + signature_der_base64: Annotated[ + str, + Field( + alias="signatureDerBase64", + description="ECDSA signature DER encoded as base64.", + ), + ] + signed_payload_base64: Annotated[ + str, + Field( + alias="signedPayloadBase64", + description="Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.", + ), + ] + + class InputTextDynamicToolCallOutputContentItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1386,6 +1514,19 @@ class GuardianUserAuthorization(Enum): high = "high" +class GuardianWarningNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + message: Annotated[ + str, Field(description="Concise guardian warning message for the user.") + ] + thread_id: Annotated[ + str, + Field(alias="threadId", description="Thread target for the guardian warning."), + ] + + class HookEventName(Enum): pre_tool_use = "preToolUse" permission_request = "permissionRequest" @@ -1677,6 +1818,28 @@ class LogoutAccountResponse(BaseModel): ) +class ManagedHooksRequirements(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + permission_request: Annotated[ + list[ConfiguredHookMatcherGroup], Field(alias="PermissionRequest") + ] + post_tool_use: Annotated[ + list[ConfiguredHookMatcherGroup], Field(alias="PostToolUse") + ] + pre_tool_use: Annotated[list[ConfiguredHookMatcherGroup], Field(alias="PreToolUse")] + session_start: Annotated[ + list[ConfiguredHookMatcherGroup], Field(alias="SessionStart") + ] + stop: Annotated[list[ConfiguredHookMatcherGroup], Field(alias="Stop")] + user_prompt_submit: Annotated[ + list[ConfiguredHookMatcherGroup], Field(alias="UserPromptSubmit") + ] + managed_dir: Annotated[str | None, Field(alias="managedDir")] = None + windows_managed_dir: Annotated[str | None, Field(alias="windowsManagedDir")] = None + + class MarketplaceAddParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1727,6 +1890,30 @@ class MarketplaceRemoveResponse(BaseModel): marketplace_name: Annotated[str, Field(alias="marketplaceName")] +class MarketplaceUpgradeErrorInfo(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + message: str + + +class MarketplaceUpgradeParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + marketplace_name: Annotated[str | None, Field(alias="marketplaceName")] = None + + +class MarketplaceUpgradeResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + errors: list[MarketplaceUpgradeErrorInfo] + selected_marketplaces: Annotated[list[str], Field(alias="selectedMarketplaces")] + upgraded_roots: Annotated[list[AbsolutePathBuf], Field(alias="upgradedRoots")] + + class McpAuthStatus(Enum): unsupported = "unsupported" not_logged_in = "notLoggedIn" @@ -1933,6 +2120,22 @@ class ModelUpgradeInfo(BaseModel): upgrade_copy: Annotated[str | None, Field(alias="upgradeCopy")] = None +class ModelVerification(RootModel[Literal["trustedAccessForCyber"]]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Literal["trustedAccessForCyber"] + + +class ModelVerificationNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str, Field(alias="turnId")] + verifications: list[ModelVerification] + + class NetworkAccess(Enum): restricted = "restricted" enabled = "enabled" @@ -2058,6 +2261,30 @@ class PatchChangeKind( root: AddPatchChangeKind | DeletePatchChangeKind | UpdatePatchChangeKind +class DisabledPermissionProfile(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[Literal["disabled"], Field(title="DisabledPermissionProfileType")] + + +class UnrestrictedPermissionProfileFileSystemPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + type: Annotated[ + Literal["unrestricted"], + Field(title="UnrestrictedPermissionProfileFileSystemPermissionsType"), + ] + + +class PermissionProfileNetworkPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + enabled: bool + + class Personality(Enum): none = "none" friendly = "friendly" @@ -2294,33 +2521,6 @@ class RateLimitWindow(BaseModel): ) -class RestrictedReadOnlyAccess(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - include_platform_defaults: Annotated[ - bool | None, Field(alias="includePlatformDefaults") - ] = True - readable_roots: Annotated[ - list[AbsolutePathBuf] | None, Field(alias="readableRoots") - ] = [] - type: Annotated[Literal["restricted"], Field(title="RestrictedReadOnlyAccessType")] - - -class FullAccessReadOnlyAccess(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - type: Annotated[Literal["fullAccess"], Field(title="FullAccessReadOnlyAccessType")] - - -class ReadOnlyAccess(RootModel[RestrictedReadOnlyAccess | FullAccessReadOnlyAccess]): - model_config = ConfigDict( - populate_by_name=True, - ) - root: RestrictedReadOnlyAccess | FullAccessReadOnlyAccess - - class RealtimeConversationVersion(Enum): v1 = "v1" v2 = "v2" @@ -2477,6 +2677,34 @@ class ReasoningTextDeltaNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class RemoteControlClientConnectionAudience( + RootModel[Literal["remote_control_client_websocket"]] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + Literal["remote_control_client_websocket"], + Field( + description="Audience for a remote-control client connection device-key proof." + ), + ] + + +class RemoteControlClientEnrollmentAudience( + RootModel[Literal["remote_control_client_enrollment"]] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + Literal["remote_control_client_enrollment"], + Field( + description="Audience for a remote-control client enrollment device-key proof." + ), + ] + + class RequestId(RootModel[str | int]): model_config = ConfigDict( populate_by_name=True, @@ -2817,7 +3045,6 @@ class ReadOnlySandboxPolicy(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - access: Annotated[ReadOnlyAccess | None, Field()] = {"type": "fullAccess"} network_access: Annotated[bool | None, Field(alias="networkAccess")] = False type: Annotated[Literal["readOnly"], Field(title="ReadOnlySandboxPolicyType")] @@ -2843,9 +3070,6 @@ class WorkspaceWriteSandboxPolicy(BaseModel): bool | None, Field(alias="excludeTmpdirEnvVar") ] = False network_access: Annotated[bool | None, Field(alias="networkAccess")] = False - read_only_access: Annotated[ - ReadOnlyAccess | None, Field(alias="readOnlyAccess") - ] = {"type": "fullAccess"} type: Annotated[ Literal["workspaceWrite"], Field(title="WorkspaceWriteSandboxPolicyType") ] @@ -3047,6 +3271,27 @@ class ModelReroutedServerNotification(BaseModel): params: ModelReroutedNotification +class ModelVerificationServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["model/verification"], + Field(title="Model/verificationNotificationMethod"), + ] + params: ModelVerificationNotification + + +class GuardianWarningServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["guardianWarning"], Field(title="GuardianWarningNotificationMethod") + ] + params: GuardianWarningNotification + + class DeprecationNoticeServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3163,7 +3408,7 @@ class SkillSummary(BaseModel): enabled: bool interface: SkillInterface | None = None name: str - path: AbsolutePathBuf + path: AbsolutePathBuf | None = None short_description: Annotated[str | None, Field(alias="shortDescription")] = None @@ -3311,6 +3556,26 @@ class ThreadActiveFlag(Enum): waiting_on_user_input = "waitingOnUserInput" +class ThreadApproveGuardianDeniedActionParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + event: Annotated[ + Any, + Field( + description="Serialized `codex_protocol::protocol::GuardianAssessmentEvent`." + ), + ] + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadApproveGuardianDeniedActionResponse(BaseModel): + pass + model_config = ConfigDict( + populate_by_name=True, + ) + + class ThreadArchiveParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3353,37 +3618,20 @@ class ThreadCompactStartResponse(BaseModel): ) -class ThreadForkParams(BaseModel): +class ThreadGoalClearedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( - None - ) - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, - Field( - alias="approvalsReviewer", - description="Override where approval requests are routed for review on this thread and subsequent turns.", - ), - ] = None - base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None - config: dict[str, Any] | None = None - cwd: str | None = None - developer_instructions: Annotated[ - str | None, Field(alias="developerInstructions") - ] = None - ephemeral: bool | None = None - model: Annotated[ - str | None, - Field(description="Configuration overrides for the forked thread, if any."), - ] = None - model_provider: Annotated[str | None, Field(alias="modelProvider")] = None - sandbox: SandboxMode | None = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None thread_id: Annotated[str, Field(alias="threadId")] +class ThreadGoalStatus(Enum): + active = "active" + paused = "paused" + budget_limited = "budgetLimited" + complete = "complete" + + class ThreadId(RootModel[str]): model_config = ConfigDict( populate_by_name=True, @@ -3588,6 +3836,13 @@ class ContextCompactionThreadItem(BaseModel): ] +class ThreadListCwdFilter(RootModel[str | list[str]]): + model_config = ConfigDict( + populate_by_name=True, + ) + root: str | list[str] + + class ThreadLoadedListParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3806,47 +4061,16 @@ class ThreadRealtimeTranscriptDoneNotification(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] -class ThreadResumeParams(BaseModel): +class ThreadRollbackParams(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( - None - ) - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, + num_turns: Annotated[ + int, Field( - alias="approvalsReviewer", - description="Override where approval requests are routed for review on this thread and subsequent turns.", - ), - ] = None - base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None - config: dict[str, Any] | None = None - cwd: str | None = None - developer_instructions: Annotated[ - str | None, Field(alias="developerInstructions") - ] = None - model: Annotated[ - str | None, - Field(description="Configuration overrides for the resumed thread, if any."), - ] = None - model_provider: Annotated[str | None, Field(alias="modelProvider")] = None - personality: Personality | None = None - sandbox: SandboxMode | None = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None - thread_id: Annotated[str, Field(alias="threadId")] - - -class ThreadRollbackParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - num_turns: Annotated[ - int, - Field( - alias="numTurns", - description="The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", - ge=0, + alias="numTurns", + description="The number of turns to drop from the end of the thread. Must be >= 1.\n\nThis only modifies the thread's history and does not revert local file changes that have been made by the agent. Clients are responsible for reverting these changes.", + ge=0, ), ] thread_id: Annotated[str, Field(alias="threadId")] @@ -4050,6 +4274,14 @@ class TurnDiffUpdatedNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class TurnEnvironmentParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cwd: AbsolutePathBuf + environment_id: Annotated[str, Field(alias="environmentId")] + + class TurnInterruptParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4302,11 +4534,11 @@ class ChatgptAccount(BaseModel): type: Annotated[Literal["chatgpt"], Field(title="ChatgptAccountType")] -class Account(RootModel[ApiKeyAccount | ChatgptAccount]): +class Account(RootModel[ApiKeyAccount | ChatgptAccount | AmazonBedrockAccount]): model_config = ConfigDict( populate_by_name=True, ) - root: ApiKeyAccount | ChatgptAccount + root: ApiKeyAccount | ChatgptAccount | AmazonBedrockAccount class AccountUpdatedNotification(BaseModel): @@ -4374,26 +4606,6 @@ class InitializeRequest(BaseModel): params: InitializeParams -class ThreadResumeRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[ - Literal["thread/resume"], Field(title="Thread/resumeRequestMethod") - ] - params: ThreadResumeParams - - -class ThreadForkRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[Literal["thread/fork"], Field(title="Thread/forkRequestMethod")] - params: ThreadForkParams - - class ThreadArchiveRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4473,6 +4685,18 @@ class ThreadShellCommandRequest(BaseModel): params: ThreadShellCommandParams +class ThreadApproveGuardianDeniedActionRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/approveGuardianDeniedAction"], + Field(title="Thread/approveGuardianDeniedActionRequestMethod"), + ] + params: ThreadApproveGuardianDeniedActionParams + + class ThreadRollbackRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4557,6 +4781,17 @@ class MarketplaceRemoveRequest(BaseModel): params: MarketplaceRemoveParams +class MarketplaceUpgradeRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["marketplace/upgrade"], Field(title="Marketplace/upgradeRequestMethod") + ] + params: MarketplaceUpgradeParams + + class PluginListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4584,6 +4819,17 @@ class AppListRequest(BaseModel): params: AppsListParams +class DeviceKeyPublicRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["device/key/public"], Field(title="Device/key/publicRequestMethod") + ] + params: DeviceKeyPublicParams + + class FsReadFileRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5047,94 +5293,6 @@ class CommandExecOutputDeltaNotification(BaseModel): ] -class CommandExecParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - command: Annotated[ - list[str], Field(description="Command argv vector. Empty arrays are rejected.") - ] - cwd: Annotated[ - str | None, - Field(description="Optional working directory. Defaults to the server cwd."), - ] = None - disable_output_cap: Annotated[ - bool | None, - Field( - alias="disableOutputCap", - description="Disable stdout/stderr capture truncation for this request.\n\nCannot be combined with `outputBytesCap`.", - ), - ] = None - disable_timeout: Annotated[ - bool | None, - Field( - alias="disableTimeout", - description="Disable the timeout entirely for this request.\n\nCannot be combined with `timeoutMs`.", - ), - ] = None - env: Annotated[ - dict[str, Any] | None, - Field( - description="Optional environment overrides merged into the server-computed environment.\n\nMatching names override inherited values. Set a key to `null` to unset an inherited variable." - ), - ] = None - output_bytes_cap: Annotated[ - int | None, - Field( - alias="outputBytesCap", - description="Optional per-stream stdout/stderr capture cap in bytes.\n\nWhen omitted, the server default applies. Cannot be combined with `disableOutputCap`.", - ge=0, - ), - ] = None - process_id: Annotated[ - str | None, - Field( - alias="processId", - description="Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", - ), - ] = None - sandbox_policy: Annotated[ - SandboxPolicy | None, - Field( - alias="sandboxPolicy", - description="Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted.", - ), - ] = None - size: Annotated[ - CommandExecTerminalSize | None, - Field( - description="Optional initial PTY size in character cells. Only valid when `tty` is true." - ), - ] = None - stream_stdin: Annotated[ - bool | None, - Field( - alias="streamStdin", - description="Allow follow-up `command/exec/write` requests to write stdin bytes.\n\nRequires a client-supplied `processId`.", - ), - ] = None - stream_stdout_stderr: Annotated[ - bool | None, - Field( - alias="streamStdoutStderr", - description="Stream stdout/stderr via `command/exec/outputDelta` notifications.\n\nStreamed bytes are not duplicated into the final response and require a client-supplied `processId`.", - ), - ] = None - timeout_ms: Annotated[ - int | None, - Field( - alias="timeoutMs", - description="Optional timeout in milliseconds.\n\nWhen omitted, the server default applies. Cannot be combined with `disableTimeout`.", - ), - ] = None - tty: Annotated[ - bool | None, - Field( - description="Enable PTY mode.\n\nThis implies `streamStdin` and `streamStdoutStderr`." - ), - ] = None - - class CommandExecResizeParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5268,6 +5426,159 @@ class ContentItem( root: InputTextContentItem | InputImageContentItem | OutputTextContentItem +class DeviceKeyCreateParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + account_user_id: Annotated[str, Field(alias="accountUserId")] + client_id: Annotated[str, Field(alias="clientId")] + protection_policy: Annotated[ + DeviceKeyProtectionPolicy | None, + Field( + alias="protectionPolicy", + description="Defaults to `hardware_only` when omitted.", + ), + ] = None + + +class DeviceKeyCreateResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + algorithm: DeviceKeyAlgorithm + key_id: Annotated[str, Field(alias="keyId")] + protection_class: Annotated[ + DeviceKeyProtectionClass, Field(alias="protectionClass") + ] + public_key_spki_der_base64: Annotated[ + str, + Field( + alias="publicKeySpkiDerBase64", + description="SubjectPublicKeyInfo DER encoded as base64.", + ), + ] + + +class RemoteControlClientConnectionDeviceKeySignPayload(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + account_user_id: Annotated[str, Field(alias="accountUserId")] + audience: RemoteControlClientConnectionAudience + client_id: Annotated[str, Field(alias="clientId")] + nonce: str + scopes: Annotated[ + list[str], + Field( + description="Must contain exactly `remote_control_controller_websocket`." + ), + ] + session_id: Annotated[ + str, + Field( + alias="sessionId", + description="Backend-issued websocket session id that this proof authorizes.", + ), + ] + target_origin: Annotated[ + str, + Field( + alias="targetOrigin", + description="Origin of the backend endpoint that issued the challenge and will verify this proof.", + ), + ] + target_path: Annotated[ + str, + Field( + alias="targetPath", + description="Websocket route path that this proof authorizes.", + ), + ] + token_expires_at: Annotated[ + int, + Field( + alias="tokenExpiresAt", + description="Remote-control token expiration as Unix seconds.", + ), + ] + token_sha256_base64url: Annotated[ + str, + Field( + alias="tokenSha256Base64url", + description="SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.", + ), + ] + type: Annotated[ + Literal["remoteControlClientConnection"], + Field(title="RemoteControlClientConnectionDeviceKeySignPayloadType"), + ] + + +class RemoteControlClientEnrollmentDeviceKeySignPayload(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + account_user_id: Annotated[str, Field(alias="accountUserId")] + audience: RemoteControlClientEnrollmentAudience + challenge_expires_at: Annotated[ + int, + Field( + alias="challengeExpiresAt", + description="Enrollment challenge expiration as Unix seconds.", + ), + ] + challenge_id: Annotated[ + str, + Field( + alias="challengeId", + description="Backend-issued enrollment challenge id that this proof authorizes.", + ), + ] + client_id: Annotated[str, Field(alias="clientId")] + device_identity_sha256_base64url: Annotated[ + str, + Field( + alias="deviceIdentitySha256Base64url", + description="SHA-256 of the requested device identity operation, encoded as unpadded base64url.", + ), + ] + nonce: str + target_origin: Annotated[ + str, + Field( + alias="targetOrigin", + description="Origin of the backend endpoint that issued the challenge and will verify this proof.", + ), + ] + target_path: Annotated[ + str, + Field( + alias="targetPath", + description="HTTP route path that this proof authorizes.", + ), + ] + type: Annotated[ + Literal["remoteControlClientEnrollment"], + Field(title="RemoteControlClientEnrollmentDeviceKeySignPayloadType"), + ] + + +class DeviceKeySignPayload( + RootModel[ + RemoteControlClientConnectionDeviceKeySignPayload + | RemoteControlClientEnrollmentDeviceKeySignPayload + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: Annotated[ + RemoteControlClientConnectionDeviceKeySignPayload + | RemoteControlClientEnrollmentDeviceKeySignPayload, + Field(description="Structured payloads accepted by `device/key/sign`."), + ] + + class ExperimentalFeature(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5598,14 +5909,53 @@ class OverriddenMetadata(BaseModel): overriding_layer: Annotated[ConfigLayerMetadata, Field(alias="overridingLayer")] -class PluginDetail(BaseModel): +class ExternalPermissionProfile(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - apps: list[AppSummary] - description: str | None = None - marketplace_name: Annotated[str, Field(alias="marketplaceName")] - marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")] + network: PermissionProfileNetworkPermissions + type: Annotated[Literal["external"], Field(title="ExternalPermissionProfileType")] + + +class RestrictedPermissionProfileFileSystemPermissions(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + entries: list[FileSystemSandboxEntry] + glob_scan_max_depth: Annotated[ + int | None, Field(alias="globScanMaxDepth", ge=1) + ] = None + type: Annotated[ + Literal["restricted"], + Field(title="RestrictedPermissionProfileFileSystemPermissionsType"), + ] + + +class PermissionProfileFileSystemPermissions( + RootModel[ + RestrictedPermissionProfileFileSystemPermissions + | UnrestrictedPermissionProfileFileSystemPermissions + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + RestrictedPermissionProfileFileSystemPermissions + | UnrestrictedPermissionProfileFileSystemPermissions + ) + + +class PluginDetail(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + apps: list[AppSummary] + description: str | None = None + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + marketplace_path: Annotated[ + AbsolutePathBuf | None, Field(alias="marketplacePath") + ] = None mcp_servers: Annotated[list[str], Field(alias="mcpServers")] skills: list[SkillSummary] summary: PluginSummary @@ -5653,7 +6003,6 @@ class MessageResponseItem(BaseModel): populate_by_name=True, ) content: list[ContentItem] - end_turn: bool | None = None id: str | None = None phase: MessagePhase | None = None role: str @@ -5748,6 +6097,17 @@ class ThreadNameUpdatedServerNotification(BaseModel): params: ThreadNameUpdatedNotification +class ThreadGoalClearedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["thread/goal/cleared"], + Field(title="Thread/goal/clearedNotificationMethod"), + ] + params: ThreadGoalClearedNotification + + class HookStartedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5999,6 +6359,29 @@ class SubAgentSource( root: SubAgentSourceValue | ThreadSpawnSubAgentSource | OtherSubAgentSource +class ThreadGoal(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + created_at: Annotated[int, Field(alias="createdAt")] + objective: str + status: ThreadGoalStatus + thread_id: Annotated[str, Field(alias="threadId")] + time_used_seconds: Annotated[int, Field(alias="timeUsedSeconds")] + token_budget: Annotated[int | None, Field(alias="tokenBudget")] = None + tokens_used: Annotated[int, Field(alias="tokensUsed")] + updated_at: Annotated[int, Field(alias="updatedAt")] + + +class ThreadGoalUpdatedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + goal: ThreadGoal + thread_id: Annotated[str, Field(alias="threadId")] + turn_id: Annotated[str | None, Field(alias="turnId")] = None + + class UserMessageThreadItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6156,9 +6539,9 @@ class ThreadListParams(BaseModel): Field(description="Opaque pagination cursor returned by a previous call."), ] = None cwd: Annotated[ - str | None, + ThreadListCwdFilter | None, Field( - description="Optional cwd filter; when set, only threads whose session cwd exactly matches this path are returned." + description="Optional cwd filter or filters; when set, only threads whose session cwd exactly matches one of these paths are returned." ), ] = None limit: Annotated[ @@ -6202,38 +6585,13 @@ class ThreadListParams(BaseModel): description="Optional source filter; when set, only sessions from these source kinds are returned. When omitted or empty, defaults to interactive sources.", ), ] = None - - -class ThreadStartParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( - None - ) - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, + use_state_db_only: Annotated[ + bool | None, Field( - alias="approvalsReviewer", - description="Override where approval requests are routed for review on this thread and subsequent turns.", + alias="useStateDbOnly", + description="If true, return from the state DB without scanning JSONL rollouts to repair thread metadata. Omitted or false preserves scan-and-repair behavior.", ), ] = None - base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None - config: dict[str, Any] | None = None - cwd: str | None = None - developer_instructions: Annotated[ - str | None, Field(alias="developerInstructions") - ] = None - ephemeral: bool | None = None - model: str | None = None - model_provider: Annotated[str | None, Field(alias="modelProvider")] = None - personality: Personality | None = None - sandbox: SandboxMode | None = None - service_name: Annotated[str | None, Field(alias="serviceName")] = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None - session_start_source: Annotated[ - ThreadStartSource | None, Field(alias="sessionStartSource") - ] = None class ThreadTokenUsage(BaseModel): @@ -6300,77 +6658,6 @@ class TurnPlanUpdatedNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] -class TurnStartParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - approval_policy: Annotated[ - AskForApproval | None, - Field( - alias="approvalPolicy", - description="Override the approval policy for this turn and subsequent turns.", - ), - ] = None - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, - Field( - alias="approvalsReviewer", - description="Override where approval requests are routed for review on this turn and subsequent turns.", - ), - ] = None - cwd: Annotated[ - str | None, - Field( - description="Override the working directory for this turn and subsequent turns." - ), - ] = None - effort: Annotated[ - ReasoningEffort | None, - Field( - description="Override the reasoning effort for this turn and subsequent turns." - ), - ] = None - input: list[UserInput] - model: Annotated[ - str | None, - Field(description="Override the model for this turn and subsequent turns."), - ] = None - output_schema: Annotated[ - Any | None, - Field( - alias="outputSchema", - description="Optional JSON Schema used to constrain the final assistant message for this turn.", - ), - ] = None - personality: Annotated[ - Personality | None, - Field( - description="Override the personality for this turn and subsequent turns." - ), - ] = None - sandbox_policy: Annotated[ - SandboxPolicy | None, - Field( - alias="sandboxPolicy", - description="Override the sandbox policy for this turn and subsequent turns.", - ), - ] = None - service_tier: Annotated[ - ServiceTier | None, - Field( - alias="serviceTier", - description="Override the service tier for this turn and subsequent turns.", - ), - ] = None - summary: Annotated[ - ReasoningSummary | None, - Field( - description="Override the reasoning summary for this turn and subsequent turns." - ), - ] = None - thread_id: Annotated[str, Field(alias="threadId")] - - class TurnSteerParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6410,8 +6697,14 @@ class AdditionalFileSystemPermissions(BaseModel): glob_scan_max_depth: Annotated[ int | None, Field(alias="globScanMaxDepth", ge=1) ] = None - read: list[AbsolutePathBuf] | None = None - write: list[AbsolutePathBuf] | None = None + read: Annotated[ + list[AbsolutePathBuf] | None, + Field(description="This will be removed in favor of `entries`."), + ] = None + write: Annotated[ + list[AbsolutePathBuf] | None, + Field(description="This will be removed in favor of `entries`."), + ] = None class AppInfo(BaseModel): @@ -6464,15 +6757,6 @@ class AppsListResponse(BaseModel): ] = None -class ThreadStartRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[Literal["thread/start"], Field(title="Thread/startRequestMethod")] - params: ThreadStartParams - - class ThreadListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6482,13 +6766,15 @@ class ThreadListRequest(BaseModel): params: ThreadListParams -class TurnStartRequest(BaseModel): +class DeviceKeyCreateRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, ) id: RequestId - method: Annotated[Literal["turn/start"], Field(title="Turn/startRequestMethod")] - params: TurnStartParams + method: Annotated[ + Literal["device/key/create"], Field(title="Device/key/createRequestMethod") + ] + params: DeviceKeyCreateParams class TurnSteerRequest(BaseModel): @@ -6521,15 +6807,6 @@ class McpServerStatusListRequest(BaseModel): params: ListMcpServerStatusParams -class CommandExecRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[Literal["command/exec"], Field(title="Command/execRequestMethod")] - params: CommandExecParams - - class CommandExecResizeRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6592,6 +6869,14 @@ class ConfigWriteResponse(BaseModel): version: str +class DeviceKeySignParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + key_id: Annotated[str, Field(alias="keyId")] + payload: DeviceKeySignPayload + + class ErrorNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6695,6 +6980,30 @@ class ListMcpServerStatusResponse(BaseModel): ] = None +class ManagedPermissionProfile(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + file_system: Annotated[ + PermissionProfileFileSystemPermissions, Field(alias="fileSystem") + ] + network: PermissionProfileNetworkPermissions + type: Annotated[Literal["managed"], Field(title="ManagedPermissionProfileType")] + + +class PermissionProfile( + RootModel[ + ManagedPermissionProfile | DisabledPermissionProfile | ExternalPermissionProfile + ] +): + model_config = ConfigDict( + populate_by_name=True, + ) + root: ( + ManagedPermissionProfile | DisabledPermissionProfile | ExternalPermissionProfile + ) + + class PluginListResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6814,6 +7123,17 @@ class ErrorServerNotification(BaseModel): params: ErrorNotification +class ThreadGoalUpdatedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["thread/goal/updated"], + Field(title="Thread/goal/updatedNotificationMethod"), + ] + params: ThreadGoalUpdatedNotification + + class ThreadTokenUsageUpdatedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6925,44 +7245,173 @@ class SessionSource( root: SessionSourceValue | CustomSessionSource | SubAgentSessionSource -class Turn(BaseModel): +class ThreadForkParams(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - completed_at: Annotated[ - int | None, + approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( + None + ) + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, Field( - alias="completedAt", - description="Unix timestamp (in seconds) when the turn completed.", + alias="approvalsReviewer", + description="Override where approval requests are routed for review on this thread and subsequent turns.", ), ] = None - duration_ms: Annotated[ - int | None, + base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None + config: dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: Annotated[ + str | None, Field(alias="developerInstructions") + ] = None + ephemeral: bool | None = None + exclude_turns: Annotated[ + bool | None, Field( - alias="durationMs", - description="Duration between turn start and completion in milliseconds, if known.", + alias="excludeTurns", + description="When true, return only thread metadata and live fork state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after forking.", ), ] = None - error: Annotated[ - TurnError | None, - Field(description="Only populated when the Turn's status is failed."), + model: Annotated[ + str | None, + Field(description="Configuration overrides for the forked thread, if any."), ] = None - id: str - items: Annotated[ - list[ThreadItem], - Field( - description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list." - ), - ] - started_at: Annotated[ - int | None, + model_provider: Annotated[str | None, Field(alias="modelProvider")] = None + permission_profile: Annotated[ + PermissionProfile | None, Field( - alias="startedAt", - description="Unix timestamp (in seconds) when the turn started.", + alias="permissionProfile", + description="Full permissions override for the forked thread. Cannot be combined with `sandbox`.", ), ] = None - status: TurnStatus - + sandbox: SandboxMode | None = None + service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadResumeParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( + None + ) + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, + Field( + alias="approvalsReviewer", + description="Override where approval requests are routed for review on this thread and subsequent turns.", + ), + ] = None + base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None + config: dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: Annotated[ + str | None, Field(alias="developerInstructions") + ] = None + exclude_turns: Annotated[ + bool | None, + Field( + alias="excludeTurns", + description="When true, return only thread metadata and live-resume state without populating `thread.turns`. This is useful when the client plans to call `thread/turns/list` immediately after resuming.", + ), + ] = None + model: Annotated[ + str | None, + Field(description="Configuration overrides for the resumed thread, if any."), + ] = None + model_provider: Annotated[str | None, Field(alias="modelProvider")] = None + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Full permissions override for the resumed thread. Cannot be combined with `sandbox`.", + ), + ] = None + personality: Personality | None = None + sandbox: SandboxMode | None = None + service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + thread_id: Annotated[str, Field(alias="threadId")] + + +class ThreadStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( + None + ) + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, + Field( + alias="approvalsReviewer", + description="Override where approval requests are routed for review on this thread and subsequent turns.", + ), + ] = None + base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None + config: dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: Annotated[ + str | None, Field(alias="developerInstructions") + ] = None + ephemeral: bool | None = None + model: str | None = None + model_provider: Annotated[str | None, Field(alias="modelProvider")] = None + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Full permissions override for this thread. Cannot be combined with `sandbox`.", + ), + ] = None + personality: Personality | None = None + sandbox: SandboxMode | None = None + service_name: Annotated[str | None, Field(alias="serviceName")] = None + service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + session_start_source: Annotated[ + ThreadStartSource | None, Field(alias="sessionStartSource") + ] = None + + +class Turn(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + completed_at: Annotated[ + int | None, + Field( + alias="completedAt", + description="Unix timestamp (in seconds) when the turn completed.", + ), + ] = None + duration_ms: Annotated[ + int | None, + Field( + alias="durationMs", + description="Duration between turn start and completion in milliseconds, if known.", + ), + ] = None + error: Annotated[ + TurnError | None, + Field(description="Only populated when the Turn's status is failed."), + ] = None + id: str + items: Annotated[ + list[ThreadItem], + Field( + description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list." + ), + ] + started_at: Annotated[ + int | None, + Field( + alias="startedAt", + description="Unix timestamp (in seconds) when the turn started.", + ), + ] = None + status: TurnStatus + class TurnCompletedNotification(BaseModel): model_config = ConfigDict( @@ -6972,6 +7421,84 @@ class TurnCompletedNotification(BaseModel): turn: Turn +class TurnStartParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: Annotated[ + AskForApproval | None, + Field( + alias="approvalPolicy", + description="Override the approval policy for this turn and subsequent turns.", + ), + ] = None + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, + Field( + alias="approvalsReviewer", + description="Override where approval requests are routed for review on this turn and subsequent turns.", + ), + ] = None + cwd: Annotated[ + str | None, + Field( + description="Override the working directory for this turn and subsequent turns." + ), + ] = None + effort: Annotated[ + ReasoningEffort | None, + Field( + description="Override the reasoning effort for this turn and subsequent turns." + ), + ] = None + input: list[UserInput] + model: Annotated[ + str | None, + Field(description="Override the model for this turn and subsequent turns."), + ] = None + output_schema: Annotated[ + Any | None, + Field( + alias="outputSchema", + description="Optional JSON Schema used to constrain the final assistant message for this turn.", + ), + ] = None + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`.", + ), + ] = None + personality: Annotated[ + Personality | None, + Field( + description="Override the personality for this turn and subsequent turns." + ), + ] = None + sandbox_policy: Annotated[ + SandboxPolicy | None, + Field( + alias="sandboxPolicy", + description="Override the sandbox policy for this turn and subsequent turns.", + ), + ] = None + service_tier: Annotated[ + ServiceTier | None, + Field( + alias="serviceTier", + description="Override the service tier for this turn and subsequent turns.", + ), + ] = None + summary: Annotated[ + ReasoningSummary | None, + Field( + description="Override the reasoning summary for this turn and subsequent turns." + ), + ] = None + thread_id: Annotated[str, Field(alias="threadId")] + + class TurnStartResponse(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6987,6 +7514,55 @@ class TurnStartedNotification(BaseModel): turn: Turn +class ThreadStartRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["thread/start"], Field(title="Thread/startRequestMethod")] + params: ThreadStartParams + + +class ThreadResumeRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["thread/resume"], Field(title="Thread/resumeRequestMethod") + ] + params: ThreadResumeParams + + +class ThreadForkRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["thread/fork"], Field(title="Thread/forkRequestMethod")] + params: ThreadForkParams + + +class DeviceKeySignRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["device/key/sign"], Field(title="Device/key/signRequestMethod") + ] + params: DeviceKeySignParams + + +class TurnStartRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["turn/start"], Field(title="Turn/startRequestMethod")] + params: TurnStartParams + + class ConfigBatchWriteRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6998,6 +7574,101 @@ class ConfigBatchWriteRequest(BaseModel): params: ConfigBatchWriteParams +class CommandExecParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + command: Annotated[ + list[str], Field(description="Command argv vector. Empty arrays are rejected.") + ] + cwd: Annotated[ + str | None, + Field(description="Optional working directory. Defaults to the server cwd."), + ] = None + disable_output_cap: Annotated[ + bool | None, + Field( + alias="disableOutputCap", + description="Disable stdout/stderr capture truncation for this request.\n\nCannot be combined with `outputBytesCap`.", + ), + ] = None + disable_timeout: Annotated[ + bool | None, + Field( + alias="disableTimeout", + description="Disable the timeout entirely for this request.\n\nCannot be combined with `timeoutMs`.", + ), + ] = None + env: Annotated[ + dict[str, Any] | None, + Field( + description="Optional environment overrides merged into the server-computed environment.\n\nMatching names override inherited values. Set a key to `null` to unset an inherited variable." + ), + ] = None + output_bytes_cap: Annotated[ + int | None, + Field( + alias="outputBytesCap", + description="Optional per-stream stdout/stderr capture cap in bytes.\n\nWhen omitted, the server default applies. Cannot be combined with `disableOutputCap`.", + ge=0, + ), + ] = None + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`.", + ), + ] = None + process_id: Annotated[ + str | None, + Field( + alias="processId", + description="Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", + ), + ] = None + sandbox_policy: Annotated[ + SandboxPolicy | None, + Field( + alias="sandboxPolicy", + description="Optional sandbox policy for this command.\n\nUses the same shape as thread/turn execution sandbox configuration and defaults to the user's configured policy when omitted. Cannot be combined with `permissionProfile`.", + ), + ] = None + size: Annotated[ + CommandExecTerminalSize | None, + Field( + description="Optional initial PTY size in character cells. Only valid when `tty` is true." + ), + ] = None + stream_stdin: Annotated[ + bool | None, + Field( + alias="streamStdin", + description="Allow follow-up `command/exec/write` requests to write stdin bytes.\n\nRequires a client-supplied `processId`.", + ), + ] = None + stream_stdout_stderr: Annotated[ + bool | None, + Field( + alias="streamStdoutStderr", + description="Stream stdout/stderr via `command/exec/outputDelta` notifications.\n\nStreamed bytes are not duplicated into the final response and require a client-supplied `processId`.", + ), + ] = None + timeout_ms: Annotated[ + int | None, + Field( + alias="timeoutMs", + description="Optional timeout in milliseconds.\n\nWhen omitted, the server default applies. Cannot be combined with `disableTimeout`.", + ), + ] = None + tty: Annotated[ + bool | None, + Field( + description="Enable PTY mode.\n\nThis implies `streamStdin` and `streamStdoutStderr`." + ), + ] = None + + class Config(BaseModel): model_config = ConfigDict( extra="allow", @@ -7320,10 +7991,22 @@ class ThreadForkResponse(BaseModel): ] = [] model: str model_provider: Annotated[str, Field(alias="modelProvider")] + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Canonical active permissions view for this thread.", + ), + ] = None reasoning_effort: Annotated[ ReasoningEffort | None, Field(alias="reasoningEffort") ] = None - sandbox: SandboxPolicy + sandbox: Annotated[ + SandboxPolicy, + Field( + description="Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view." + ), + ] service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None thread: Thread @@ -7385,10 +8068,22 @@ class ThreadResumeResponse(BaseModel): ] = [] model: str model_provider: Annotated[str, Field(alias="modelProvider")] + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Canonical active permissions view for this thread.", + ), + ] = None reasoning_effort: Annotated[ ReasoningEffort | None, Field(alias="reasoningEffort") ] = None - sandbox: SandboxPolicy + sandbox: Annotated[ + SandboxPolicy, + Field( + description="Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view." + ), + ] service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None thread: Thread @@ -7427,10 +8122,22 @@ class ThreadStartResponse(BaseModel): ] = [] model: str model_provider: Annotated[str, Field(alias="modelProvider")] + permission_profile: Annotated[ + PermissionProfile | None, + Field( + alias="permissionProfile", + description="Canonical active permissions view for this thread.", + ), + ] = None reasoning_effort: Annotated[ ReasoningEffort | None, Field(alias="reasoningEffort") ] = None - sandbox: SandboxPolicy + sandbox: Annotated[ + SandboxPolicy, + Field( + description="Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view." + ), + ] service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None thread: Thread @@ -7470,6 +8177,15 @@ class ThreadUnarchiveResponse(BaseModel): thread: Thread +class CommandExecRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["command/exec"], Field(title="Command/execRequestMethod")] + params: CommandExecParams + + class ExternalAgentConfigImportRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -7495,6 +8211,7 @@ class ClientRequest( | ThreadUnarchiveRequest | ThreadCompactStartRequest | ThreadShellCommandRequest + | ThreadApproveGuardianDeniedActionRequest | ThreadRollbackRequest | ThreadListRequest | ThreadLoadedListRequest @@ -7504,9 +8221,13 @@ class ClientRequest( | SkillsListRequest | MarketplaceAddRequest | MarketplaceRemoveRequest + | MarketplaceUpgradeRequest | PluginListRequest | PluginReadRequest | AppListRequest + | DeviceKeyCreateRequest + | DeviceKeyPublicRequest + | DeviceKeySignRequest | FsReadFileRequest | FsWriteFileRequest | FsCreateDirectoryRequest @@ -7567,6 +8288,7 @@ class ClientRequest( | ThreadUnarchiveRequest | ThreadCompactStartRequest | ThreadShellCommandRequest + | ThreadApproveGuardianDeniedActionRequest | ThreadRollbackRequest | ThreadListRequest | ThreadLoadedListRequest @@ -7576,9 +8298,13 @@ class ClientRequest( | SkillsListRequest | MarketplaceAddRequest | MarketplaceRemoveRequest + | MarketplaceUpgradeRequest | PluginListRequest | PluginReadRequest | AppListRequest + | DeviceKeyCreateRequest + | DeviceKeyPublicRequest + | DeviceKeySignRequest | FsReadFileRequest | FsWriteFileRequest | FsCreateDirectoryRequest @@ -7648,6 +8374,8 @@ class ServerNotification( | ThreadClosedServerNotification | SkillsChangedServerNotification | ThreadNameUpdatedServerNotification + | ThreadGoalUpdatedServerNotification + | ThreadGoalClearedServerNotification | ThreadTokenUsageUpdatedServerNotification | TurnStartedServerNotification | HookStartedServerNotification @@ -7680,7 +8408,9 @@ class ServerNotification( | ItemReasoningTextDeltaServerNotification | ThreadCompactedServerNotification | ModelReroutedServerNotification + | ModelVerificationServerNotification | WarningServerNotification + | GuardianWarningServerNotification | DeprecationNoticeServerNotification | ConfigWarningServerNotification | FuzzyFileSearchSessionUpdatedServerNotification @@ -7710,6 +8440,8 @@ class ServerNotification( | ThreadClosedServerNotification | SkillsChangedServerNotification | ThreadNameUpdatedServerNotification + | ThreadGoalUpdatedServerNotification + | ThreadGoalClearedServerNotification | ThreadTokenUsageUpdatedServerNotification | TurnStartedServerNotification | HookStartedServerNotification @@ -7742,7 +8474,9 @@ class ServerNotification( | ItemReasoningTextDeltaServerNotification | ThreadCompactedServerNotification | ModelReroutedServerNotification + | ModelVerificationServerNotification | WarningServerNotification + | GuardianWarningServerNotification | DeprecationNoticeServerNotification | ConfigWarningServerNotification | FuzzyFileSearchSessionUpdatedServerNotification diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 03252154e70e..a30582517ab5 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -29,7 +29,9 @@ def _load_runtime_setup_module(): runtime_setup_path = ROOT / "_runtime_setup.py" spec = importlib.util.spec_from_file_location("_runtime_setup", runtime_setup_path) if spec is None or spec.loader is None: - raise AssertionError(f"Failed to load runtime setup module: {runtime_setup_path}") + raise AssertionError( + f"Failed to load runtime setup module: {runtime_setup_path}" + ) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) @@ -159,29 +161,32 @@ def test_runtime_package_template_has_no_checked_in_binaries() -> None: ) == ["__init__.py"] -def test_examples_readme_matches_pinned_runtime_version() -> None: - runtime_setup = _load_runtime_setup_module() +def test_examples_readme_points_to_runtime_version_source_of_truth() -> None: readme = (ROOT / "examples" / "README.md").read_text() - assert ( - f"Current pinned runtime version: `{runtime_setup.pinned_runtime_version()}`" - in readme - ) + assert "The pinned runtime version comes from the SDK package version." in readme def test_runtime_distribution_name_is_consistent() -> None: script = _load_update_script_module() runtime_setup = _load_runtime_setup_module() from codex_app_server import client as client_module + from codex_app_server import _version + assert script.SDK_DISTRIBUTION_NAME == "openai-codex-app-server-sdk" + assert runtime_setup.SDK_PACKAGE_NAME == "openai-codex-app-server-sdk" + assert _version.DISTRIBUTION_NAME == "openai-codex-app-server-sdk" assert script.RUNTIME_DISTRIBUTION_NAME == "openai-codex-cli-bin" assert runtime_setup.PACKAGE_NAME == "openai-codex-cli-bin" assert client_module.RUNTIME_PKG_NAME == "openai-codex-cli-bin" - assert "importlib.metadata.version('codex-cli-bin')" not in ( - ROOT / "_runtime_setup.py" - ).read_text() + assert ( + "importlib.metadata.version('codex-cli-bin')" + not in (ROOT / "_runtime_setup.py").read_text() + ) -def test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.MonkeyPatch) -> None: +def test_release_metadata_retries_without_invalid_auth( + monkeypatch: pytest.MonkeyPatch, +) -> None: runtime_setup = _load_runtime_setup_module() authorizations: list[str | None] = [] @@ -205,6 +210,19 @@ def fake_urlopen(request): assert authorizations == ["Bearer invalid-token", None] +def test_runtime_setup_uses_pep440_package_version_and_codex_release_tags() -> None: + runtime_setup = _load_runtime_setup_module() + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text()) + + assert runtime_setup.PACKAGE_NAME == "openai-codex-cli-bin" + assert runtime_setup.pinned_runtime_version() == pyproject["project"]["version"] + assert ( + runtime_setup._normalized_package_version("rust-v0.116.0-alpha.1") + == "0.116.0a1" + ) + assert runtime_setup._release_tag("0.116.0a1") == "rust-v0.116.0-alpha.1" + + def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() -> None: pyproject = tomllib.loads( (ROOT.parent / "python-runtime" / "pyproject.toml").read_text() @@ -334,12 +352,23 @@ def test_stage_runtime_release_can_pin_wheel_platform_tag(tmp_path: Path) -> Non def test_stage_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None: script = _load_update_script_module() - staged = script.stage_python_sdk_package(tmp_path / "sdk-stage", "0.2.1", "1.2.3") + staged = script.stage_python_sdk_package( + tmp_path / "sdk-stage", + "rust-v0.116.0-alpha.1", + ) pyproject = (staged / "pyproject.toml").read_text() - assert 'version = "0.2.1"' in pyproject - assert '"openai-codex-cli-bin==1.2.3"' in pyproject - assert '"codex-cli-bin==1.2.3"' not in pyproject + assert 'name = "openai-codex-app-server-sdk"' in pyproject + assert 'version = "0.116.0a1"' in pyproject + assert '"openai-codex-cli-bin==0.116.0a1"' in pyproject + assert ( + '__version__ = "0.116.0a1"' + not in (staged / "src" / "codex_app_server" / "__init__.py").read_text() + ) + assert ( + 'client_version: str = "0.116.0a1"' + not in (staged / "src" / "codex_app_server" / "client.py").read_text() + ) assert not any((staged / "src" / "codex_app_server").glob("bin/**")) @@ -350,12 +379,39 @@ def test_stage_sdk_release_replaces_existing_staging_dir(tmp_path: Path) -> None old_file.parent.mkdir(parents=True) old_file.write_text("stale") - staged = script.stage_python_sdk_package(staging_dir, "0.2.1", "1.2.3") + staged = script.stage_python_sdk_package(staging_dir, "0.116.0a1") assert staged == staging_dir assert not old_file.exists() +def test_staged_sdk_and_runtime_versions_match(tmp_path: Path) -> None: + script = _load_update_script_module() + fake_binary = tmp_path / script.runtime_binary_name() + fake_binary.write_text("fake codex\n") + + sdk_stage = script.stage_python_sdk_package( + tmp_path / "sdk-stage", + "rust-v0.116.0-alpha.1", + ) + runtime_stage = script.stage_python_runtime_package( + tmp_path / "runtime-stage", + "rust-v0.116.0-alpha.1", + fake_binary, + ) + + sdk_pyproject = tomllib.loads((sdk_stage / "pyproject.toml").read_text()) + runtime_pyproject = tomllib.loads((runtime_stage / "pyproject.toml").read_text()) + + assert ( + sdk_pyproject["project"]["version"] == runtime_pyproject["project"]["version"] + ) + assert sdk_pyproject["project"]["dependencies"] == [ + "pydantic>=2.12", + "openai-codex-cli-bin==0.116.0a1", + ] + + def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None: script = _load_update_script_module() calls: list[str] = [] @@ -363,18 +419,16 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None: [ "stage-sdk", str(tmp_path / "sdk-stage"), - "--runtime-version", - "1.2.3", + "--codex-version", + "rust-v0.116.0-alpha.1", ] ) def fake_generate_types() -> None: calls.append("generate_types") - def fake_stage_sdk_package( - _staging_dir: Path, _sdk_version: str, _runtime_version: str - ) -> Path: - calls.append("stage_sdk") + def fake_stage_sdk_package(_staging_dir: Path, codex_version: str) -> Path: + calls.append(f"stage_sdk:{codex_version}") return tmp_path / "sdk-stage" def fake_stage_runtime_package( @@ -386,7 +440,7 @@ def fake_stage_runtime_package( raise AssertionError("runtime staging should not run for stage-sdk") def fake_current_sdk_version() -> str: - return "0.2.0" + return "0.116.0a1" ops = script.CliOps( generate_types=fake_generate_types, @@ -397,7 +451,26 @@ def fake_current_sdk_version() -> str: script.run_command(args, ops) - assert calls == ["generate_types", "stage_sdk"] + assert calls == ["generate_types", "stage_sdk:0.116.0a1"] + + +def test_stage_sdk_rejects_mismatched_legacy_versions(tmp_path: Path) -> None: + script = _load_update_script_module() + args = script.parse_args( + [ + "stage-sdk", + str(tmp_path / "sdk-stage"), + "--codex-version", + "0.116.0a1", + "--runtime-version", + "0.116.0a1", + "--sdk-version", + "0.115.0", + ] + ) + + with pytest.raises(RuntimeError, match="versions must match"): + script.run_command(args, script.default_cli_ops()) def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> None: @@ -420,9 +493,7 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> def fake_generate_types() -> None: calls.append("generate_types") - def fake_stage_sdk_package( - _staging_dir: Path, _sdk_version: str, _runtime_version: str - ) -> Path: + def fake_stage_sdk_package(_staging_dir: Path, _codex_version: str) -> Path: raise AssertionError("sdk staging should not run for stage-runtime") def fake_stage_runtime_package( @@ -435,7 +506,7 @@ def fake_stage_runtime_package( return tmp_path / "runtime-stage" def fake_current_sdk_version() -> str: - return "0.2.0" + return "0.116.0a1" ops = script.CliOps( generate_types=fake_generate_types, diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index 68097d4bc6a3..99e5d55b669c 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -2,8 +2,11 @@ import importlib.resources as resources import inspect +import tomllib +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 @@ -37,6 +40,14 @@ def test_root_exports_run_result() -> None: assert RunResult.__name__ == "RunResult" +def test_package_and_default_client_versions_follow_project_version() -> None: + pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml" + pyproject = tomllib.loads(pyproject_path.read_text()) + + assert codex_app_server.__version__ == pyproject["project"]["version"] + assert AppServerConfig().client_version == codex_app_server.__version__ + + def test_package_includes_py_typed_marker() -> None: marker = resources.files("codex_app_server").joinpath("py.typed") assert marker.is_file() @@ -54,6 +65,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "ephemeral", "model", "model_provider", + "permission_profile", "personality", "sandbox", "service_name", @@ -70,6 +82,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "sort_direction", "sort_key", "source_kinds", + "use_state_db_only", ], Codex.thread_resume: [ "approval_policy", @@ -78,8 +91,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "config", "cwd", "developer_instructions", + "exclude_turns", "model", "model_provider", + "permission_profile", "personality", "sandbox", "service_tier", @@ -92,8 +107,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "cwd", "developer_instructions", "ephemeral", + "exclude_turns", "model", "model_provider", + "permission_profile", "sandbox", "service_tier", ], @@ -104,6 +121,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "effort", "model", "output_schema", + "permission_profile", "personality", "sandbox_policy", "service_tier", @@ -116,6 +134,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "effort", "model", "output_schema", + "permission_profile", "personality", "sandbox_policy", "service_tier", @@ -131,6 +150,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "ephemeral", "model", "model_provider", + "permission_profile", "personality", "sandbox", "service_name", @@ -147,6 +167,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "sort_direction", "sort_key", "source_kinds", + "use_state_db_only", ], AsyncCodex.thread_resume: [ "approval_policy", @@ -155,8 +176,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "config", "cwd", "developer_instructions", + "exclude_turns", "model", "model_provider", + "permission_profile", "personality", "sandbox", "service_tier", @@ -169,8 +192,10 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "cwd", "developer_instructions", "ephemeral", + "exclude_turns", "model", "model_provider", + "permission_profile", "sandbox", "service_tier", ], @@ -181,6 +206,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "effort", "model", "output_schema", + "permission_profile", "personality", "sandbox_policy", "service_tier", @@ -193,6 +219,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "effort", "model", "output_schema", + "permission_profile", "personality", "sandbox_policy", "service_tier", diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 8ddc4455fb4a..d0e2cf737239 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.10" [options] -exclude-newer = "2026-04-16T16:29:01.461661899Z" +exclude-newer = "2026-04-20T18:19:27.620299Z" exclude-newer-span = "P7D" [[package]] @@ -80,30 +80,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, ] -[[package]] -name = "codex-app-server-sdk" -version = "0.2.0" -source = { editable = "." } -dependencies = [ - { name = "pydantic" }, -] - -[package.optional-dependencies] -dev = [ - { name = "datamodel-code-generator" }, - { name = "pytest" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.31.2" }, - { name = "pydantic", specifier = ">=2.12" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11" }, -] -provides-extras = ["dev"] - [[package]] name = "colorama" version = "0.4.6" @@ -301,6 +277,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "openai-codex-app-server-sdk" +version = "0.116.0a1" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.optional-dependencies] +dev = [ + { name = "datamodel-code-generator" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.31.2" }, + { name = "pydantic", specifier = ">=2.12" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11" }, +] +provides-extras = ["dev"] + [[package]] name = "packaging" version = "26.1"