sdk: launch packaged Codex runtimes

## Why

The Python and TypeScript SDKs launch the native Codex runtime directly, so they need to understand the same package layout that the release artifacts now use. Without this, SDK runtime wheels would keep carrying only a loose `bin/codex` plus ad hoc helper binaries, the TypeScript SDK would keep resolving the legacy optional-dependency layout, and bundled `codex-path` tools such as `rg` would not be prepended when the SDK starts Codex.

## What Changed

- Stage `openai-codex-cli-bin` from a Codex package directory, including `codex-package.json`, `bin/`, `codex-resources/`, and `codex-path/`.
- Make `stage-runtime` package-dir-only; legacy loose-binary wrapping now happens in `_runtime_setup.py` before calling the staging script.
- Expose Python runtime helpers for the bundled package directory and `codex-path`, and prepend that path when `openai_codex` launches the installed runtime without duplicating Windows `Path`/`PATH` keys.
- Teach the TypeScript SDK to resolve package-layout optional dependencies while keeping the legacy layout fallback, and preserve the existing Windows path variable casing when prepending `codex-path`.
- Update the Rust release workflows to build Python runtime wheels from the generated Codex package directories instead of loose binaries.

## Test Plan

- `python3 -m py_compile sdk/python/scripts/update_sdk_artifacts.py sdk/python/_runtime_setup.py sdk/python/src/openai_codex/client.py sdk/python-runtime/src/codex_cli_bin/__init__.py`
- `uv run --frozen --project sdk/python --extra dev ruff check sdk/python/scripts/update_sdk_artifacts.py sdk/python/_runtime_setup.py sdk/python/src/openai_codex/client.py sdk/python/tests/test_artifact_workflow_and_binaries.py sdk/python-runtime/src/codex_cli_bin/__init__.py`
- `uv run --frozen --project sdk/python --extra dev pytest sdk/python/tests/test_artifact_workflow_and_binaries.py`
- `pnpm eslint src/exec.ts tests/exec.test.ts`
- `pnpm test --runInBand tests/exec.test.ts`
This commit is contained in:
Michael Bolin
2026-05-20 16:54:50 -07:00
parent 896ee672cc
commit 3e77859d0a
10 changed files with 479 additions and 167 deletions

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import json
import os
import subprocess
@@ -103,6 +101,45 @@ def _installed_codex_path() -> Path:
return bundled_codex_path()
def _installed_codex_path_dirs() -> tuple[Path, ...]:
try:
from codex_cli_bin import bundled_path_dir
except (ImportError, AttributeError):
return ()
path_dir = bundled_path_dir()
return (path_dir,) if path_dir is not None else ()
def _prepend_path_dirs(env: dict[str, str], path_dirs: tuple[Path, ...]) -> None:
if not path_dirs:
return
path_key = _path_env_key(env)
if os.name == "nt":
for key in list(env):
if key.upper() == "PATH" and key != path_key:
env.pop(key)
path_sep = os.pathsep
existing_path = env.get(path_key, "")
path_dir_values = [str(path_dir) for path_dir in path_dirs]
existing_entries = [
entry for entry in existing_path.split(path_sep) if entry and entry not in path_dir_values
]
env[path_key] = path_sep.join([*path_dir_values, *existing_entries])
def _path_env_key(env: dict[str, str]) -> str:
if os.name != "nt":
return "PATH"
matching_keys = [key for key in env if key.upper() == "PATH"]
if "Path" in matching_keys:
return "Path"
return matching_keys[-1] if matching_keys else "PATH"
@dataclass(frozen=True)
class CodexBinResolverOps:
installed_codex_path: Callable[[], Path]
@@ -174,10 +211,13 @@ class AppServerClient:
if self._proc is not None:
return
path_dirs: tuple[Path, ...] = ()
if self.config.launch_args_override is not None:
args = list(self.config.launch_args_override)
else:
codex_bin = _resolve_codex_bin(self.config)
if self.config.codex_bin is None:
path_dirs = _installed_codex_path_dirs()
args = [str(codex_bin)]
for kv in self.config.config_overrides:
args.extend(["--config", kv])
@@ -186,6 +226,7 @@ class AppServerClient:
env = os.environ.copy()
if self.config.env:
env.update(self.config.env)
_prepend_path_dirs(env, path_dirs)
self._proc = subprocess.Popen(
args,