Compare commits

...

3 Commits

Author SHA1 Message Date
Shaqayeq
e1a9b32590 python-sdk: generate types from pinned runtime schema 2026-04-13 00:15:40 -07:00
Shaqayeq
8748ac15ca python-sdk: prepare openai-codex package 2026-04-12 23:26:32 -07:00
Shaqayeq
f412b39118 python-runtime: prepare openai-codex-cli-bin package 2026-04-12 23:23:33 -07:00
15 changed files with 1077 additions and 849 deletions

View File

@@ -1,9 +1,17 @@
# 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` SDK.
This package is staged during release so the SDK can pin an exact Codex CLI
version without checking platform binaries into the repo.
version without checking platform binaries into the repo. The distribution name
is `openai-codex-cli-bin`, while the import module remains `codex_cli_bin`.
`codex-cli-bin` is intentionally wheel-only. Do not build or publish an sdist
for this package.
`openai-codex-cli-bin` is intentionally wheel-only. Do not build or publish an
sdist for this package.
Expected wheel contents:
- macOS/Linux: `codex_cli_bin/bin/codex`
- Windows: `codex_cli_bin/bin/codex.exe`,
`codex_cli_bin/bin/codex-command-runner.exe`, and
`codex_cli_bin/bin/codex-windows-sandbox-setup.exe`

View File

@@ -1,15 +1,34 @@
from __future__ import annotations
import os
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
PLATFORM_TAG_BY_TARGET = {
"aarch64-apple-darwin": "macosx_11_0_arm64",
"x86_64-apple-darwin": "macosx_10_12_x86_64",
"aarch64-unknown-linux-musl": "musllinux_1_2_aarch64",
"x86_64-unknown-linux-musl": "musllinux_1_2_x86_64",
"aarch64-pc-windows-msvc": "win_arm64",
"x86_64-pc-windows-msvc": "win_amd64",
}
class RuntimeBuildHook(BuildHookInterface):
def initialize(self, version: str, build_data: dict[str, object]) -> None:
del version
if self.target_name == "sdist":
raise RuntimeError(
"codex-cli-bin is wheel-only; build and publish platform wheels only."
"openai-codex-cli-bin is wheel-only; build and publish platform wheels only."
)
build_data["pure_python"] = False
build_data["infer_tag"] = True
target = os.environ.get("CODEX_PYTHON_RUNTIME_TARGET")
if target is None:
build_data["infer_tag"] = True
return
platform_tag = PLATFORM_TAG_BY_TARGET.get(target)
if platform_tag is None:
raise RuntimeError(f"Unsupported Codex Python runtime target: {target}")
build_data["tag"] = f"py3-none-{platform_tag}"

View File

@@ -3,7 +3,7 @@ requires = ["hatchling>=1.24.0"]
build-backend = "hatchling.build"
[project]
name = "codex-cli-bin"
name = "openai-codex-cli-bin"
version = "0.0.0-dev"
description = "Pinned Codex CLI runtime for the Python SDK"
readme = "README.md"

View File

@@ -3,12 +3,25 @@ from __future__ import annotations
import os
from pathlib import Path
PACKAGE_NAME = "codex-cli-bin"
PACKAGE_NAME = "openai-codex-cli-bin"
def bundled_bin_dir() -> Path:
return Path(__file__).resolve().parent / "bin"
def bundled_runtime_files() -> tuple[Path, ...]:
names = (
("codex.exe", "codex-command-runner.exe", "codex-windows-sandbox-setup.exe")
if os.name == "nt"
else ("codex",)
)
return tuple(bundled_bin_dir() / name for name in names)
def bundled_codex_path() -> Path:
exe = "codex.exe" if os.name == "nt" else "codex"
path = Path(__file__).resolve().parent / "bin" / exe
path = bundled_bin_dir() / exe
if not path.is_file():
raise FileNotFoundError(
f"{PACKAGE_NAME} is installed but missing its packaged codex binary at {path}"
@@ -16,4 +29,9 @@ def bundled_codex_path() -> Path:
return path
__all__ = ["PACKAGE_NAME", "bundled_codex_path"]
__all__ = [
"PACKAGE_NAME",
"bundled_bin_dir",
"bundled_codex_path",
"bundled_runtime_files",
]

View File

@@ -2,7 +2,7 @@
Experimental Python SDK for `codex app-server` JSON-RPC v2 over stdio, with a small default surface optimized for real scripts and apps.
The generated wire-model layer is currently sourced from the bundled v2 schema and exposed as Pydantic models with snake_case Python fields that serialize back to the app-servers camelCase wire format.
The generated wire-model layer is sourced from the pinned runtime's `codex app-server generate-json-schema` output and exposed as Pydantic models with snake_case Python fields that serialize back to the app-servers camelCase wire format.
## Install
@@ -11,10 +11,16 @@ cd sdk/python
python -m pip install -e .
```
Published SDK builds pin an exact `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.
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.
When published, normal installs should use:
```bash
python -m pip install openai-codex
```
## Quickstart
@@ -53,9 +59,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 `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 bundle
for the target wheel.
For local repo development, the checked-in `sdk/python-runtime` package is only
a template for staged release artifacts. Editable installs should use an
@@ -69,30 +75,34 @@ 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 \
/tmp/codex-python-release/openai-codex \
--runtime-version 1.2.3
python scripts/update_sdk_artifacts.py \
stage-runtime \
/tmp/codex-python-release/codex-cli-bin \
/path/to/codex \
/tmp/codex-python-release/openai-codex-cli-bin \
/path/to/runtime-bundle-dir \
--runtime-version 1.2.3
```
This supports the CI release flow:
- run `generate-types` before packaging
- stage `codex-app-server-sdk` once with an exact `codex-cli-bin==...` dependency
- stage `codex-cli-bin` on each supported platform runner with the same pinned runtime version
- build and publish `codex-cli-bin` as platform wheels only; do not publish an sdist
- generate types from the pinned runtime schema, then convert that schema to Python
- stage `openai-codex` 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`
- Runtime package: `codex-cli-bin`
- Package: `openai-codex`
- 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
- Release tags map to Python package versions as follows: `rust-v1.2.3` ->
`1.2.3`, `rust-v1.2.3-alpha.4` -> `1.2.3a4`, and
`rust-v1.2.3-beta.5` -> `1.2.3b5`.
- Recommendation: keep SDK and `codex` CLI at the exact same published version.
## Notes

View File

@@ -15,7 +15,7 @@ import urllib.request
import zipfile
from pathlib import Path
PACKAGE_NAME = "codex-cli-bin"
PACKAGE_NAME = "openai-codex-cli-bin"
PINNED_RUNTIME_VERSION = "0.116.0-alpha.1"
REPO_SLUG = "openai/codex"
@@ -39,17 +39,20 @@ 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:
temp_root = Path(temp_root_str)
archive_path = _download_release_archive(requested_version, temp_root)
runtime_binary = _extract_runtime_binary(archive_path, temp_root)
runtime_bundle_dir = _extract_runtime_bundle(archive_path, temp_root)
staged_runtime_dir = _stage_runtime_package(
sdk_python_dir,
requested_version,
runtime_binary,
runtime_bundle_dir,
temp_root / "runtime-stage",
)
_install_runtime_package(python_executable, staged_runtime_dir, install_target)
@@ -61,7 +64,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."
@@ -105,7 +111,7 @@ def _installed_runtime_version(python_executable: str | Path) -> str | None:
"try:\n"
" from codex_cli_bin import bundled_codex_path\n"
" bundled_codex_path()\n"
" print(json.dumps({'version': importlib.metadata.version('codex-cli-bin')}))\n"
f" print(json.dumps({{'version': importlib.metadata.version({PACKAGE_NAME!r})}}))\n"
"except Exception:\n"
" sys.exit(1)\n"
)
@@ -172,7 +178,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 rust-v{version} returned malformed assets metadata."
)
asset = next(
(
item
@@ -198,7 +206,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:
@@ -236,7 +247,7 @@ def _download_release_archive(version: str, temp_root: Path) -> Path:
return archive_path
def _extract_runtime_binary(archive_path: Path, temp_root: Path) -> Path:
def _extract_runtime_bundle(archive_path: Path, temp_root: Path) -> Path:
extract_dir = temp_root / "extracted"
extract_dir.mkdir(parents=True, exist_ok=True)
if archive_path.name.endswith(".tar.gz"):
@@ -249,38 +260,24 @@ 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}")
binary_name = runtime_binary_name()
archive_stem = archive_path.name.removesuffix(".tar.gz").removesuffix(".zip")
candidates = [
path
for path in extract_dir.rglob("*")
if path.is_file()
and (
path.name == binary_name
or path.name == archive_stem
or path.name.startswith("codex-")
)
]
if not candidates:
raise RuntimeSetupError(
f"Failed to find {binary_name} in extracted runtime archive {archive_path.name}."
f"Unsupported release archive format: {archive_path.name}"
)
return candidates[0]
return extract_dir
def _stage_runtime_package(
sdk_python_dir: Path,
runtime_version: str,
runtime_binary: Path,
runtime_bundle_dir: Path,
staging_dir: Path,
) -> Path:
script_module = _load_update_script_module(sdk_python_dir)
return script_module.stage_python_runtime_package( # type: ignore[no-any-return]
staging_dir,
runtime_version,
runtime_binary.resolve(),
runtime_bundle_dir.resolve(),
)

View File

@@ -54,26 +54,28 @@ This avoids duplicate ways to do the same operation and keeps behavior explicit.
Common causes:
- published runtime package (`codex-cli-bin`) is not installed
- published runtime package (`openai-codex-cli-bin`) is not installed
- local `codex_bin` override points to a missing file
- local auth/session is missing
- 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 `codex-cli-bin` as
platform wheels only; do not publish an sdist:
platform with the same pinned runtime version. `generate-types` first asks that
pinned runtime to emit the app-server JSON schema, then converts the emitted
schema to Python. 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 \
/tmp/codex-python-release/openai-codex \
--runtime-version 1.2.3
python scripts/update_sdk_artifacts.py \
stage-runtime \
/tmp/codex-python-release/codex-cli-bin \
/path/to/codex \
/tmp/codex-python-release/openai-codex-cli-bin \
/path/to/runtime-bundle-dir \
--runtime-version 1.2.3
```

View File

@@ -16,7 +16,7 @@ python -m pip install -e .
Requirements:
- Python `>=3.10`
- installed `codex-cli-bin` runtime package, or an explicit `codex_bin` override
- installed `openai-codex-cli-bin` runtime package, or an explicit `codex_bin` override
- local Codex auth/session configured
## 2) Run your first turn (sync)

View File

@@ -23,12 +23,12 @@ python -m pip install -e .
When running examples from this repo checkout, the SDK source uses the local
tree and does not bundle a runtime binary. The helper in `examples/_bootstrap.py`
uses the installed `codex-cli-bin` runtime package.
uses the installed `openai-codex-cli-bin` runtime package.
If the pinned `codex-cli-bin` runtime is not already installed, the bootstrap
If the pinned `openai-codex-cli-bin` runtime is not already installed, the bootstrap
will download the matching GitHub release artifact, stage a temporary local
`codex-cli-bin` package, install it into your active interpreter, and clean up
the temporary files afterward.
`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`
@@ -43,8 +43,8 @@ python examples/<example-folder>/async.py
The examples bootstrap local imports from `sdk/python/src` automatically, so no
SDK wheel install is required. You only need the Python dependencies for your
active interpreter and an installed `codex-cli-bin` runtime package (either
already present or automatically provisioned by the bootstrap).
active interpreter and an installed `openai-codex-cli-bin` runtime package
(either already present or automatically provisioned by the bootstrap).
## Recommended first run

View File

@@ -3,13 +3,13 @@ requires = ["hatchling>=1.24.0"]
build-backend = "hatchling.build"
[project]
name = "codex-app-server-sdk"
name = "openai-codex"
version = "0.2.0"
description = "Python SDK for Codex app-server v2"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "Apache-2.0" }
authors = [{ name = "OpenClaw Assistant" }]
authors = [{ name = "OpenAI" }]
keywords = ["codex", "json-rpc", "sdk", "llm", "app-server"]
classifiers = [
"Development Status :: 4 - Beta",

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import argparse
import importlib
import importlib.util
import json
import platform
import re
@@ -17,6 +18,9 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Sequence, get_args, get_origin
SDK_PKG_NAME = "openai-codex"
RUNTIME_PKG_NAME = "openai-codex-cli-bin"
def repo_root() -> Path:
return Path(__file__).resolve().parents[3]
@@ -30,31 +34,44 @@ def python_runtime_root() -> Path:
return repo_root() / "sdk" / "python-runtime"
def schema_bundle_path() -> Path:
return (
repo_root()
/ "codex-rs"
/ "app-server-protocol"
/ "schema"
/ "json"
/ "codex_app_server_protocol.v2.schemas.json"
)
def schema_bundle_path(schema_dir: Path | None = None) -> Path:
return schema_root_dir(schema_dir) / "codex_app_server_protocol.v2.schemas.json"
def schema_root_dir() -> Path:
def schema_root_dir(schema_dir: Path | None = None) -> Path:
if schema_dir is not None:
return schema_dir
return repo_root() / "codex-rs" / "app-server-protocol" / "schema" / "json"
def _is_windows() -> bool:
return platform.system().lower().startswith("win")
def runtime_setup_path() -> Path:
return sdk_root() / "_runtime_setup.py"
def runtime_binary_name() -> str:
return "codex.exe" if _is_windows() else "codex"
def _is_windows(system_name: str | None = None) -> bool:
return (system_name or platform.system()).lower().startswith("win")
def runtime_binary_name(system_name: str | None = None) -> str:
return "codex.exe" if _is_windows(system_name) else "codex"
def runtime_file_names(system_name: str | None = None) -> tuple[str, ...]:
if _is_windows(system_name):
return (
"codex.exe",
"codex-command-runner.exe",
"codex-windows-sandbox-setup.exe",
)
return ("codex",)
def staged_runtime_bin_dir(root: Path) -> Path:
return root / "src" / "codex_cli_bin" / "bin"
def staged_runtime_bin_path(root: Path) -> Path:
return root / "src" / "codex_cli_bin" / "bin" / runtime_binary_name()
return staged_runtime_bin_dir(root) / runtime_binary_name()
def run(cmd: list[str], cwd: Path) -> None:
@@ -110,6 +127,39 @@ def _rewrite_project_version(pyproject_text: str, version: str) -> str:
return updated
def _rewrite_project_name(pyproject_text: str, name: str) -> str:
updated, count = re.subn(
r'^name = "[^"]+"$',
f'name = "{name}"',
pyproject_text,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise RuntimeError("Could not rewrite project name in pyproject.toml")
return updated
def normalize_python_package_version(version: str) -> str:
stripped = version.strip()
if re.fullmatch(r"\d+\.\d+\.\d+(?:a\d+|b\d+|\.dev\d+)?", stripped):
return stripped
prerelease_match = re.fullmatch(
r"(\d+\.\d+\.\d+)-(alpha|beta)\.(\d+)",
stripped,
)
if prerelease_match is not None:
base, prerelease, number = prerelease_match.groups()
marker = "a" if prerelease == "alpha" else "b"
return f"{base}{marker}{number}"
raise RuntimeError(
"Unsupported Python package version. Expected x.y.z, x.y.z-alpha.n, "
f"x.y.z-beta.n, or an already-normalized PEP 440 version; got {version!r}."
)
def _rewrite_sdk_runtime_dependency(pyproject_text: str, runtime_version: str) -> str:
match = re.search(r"^dependencies = \[(.*?)\]$", pyproject_text, flags=re.MULTILINE)
if match is None:
@@ -118,15 +168,46 @@ 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.append(f'"codex-cli-bin=={runtime_version}"')
raw_items = [
item
for item in raw_items
if "codex-cli-bin" not in item and RUNTIME_PKG_NAME not in item
]
raw_items.append(f'"{RUNTIME_PKG_NAME}=={runtime_version}"')
replacement = "dependencies = [\n " + ",\n ".join(raw_items) + ",\n]"
return pyproject_text[: match.start()] + replacement + pyproject_text[match.end() :]
def _rewrite_sdk_init_version(init_text: str, sdk_version: str) -> str:
updated, count = re.subn(
r'^__version__ = "[^"]+"$',
f'__version__ = "{sdk_version}"',
init_text,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise RuntimeError("Could not rewrite SDK __version__")
return updated
def _rewrite_sdk_client_version(client_text: str, sdk_version: str) -> str:
updated, count = re.subn(
r'client_version: str = "[^"]+"',
f'client_version: str = "{sdk_version}"',
client_text,
count=1,
)
if count != 1:
raise RuntimeError("Could not rewrite AppServerConfig.client_version")
return updated
def stage_python_sdk_package(
staging_dir: Path, sdk_version: str, runtime_version: str
) -> Path:
sdk_version = normalize_python_package_version(sdk_version)
runtime_version = normalize_python_package_version(runtime_version)
_copy_package_tree(sdk_root(), staging_dir)
sdk_bin_dir = staging_dir / "src" / "codex_app_server" / "bin"
if sdk_bin_dir.exists():
@@ -134,30 +215,148 @@ def stage_python_sdk_package(
pyproject_path = staging_dir / "pyproject.toml"
pyproject_text = pyproject_path.read_text()
pyproject_text = _rewrite_project_name(pyproject_text, SDK_PKG_NAME)
pyproject_text = _rewrite_project_version(pyproject_text, sdk_version)
pyproject_text = _rewrite_sdk_runtime_dependency(pyproject_text, runtime_version)
pyproject_path.write_text(pyproject_text)
init_path = staging_dir / "src" / "codex_app_server" / "__init__.py"
init_path.write_text(_rewrite_sdk_init_version(init_path.read_text(), sdk_version))
client_path = staging_dir / "src" / "codex_app_server" / "client.py"
client_path.write_text(
_rewrite_sdk_client_version(client_path.read_text(), sdk_version)
)
return staging_dir
def stage_python_runtime_package(
staging_dir: Path, runtime_version: str, binary_path: Path
staging_dir: Path, runtime_version: str, runtime_bundle_dir: Path
) -> Path:
runtime_version = normalize_python_package_version(runtime_version)
_copy_package_tree(python_runtime_root(), staging_dir)
pyproject_path = staging_dir / "pyproject.toml"
pyproject_path.write_text(
_rewrite_project_version(pyproject_path.read_text(), runtime_version)
pyproject_text = _rewrite_project_name(pyproject_path.read_text(), RUNTIME_PKG_NAME)
pyproject_text = _rewrite_project_version(pyproject_text, runtime_version)
pyproject_path.write_text(pyproject_text)
out_bin_dir = staged_runtime_bin_dir(staging_dir)
out_bin_dir.mkdir(parents=True, exist_ok=True)
for runtime_file_name in runtime_file_names():
source = _find_runtime_bundle_file(runtime_bundle_dir, runtime_file_name)
out_path = out_bin_dir / runtime_file_name
shutil.copy2(source, out_path)
if not _is_windows():
out_path.chmod(
out_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
return staging_dir
def _find_runtime_bundle_file(runtime_bundle_dir: Path, destination_name: str) -> Path:
if not runtime_bundle_dir.is_dir():
raise RuntimeError(f"Runtime bundle directory not found: {runtime_bundle_dir}")
exact = runtime_bundle_dir / destination_name
if exact.is_file():
return exact
patterns = {
"codex": re.compile(r"^codex-(?!responses-api-proxy)[^.]+$"),
"codex.exe": re.compile(
r"^codex-(?!command-runner|windows-sandbox-setup|responses-api-proxy).+\.exe$"
),
"codex-command-runner.exe": re.compile(r"^codex-command-runner-.+\.exe$"),
"codex-windows-sandbox-setup.exe": re.compile(
r"^codex-windows-sandbox-setup-.+\.exe$"
),
}
pattern = patterns.get(destination_name)
candidates = (
[]
if pattern is None
else sorted(
path
for path in runtime_bundle_dir.iterdir()
if path.is_file() and pattern.fullmatch(path.name)
)
)
if len(candidates) == 1:
return candidates[0]
if len(candidates) > 1:
candidate_names = ", ".join(path.name for path in candidates)
raise RuntimeError(
f"Runtime bundle has multiple candidates for {destination_name}: "
f"{candidate_names}"
)
raise RuntimeError(
f"Runtime bundle {runtime_bundle_dir} is missing required file "
f"{destination_name}"
)
out_bin = staged_runtime_bin_path(staging_dir)
out_bin.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(binary_path, out_bin)
if not _is_windows():
out_bin.chmod(
out_bin.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
return staging_dir
def _load_runtime_setup_module() -> Any:
spec = importlib.util.spec_from_file_location(
"_codex_python_runtime_setup", runtime_setup_path()
)
if spec is None or spec.loader is None:
raise RuntimeError(f"Failed to load {runtime_setup_path()}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _bundled_codex_path_from_install_target(install_target: Path) -> Path:
package_init = install_target / "codex_cli_bin" / "__init__.py"
spec = importlib.util.spec_from_file_location(
"_codex_cli_bin_for_schema",
package_init,
submodule_search_locations=[str(package_init.parent)],
)
if spec is None or spec.loader is None:
raise RuntimeError(f"Failed to load installed runtime package: {package_init}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module.bundled_codex_path()
def _run_runtime_schema_generator(codex_bin: Path, out_dir: Path) -> None:
run(
[
str(codex_bin),
"app-server",
"generate-json-schema",
"--out",
str(out_dir),
],
cwd=repo_root(),
)
def _generate_json_schema_from_runtime(
out_dir: Path, runtime_version: str | None = None
) -> str:
runtime_setup = _load_runtime_setup_module()
requested_version = runtime_version or runtime_setup.pinned_runtime_version()
with tempfile.TemporaryDirectory(prefix="codex-python-schema-runtime-") as td:
install_target = Path(td) / "runtime-package"
original_pinned_runtime_version = runtime_setup.PINNED_RUNTIME_VERSION
runtime_setup.PINNED_RUNTIME_VERSION = requested_version
try:
runtime_setup.ensure_runtime_package_installed(
sys.executable,
sdk_root(),
install_target,
)
finally:
runtime_setup.PINNED_RUNTIME_VERSION = original_pinned_runtime_version
codex_bin = _bundled_codex_path_from_install_target(install_target)
_run_runtime_schema_generator(codex_bin, out_dir)
return requested_version
def _flatten_string_enum_one_of(definition: dict[str, Any]) -> bool:
@@ -396,8 +595,8 @@ def _annotate_schema(value: Any, base: str | None = None) -> None:
_annotate_schema(child, base)
def _normalized_schema_bundle_text() -> str:
schema = json.loads(schema_bundle_path().read_text())
def _normalized_schema_bundle_text(schema_dir: Path | None = None) -> str:
schema = json.loads(schema_bundle_path(schema_dir).read_text())
definitions = schema.get("definitions", {})
if isinstance(definitions, dict):
for definition in definitions.values():
@@ -409,7 +608,7 @@ def _normalized_schema_bundle_text() -> str:
return json.dumps(schema, indent=2, sort_keys=True) + "\n"
def generate_v2_all() -> None:
def generate_v2_all(schema_dir: Path | None = None) -> None:
out_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py"
out_dir = out_path.parent
old_package_dir = out_dir / "v2_all"
@@ -417,8 +616,8 @@ def generate_v2_all() -> None:
shutil.rmtree(old_package_dir)
out_dir.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as td:
normalized_bundle = Path(td) / schema_bundle_path().name
normalized_bundle.write_text(_normalized_schema_bundle_text())
normalized_bundle = Path(td) / schema_bundle_path(schema_dir).name
normalized_bundle.write_text(_normalized_schema_bundle_text(schema_dir))
run_python_module(
"datamodel_code_generator",
[
@@ -455,9 +654,9 @@ def generate_v2_all() -> None:
_normalize_generated_timestamps(out_path)
def _notification_specs() -> list[tuple[str, str]]:
def _notification_specs(schema_dir: Path | None = None) -> list[tuple[str, str]]:
server_notifications = json.loads(
(schema_root_dir() / "ServerNotification.json").read_text()
(schema_root_dir(schema_dir) / "ServerNotification.json").read_text()
)
one_of = server_notifications.get("oneOf", [])
generated_source = (
@@ -494,7 +693,7 @@ def _notification_specs() -> list[tuple[str, str]]:
return specs
def generate_notification_registry() -> None:
def generate_notification_registry(schema_dir: Path | None = None) -> None:
out = (
sdk_root()
/ "src"
@@ -502,7 +701,7 @@ def generate_notification_registry() -> None:
/ "generated"
/ "notification_registry.py"
)
specs = _notification_specs()
specs = _notification_specs(schema_dir)
class_names = sorted({class_name for _, class_name in specs})
lines = [
@@ -557,7 +756,7 @@ class PublicFieldSpec:
@dataclass(frozen=True)
class CliOps:
generate_types: Callable[[], None]
generate_types: Callable[[str | None], None]
stage_python_sdk_package: Callable[[Path, str, str], Path]
stage_python_runtime_package: Callable[[Path, str, Path], Path]
current_sdk_version: Callable[[], str]
@@ -901,20 +1100,30 @@ def generate_public_api_flat_methods() -> None:
public_api_path.write_text(source)
def generate_types() -> None:
# v2_all is the authoritative generated surface.
generate_v2_all()
generate_notification_registry()
generate_public_api_flat_methods()
def generate_types(runtime_version: str | None = None) -> None:
with tempfile.TemporaryDirectory(prefix="codex-python-schema-") as schema_root:
schema_dir = Path(schema_root)
_generate_json_schema_from_runtime(schema_dir, runtime_version)
# v2_all is the authoritative generated surface.
generate_v2_all(schema_dir)
generate_notification_registry(schema_dir)
generate_public_api_flat_methods()
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Single SDK maintenance entrypoint")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser(
generate_types_parser = subparsers.add_parser(
"generate-types", help="Regenerate Python protocol-derived types"
)
generate_types_parser.add_argument(
"--runtime-version",
help=(
"Runtime release version used to emit app-server JSON schema "
"(defaults to sdk/python/_runtime_setup.py's pinned version)"
),
)
stage_sdk_parser = subparsers.add_parser(
"stage-sdk",
@@ -928,7 +1137,7 @@ def build_parser() -> argparse.ArgumentParser:
stage_sdk_parser.add_argument(
"--runtime-version",
required=True,
help="Pinned codex-cli-bin version for the staged SDK package",
help=f"Pinned {RUNTIME_PKG_NAME} version for the staged SDK package",
)
stage_sdk_parser.add_argument(
"--sdk-version",
@@ -945,9 +1154,9 @@ def build_parser() -> argparse.ArgumentParser:
help="Output directory for the staged runtime package",
)
stage_runtime_parser.add_argument(
"runtime_binary",
"runtime_bundle_dir",
type=Path,
help="Path to the codex binary to package for this platform",
help="Directory containing the Codex runtime files to package for this platform",
)
stage_runtime_parser.add_argument(
"--runtime-version",
@@ -972,9 +1181,9 @@ def default_cli_ops() -> CliOps:
def run_command(args: argparse.Namespace, ops: CliOps) -> None:
if args.command == "generate-types":
ops.generate_types()
ops.generate_types(args.runtime_version)
elif args.command == "stage-sdk":
ops.generate_types()
ops.generate_types(None)
ops.stage_python_sdk_package(
args.staging_dir,
args.sdk_version or ops.current_sdk_version(),
@@ -984,7 +1193,7 @@ def run_command(args: argparse.Namespace, ops: CliOps) -> None:
ops.stage_python_runtime_package(
args.staging_dir,
args.runtime_version,
args.runtime_binary.resolve(),
args.runtime_bundle_dir.resolve(),
)

View File

@@ -47,7 +47,8 @@ from .retry import retry_on_overload
ModelT = TypeVar("ModelT", bound=BaseModel)
ApprovalHandler = Callable[[str, JsonObject | None], JsonObject]
RUNTIME_PKG_NAME = "codex-cli-bin"
SDK_PKG_NAME = "openai-codex"
RUNTIME_PKG_NAME = "openai-codex-cli-bin"
def _params_dict(

View File

@@ -17,7 +17,6 @@ from .v2_all import ContextCompactedNotification
from .v2_all import DeprecationNoticeNotification
from .v2_all import ErrorNotification
from .v2_all import FileChangeOutputDeltaNotification
from .v2_all import FsChangedNotification
from .v2_all import FuzzyFileSearchSessionCompletedNotification
from .v2_all import FuzzyFileSearchSessionUpdatedNotification
from .v2_all import HookCompletedNotification
@@ -27,7 +26,6 @@ from .v2_all import ItemGuardianApprovalReviewCompletedNotification
from .v2_all import ItemGuardianApprovalReviewStartedNotification
from .v2_all import ItemStartedNotification
from .v2_all import McpServerOauthLoginCompletedNotification
from .v2_all import McpServerStatusUpdatedNotification
from .v2_all import McpToolCallProgressNotification
from .v2_all import ModelReroutedNotification
from .v2_all import PlanDeltaNotification
@@ -45,7 +43,6 @@ from .v2_all import ThreadRealtimeErrorNotification
from .v2_all import ThreadRealtimeItemAddedNotification
from .v2_all import ThreadRealtimeOutputAudioDeltaNotification
from .v2_all import ThreadRealtimeStartedNotification
from .v2_all import ThreadRealtimeTranscriptUpdatedNotification
from .v2_all import ThreadStartedNotification
from .v2_all import ThreadStatusChangedNotification
from .v2_all import ThreadTokenUsageUpdatedNotification
@@ -66,7 +63,6 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
"configWarning": ConfigWarningNotification,
"deprecationNotice": DeprecationNoticeNotification,
"error": ErrorNotification,
"fs/changed": FsChangedNotification,
"fuzzyFileSearch/sessionCompleted": FuzzyFileSearchSessionCompletedNotification,
"fuzzyFileSearch/sessionUpdated": FuzzyFileSearchSessionUpdatedNotification,
"hook/completed": HookCompletedNotification,
@@ -85,7 +81,6 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
"item/reasoning/textDelta": ReasoningTextDeltaNotification,
"item/started": ItemStartedNotification,
"mcpServer/oauthLogin/completed": McpServerOauthLoginCompletedNotification,
"mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification,
"model/rerouted": ModelReroutedNotification,
"serverRequest/resolved": ServerRequestResolvedNotification,
"skills/changed": SkillsChangedNotification,
@@ -98,7 +93,6 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
"thread/realtime/itemAdded": ThreadRealtimeItemAddedNotification,
"thread/realtime/outputAudio/delta": ThreadRealtimeOutputAudioDeltaNotification,
"thread/realtime/started": ThreadRealtimeStartedNotification,
"thread/realtime/transcriptUpdated": ThreadRealtimeTranscriptUpdatedNotification,
"thread/started": ThreadStartedNotification,
"thread/status/changed": ThreadStatusChangedNotification,
"thread/tokenUsage/updated": ThreadTokenUsageUpdatedNotification,

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,22 @@ 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)
return module
def _load_runtime_package_module(package_root: Path):
runtime_init = package_root / "src" / "codex_cli_bin" / "__init__.py"
spec = importlib.util.spec_from_file_location(
"codex_cli_bin_under_test", runtime_init
)
if spec is None or spec.loader is None:
raise AssertionError(f"Failed to load runtime package module: {runtime_init}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
@@ -55,18 +70,18 @@ def test_generate_types_wires_all_generation_steps() -> None:
)
assert generate_types_fn is not None
calls: list[str] = []
for node in generate_types_fn.body:
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
fn = node.value.func
if isinstance(fn, ast.Name):
calls.append(fn.id)
calls = {
node.func.id
for node in ast.walk(generate_types_fn)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
assert calls == [
assert {
"_generate_json_schema_from_runtime",
"generate_v2_all",
"generate_notification_registry",
"generate_public_api_flat_methods",
]
} <= calls
def test_schema_normalization_only_flattens_string_literal_oneofs() -> None:
@@ -168,7 +183,9 @@ def test_examples_readme_matches_pinned_runtime_version() -> None:
)
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] = []
@@ -198,6 +215,14 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() ->
)
hook_source = (ROOT.parent / "python-runtime" / "hatch_build.py").read_text()
hook_tree = ast.parse(hook_source)
platform_tag_assignment = next(
node
for node in hook_tree.body
if isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)
and node.targets[0].id == "PLATFORM_TAG_BY_TARGET"
)
initialize_fn = next(
node
for node in ast.walk(hook_tree)
@@ -235,6 +260,7 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() ->
and isinstance(node.value, ast.Constant)
}
assert pyproject["project"]["name"] == "openai-codex-cli-bin"
assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"] == {
"packages": ["src/codex_cli_bin"],
"include": ["src/codex_cli_bin/bin/**"],
@@ -244,23 +270,51 @@ def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() ->
"hooks": {"custom": {}},
}
assert sdist_guard is not None
assert build_data_assignments == {"pure_python": False, "infer_tag": True}
assert build_data_assignments == {"pure_python": False}
assert ast.literal_eval(platform_tag_assignment.value) == {
"aarch64-apple-darwin": "macosx_11_0_arm64",
"x86_64-apple-darwin": "macosx_10_12_x86_64",
"aarch64-unknown-linux-musl": "musllinux_1_2_aarch64",
"x86_64-unknown-linux-musl": "musllinux_1_2_x86_64",
"aarch64-pc-windows-msvc": "win_arm64",
"x86_64-pc-windows-msvc": "win_amd64",
}
assert "CODEX_PYTHON_RUNTIME_TARGET" in hook_source
assert '"infer_tag"' in hook_source
assert '"tag"' in hook_source
def test_stage_runtime_release_copies_binary_and_sets_version(tmp_path: Path) -> None:
def test_python_release_version_normalization() -> None:
script = _load_update_script_module()
fake_binary = tmp_path / script.runtime_binary_name()
assert script.normalize_python_package_version("1.2.3") == "1.2.3"
assert script.normalize_python_package_version("1.2.3-alpha.4") == "1.2.3a4"
assert script.normalize_python_package_version("1.2.3-beta.5") == "1.2.3b5"
assert script.normalize_python_package_version("1.2.3a4") == "1.2.3a4"
assert script.normalize_python_package_version("0.0.0.dev0") == "0.0.0.dev0"
with pytest.raises(RuntimeError, match="Unsupported Python package version"):
script.normalize_python_package_version("1.2.3-rc.1")
def test_stage_runtime_release_copies_bundle_and_sets_version(tmp_path: Path) -> None:
script = _load_update_script_module()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
fake_binary = bundle_dir / script.runtime_binary_name()
fake_binary.write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
fake_binary,
"1.2.3-alpha.4",
bundle_dir,
)
assert staged == tmp_path / "runtime-stage"
assert script.staged_runtime_bin_path(staged).read_text() == "fake codex\n"
assert 'version = "1.2.3"' in (staged / "pyproject.toml").read_text()
pyproject = (staged / "pyproject.toml").read_text()
assert 'name = "openai-codex-cli-bin"' in pyproject
assert 'version = "1.2.3a4"' in pyproject
def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) -> None:
@@ -270,13 +324,15 @@ def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) ->
old_file.parent.mkdir(parents=True)
old_file.write_text("stale")
fake_binary = tmp_path / script.runtime_binary_name()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
fake_binary = bundle_dir / script.runtime_binary_name()
fake_binary.write_text("fake codex\n")
staged = script.stage_python_runtime_package(
staging_dir,
"1.2.3",
fake_binary,
bundle_dir,
)
assert staged == staging_dir
@@ -284,13 +340,132 @@ def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) ->
assert script.staged_runtime_bin_path(staged).read_text() == "fake codex\n"
def test_stage_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None:
def test_stage_runtime_release_normalizes_target_suffixed_names(
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")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex-x86_64-unknown-linux-musl").write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
assert (staged / "src" / "codex_cli_bin" / "bin" / "codex").read_text() == (
"fake codex\n"
)
def test_stage_runtime_release_requires_complete_windows_bundle(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script.platform, "system", lambda: "Windows")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex-x86_64-pc-windows-msvc.exe").write_text("codex\n")
(bundle_dir / "codex-command-runner-x86_64-pc-windows-msvc.exe").write_text(
"runner\n"
)
(bundle_dir / "codex-windows-sandbox-setup-x86_64-pc-windows-msvc.exe").write_text(
"setup\n"
)
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
bin_dir = staged / "src" / "codex_cli_bin" / "bin"
assert (bin_dir / "codex.exe").read_text() == "codex\n"
assert (bin_dir / "codex-command-runner.exe").read_text() == "runner\n"
assert (bin_dir / "codex-windows-sandbox-setup.exe").read_text() == "setup\n"
def test_stage_runtime_release_fails_for_missing_required_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
script = _load_update_script_module()
monkeypatch.setattr(script.platform, "system", lambda: "Windows")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex.exe").write_text("codex\n")
with pytest.raises(RuntimeError, match="codex-command-runner.exe"):
script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
def test_runtime_package_helpers_return_packaged_paths(tmp_path: Path) -> None:
script = _load_update_script_module()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex").write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
runtime_module = _load_runtime_package_module(staged)
assert runtime_module.PACKAGE_NAME == "openai-codex-cli-bin"
assert runtime_module.bundled_bin_dir() == staged / "src" / "codex_cli_bin" / "bin"
assert runtime_module.bundled_runtime_files() == (
staged / "src" / "codex_cli_bin" / "bin" / "codex",
)
assert runtime_module.bundled_codex_path() == (
staged / "src" / "codex_cli_bin" / "bin" / "codex"
)
def test_runtime_package_helpers_report_missing_binary(tmp_path: Path) -> None:
script = _load_update_script_module()
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / "codex").write_text("fake codex\n")
staged = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"1.2.3",
bundle_dir,
)
(staged / "src" / "codex_cli_bin" / "bin" / "codex").unlink()
runtime_module = _load_runtime_package_module(staged)
with pytest.raises(FileNotFoundError, match="openai-codex-cli-bin"):
runtime_module.bundled_codex_path()
def test_stage_sdk_release_injects_exact_runtime_pin_and_versions(
tmp_path: Path,
) -> None:
script = _load_update_script_module()
staged = script.stage_python_sdk_package(
tmp_path / "sdk-stage",
"0.2.1-beta.2",
"1.2.3-alpha.4",
)
pyproject = (staged / "pyproject.toml").read_text()
assert 'version = "0.2.1"' in pyproject
assert '"codex-cli-bin==1.2.3"' in pyproject
assert 'name = "openai-codex"' in pyproject
assert 'version = "0.2.1b2"' in pyproject
assert '"openai-codex-cli-bin==1.2.3a4"' in pyproject
assert (
'__version__ = "0.2.1b2"'
in (staged / "src" / "codex_app_server" / "__init__.py").read_text()
)
assert (
'client_version: str = "0.2.1b2"'
in (staged / "src" / "codex_app_server" / "client.py").read_text()
)
assert not any((staged / "src" / "codex_app_server").glob("bin/**"))
@@ -319,8 +494,8 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
]
)
def fake_generate_types() -> None:
calls.append("generate_types")
def fake_generate_types(runtime_version: str | None) -> None:
calls.append(f"generate_types:{runtime_version}")
def fake_stage_sdk_package(
_staging_dir: Path, _sdk_version: str, _runtime_version: str
@@ -329,7 +504,7 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
return tmp_path / "sdk-stage"
def fake_stage_runtime_package(
_staging_dir: Path, _runtime_version: str, _runtime_binary: Path
_staging_dir: Path, _runtime_version: str, _runtime_bundle_dir: Path
) -> Path:
raise AssertionError("runtime staging should not run for stage-sdk")
@@ -345,25 +520,91 @@ 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:None", "stage_sdk"]
def test_generate_types_accepts_runtime_version_override() -> None:
script = _load_update_script_module()
calls: list[str] = []
args = script.parse_args(
[
"generate-types",
"--runtime-version",
"1.2.3-alpha.4",
]
)
def fake_generate_types(runtime_version: str | None) -> None:
calls.append(f"generate_types:{runtime_version}")
def fake_stage_sdk_package(
_staging_dir: Path, _sdk_version: str, _runtime_version: str
) -> Path:
raise AssertionError("sdk staging should not run for generate-types")
def fake_stage_runtime_package(
_staging_dir: Path, _runtime_version: str, _runtime_bundle_dir: Path
) -> Path:
raise AssertionError("runtime staging should not run for generate-types")
def fake_current_sdk_version() -> str:
return "0.2.0"
ops = script.CliOps(
generate_types=fake_generate_types,
stage_python_sdk_package=fake_stage_sdk_package,
stage_python_runtime_package=fake_stage_runtime_package,
current_sdk_version=fake_current_sdk_version,
)
script.run_command(args, ops)
assert calls == ["generate_types:1.2.3-alpha.4"]
def test_runtime_schema_generator_uses_app_server_json_schema_command(
tmp_path: Path,
) -> None:
script = _load_update_script_module()
codex_bin = tmp_path / "codex"
out_dir = tmp_path / "schema"
args_path = tmp_path / "args.txt"
codex_bin.write_text(
"#!/usr/bin/env sh\n"
f'printf \'%s\\n\' "$@" > "{args_path}"\n'
'mkdir -p "$4"\n'
"printf '{}' > \"$4/codex_app_server_protocol.v2.schemas.json\"\n"
"printf '{}' > \"$4/ServerNotification.json\"\n"
)
codex_bin.chmod(0o755)
script._run_runtime_schema_generator(codex_bin, out_dir)
assert args_path.read_text().splitlines() == [
"app-server",
"generate-json-schema",
"--out",
str(out_dir),
]
def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) -> None:
script = _load_update_script_module()
fake_binary = tmp_path / script.runtime_binary_name()
fake_binary.write_text("fake codex\n")
bundle_dir = tmp_path / "bundle"
bundle_dir.mkdir()
(bundle_dir / script.runtime_binary_name()).write_text("fake codex\n")
calls: list[str] = []
args = script.parse_args(
[
"stage-runtime",
str(tmp_path / "runtime-stage"),
str(fake_binary),
str(bundle_dir),
"--runtime-version",
"1.2.3",
]
)
def fake_generate_types() -> None:
def fake_generate_types(_runtime_version: str | None) -> None:
calls.append("generate_types")
def fake_stage_sdk_package(
@@ -372,7 +613,7 @@ def test_stage_runtime_stages_binary_without_type_generation(tmp_path: Path) ->
raise AssertionError("sdk staging should not run for stage-runtime")
def fake_stage_runtime_package(
_staging_dir: Path, _runtime_version: str, _runtime_binary: Path
_staging_dir: Path, _runtime_version: str, _runtime_bundle_dir: Path
) -> Path:
calls.append("stage_runtime")
return tmp_path / "runtime-stage"