From 0f40261e86ea7dc020e30e2f89225d103435d670 Mon Sep 17 00:00:00 2001 From: Steve Coffey Date: Mon, 27 Apr 2026 14:28:46 -0700 Subject: [PATCH] Publish Python SDK with Codex-pinned versioning (#18996) **note**: a large chunk of this diff comes from regenerating Python types after app-server schema changes on `main`. This is PR 3 of 3 for the Python SDK PyPI publishing split. PR #18862 refreshed the generated SDK surface, and PR #18865 made the runtime package publishable as `openai-codex-cli-bin`; this final PR makes the SDK package publishable as `openai-codex-app-server-sdk` and pins both packages to the same Codex runtime version. The key idea is that the published SDK version is the Codex runtime version. That one version now drives the SDK package version, the exact runtime dependency, the client version reported by the SDK, and the bootstrap runtime pin. This keeps release-time versioning in one lane instead of scattering checked-in literals through the package. ## What changed - Rename the SDK distribution from `codex-app-server-sdk` to `openai-codex-app-server-sdk` for conflict-free PyPI publishing. - Use `stage-sdk --codex-version ...` with one Codex version for both the SDK package version and exact `openai-codex-cli-bin` dependency. - Preserve hidden legacy `--runtime-version` / `--sdk-version` args only to reject mismatched versions during staging. - Map PEP 440 package versions back to Codex release tags for runtime setup downloads, e.g. `0.116.0a1` -> `rust-v0.116.0-alpha.1`. - Derive `codex_app_server.__version__`, the default `AppServerConfig.client_version`, and `_runtime_setup.pinned_runtime_version()` from the SDK package/project version instead of hardcoding duplicate version strings. - Carry the current generated SDK refresh from `main` so `generate-types` stays clean after recent app-server schema changes. - Update `sdk/python/uv.lock` for the renamed editable package. ## Validation - `uv run --extra dev pytest` in `sdk/python` -> 59 passed, 37 skipped. - Targeted `uv run ruff check` for the touched SDK files. - `git diff --check`. - Staged runtime with `--codex-version rust-v0.116.0-alpha.1 --platform-tag macosx_11_0_arm64`. - Staged SDK with `--codex-version rust-v0.116.0-alpha.1`. - Built runtime wheel, SDK wheel, and SDK sdist. - `twine check /tmp/codex-python-pr3-build/dist/*` -> passed. - Clean venv smoke installed `openai-codex-app-server-sdk==0.116.0a1` from local dist and pulled `openai-codex-cli-bin==0.116.0a1`. - Smoke imports passed for `Codex` and `bundled_codex_path()`. --- 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 | 1390 +++++++++++++---- .../test_artifact_workflow_and_binaries.py | 127 +- .../tests/test_public_api_signatures.py | 27 + sdk/python/uv.lock | 50 +- 16 files changed, 1443 insertions(+), 441 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 22c59ef15..27623b28a 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 7d69e2335..149420ad9 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 6c4cf457a..8d33da6c8 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 @@ import zipfile 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 af688a3a1..bc5cec6e3 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 99ea0a31f..59428ba32 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 d67cb54c2..f54838bfa 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 42c1ec091..0d0e739c7 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -17,6 +17,7 @@ from dataclasses import dataclass 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 c35ce0ebe..33f9e628d 100644 --- a/sdk/python/src/codex_app_server/__init__.py +++ b/sdk/python/src/codex_app_server/__init__.py @@ -54,8 +54,7 @@ from .api import ( 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 000000000..b4b724e38 --- /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 c330e3f74..ed0535db8 100644 --- a/sdk/python/src/codex_app_server/api.py +++ b/sdk/python/src/codex_app_server/api.py @@ -10,15 +10,18 @@ from .generated.v2_all import ( ApprovalsReviewer, AskForApproval, ModelListResponse, + PermissionProfile, Personality, ReasoningEffort, ReasoningSummary, SandboxMode, SandboxPolicy, ServiceTier, + SortDirection, ThreadArchiveResponse, ThreadCompactStartResponse, ThreadForkParams, + ThreadListCwdFilter, ThreadListParams, ThreadListResponse, ThreadReadResponse, @@ -26,6 +29,7 @@ from .generated.v2_all import ( ThreadSetNameResponse, ThreadSortKey, ThreadSourceKind, + ThreadStartSource, ThreadStartParams, Turn as AppServerTurn, TurnCompletedNotification, @@ -146,6 +150,7 @@ class Codex: 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 @@ class Codex: ephemeral=ephemeral, model=model, model_provider=model_provider, + permission_profile=permission_profile, personality=personality, sandbox=sandbox, service_name=service_name, @@ -176,13 +182,14 @@ class Codex: *, 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 @@ class Codex: 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 @@ class Codex: 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 @@ class Codex: 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 @@ class Codex: 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 @@ class Codex: 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 @@ class AsyncCodex: 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 @@ class AsyncCodex: ephemeral=ephemeral, model=model, model_provider=model_provider, + permission_profile=permission_profile, personality=personality, sandbox=sandbox, service_name=service_name, @@ -371,13 +389,14 @@ class AsyncCodex: *, 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 @@ class AsyncCodex: 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 @@ class AsyncCodex: 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 @@ class AsyncCodex: 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 @@ class AsyncCodex: 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 @@ class AsyncCodex: 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 @@ class Thread: 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 @@ class Thread: 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 @@ class Thread: 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 @@ class Thread: 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 @@ class AsyncThread: 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 @@ class AsyncThread: 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 @@ class AsyncThread: 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 @@ class AsyncThread: 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 db7cf77cd..665e1c672 100644 --- a/sdk/python/src/codex_app_server/client.py +++ b/sdk/python/src/codex_app_server/client.py @@ -44,6 +44,7 @@ from .models import ( 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 ab6f87f11..5b54207b5 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 FileChangePatchUpdatedNotification 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 McpServerOauthLoginCompletedNotification 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 SkillsChangedNotification 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 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "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 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "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 fac98223d..70c700928 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,37 +4061,6 @@ class ThreadRealtimeTranscriptDoneNotification(BaseModel): 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 - 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, @@ -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,6 +5909,43 @@ class OverriddenMetadata(BaseModel): overriding_layer: Annotated[ConfigLayerMetadata, Field(alias="overridingLayer")] +class ExternalPermissionProfile(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + 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, @@ -5605,7 +5953,9 @@ class PluginDetail(BaseModel): apps: list[AppSummary] description: str | None = None marketplace_name: Annotated[str, Field(alias="marketplaceName")] - marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")] + 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,6 +7245,135 @@ class SessionSource( root: SessionSourceValue | CustomSessionSource | SubAgentSessionSource +class ThreadForkParams(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 + exclude_turns: Annotated[ + bool | None, + Field( + 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 + model: Annotated[ + str | None, + Field(description="Configuration overrides for the forked 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 forked thread. Cannot be combined with `sandbox`.", + ), + ] = None + 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, @@ -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 03252154e..a30582517 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 test_release_metadata_retries_without_invalid_auth(monkeypatch: pytest.Monke 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 test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None: 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 test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None: 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 test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> 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 68097d4bc..99e5d55b6 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -2,8 +2,11 @@ from __future__ import annotations 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 8ddc4455f..d0e2cf737 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"