Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ python -m pip install -e .
```

Published SDK builds pin an exact `codex-cli-bin` runtime dependency. For local
repo development, pass `AppServerConfig(codex_bin=...)` to point at a local
build explicitly.
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

Expand All @@ -22,8 +23,9 @@ from codex_app_server import Codex, TextInput

with Codex() as codex:
thread = codex.thread_start(model="gpt-5")
result = thread.turn(TextInput("Say hello in one sentence.")).run()
print(result.text)
completed_turn = thread.turn(TextInput("Say hello in one sentence.")).run()
print(completed_turn.status)
print(completed_turn.id)
```

## Docs map
Expand Down Expand Up @@ -54,7 +56,8 @@ 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
explicit `codex_bin` override instead.
explicit `codex_bin` override for manual SDK usage; the repo examples and
notebook bootstrap the pinned runtime package automatically.

## Maintainer workflow

Expand Down
359 changes: 359 additions & 0 deletions sdk/python/_runtime_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
from __future__ import annotations

import importlib
import importlib.util
import json
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.error
import urllib.request
import zipfile
from pathlib import Path

PACKAGE_NAME = "codex-cli-bin"
PINNED_RUNTIME_VERSION = "0.116.0-alpha.1"
REPO_SLUG = "openai/codex"


class RuntimeSetupError(RuntimeError):
pass


def pinned_runtime_version() -> str:
return PINNED_RUNTIME_VERSION


def ensure_runtime_package_installed(
python_executable: str | Path,
sdk_python_dir: Path,
install_target: Path | None = None,
) -> str:
requested_version = pinned_runtime_version()
installed_version = None
if install_target is None:
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:
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)
staged_runtime_dir = _stage_runtime_package(
sdk_python_dir,
requested_version,
runtime_binary,
temp_root / "runtime-stage",
)
_install_runtime_package(python_executable, staged_runtime_dir, install_target)

if install_target is not None:
return requested_version

if Path(python_executable).resolve() == Path(sys.executable).resolve():
importlib.invalidate_caches()

installed_version = _installed_runtime_version(python_executable)
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."
)
return requested_version


def platform_asset_name() -> str:
system = platform.system().lower()
machine = platform.machine().lower()

if system == "darwin":
if machine in {"arm64", "aarch64"}:
return "codex-aarch64-apple-darwin.tar.gz"
if machine in {"x86_64", "amd64"}:
return "codex-x86_64-apple-darwin.tar.gz"
elif system == "linux":
if machine in {"aarch64", "arm64"}:
return "codex-aarch64-unknown-linux-musl.tar.gz"
if machine in {"x86_64", "amd64"}:
return "codex-x86_64-unknown-linux-musl.tar.gz"
elif system == "windows":
if machine in {"aarch64", "arm64"}:
return "codex-aarch64-pc-windows-msvc.exe.zip"
if machine in {"x86_64", "amd64"}:
return "codex-x86_64-pc-windows-msvc.exe.zip"

raise RuntimeSetupError(
f"Unsupported runtime artifact platform: system={platform.system()!r}, "
f"machine={platform.machine()!r}"
)


def runtime_binary_name() -> str:
return "codex.exe" if platform.system().lower() == "windows" else "codex"


def _installed_runtime_version(python_executable: str | Path) -> str | None:
snippet = (
"import importlib.metadata, json, sys\n"
"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"
"except Exception:\n"
" sys.exit(1)\n"
)
result = subprocess.run(
[str(python_executable), "-c", snippet],
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
return None
return json.loads(result.stdout)["version"]


def _release_metadata(version: str) -> dict[str, object]:
url = f"https://api.github.com/repos/{REPO_SLUG}/releases/tags/rust-v{version}"
token = _github_token()
attempts = [True, False] if token is not None else [False]
last_error: urllib.error.HTTPError | None = None

for include_auth in attempts:
headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "codex-python-runtime-setup",
}
if include_auth and token is not None:
headers["Authorization"] = f"Bearer {token}"

request = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(request) as response:
return json.load(response)
except urllib.error.HTTPError as exc:
last_error = exc
if include_auth and exc.code == 401:
continue
break

assert last_error is not None
raise RuntimeSetupError(
f"Failed to resolve release metadata for rust-v{version} from {REPO_SLUG}: "
f"{last_error.code} {last_error.reason}"
) from last_error


def _download_release_archive(version: str, temp_root: Path) -> Path:
asset_name = platform_asset_name()
archive_path = temp_root / asset_name

browser_download_url = (
f"https://github.com/{REPO_SLUG}/releases/download/rust-v{version}/{asset_name}"
)
request = urllib.request.Request(
browser_download_url,
headers={"User-Agent": "codex-python-runtime-setup"},
)
try:
with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh:
shutil.copyfileobj(response, fh)
return archive_path
except urllib.error.HTTPError:
pass

metadata = _release_metadata(version)
assets = metadata.get("assets")
if not isinstance(assets, list):
raise RuntimeSetupError(f"Release rust-v{version} returned malformed assets metadata.")
asset = next(
(
item
for item in assets
if isinstance(item, dict) and item.get("name") == asset_name
),
None,
)
if asset is None:
raise RuntimeSetupError(
f"Release rust-v{version} does not contain asset {asset_name} for this platform."
)

api_url = asset.get("url")
if not isinstance(api_url, str):
api_url = None

if api_url is not None:
token = _github_token()
if token is not None:
request = urllib.request.Request(
api_url,
headers=_github_api_headers("application/octet-stream"),
)
try:
with urllib.request.urlopen(request) as response, archive_path.open("wb") as fh:
shutil.copyfileobj(response, fh)
return archive_path
except urllib.error.HTTPError:
pass

if shutil.which("gh") is None:
raise RuntimeSetupError(
f"Unable to download {asset_name} for rust-v{version}. "
"Provide GH_TOKEN/GITHUB_TOKEN or install/authenticate GitHub CLI."
)

try:
subprocess.run(
[
"gh",
"release",
"download",
f"rust-v{version}",
"--repo",
REPO_SLUG,
"--pattern",
asset_name,
"--dir",
str(temp_root),
],
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
raise RuntimeSetupError(
f"gh release download failed for rust-v{version} asset {asset_name}.\n"
f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}"
) from exc
return archive_path


def _extract_runtime_binary(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"):
with tarfile.open(archive_path, "r:gz") as tar:
try:
tar.extractall(extract_dir, filter="data")
except TypeError:
tar.extractall(extract_dir)
elif archive_path.suffix == ".zip":
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}."
)
return candidates[0]


def _stage_runtime_package(
sdk_python_dir: Path,
runtime_version: str,
runtime_binary: 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(),
)


def _install_runtime_package(
python_executable: str | Path,
staged_runtime_dir: Path,
install_target: Path | None,
) -> None:
args = [
str(python_executable),
"-m",
"pip",
"install",
"--force-reinstall",
"--no-deps",
]
if install_target is not None:
install_target.mkdir(parents=True, exist_ok=True)
args.extend(["--target", str(install_target)])
args.append(str(staged_runtime_dir))
try:
subprocess.run(
args,
check=True,
text=True,
capture_output=True,
)
except subprocess.CalledProcessError as exc:
raise RuntimeSetupError(
f"Failed to install {PACKAGE_NAME} into {python_executable} from {staged_runtime_dir}.\n"
f"STDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}"
) from exc


def _load_update_script_module(sdk_python_dir: Path):
script_path = sdk_python_dir / "scripts" / "update_sdk_artifacts.py"
spec = importlib.util.spec_from_file_location("update_sdk_artifacts", script_path)
if spec is None or spec.loader is None:
raise RuntimeSetupError(f"Failed to load {script_path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module


def _github_api_headers(accept: str) -> dict[str, str]:
headers = {
"Accept": accept,
"User-Agent": "codex-python-runtime-setup",
}
token = _github_token()
if token is not None:
headers["Authorization"] = f"Bearer {token}"
return headers


def _github_token() -> str | None:
for env_name in ("GH_TOKEN", "GITHUB_TOKEN"):
token = os.environ.get(env_name)
if token:
return token
return None


def _normalized_package_version(version: str) -> str:
return version.strip().replace("-alpha.", "a").replace("-beta.", "b")


__all__ = [
"PACKAGE_NAME",
"PINNED_RUNTIME_VERSION",
"RuntimeSetupError",
"ensure_runtime_package_installed",
"pinned_runtime_version",
"platform_asset_name",
]
Loading
Loading