[codex] Add independent beta release for the Python SDK (#24828)

## Why

`openai-codex` needs a beta release lifecycle without requiring beta
releases of its pinned runtime package. Previously, SDK staging rewrote
its runtime dependency to the SDK version, which made an SDK-only beta
impossible.

## What changed

- Set the initial SDK beta version to `0.1.0b1` and pin it to published
stable `openai-codex-cli-bin==0.132.0`.
- Decoupled SDK release staging from runtime versioning so it preserves
the reviewed exact runtime pin.
- Added a `python-v*` tag workflow that builds and publishes only
`openai-codex` through PyPI trusted publishing.
- Removed the Beta classifier from runtime package metadata for future
runtime publications.
- Regenerated protocol-derived SDK models from the selected stable
runtime package.

`0.132.0` is the newest stable runtime admitted by the checked-in
dependency date fence and retains the Linux wheel family currently used
by SDK CI.

## Release setup

Before pushing `python-v0.1.0b1`, configure PyPI trusted publishing for
the `openai-codex` project with workflow `python-sdk-release.yml`,
environment `pypi`, and job `publish-python-sdk`.

## Validation

- `uv run --frozen --extra dev ruff check src/openai_codex scripts
examples tests`
- Parsed `.github/workflows/python-sdk-release.yml` with PyYAML.
- Built staged release artifacts locally:
`openai_codex-0.1.0b1-py3-none-any.whl` and
`openai_codex-0.1.0b1.tar.gz`.
- Verified wheel metadata pins `openai-codex-cli-bin==0.132.0`.
- Tests are deferred to online CI for this PR.
This commit is contained in:
Ahmed Ibrahim
2026-05-27 17:57:51 -07:00
committed by GitHub
Unverified
parent 304d15cab0
commit 4d0c4cd058
9 changed files with 335 additions and 292 deletions
+4 -4
View File
@@ -16,8 +16,8 @@ uv sync
source .venv/bin/activate
```
Published SDK builds pin an exact `openai-codex-cli-bin` runtime dependency
with the same version as the SDK. Pass `CodexConfig(codex_bin=...)` only
Published SDK builds pin an exact compatible `openai-codex-cli-bin` runtime
dependency. Pass `CodexConfig(codex_bin=...)` only
when you intentionally want to run against a specific local app-server binary.
## Quickstart
@@ -111,7 +111,7 @@ python examples/01_quickstart_constructor/async.py
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.
target wheel. SDK beta releases are versioned independently of runtime releases.
## Compatibility and versioning
@@ -119,7 +119,7 @@ target wheel. The SDK package version and runtime package version must match.
- Runtime package: `openai-codex-cli-bin`
- Python: `>=3.10`
- Target protocol: Codex `app-server` JSON-RPC v2
- Versioning rule: the SDK package version is the underlying Codex runtime version
- Versioning rule: SDK releases pin one exact compatible Codex runtime version
## Notes
+4 -4
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "openai-codex"
version = "0.131.0a4"
version = "0.1.0b1"
description = "Python SDK for Codex app-server v2"
readme = "README.md"
requires-python = ">=3.10"
@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = ["pydantic>=2.12", "openai-codex-cli-bin==0.131.0a4"]
dependencies = ["pydantic>=2.12", "openai-codex-cli-bin==0.132.0"]
[project.urls]
Homepage = "https://github.com/openai/codex"
@@ -86,10 +86,10 @@ combine-as-imports = true
[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { openai-codex-cli-bin = "2026-05-10T00:00:00Z" }
exclude-newer-package = { openai-codex-cli-bin = "2026-05-20T21: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" }
exclude-newer-package = { openai-codex-cli-bin = "2026-05-20T21:00:00Z" }
index-strategy = "first-index"
+11 -61
View File
@@ -216,25 +216,8 @@ def _rewrite_project_name(pyproject_text: str, name: str) -> str:
return updated
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:
raise RuntimeError("Could not find dependencies array in sdk/python/pyproject.toml")
raw_items = [item.strip() for item in match.group(1).split(",") if item.strip()]
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, codex_version: str) -> Path:
package_version = normalize_codex_version(codex_version)
def stage_python_sdk_package(staging_dir: Path, sdk_version: str) -> Path:
package_version = normalize_codex_version(sdk_version)
_copy_package_tree(sdk_root(), staging_dir)
sdk_bin_dir = staging_dir / "src" / "openai_codex" / "bin"
if sdk_bin_dir.exists():
@@ -244,7 +227,6 @@ def stage_python_sdk_package(staging_dir: Path, codex_version: str) -> Path:
pyproject_text = pyproject_path.read_text()
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
@@ -1229,28 +1211,20 @@ def build_parser() -> argparse.ArgumentParser:
stage_sdk_parser = subparsers.add_parser(
"stage-sdk",
help="Stage a releasable SDK package pinned to a runtime version",
help="Stage a releasable SDK package while preserving its reviewed runtime pin",
)
stage_sdk_parser.add_argument(
"staging_dir",
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",
help=argparse.SUPPRESS,
)
stage_sdk_parser.add_argument(
"--sdk-version",
help=argparse.SUPPRESS,
required=True,
help=(
"Python SDK release version to write into the staged package. "
"Accepts PEP 440 versions such as 0.1.0b1."
),
)
stage_runtime_parser = subparsers.add_parser(
@@ -1269,15 +1243,12 @@ def build_parser() -> argparse.ArgumentParser:
)
stage_runtime_parser.add_argument(
"--codex-version",
required=True,
help=(
"Codex release version to write into the staged runtime package. "
"Accepts PEP 440 versions or release tags such as rust-v0.116.0-alpha.1."
),
)
stage_runtime_parser.add_argument(
"--runtime-version",
help=argparse.SUPPRESS,
)
stage_runtime_parser.add_argument(
"--platform-tag",
help=(
@@ -1301,40 +1272,19 @@ def default_cli_ops() -> CliOps:
)
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 Python release artifacts")
normalized_versions = [normalize_codex_version(version) for version in versions]
if len(set(normalized_versions)) != 1:
raise RuntimeError("SDK and runtime package versions must match; pass one --codex-version")
return normalized_versions[0]
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,
codex_version,
normalize_codex_version(args.sdk_version),
)
elif args.command == "stage-runtime":
codex_version = _resolve_codex_version(args)
ops.stage_python_runtime_package(
args.staging_dir,
codex_version,
normalize_codex_version(args.codex_version),
args.package_archive.resolve(),
args.platform_tag,
)
+149 -157
View File
@@ -49,26 +49,24 @@ class AccountLoginCompletedNotification(BaseModel):
success: bool
class AdditionalWritableRootActivePermissionProfileModification(BaseModel):
class ActivePermissionProfile(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
path: AbsolutePathBuf
type: Annotated[
Literal["additionalWritableRoot"],
Field(title="AdditionalWritableRootActivePermissionProfileModificationType"),
extends: Annotated[
str | None,
Field(
description="Parent profile identifier once permissions profiles support inheritance. This is currently always `null`."
),
] = None
id: Annotated[
str,
Field(
description="Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile."
),
]
class ActivePermissionProfileModification(
RootModel[AdditionalWritableRootActivePermissionProfileModification]
):
model_config = ConfigDict(
populate_by_name=True,
)
root: AdditionalWritableRootActivePermissionProfileModification
class AddCreditsNudgeCreditType(Enum):
credits = "credits"
usage_limit = "usage_limit"
@@ -597,6 +595,12 @@ class UserConfigLayerSource(BaseModel):
description="This is the path to the user's config.toml file, though it is not guaranteed to exist."
),
]
profile: Annotated[
str | None,
Field(
description="Name of the selected profile-v2 config layered on top of the base user config, when this layer represents one."
),
] = None
type: Annotated[Literal["user"], Field(title="UserConfigLayerSourceType")]
@@ -680,6 +684,7 @@ class CommandConfiguredHookHandler(BaseModel):
)
async_: Annotated[bool, Field(alias="async")]
command: str
command_windows: Annotated[str | None, Field(alias="commandWindows")] = None
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")]
@@ -842,6 +847,13 @@ class ExperimentalFeatureListParams(BaseModel):
int | None,
Field(description="Optional page size; defaults to a reasonable server-side value.", ge=0),
] = None
thread_id: Annotated[
str | None,
Field(
alias="threadId",
description="Optional loaded thread id. Pass this when showing feature state for an existing thread so enablement is computed from that thread's refreshed config, including project-local config for the thread's cwd.",
),
] = None
class ExperimentalFeatureStage(Enum):
@@ -1014,6 +1026,18 @@ class FileSystemSpecialPath(
)
class ForcedChatgptWorkspaceIds(RootModel[str | list[str]]):
model_config = ConfigDict(
populate_by_name=True,
)
root: Annotated[
str | list[str],
Field(
description="Backward-compatible API shape for ChatGPT workspace login restrictions."
),
]
class ForcedLoginMethod(Enum):
chatgpt = "chatgpt"
api = "api"
@@ -1482,8 +1506,6 @@ class HooksListParams(BaseModel):
class ImageDetail(Enum):
auto = "auto"
low = "low"
high = "high"
original = "original"
@@ -2166,66 +2188,6 @@ 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 AdditionalWritableRootPermissionProfileModificationParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
path: AbsolutePathBuf
type: Annotated[
Literal["additionalWritableRoot"],
Field(title="AdditionalWritableRootPermissionProfileModificationParamsType"),
]
class PermissionProfileModificationParams(
RootModel[AdditionalWritableRootPermissionProfileModificationParams]
):
model_config = ConfigDict(
populate_by_name=True,
)
root: AdditionalWritableRootPermissionProfileModificationParams
class PermissionProfileNetworkPermissions(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
enabled: bool
class ProfilePermissionProfileSelectionParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: str
modifications: list[PermissionProfileModificationParams] | None = None
type: Annotated[Literal["profile"], Field(title="ProfilePermissionProfileSelectionParamsType")]
class PermissionProfileSelectionParams(RootModel[ProfilePermissionProfileSelectionParams]):
model_config = ConfigDict(
populate_by_name=True,
)
root: ProfilePermissionProfileSelectionParams
class Personality(Enum):
none = "none"
friendly = "friendly"
@@ -2298,6 +2260,23 @@ class PluginInstallResponse(BaseModel):
auth_policy: Annotated[PluginAuthPolicy, Field(alias="authPolicy")]
class PluginInstalledParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
cwds: Annotated[
list[AbsolutePathBuf] | None,
Field(description="Optional working directories used to discover repo marketplaces."),
] = None
install_suggestion_plugin_names: Annotated[
list[str] | None,
Field(
alias="installSuggestionPluginNames",
description="Additional uninstalled plugin names that should be returned when present locally. This is used by mention surfaces that intentionally expose install entrypoints.",
),
] = None
class PluginInterface(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -2385,6 +2364,26 @@ class PluginReadParams(BaseModel):
remote_marketplace_name: Annotated[str | None, Field(alias="remoteMarketplaceName")] = None
class PluginShareCheckoutParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
remote_plugin_id: Annotated[str, Field(alias="remotePluginId")]
class PluginShareCheckoutResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
marketplace_name: Annotated[str, Field(alias="marketplaceName")]
marketplace_path: Annotated[AbsolutePathBuf, Field(alias="marketplacePath")]
plugin_id: Annotated[str, Field(alias="pluginId")]
plugin_name: Annotated[str, Field(alias="pluginName")]
plugin_path: Annotated[AbsolutePathBuf, Field(alias="pluginPath")]
remote_plugin_id: Annotated[str, Field(alias="remotePluginId")]
remote_version: Annotated[str | None, Field(alias="remoteVersion")] = None
class PluginShareDeleteParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -2745,6 +2744,7 @@ class RemoteControlStatusChangedNotification(BaseModel):
)
environment_id: Annotated[str | None, Field(alias="environmentId")] = None
installation_id: Annotated[str, Field(alias="installationId")]
server_name: Annotated[str, Field(alias="serverName")]
status: RemoteControlConnectionStatus
@@ -2911,6 +2911,13 @@ class CompactionResponseItem(BaseModel):
type: Annotated[Literal["compaction"], Field(title="CompactionResponseItemType")]
class CompactionTriggerResponseItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
type: Annotated[Literal["compaction_trigger"], Field(title="CompactionTriggerResponseItemType")]
class ContextCompactionResponseItem(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -3627,6 +3634,8 @@ class ThreadGoalClearedNotification(BaseModel):
class ThreadGoalStatus(Enum):
active = "active"
paused = "paused"
blocked = "blocked"
usage_limited = "usageLimited"
budget_limited = "budgetLimited"
complete = "complete"
@@ -4308,6 +4317,7 @@ class ImageUserInput(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
detail: ImageDetail | None = None
type: Annotated[Literal["image"], Field(title="ImageUserInputType")]
url: str
@@ -4316,6 +4326,7 @@ class LocalImageUserInput(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
detail: ImageDetail | None = None
path: str
type: Annotated[Literal["localImage"], Field(title="LocalImageUserInputType")]
@@ -4525,30 +4536,6 @@ class AccountUpdatedNotification(BaseModel):
plan_type: Annotated[PlanType | None, Field(alias="planType")] = None
class ActivePermissionProfile(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
extends: Annotated[
str | None,
Field(
description="Parent profile identifier once permissions profiles support inheritance. This is currently always `null`."
),
] = None
id: Annotated[
str,
Field(
description="Identifier from `default_permissions` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile."
),
]
modifications: Annotated[
list[ActivePermissionProfileModification] | None,
Field(
description="Bounded user-requested modifications applied on top of the named profile, if any."
),
] = []
class AppConfig(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -4790,6 +4777,15 @@ class PluginListRequest(BaseModel):
params: PluginListParams
class PluginInstalledRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[Literal["plugin/installed"], Field(title="Plugin/installedRequestMethod")]
params: PluginInstalledParams
class PluginReadRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -4817,6 +4813,17 @@ class PluginShareListRequest(BaseModel):
params: PluginShareListParams
class PluginShareCheckoutRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[
Literal["plugin/share/checkout"], Field(title="Plugin/share/checkoutRequestMethod")
]
params: PluginShareCheckoutParams
class PluginShareDeleteRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -5424,6 +5431,7 @@ class ConfigRequirements(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
allow_managed_hooks_only: Annotated[bool | None, Field(alias="allowManagedHooksOnly")] = None
allowed_approval_policies: Annotated[
list[AskForApproval] | None, Field(alias="allowedApprovalPolicies")
] = None
@@ -5851,40 +5859,6 @@ 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 PluginSharePrincipal(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -6626,7 +6600,6 @@ class ToolsV2(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
view_image: bool | None = None
web_search: WebSearchToolConfig | None = None
@@ -7072,24 +7045,6 @@ 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 PluginShareContext(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -7098,6 +7053,13 @@ class PluginShareContext(BaseModel):
creator_name: Annotated[str | None, Field(alias="creatorName")] = None
discoverability: PluginShareDiscoverability | None = None
remote_plugin_id: Annotated[str, Field(alias="remotePluginId")]
remote_version: Annotated[
str | None,
Field(
alias="remoteVersion",
description="Version of the remote shared plugin release when available.",
),
] = None
share_principals: Annotated[
list[PluginSharePrincipal] | None, Field(alias="sharePrincipals")
] = None
@@ -7129,7 +7091,20 @@ class PluginSummary(BaseModel):
installed: bool
interface: PluginInterface | None = None
keywords: list[str] | None = []
local_version: Annotated[
str | None,
Field(
alias="localVersion",
description="Version of the locally materialized plugin package when available.",
),
] = None
name: str
remote_plugin_id: Annotated[
str | None,
Field(
alias="remotePluginId", description="Backend remote plugin identifier when available."
),
] = None
share_context: Annotated[
PluginShareContext | None,
Field(
@@ -7209,6 +7184,7 @@ class ResponseItem(
| WebSearchCallResponseItem
| ImageGenerationCallResponseItem
| CompactionResponseItem
| CompactionTriggerResponseItem
| ContextCompactionResponseItem
| OtherResponseItem
]
@@ -7229,6 +7205,7 @@ class ResponseItem(
| WebSearchCallResponseItem
| ImageGenerationCallResponseItem
| CompactionResponseItem
| CompactionTriggerResponseItem
| ContextCompactionResponseItem
| OtherResponseItem
)
@@ -7448,8 +7425,9 @@ class Config(BaseModel):
),
] = None
compact_prompt: str | None = None
desktop: dict[str, Any] | None = None
developer_instructions: str | None = None
forced_chatgpt_workspace_id: str | None = None
forced_chatgpt_workspace_id: ForcedChatgptWorkspaceIds | None = None
forced_login_method: ForcedLoginMethod | None = None
instructions: str | None = None
model: str | None = None
@@ -7827,7 +7805,7 @@ class ThreadForkResponse(BaseModel):
sandbox: Annotated[
SandboxPolicy,
Field(
description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance."
),
]
service_tier: Annotated[str | None, Field(alias="serviceTier")] = None
@@ -7895,7 +7873,7 @@ class ThreadResumeResponse(BaseModel):
sandbox: Annotated[
SandboxPolicy,
Field(
description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance."
),
]
service_tier: Annotated[str | None, Field(alias="serviceTier")] = None
@@ -7940,7 +7918,7 @@ class ThreadStartResponse(BaseModel):
sandbox: Annotated[
SandboxPolicy,
Field(
description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` for profile provenance."
),
]
service_tier: Annotated[str | None, Field(alias="serviceTier")] = None
@@ -7998,11 +7976,13 @@ class ClientRequest(
| MarketplaceRemoveRequest
| MarketplaceUpgradeRequest
| PluginListRequest
| PluginInstalledRequest
| PluginReadRequest
| PluginSkillReadRequest
| PluginShareSaveRequest
| PluginShareUpdateTargetsRequest
| PluginShareListRequest
| PluginShareCheckoutRequest
| PluginShareDeleteRequest
| AppListRequest
| FsReadFileRequest
@@ -8079,11 +8059,13 @@ class ClientRequest(
| MarketplaceRemoveRequest
| MarketplaceUpgradeRequest
| PluginListRequest
| PluginInstalledRequest
| PluginReadRequest
| PluginSkillReadRequest
| PluginShareSaveRequest
| PluginShareUpdateTargetsRequest
| PluginShareListRequest
| PluginShareCheckoutRequest
| PluginShareDeleteRequest
| AppListRequest
| FsReadFileRequest
@@ -8135,6 +8117,16 @@ class ClientRequest(
]
class PluginInstalledResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
marketplace_load_errors: Annotated[
list[MarketplaceLoadErrorInfo] | None, Field(alias="marketplaceLoadErrors")
] = []
marketplaces: list[PluginMarketplaceEntry]
class PluginListResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -158,8 +158,8 @@ def test_schema_normalization_only_flattens_string_literal_oneofs(
"AuthMode",
"InputModality",
"ExperimentalFeatureStage",
"CommandExecOutputStream",
"ProcessOutputStream",
"CommandExecOutputStream",
]
@@ -249,11 +249,11 @@ def test_source_sdk_package_pins_published_runtime() -> None:
"runtime_pin": script.pinned_runtime_version(),
"dependencies": pyproject["project"]["dependencies"],
} == {
"sdk_version": "0.131.0a4",
"runtime_pin": "0.131.0a4",
"sdk_version": "0.1.0b1",
"runtime_pin": "0.132.0",
"dependencies": [
"pydantic>=2.12",
"openai-codex-cli-bin==0.131.0a4",
"openai-codex-cli-bin==0.132.0",
],
}
@@ -284,19 +284,26 @@ def test_release_metadata_retries_without_invalid_auth(
assert authorizations == ["Bearer invalid-token", None]
def test_runtime_setup_uses_pep440_package_version_and_codex_release_tags() -> None:
"""The SDK uses PEP 440 package pins and converts only when fetching releases."""
def test_runtime_setup_reads_independent_runtime_pin_and_release_tags() -> None:
"""Runtime package pins remain independent of the SDK beta version."""
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 (
f"{runtime_setup.PACKAGE_NAME}=={pyproject['project']['version']}"
in pyproject["project"]["dependencies"]
)
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"
assert {
"package_name": runtime_setup.PACKAGE_NAME,
"sdk_version": pyproject["project"]["version"],
"runtime_pin": runtime_setup.pinned_runtime_version(),
"normalized_release_version": runtime_setup._normalized_package_version(
"rust-v0.116.0-alpha.1"
),
"release_tag": runtime_setup._release_tag("0.116.0a1"),
} == {
"package_name": "openai-codex-cli-bin",
"sdk_version": "0.1.0b1",
"runtime_pin": "0.132.0",
"normalized_release_version": "0.116.0a1",
"release_tag": "rust-v0.116.0-alpha.1",
}
@pytest.mark.parametrize(
@@ -491,23 +498,32 @@ def test_runtime_package_layout_is_included_by_wheel_config(
]
def test_stage_sdk_release_injects_exact_runtime_pin(tmp_path: Path) -> None:
def test_stage_sdk_release_preserves_reviewed_runtime_pin(tmp_path: Path) -> None:
script = _load_update_script_module()
staged = script.stage_python_sdk_package(
tmp_path / "sdk-stage",
"rust-v0.116.0-alpha.1",
"0.1.0b1",
)
pyproject = (staged / "pyproject.toml").read_text()
assert 'name = "openai-codex"' in pyproject
assert 'version = "0.116.0a1"' in pyproject
assert '"openai-codex-cli-bin==0.116.0a1"' in pyproject
pyproject = tomllib.loads((staged / "pyproject.toml").read_text())
assert {
"name": pyproject["project"]["name"],
"version": pyproject["project"]["version"],
"dependencies": pyproject["project"]["dependencies"],
} == {
"name": "openai-codex",
"version": "0.1.0b1",
"dependencies": [
"pydantic>=2.12",
"openai-codex-cli-bin==0.132.0",
],
}
assert (
'__version__ = "0.116.0a1"'
'__version__ = "0.1.0b1"'
not in (staged / "src" / "openai_codex" / "__init__.py").read_text()
)
assert (
'client_version: str = "0.116.0a1"'
'client_version: str = "0.1.0b1"'
not in (staged / "src" / "openai_codex" / "client.py").read_text()
)
assert not any((staged / "src" / "openai_codex").glob("bin/**"))
@@ -520,34 +536,41 @@ 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.116.0a1")
staged = script.stage_python_sdk_package(staging_dir, "0.1.0b1")
assert staged == staging_dir
assert not old_file.exists()
def test_staged_sdk_and_runtime_versions_match(tmp_path: Path) -> None:
def test_sdk_beta_release_can_pin_stable_runtime(tmp_path: Path) -> None:
script = _load_update_script_module()
package_archive = _write_fake_codex_package_archive(tmp_path, script)
sdk_stage = script.stage_python_sdk_package(
tmp_path / "sdk-stage",
"rust-v0.116.0-alpha.1",
"0.1.0b1",
)
runtime_stage = script.stage_python_runtime_package(
tmp_path / "runtime-stage",
"rust-v0.116.0-alpha.1",
"0.132.0",
package_archive,
)
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",
]
assert {
"sdk_version": sdk_pyproject["project"]["version"],
"runtime_version": runtime_pyproject["project"]["version"],
"sdk_dependencies": sdk_pyproject["project"]["dependencies"],
} == {
"sdk_version": "0.1.0b1",
"runtime_version": "0.132.0",
"sdk_dependencies": [
"pydantic>=2.12",
"openai-codex-cli-bin==0.132.0",
],
}
def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
@@ -557,16 +580,16 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
[
"stage-sdk",
str(tmp_path / "sdk-stage"),
"--codex-version",
"rust-v0.116.0-alpha.1",
"--sdk-version",
"0.1.0b1",
]
)
def fake_generate_types() -> None:
calls.append("generate_types")
def fake_stage_sdk_package(_staging_dir: Path, codex_version: str) -> Path:
calls.append(f"stage_sdk:{codex_version}")
def fake_stage_sdk_package(_staging_dir: Path, sdk_version: str) -> Path:
calls.append(f"stage_sdk:{sdk_version}")
return tmp_path / "sdk-stage"
def fake_stage_runtime_package(
@@ -589,26 +612,7 @@ def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
script.run_command(args, ops)
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())
assert calls == ["generate_types", "stage_sdk:0.1.0b1"]
def test_stage_runtime_stages_package_without_type_generation(tmp_path: Path) -> None:
+1 -1
View File
@@ -40,7 +40,7 @@ def test_generated_files_are_up_to_date():
# Regenerate contract artifacts via the pinned runtime package, not a local
# app-server binary from the checkout or CI environment.
assert importlib.metadata.version("openai-codex-cli-bin") == "0.131.0a4"
assert importlib.metadata.version("openai-codex-cli-bin") == "0.132.0"
env = os.environ.copy()
env.pop("CODEX_EXEC_PATH", None)
python_bin = str(Path(sys.executable).parent)
+16 -9
View File
@@ -2,6 +2,13 @@ version = 1
revision = 3
requires-python = ">=3.10"
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P7D"
[options.exclude-newer-package]
openai-codex-cli-bin = "2026-05-20T21:00:00Z"
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -275,7 +282,7 @@ wheels = [
[[package]]
name = "openai-codex"
version = "0.131.0a4"
version = "0.1.0b1"
source = { editable = "." }
dependencies = [
{ name = "openai-codex-cli-bin" },
@@ -292,7 +299,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = "==0.31.2" },
{ name = "openai-codex-cli-bin", specifier = "==0.131.0a4" },
{ name = "openai-codex-cli-bin", specifier = "==0.132.0" },
{ name = "pydantic", specifier = ">=2.12" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.8" },
@@ -301,15 +308,15 @@ provides-extras = ["dev"]
[[package]]
name = "openai-codex-cli-bin"
version = "0.131.0a4"
version = "0.132.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/9f/f9fc4bb1b2b7a20d4d65143ebb4c4dcd2301a718183b539ecb5b1c0ac3ec/openai_codex_cli_bin-0.131.0a4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:db0f3cb7dda310641ac04fbaf3f128693a3817ab83ae59b67a3c9c74bd53f8b8", size = 88367585, upload-time = "2026-05-09T06:14:09.453Z" },
{ url = "https://files.pythonhosted.org/packages/dc/39/eb95ed0e8156669e895a192dec760be07dabe891c3c6340f7c6487b9a976/openai_codex_cli_bin-0.131.0a4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6cae5af6edca7f6d3f0bcbbd93cfc8a6dc3e33fb5955af21ae492b6d5d0dcb72", size = 79245567, upload-time = "2026-05-09T06:14:13.581Z" },
{ url = "https://files.pythonhosted.org/packages/0c/92/ade176fa78d746d5ff7a6e371d64740c0d95ab299b0dd58a5404b89b3915/openai_codex_cli_bin-0.131.0a4-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5728f9887baf62d7e72f4f242093b3ff81e26c81d80d346fe1eef7eda6838aa8", size = 77758628, upload-time = "2026-05-09T06:14:18.374Z" },
{ url = "https://files.pythonhosted.org/packages/28/e6/bfe6c65f8e3e5499f71b24c3b6e8d07e4d426543d25e429b9b141b544e5f/openai_codex_cli_bin-0.131.0a4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d7a47fd3667fbcc216593839c202deffa056e9b3d46c6933e72594d461f4fea0", size = 84535509, upload-time = "2026-05-09T06:14:22.851Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b7/53dc094a691ab6f2ca079e8e865b122843809ac4fad51cac4d59021e599d/openai_codex_cli_bin-0.131.0a4-py3-none-win_amd64.whl", hash = "sha256:c61bcf029672494c4c7fdc8567dbaa659a48bb75641d91c2ade27c1e46803434", size = 88185543, upload-time = "2026-05-09T06:14:27.282Z" },
{ url = "https://files.pythonhosted.org/packages/82/99/e0852ffcf9b4d2794fef83e0c3a267b3c773a776f136e9f7ce19f0c8df42/openai_codex_cli_bin-0.131.0a4-py3-none-win_arm64.whl", hash = "sha256:bbde750186861f102e346ac066f4e9608f515f7b71b16a6e8b7ef1ddc02a97a5", size = 81196380, upload-time = "2026-05-09T06:14:32.103Z" },
{ url = "https://files.pythonhosted.org/packages/be/a1/b92b7a1b73a83785d2e1dcd0faecd1b7f886a38cf02a30abe1c35f42f0f7/openai_codex_cli_bin-0.132.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1c22b51dbd679413f84f00b9d8fd4e5cf8a1c0d1c7cc8c42bcb3f9f1b33e2334", size = 89403211, upload-time = "2026-05-20T02:37:22.311Z" },
{ url = "https://files.pythonhosted.org/packages/5f/68/163272e582de55a7f460e2329281267908d75d0fbcbbbb2c6749a6329e6b/openai_codex_cli_bin-0.132.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:56217495e6635c8a5d96df820cc0da5f46cd9b6ec6f3a5f67f1607d69ef74256", size = 79058685, upload-time = "2026-05-20T02:37:27.165Z" },
{ url = "https://files.pythonhosted.org/packages/0b/18/a60c6b137e7cd3959cae16ba757f57ca5702979b0ea107a21f516ba15d98/openai_codex_cli_bin-0.132.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:09642e7578a3078bfccc82af4077b085d42b0022b529e4b5c645e0a0af3397a4", size = 78689038, upload-time = "2026-05-20T02:37:31.548Z" },
{ url = "https://files.pythonhosted.org/packages/f8/eb/1b184307a67c1006d59b61636bcfcea73a89aa95271f6516ed28dce554ca/openai_codex_cli_bin-0.132.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:85aec095f9d144d7a2d1aff39fce77b7240f42014580c35801ba74b9317aa5f7", size = 85528820, upload-time = "2026-05-20T02:37:36.559Z" },
{ url = "https://files.pythonhosted.org/packages/0e/e8/1b823a8bf7b96d1513905ad79b16a146d797f81a19a6bc350a2f95a16661/openai_codex_cli_bin-0.132.0-py3-none-win_amd64.whl", hash = "sha256:3cb5c90c55baa39bd5ddc890d2068d3e1322a57a54d1d0e623819009a205c7f5", size = 86916218, upload-time = "2026-05-20T02:37:41.886Z" },
{ url = "https://files.pythonhosted.org/packages/6b/e6/bb8634bd4f3adaea299c95d7b03105ac417e32dd6d8bc2af5dda141d6f28/openai_codex_cli_bin-0.132.0-py3-none-win_arm64.whl", hash = "sha256:74ef93d3deef7cb83c71d19fc667defe749cdab337ec331f59a23511561b6f6a", size = 79892931, upload-time = "2026-05-20T02:37:46.828Z" },
]
[[package]]