Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions .github/workflows/rust-release-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,48 @@ jobs:
"$dest/${binary}-${{ matrix.target }}.exe"
done

- name: Build Python runtime wheel
shell: bash
run: |
set -euo pipefail

case "${{ matrix.target }}" in
aarch64-pc-windows-msvc)
platform_tag="win_arm64"
;;
x86_64-pc-windows-msvc)
platform_tag="win_amd64"
;;
*)
echo "No Python runtime wheel platform tag for ${{ matrix.target }}"
exit 1
;;
esac

python -m venv "${RUNNER_TEMP}/python-runtime-build-venv"
"${RUNNER_TEMP}/python-runtime-build-venv/Scripts/python.exe" -m pip install build

stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}"
wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}"
# Keep the helpers next to codex.exe in the runtime wheel so Windows
# sandbox/elevation lookup matches the standalone release zip.
python "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \
stage-runtime \
"$stage_dir" \
"${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex.exe" \
--codex-version "${GITHUB_REF_NAME}" \
--platform-tag "$platform_tag" \
--resource-binary "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex-command-runner.exe" \
--resource-binary "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe"
"${RUNNER_TEMP}/python-runtime-build-venv/Scripts/python.exe" -m build --wheel --outdir "$wheel_dir" "$stage_dir"

- name: Upload Python runtime wheel
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-runtime-wheel-${{ matrix.target }}
path: python-runtime-dist/${{ matrix.target }}/*.whl
if-no-files-found: error

- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2

Expand Down
118 changes: 118 additions & 0 deletions .github/workflows/rust-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,65 @@ jobs:
cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg"
fi

- name: Build Python runtime wheel
if: ${{ matrix.bundle == 'primary' }}
shell: bash
run: |
set -euo pipefail

case "${{ matrix.target }}" in
aarch64-apple-darwin)
platform_tag="macosx_11_0_arm64"
;;
x86_64-apple-darwin)
platform_tag="macosx_10_9_x86_64"
;;
aarch64-unknown-linux-musl)
platform_tag="musllinux_1_1_aarch64"
;;
x86_64-unknown-linux-musl)
platform_tag="musllinux_1_1_x86_64"
;;
*)
echo "No Python runtime wheel platform tag for ${{ matrix.target }}"
exit 1
;;
esac

python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv"
# Do not install into the runner's system Python; macOS runners mark
# the Homebrew Python as externally managed under PEP 668.
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build

stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}"
wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}"
stage_runtime_args=(
"${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py"
stage-runtime
"$stage_dir"
"${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex"
--codex-version "${GITHUB_REF_NAME}"
--platform-tag "$platform_tag"
)
if [[ "${{ matrix.target }}" == *linux* ]]; then
# Keep bwrap in the runtime wheel so Linux sandbox fallback behavior
# matches the standalone release bundle on hosts without system bwrap.
stage_runtime_args+=(
--resource-binary
"${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap"
)
fi
python3 "${stage_runtime_args[@]}"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir"

- name: Upload Python runtime wheel
if: ${{ matrix.bundle == 'primary' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-runtime-wheel-${{ matrix.target }}
path: python-runtime-dist/${{ matrix.target }}/*.whl
if-no-files-found: error

- name: Compress artifacts
shell: bash
run: |
Expand Down Expand Up @@ -478,6 +537,7 @@ jobs:
tag: ${{ github.ref_name }}
should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }}
npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }}
should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }}

steps:
- name: Checkout repository
Expand Down Expand Up @@ -554,6 +614,22 @@ jobs:
echo "npm_tag=" >> "$GITHUB_OUTPUT"
fi

- name: Determine Python runtime publish settings
id: python_runtime_publish_settings
env:
VERSION: ${{ steps.release_name.outputs.name }}
run: |
set -euo pipefail
version="${VERSION}"

if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "should_publish=true" >> "$GITHUB_OUTPUT"
elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
echo "should_publish=true" >> "$GITHUB_OUTPUT"
else
echo "should_publish=false" >> "$GITHUB_OUTPUT"
fi

- name: Setup pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
Expand Down Expand Up @@ -787,6 +863,48 @@ jobs:
exit "${publish_status}"
done

# Publish the platform-specific Python runtime wheels using PyPI trusted publishing.
# PyPI project configuration must trust this workflow and job. Keep this
# non-blocking while the Python runtime publishing path is new; failures still
# need release follow-up, but should not invalidate the Rust release itself.
publish-python-runtime:
# Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes.
if: ${{ needs.release.outputs.should_publish_python_runtime == 'true' }}
name: publish-python-runtime
needs: release
runs-on: ubuntu-latest
continue-on-error: true
environment: pypi
permissions:
id-token: write # Required for PyPI trusted publishing.
contents: read

steps:
- name: Download Python runtime wheels from release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
RELEASE_VERSION: ${{ needs.release.outputs.version }}
run: |
set -euo pipefail
python_version="$RELEASE_VERSION"
python_version="${python_version/-alpha./a}"
python_version="${python_version/-beta./b}"
python_version="${python_version/-rc./rc}"

mkdir -p dist/python-runtime
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "openai_codex_cli_bin-${python_version}-*.whl" \
--dir dist/python-runtime
ls -lh dist/python-runtime

- name: Publish Python runtime wheels to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: dist/python-runtime
skip-existing: true

winget:
name: winget
needs: release
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ This supports the CI release flow:
- run `generate-types` before packaging
- 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
- build and publish `openai-codex-cli-bin` as platform wheels only through PyPI trusted publishing; do not publish an sdist

## Compatibility and versioning

Expand Down
39 changes: 29 additions & 10 deletions sdk/python/_runtime_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,22 @@ class RuntimeSetupError(RuntimeError):


def pinned_runtime_version() -> str:
source_version = _source_tree_project_version()
if source_version is not None:
return _normalized_package_version(source_version)
"""Return the exact runtime version pinned by the SDK package dependency."""
source_pin = _source_tree_runtime_dependency_version()
if source_pin is not None:
return _normalized_package_version(source_pin)

try:
return _normalized_package_version(importlib.metadata.version(SDK_PACKAGE_NAME))
installed_pin = _installed_sdk_runtime_dependency_version()
except importlib.metadata.PackageNotFoundError as exc:
raise RuntimeSetupError(
f"Unable to resolve {SDK_PACKAGE_NAME} version for runtime pinning."
f"Unable to resolve {SDK_PACKAGE_NAME} metadata for runtime pinning."
) from exc
if installed_pin is None:
raise RuntimeSetupError(
f"Unable to resolve {PACKAGE_NAME} dependency pin from {SDK_PACKAGE_NAME}."
)
return _normalized_package_version(installed_pin)


def ensure_runtime_package_installed(
Expand Down Expand Up @@ -399,20 +405,33 @@ def _release_tag(version: str) -> str:
return f"rust-v{_codex_release_version(version)}"


def _source_tree_project_version() -> str | None:
def _source_tree_runtime_dependency_version() -> str | None:
"""Read the runtime dependency pin when the SDK is running from a checkout."""
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"),
)
match = re.search(_runtime_dependency_pin_pattern(), pyproject_path.read_text())
if match is None:
return None
return match.group(1)


def _installed_sdk_runtime_dependency_version() -> str | None:
"""Read the runtime dependency pin from installed package metadata."""
requirements = importlib.metadata.requires(SDK_PACKAGE_NAME) or []
for requirement in requirements:
match = re.search(_runtime_dependency_pin_pattern(), requirement)
if match is not None:
return match.group(1)
return None


def _runtime_dependency_pin_pattern() -> str:
"""Match the exact runtime dependency pin in TOML and wheel metadata."""
return rf'{re.escape(PACKAGE_NAME)}\s*==\s*"?([^",;\s]+)"?'


__all__ = [
"PACKAGE_NAME",
"SDK_PACKAGE_NAME",
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

The pinned runtime version comes from the SDK package version.
The pinned runtime version comes from the SDK package dependency.

## Run examples

Expand Down
6 changes: 4 additions & 2 deletions sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "openai-codex-app-server-sdk"
version = "0.116.0a1"
version = "0.131.0a4"
description = "Python SDK for Codex app-server v2"
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = ["pydantic>=2.12"]
dependencies = ["pydantic>=2.12", "openai-codex-cli-bin==0.131.0a4"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Publish Linux runtime wheels pip can install

Pinning this dependency makes every SDK install resolve openai-codex-cli-bin, but the new lockfile only has Linux wheels tagged musllinux_1_1_*. Standard glibc Linux interpreters advertise manylinux/linux tags, not musllinux, so pip/uv install openai-codex-app-server-sdk will fail on the common Linux target until manylinux/linux-compatible runtime wheels are published or the dependency is conditional.

Useful? React with 👍 / 👎.


[project.urls]
Homepage = "https://github.com/openai/codex"
Expand Down Expand Up @@ -63,8 +63,10 @@ testpaths = ["tests"]

[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { openai-codex-cli-bin = "2026-05-10T00:00:00Z" }
index-strategy = "first-index"

[tool.uv.pip]
exclude-newer = "7 days"
exclude-newer-package = { openai-codex-cli-bin = "2026-05-10T00:00:00Z" }
index-strategy = "first-index"
5 changes: 5 additions & 0 deletions sdk/python/scripts/update_sdk_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def staged_runtime_bin_path(root: Path) -> Path:


def staged_runtime_resource_path(root: Path, resource: Path) -> Path:
"""Stage runtime helper binaries beside the main bundled Codex binary."""
# Runtime wheels include the whole bin/ directory, so helper executables
# should be staged beside the main Codex binary instead of changing the
# package template for each platform.
Expand Down Expand Up @@ -588,6 +589,7 @@ def _notification_specs() -> list[tuple[str, str]]:
def _notification_turn_id_specs(
specs: list[tuple[str, str]],
) -> tuple[list[str], list[str]]:
"""Classify generated notification payloads by where the turn id lives."""
server_notifications = json.loads(
(schema_root_dir() / "ServerNotification.json").read_text()
)
Expand Down Expand Up @@ -615,6 +617,7 @@ def _notification_turn_id_specs(


def _type_tuple_source(class_names: list[str]) -> str:
"""Render a generated tuple literal for notification payload classes."""
if not class_names:
return "()"
if len(class_names) == 1:
Expand All @@ -623,6 +626,7 @@ def _type_tuple_source(class_names: list[str]) -> str:


def generate_notification_registry() -> None:
"""Regenerate notification models and routing metadata from generated schemas."""
out = (
sdk_root()
/ "src"
Expand Down Expand Up @@ -666,6 +670,7 @@ def generate_notification_registry() -> None:
"",
"",
"def notification_turn_id(payload: BaseModel) -> str | None:",
' """Return the turn id carried by generated notification payload metadata."""',
" if isinstance(payload, DIRECT_TURN_ID_NOTIFICATION_TYPES):",
" return payload.turn_id if isinstance(payload.turn_id, str) else None",
" if isinstance(payload, NESTED_TURN_NOTIFICATION_TYPES):",
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/src/codex_app_server/_message_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class MessageRouter:
"""

def __init__(self) -> None:
"""Create empty response, turn, and global notification queues."""
self._lock = threading.Lock()
self._response_waiters: dict[str, queue.Queue[ResponseQueueItem]] = {}
self._turn_notifications: dict[str, queue.Queue[NotificationQueueItem]] = {}
Expand Down Expand Up @@ -144,6 +145,7 @@ def fail_all(self, exc: BaseException) -> None:
self._global_notifications.put(exc)

def _notification_turn_id(self, notification: Notification) -> str | None:
"""Extract routing ids from known generated payloads or raw unknown payloads."""
payload = notification.payload
if isinstance(payload, UnknownNotification):
raw_turn_id = payload.params.get("turnId")
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/src/codex_app_server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ def interrupt(self) -> TurnInterruptResponse:
return self._client.turn_interrupt(self.thread_id, self.id)

def stream(self) -> Iterator[Notification]:
"""Yield only notifications routed to this turn handle."""
self._client.register_turn_notifications(self.id)
try:
while True:
Expand Down Expand Up @@ -730,6 +731,7 @@ async def interrupt(self) -> TurnInterruptResponse:
return await self._codex._client.turn_interrupt(self.thread_id, self.id)

async def stream(self) -> AsyncIterator[Notification]:
"""Yield only notifications routed to this async turn handle."""
await self._codex._ensure_initialized()
self._codex._client.register_turn_notifications(self.id)
try:
Expand Down
Loading
Loading