From b7635f4d7741305eff87d8bbd074d22d8a89d276 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sat, 9 May 2026 10:03:54 +0300 Subject: [PATCH] Generate Python SDK types from pinned runtime Make the SDK artifact generator fetch schema from the pinned runtime package, regenerate the checked-in Python types from that schema, and assert generated artifacts stay up to date. Co-authored-by: Codex --- sdk/python/README.md | 5 +- sdk/python/scripts/update_sdk_artifacts.py | 146 ++- sdk/python/src/codex_app_server/__init__.py | 4 +- sdk/python/src/codex_app_server/api.py | 30 +- .../generated/notification_registry.py | 4 + .../src/codex_app_server/generated/v2_all.py | 948 ++++++++++-------- .../test_artifact_workflow_and_binaries.py | 89 +- sdk/python/tests/test_client_rpc_methods.py | 1 + sdk/python/tests/test_contract_generation.py | 9 +- .../tests/test_public_api_runtime_behavior.py | 1 + .../tests/test_public_api_signatures.py | 4 + 11 files changed, 722 insertions(+), 519 deletions(-) diff --git a/sdk/python/README.md b/sdk/python/README.md index 031471b811..0c5a8bd3a6 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -2,7 +2,9 @@ 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-server’s camelCase wire format. +The generated wire-model layer is sourced from the pinned `openai-codex-cli-bin` +runtime package and exposed as Pydantic models with snake_case Python fields +that serialize back to the app-server’s camelCase wire format. ## Install @@ -68,6 +70,7 @@ notebook bootstrap the pinned runtime package automatically. ```bash cd sdk/python +uv sync python scripts/update_sdk_artifacts.py generate-types python scripts/update_sdk_artifacts.py \ stage-sdk \ diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index c44a1444c0..10ae4d2db5 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import importlib +import importlib.metadata import json import platform import re @@ -33,19 +34,12 @@ 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 sdk_pyproject_path() -> Path: + return sdk_root() / "pyproject.toml" -def schema_root_dir() -> Path: - return repo_root() / "codex-rs" / "app-server-protocol" / "schema" / "json" +def schema_bundle_path(schema_dir: Path) -> Path: + return schema_dir / "codex_app_server_protocol.v2.schemas.json" def _is_windows() -> bool: @@ -79,7 +73,7 @@ def run_python_module(module: str, args: list[str], cwd: Path) -> None: def current_sdk_version() -> str: match = re.search( r'^version = "([^"]+)"$', - (sdk_root() / "pyproject.toml").read_text(), + sdk_pyproject_path().read_text(), flags=re.MULTILINE, ) if match is None: @@ -87,6 +81,57 @@ def current_sdk_version() -> str: return match.group(1) +def pinned_runtime_version() -> str: + pyproject_text = sdk_pyproject_path().read_text() + match = re.search(r"(?ms)^dependencies = \[(.*?)\]$", pyproject_text) + if match is None: + raise RuntimeError( + "Could not find dependencies array in sdk/python/pyproject.toml" + ) + + pins = re.findall( + rf'"{re.escape(RUNTIME_DISTRIBUTION_NAME)}==([^"]+)"', + match.group(1), + ) + if len(pins) != 1: + raise RuntimeError( + f"Expected exactly one {RUNTIME_DISTRIBUTION_NAME} dependency pin " + "in sdk/python/pyproject.toml" + ) + return normalize_codex_version(pins[0]) + + +def pinned_runtime_codex_path() -> Path: + expected_version = pinned_runtime_version() + try: + installed_version = importlib.metadata.version(RUNTIME_DISTRIBUTION_NAME) + except importlib.metadata.PackageNotFoundError as exc: + raise RuntimeError( + f"Install {RUNTIME_DISTRIBUTION_NAME}=={expected_version} before " + "generating Python SDK types." + ) from exc + + normalized_installed_version = normalize_codex_version(installed_version) + if normalized_installed_version != expected_version: + raise RuntimeError( + f"Expected {RUNTIME_DISTRIBUTION_NAME}=={expected_version}, " + f"but found {installed_version}." + ) + + try: + from codex_cli_bin import bundled_codex_path + except ImportError as exc: + raise RuntimeError( + f"Installed {RUNTIME_DISTRIBUTION_NAME} package does not expose " + "bundled_codex_path." + ) from exc + + codex_path = bundled_codex_path() + if not codex_path.exists(): + raise RuntimeError(f"Pinned Codex runtime binary not found at {codex_path}.") + return codex_path + + def normalize_codex_version(version: str) -> str: normalized = version.strip() if normalized.startswith("rust-v"): @@ -488,8 +533,26 @@ 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 generate_schema_from_pinned_runtime(schema_dir: Path) -> Path: + codex_path = pinned_runtime_codex_path() + if schema_dir.exists(): + shutil.rmtree(schema_dir) + schema_dir.mkdir(parents=True) + run( + [ + str(codex_path), + "app-server", + "generate-json-schema", + "--out", + str(schema_dir), + ], + cwd=sdk_root(), + ) + return schema_dir + + +def _normalized_schema_bundle_text(schema_dir: Path) -> str: + schema = json.loads(schema_bundle_path(schema_dir).read_text()) definitions = schema.get("definitions", {}) if isinstance(definitions, dict): for definition in definitions.values(): @@ -501,7 +564,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: out_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py" out_dir = out_path.parent old_package_dir = out_dir / "v2_all" @@ -509,8 +572,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", [ @@ -547,9 +610,9 @@ def generate_v2_all() -> None: _normalize_generated_timestamps(out_path) -def _notification_specs() -> list[tuple[str, str]]: +def _notification_specs(schema_dir: Path) -> list[tuple[str, str]]: server_notifications = json.loads( - (schema_root_dir() / "ServerNotification.json").read_text() + (schema_dir / "ServerNotification.json").read_text() ) one_of = server_notifications.get("oneOf", []) generated_source = ( @@ -587,11 +650,12 @@ def _notification_specs() -> list[tuple[str, str]]: def _notification_turn_id_specs( + schema_dir: Path, 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() + (schema_dir / "ServerNotification.json").read_text() ) definitions = server_notifications.get("definitions", {}) if not isinstance(definitions, dict): @@ -625,7 +689,7 @@ def _type_tuple_source(class_names: list[str]) -> str: return "(\n" + "".join(f" {class_name},\n" for class_name in class_names) + ")" -def generate_notification_registry() -> None: +def generate_notification_registry(schema_dir: Path) -> None: """Regenerate notification models and routing metadata from generated schemas.""" out = ( sdk_root() @@ -634,9 +698,12 @@ 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}) - direct_turn_id_types, nested_turn_types = _notification_turn_id_specs(specs) + direct_turn_id_types, nested_turn_types = _notification_turn_id_specs( + schema_dir, + specs, + ) lines = [ "# Auto-generated by scripts/update_sdk_artifacts.py", @@ -758,7 +825,10 @@ def _load_public_fields( module_name: str, class_name: str, *, exclude: set[str] | None = None ) -> list[PublicFieldSpec]: exclude = exclude or set() - module = importlib.import_module(module_name) + if module_name == "codex_app_server.generated.v2_all": + module = _load_generated_v2_all_module() + else: + module = importlib.import_module(module_name) model = getattr(module, class_name) fields: list[PublicFieldSpec] = [] for name, field in model.model_fields.items(): @@ -780,6 +850,19 @@ def _load_public_fields( return fields +def _load_generated_v2_all_module() -> types.ModuleType: + module_name = "_codex_app_server_generated_v2_all_for_artifacts" + sys.modules.pop(module_name, None) + module_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py" + spec = importlib.util.spec_from_file_location(module_name, module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Failed to load generated module from {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + + def _kw_signature_lines(fields: list[PublicFieldSpec]) -> list[str]: lines: list[str] = [] for field in fields: @@ -1054,13 +1137,20 @@ def generate_public_api_flat_methods() -> None: _render_async_thread_block(turn_start_fields), ) public_api_path.write_text(source) + run_python_module("ruff", ["format", str(public_api_path)], cwd=sdk_root()) + + +def generate_types_from_schema_dir(schema_dir: Path) -> None: + # v2_all is the authoritative generated surface. + generate_v2_all(schema_dir) + generate_notification_registry(schema_dir) + generate_public_api_flat_methods() def generate_types() -> None: - # v2_all is the authoritative generated surface. - generate_v2_all() - generate_notification_registry() - generate_public_api_flat_methods() + with tempfile.TemporaryDirectory(prefix="codex-python-schema-") as td: + schema_dir = generate_schema_from_pinned_runtime(Path(td) / "schema") + generate_types_from_schema_dir(schema_dir) def build_parser() -> argparse.ArgumentParser: diff --git a/sdk/python/src/codex_app_server/__init__.py b/sdk/python/src/codex_app_server/__init__.py index 33f9e628d9..281e9a472d 100644 --- a/sdk/python/src/codex_app_server/__init__.py +++ b/sdk/python/src/codex_app_server/__init__.py @@ -22,12 +22,12 @@ from .generated.v2_all import ( ReasoningSummary, SandboxMode, SandboxPolicy, - ServiceTier, ThreadItem, ThreadForkParams, ThreadListParams, ThreadResumeParams, ThreadSortKey, + ThreadSource, ThreadSourceKind, ThreadStartParams, ThreadTokenUsageUpdatedNotification, @@ -86,11 +86,11 @@ __all__ = [ "ReasoningSummary", "SandboxMode", "SandboxPolicy", - "ServiceTier", "ThreadStartParams", "ThreadResumeParams", "ThreadListParams", "ThreadSortKey", + "ThreadSource", "ThreadSourceKind", "ThreadForkParams", "TurnStatus", diff --git a/sdk/python/src/codex_app_server/api.py b/sdk/python/src/codex_app_server/api.py index 886e4dd826..54ef491787 100644 --- a/sdk/python/src/codex_app_server/api.py +++ b/sdk/python/src/codex_app_server/api.py @@ -15,7 +15,6 @@ from .generated.v2_all import ( ReasoningSummary, SandboxMode, SandboxPolicy, - ServiceTier, SortDirection, ThreadArchiveResponse, ThreadCompactStartResponse, @@ -27,6 +26,7 @@ from .generated.v2_all import ( ThreadResumeParams, ThreadSetNameResponse, ThreadSortKey, + ThreadSource, ThreadSourceKind, ThreadStartSource, ThreadStartParams, @@ -152,8 +152,9 @@ class Codex: personality: Personality | None = None, sandbox: SandboxMode | None = None, service_name: str | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, session_start_source: ThreadStartSource | None = None, + thread_source: ThreadSource | None = None, ) -> Thread: params = ThreadStartParams( approval_policy=approval_policy, @@ -170,6 +171,7 @@ class Codex: service_name=service_name, service_tier=service_tier, session_start_source=session_start_source, + thread_source=thread_source, ) started = self._client.thread_start(params) return Thread(self._client, started.thread.id) @@ -216,7 +218,7 @@ class Codex: model_provider: str | None = None, personality: Personality | None = None, sandbox: SandboxMode | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, ) -> Thread: params = ThreadResumeParams( thread_id=thread_id, @@ -249,7 +251,8 @@ class Codex: model: str | None = None, model_provider: str | None = None, sandbox: SandboxMode | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, + thread_source: ThreadSource | None = None, ) -> Thread: params = ThreadForkParams( thread_id=thread_id, @@ -264,6 +267,7 @@ class Codex: model_provider=model_provider, sandbox=sandbox, service_tier=service_tier, + thread_source=thread_source, ) forked = self._client.thread_fork(thread_id, params) return Thread(self._client, forked.thread.id) @@ -349,8 +353,9 @@ class AsyncCodex: personality: Personality | None = None, sandbox: SandboxMode | None = None, service_name: str | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, session_start_source: ThreadStartSource | None = None, + thread_source: ThreadSource | None = None, ) -> AsyncThread: await self._ensure_initialized() params = ThreadStartParams( @@ -368,6 +373,7 @@ class AsyncCodex: service_name=service_name, service_tier=service_tier, session_start_source=session_start_source, + thread_source=thread_source, ) started = await self._client.thread_start(params) return AsyncThread(self, started.thread.id) @@ -415,7 +421,7 @@ class AsyncCodex: model_provider: str | None = None, personality: Personality | None = None, sandbox: SandboxMode | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, ) -> AsyncThread: await self._ensure_initialized() params = ThreadResumeParams( @@ -449,7 +455,8 @@ class AsyncCodex: model: str | None = None, model_provider: str | None = None, sandbox: SandboxMode | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, + thread_source: ThreadSource | None = None, ) -> AsyncThread: await self._ensure_initialized() params = ThreadForkParams( @@ -465,6 +472,7 @@ class AsyncCodex: model_provider=model_provider, sandbox=sandbox, service_tier=service_tier, + thread_source=thread_source, ) forked = await self._client.thread_fork(thread_id, params) return AsyncThread(self, forked.thread.id) @@ -502,7 +510,7 @@ class Thread: output_schema: JsonObject | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> RunResult: turn = self.turn( @@ -537,7 +545,7 @@ class Thread: output_schema: JsonObject | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> TurnHandle: wire_input = _to_wire_input(input) @@ -587,7 +595,7 @@ class AsyncThread: output_schema: JsonObject | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> RunResult: turn = await self.turn( @@ -622,7 +630,7 @@ class AsyncThread: output_schema: JsonObject | None = None, personality: Personality | None = None, sandbox_policy: SandboxPolicy | None = None, - service_tier: ServiceTier | None = None, + service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> AsyncTurnHandle: await self._codex._ensure_initialized() diff --git a/sdk/python/src/codex_app_server/generated/notification_registry.py b/sdk/python/src/codex_app_server/generated/notification_registry.py index 3319f4edb5..c55eb5b9b7 100644 --- a/sdk/python/src/codex_app_server/generated/notification_registry.py +++ b/sdk/python/src/codex_app_server/generated/notification_registry.py @@ -35,6 +35,8 @@ from .v2_all import McpToolCallProgressNotification from .v2_all import ModelReroutedNotification from .v2_all import ModelVerificationNotification from .v2_all import PlanDeltaNotification +from .v2_all import ProcessExitedNotification +from .v2_all import ProcessOutputDeltaNotification from .v2_all import ReasoningSummaryPartAddedNotification from .v2_all import ReasoningSummaryTextDeltaNotification from .v2_all import ReasoningTextDeltaNotification @@ -101,6 +103,8 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = { "mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification, "model/rerouted": ModelReroutedNotification, "model/verification": ModelVerificationNotification, + "process/exited": ProcessExitedNotification, + "process/outputDelta": ProcessOutputDeltaNotification, "remoteControl/status/changed": RemoteControlStatusChangedNotification, "serverRequest/resolved": ServerRequestResolvedNotification, "skills/changed": SkillsChangedNotification, diff --git a/sdk/python/src/codex_app_server/generated/v2_all.py b/sdk/python/src/codex_app_server/generated/v2_all.py index ae85d122cc..f573f667a7 100644 --- a/sdk/python/src/codex_app_server/generated/v2_all.py +++ b/sdk/python/src/codex_app_server/generated/v2_all.py @@ -790,75 +790,6 @@ class DeprecationNoticeNotification(BaseModel): summary: Annotated[str, Field(description="Concise summary of what is deprecated.")] -class DeviceKeyAlgorithm(RootModel[Literal["ecdsa_p256_sha256"]]): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - Literal["ecdsa_p256_sha256"], - Field( - description="Device-key algorithm reported at enrollment and signing boundaries." - ), - ] - - -class DeviceKeyProtectionClass(Enum): - hardware_secure_enclave = "hardware_secure_enclave" - hardware_tpm = "hardware_tpm" - os_protected_nonextractable = "os_protected_nonextractable" - - -class DeviceKeyProtectionPolicy(Enum): - hardware_only = "hardware_only" - allow_os_protected_nonextractable = "allow_os_protected_nonextractable" - - -class DeviceKeyPublicParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - key_id: Annotated[str, Field(alias="keyId")] - - -class DeviceKeyPublicResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - algorithm: DeviceKeyAlgorithm - key_id: Annotated[str, Field(alias="keyId")] - protection_class: Annotated[ - DeviceKeyProtectionClass, Field(alias="protectionClass") - ] - public_key_spki_der_base64: Annotated[ - str, - Field( - alias="publicKeySpkiDerBase64", - description="SubjectPublicKeyInfo DER encoded as base64.", - ), - ] - - -class DeviceKeySignResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - algorithm: DeviceKeyAlgorithm - signature_der_base64: Annotated[ - str, - Field( - alias="signatureDerBase64", - description="ECDSA signature DER encoded as base64.", - ), - ] - signed_payload_base64: Annotated[ - str, - Field( - alias="signedPayloadBase64", - description="Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.", - ), - ] - - class InputTextDynamicToolCallOutputContentItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1551,6 +1482,8 @@ class HookEventName(Enum): pre_tool_use = "preToolUse" permission_request = "permissionRequest" post_tool_use = "postToolUse" + pre_compact = "preCompact" + post_compact = "postCompact" session_start = "sessionStart" user_prompt_submit = "userPromptSubmit" stop = "stop" @@ -1616,6 +1549,13 @@ class HookSource(Enum): unknown = "unknown" +class HookTrustStatus(Enum): + managed = "managed" + untrusted = "untrusted" + trusted = "trusted" + modified = "modified" + + class HooksListParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -1653,6 +1593,13 @@ class InitializeCapabilities(BaseModel): description="Exact notification method names that should be suppressed for this connection (for example `thread/started`).", ), ] = None + request_attestation: Annotated[ + bool | None, + Field( + alias="requestAttestation", + description="Opt into `attestation/generate` requests for upstream `x-oai-attestation`.", + ), + ] = False class InitializeParams(BaseModel): @@ -1869,9 +1816,13 @@ class ManagedHooksRequirements(BaseModel): permission_request: Annotated[ list[ConfiguredHookMatcherGroup], Field(alias="PermissionRequest") ] + post_compact: Annotated[ + list[ConfiguredHookMatcherGroup], Field(alias="PostCompact") + ] post_tool_use: Annotated[ list[ConfiguredHookMatcherGroup], Field(alias="PostToolUse") ] + pre_compact: Annotated[list[ConfiguredHookMatcherGroup], Field(alias="PreCompact")] pre_tool_use: Annotated[list[ConfiguredHookMatcherGroup], Field(alias="PreToolUse")] session_start: Annotated[ list[ConfiguredHookMatcherGroup], Field(alias="SessionStart") @@ -2177,6 +2128,15 @@ class ModelReroutedNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class ModelServiceTier(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + description: str + id: str + name: str + + class ModelUpgradeInfo(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2428,6 +2388,19 @@ class PluginAuthPolicy(Enum): on_use = "ON_USE" +class PluginAvailability(Enum): + disabled_by_admin = "DISABLED_BY_ADMIN" + available = "AVAILABLE" + + +class PluginHookSummary(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + event_name: Annotated[HookEventName, Field(alias="eventName")] + key: str + + class PluginInstallParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2515,6 +2488,12 @@ class PluginInterface(BaseModel): website_url: Annotated[str | None, Field(alias="websiteUrl")] = None +class PluginListMarketplaceKind(Enum): + local = "local" + workspace_directory = "workspace-directory" + shared_with_me = "shared-with-me" + + class PluginListParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2525,6 +2504,13 @@ class PluginListParams(BaseModel): description="Optional working directories used to discover repo marketplaces. When omitted, only home-scoped marketplaces and the official curated marketplace are considered." ), ] = None + marketplace_kinds: Annotated[ + list[PluginListMarketplaceKind] | None, + Field( + alias="marketplaceKinds", + description="Optional marketplace kind filter. When omitted, only local marketplaces are queried, plus the default remote catalog when enabled by feature flag.", + ), + ] = None class PluginReadParams(BaseModel): @@ -2554,6 +2540,12 @@ class PluginShareDeleteResponse(BaseModel): ) +class PluginShareDiscoverability(Enum): + listed = "LISTED" + unlisted = "UNLISTED" + private = "PRIVATE" + + class PluginShareListParams(BaseModel): pass model_config = ConfigDict( @@ -2561,12 +2553,16 @@ class PluginShareListParams(BaseModel): ) -class PluginShareSaveParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - plugin_path: Annotated[AbsolutePathBuf, Field(alias="pluginPath")] - remote_plugin_id: Annotated[str | None, Field(alias="remotePluginId")] = None +class PluginSharePrincipalRole(Enum): + reader = "reader" + editor = "editor" + owner = "owner" + + +class PluginSharePrincipalType(Enum): + user = "user" + group = "group" + workspace = "workspace" class PluginShareSaveResponse(BaseModel): @@ -2577,6 +2573,16 @@ class PluginShareSaveResponse(BaseModel): share_url: Annotated[str, Field(alias="shareUrl")] +class PluginShareTargetRole(Enum): + reader = "reader" + editor = "editor" + + +class PluginShareUpdateDiscoverability(Enum): + unlisted = "UNLISTED" + private = "PRIVATE" + + class PluginSkillReadParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2626,20 +2632,6 @@ class PluginSource(RootModel[LocalPluginSource | GitPluginSource | RemotePluginS root: LocalPluginSource | GitPluginSource | RemotePluginSource -class PluginSummary(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - auth_policy: Annotated[PluginAuthPolicy, Field(alias="authPolicy")] - enabled: bool - id: str - install_policy: Annotated[PluginInstallPolicy, Field(alias="installPolicy")] - installed: bool - interface: PluginInterface | None = None - name: str - source: PluginSource - - class PluginUninstallParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -2662,6 +2654,59 @@ class PluginsMigration(BaseModel): plugin_names: Annotated[list[str], Field(alias="pluginNames")] +class ProcessExitedNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + exit_code: Annotated[int, Field(alias="exitCode", description="Process exit code.")] + process_handle: Annotated[ + str, + Field( + alias="processHandle", + description="Client-supplied, connection-scoped `processHandle` from `process/spawn`.", + ), + ] + stderr: Annotated[ + str, + Field( + description="Buffered stderr capture.\n\nEmpty when stderr was streamed via `process/outputDelta`." + ), + ] + stderr_cap_reached: Annotated[ + bool, + Field( + alias="stderrCapReached", + description="Whether stderr reached `outputBytesCap`.\n\nIn streaming mode, stderr is empty and cap state is also reported on the final stderr `process/outputDelta` notification.", + ), + ] + stdout: Annotated[ + str, + Field( + description="Buffered stdout capture.\n\nEmpty when stdout was streamed via `process/outputDelta`." + ), + ] + stdout_cap_reached: Annotated[ + bool, + Field( + alias="stdoutCapReached", + description="Whether stdout reached `outputBytesCap`.\n\nIn streaming mode, stdout is empty and cap state is also reported on the final stdout `process/outputDelta` notification.", + ), + ] + + +class ProcessOutputStream(Enum): + stdout = "stdout" + stderr = "stderr" + + +class ProcessTerminalSize(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cols: Annotated[int, Field(description="Terminal width in character cells.", ge=0)] + rows: Annotated[int, Field(description="Terminal height in character cells.", ge=0)] + + class RateLimitReachedType(Enum): rate_limit_reached = "rate_limit_reached" workspace_owner_credits_depleted = "workspace_owner_credits_depleted" @@ -2837,34 +2882,6 @@ class ReasoningTextDeltaNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] -class RemoteControlClientConnectionAudience( - RootModel[Literal["remote_control_client_websocket"]] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - Literal["remote_control_client_websocket"], - Field( - description="Audience for a remote-control client connection device-key proof." - ), - ] - - -class RemoteControlClientEnrollmentAudience( - RootModel[Literal["remote_control_client_enrollment"]] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - Literal["remote_control_client_enrollment"], - Field( - description="Audience for a remote-control client enrollment device-key proof." - ), - ] - - class RemoteControlConnectionStatus(Enum): disabled = "disabled" connecting = "connecting" @@ -2877,6 +2894,7 @@ class RemoteControlStatusChangedNotification(BaseModel): populate_by_name=True, ) environment_id: Annotated[str | None, Field(alias="environmentId")] = None + installation_id: Annotated[str, Field(alias="installationId")] status: RemoteControlConnectionStatus @@ -3060,6 +3078,16 @@ class CompactionResponseItem(BaseModel): type: Annotated[Literal["compaction"], Field(title="CompactionResponseItemType")] +class ContextCompactionResponseItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + encrypted_content: str | None = None + type: Annotated[ + Literal["context_compaction"], Field(title="ContextCompactionResponseItemType") + ] + + class OtherResponseItem(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3307,6 +3335,16 @@ class ItemPlanDeltaServerNotification(BaseModel): params: PlanDeltaNotification +class ProcessExitedServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["process/exited"], Field(title="Process/exitedNotificationMethod") + ] + params: ProcessExitedNotification + + class ItemCommandExecutionOutputDeltaServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3519,11 +3557,6 @@ class ServerRequestResolvedNotification(BaseModel): thread_id: Annotated[str, Field(alias="threadId")] -class ServiceTier(Enum): - fast = "fast" - flex = "flex" - - class SessionMigration(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3634,14 +3667,6 @@ class SkillsConfigWriteResponse(BaseModel): effective_enabled: Annotated[bool, Field(alias="effectiveEnabled")] -class SkillsListExtraRootsForCwd(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - cwd: str - extra_user_roots: Annotated[list[str], Field(alias="extraUserRoots")] - - class SkillsListParams(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -3659,13 +3684,6 @@ class SkillsListParams(BaseModel): description="When true, bypass the skills cache and re-scan skills from disk.", ), ] = None - per_cwd_extra_user_roots: Annotated[ - list[SkillsListExtraRootsForCwd] | None, - Field( - alias="perCwdExtraUserRoots", - description="Optional per-cwd extra roots to scan as user-scoped skills.", - ), - ] = None class SortDirection(Enum): @@ -3810,37 +3828,6 @@ class ThreadCompactStartResponse(BaseModel): ) -class ThreadForkParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( - None - ) - approvals_reviewer: Annotated[ - ApprovalsReviewer | None, - Field( - alias="approvalsReviewer", - description="Override where approval requests are routed for review on this thread and subsequent turns.", - ), - ] = None - base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None - config: dict[str, Any] | None = None - cwd: str | None = None - developer_instructions: Annotated[ - str | None, Field(alias="developerInstructions") - ] = None - ephemeral: bool | None = None - model: Annotated[ - str | None, - Field(description="Configuration overrides for the forked thread, if any."), - ] = None - model_provider: Annotated[str | None, Field(alias="modelProvider")] = None - sandbox: SandboxMode | None = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None - thread_id: Annotated[str, Field(alias="threadId")] - - class ThreadGoalClearedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -4311,7 +4298,7 @@ class ThreadResumeParams(BaseModel): model_provider: Annotated[str | None, Field(alias="modelProvider")] = None personality: Personality | None = None sandbox: SandboxMode | None = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + service_tier: Annotated[str | None, Field(alias="serviceTier")] = None thread_id: Annotated[str, Field(alias="threadId")] @@ -4370,6 +4357,12 @@ class ThreadSortKey(Enum): updated_at = "updated_at" +class ThreadSource(Enum): + user = "user" + subagent = "subagent" + memory_consolidation = "memory_consolidation" + + class ThreadSourceKind(Enum): cli = "cli" vscode = "vscode" @@ -4528,6 +4521,12 @@ class TurnInterruptResponse(BaseModel): ) +class TurnItemsView(Enum): + not_loaded = "notLoaded" + summary = "summary" + full = "full" + + class TurnPlanStepStatus(Enum): pending = "pending" in_progress = "inProgress" @@ -4722,6 +4721,19 @@ class WebSearchToolConfig(BaseModel): location: WebSearchLocation | None = None +class WindowsSandboxReadiness(Enum): + ready = "ready" + not_configured = "notConfigured" + update_required = "updateRequired" + + +class WindowsSandboxReadinessResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + status: WindowsSandboxReadiness + + class WindowsSandboxSetupMode(Enum): elevated = "elevated" unelevated = "unelevated" @@ -4872,15 +4884,6 @@ class ThreadResumeRequest(BaseModel): params: ThreadResumeParams -class ThreadForkRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[Literal["thread/fork"], Field(title="Thread/forkRequestMethod")] - params: ThreadForkParams - - class ThreadArchiveRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5094,17 +5097,6 @@ class PluginSkillReadRequest(BaseModel): params: PluginSkillReadParams -class PluginShareSaveRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[ - Literal["plugin/share/save"], Field(title="Plugin/share/saveRequestMethod") - ] - params: PluginShareSaveParams - - class PluginShareListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5136,17 +5128,6 @@ class AppListRequest(BaseModel): params: AppsListParams -class DeviceKeyPublicRequest(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - id: RequestId - method: Annotated[ - Literal["device/key/public"], Field(title="Device/key/publicRequestMethod") - ] - params: DeviceKeyPublicParams - - class FsReadFileRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5382,6 +5363,18 @@ class WindowsSandboxSetupStartRequest(BaseModel): params: WindowsSandboxSetupStartParams +class WindowsSandboxReadinessRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[ + Literal["windowsSandbox/readiness"], + Field(title="WindowsSandbox/readinessRequestMethod"), + ] + params: None = None + + class AccountLoginStartRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -5843,159 +5836,6 @@ class ContentItem( root: InputTextContentItem | InputImageContentItem | OutputTextContentItem -class DeviceKeyCreateParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - account_user_id: Annotated[str, Field(alias="accountUserId")] - client_id: Annotated[str, Field(alias="clientId")] - protection_policy: Annotated[ - DeviceKeyProtectionPolicy | None, - Field( - alias="protectionPolicy", - description="Defaults to `hardware_only` when omitted.", - ), - ] = None - - -class DeviceKeyCreateResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - algorithm: DeviceKeyAlgorithm - key_id: Annotated[str, Field(alias="keyId")] - protection_class: Annotated[ - DeviceKeyProtectionClass, Field(alias="protectionClass") - ] - public_key_spki_der_base64: Annotated[ - str, - Field( - alias="publicKeySpkiDerBase64", - description="SubjectPublicKeyInfo DER encoded as base64.", - ), - ] - - -class RemoteControlClientConnectionDeviceKeySignPayload(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - account_user_id: Annotated[str, Field(alias="accountUserId")] - audience: RemoteControlClientConnectionAudience - client_id: Annotated[str, Field(alias="clientId")] - nonce: str - scopes: Annotated[ - list[str], - Field( - description="Must contain exactly `remote_control_controller_websocket`." - ), - ] - session_id: Annotated[ - str, - Field( - alias="sessionId", - description="Backend-issued websocket session id that this proof authorizes.", - ), - ] - target_origin: Annotated[ - str, - Field( - alias="targetOrigin", - description="Origin of the backend endpoint that issued the challenge and will verify this proof.", - ), - ] - target_path: Annotated[ - str, - Field( - alias="targetPath", - description="Websocket route path that this proof authorizes.", - ), - ] - token_expires_at: Annotated[ - int, - Field( - alias="tokenExpiresAt", - description="Remote-control token expiration as Unix seconds.", - ), - ] - token_sha256_base64url: Annotated[ - str, - Field( - alias="tokenSha256Base64url", - description="SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.", - ), - ] - type: Annotated[ - Literal["remoteControlClientConnection"], - Field(title="RemoteControlClientConnectionDeviceKeySignPayloadType"), - ] - - -class RemoteControlClientEnrollmentDeviceKeySignPayload(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - account_user_id: Annotated[str, Field(alias="accountUserId")] - audience: RemoteControlClientEnrollmentAudience - challenge_expires_at: Annotated[ - int, - Field( - alias="challengeExpiresAt", - description="Enrollment challenge expiration as Unix seconds.", - ), - ] - challenge_id: Annotated[ - str, - Field( - alias="challengeId", - description="Backend-issued enrollment challenge id that this proof authorizes.", - ), - ] - client_id: Annotated[str, Field(alias="clientId")] - device_identity_sha256_base64url: Annotated[ - str, - Field( - alias="deviceIdentitySha256Base64url", - description="SHA-256 of the requested device identity operation, encoded as unpadded base64url.", - ), - ] - nonce: str - target_origin: Annotated[ - str, - Field( - alias="targetOrigin", - description="Origin of the backend endpoint that issued the challenge and will verify this proof.", - ), - ] - target_path: Annotated[ - str, - Field( - alias="targetPath", - description="HTTP route path that this proof authorizes.", - ), - ] - type: Annotated[ - Literal["remoteControlClientEnrollment"], - Field(title="RemoteControlClientEnrollmentDeviceKeySignPayloadType"), - ] - - -class DeviceKeySignPayload( - RootModel[ - RemoteControlClientConnectionDeviceKeySignPayload - | RemoteControlClientEnrollmentDeviceKeySignPayload - ] -): - model_config = ConfigDict( - populate_by_name=True, - ) - root: Annotated[ - RemoteControlClientConnectionDeviceKeySignPayload - | RemoteControlClientEnrollmentDeviceKeySignPayload, - Field(description="Structured payloads accepted by `device/key/sign`."), - ] - - class ExperimentalFeature(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6182,6 +6022,7 @@ class HookMetadata(BaseModel): populate_by_name=True, ) command: str | None = None + current_hash: Annotated[str, Field(alias="currentHash")] display_order: Annotated[int, Field(alias="displayOrder")] enabled: bool event_name: Annotated[HookEventName, Field(alias="eventName")] @@ -6194,6 +6035,7 @@ class HookMetadata(BaseModel): source_path: Annotated[AbsolutePathBuf, Field(alias="sourcePath")] status_message: Annotated[str | None, Field(alias="statusMessage")] = None timeout_sec: Annotated[int, Field(alias="timeoutSec", ge=0)] + trust_status: Annotated[HookTrustStatus, Field(alias="trustStatus")] class HookOutputEntry(BaseModel): @@ -6319,7 +6161,11 @@ class Model(BaseModel): populate_by_name=True, ) additional_speed_tiers: Annotated[ - list[str] | None, Field(alias="additionalSpeedTiers") + list[str] | None, + Field( + alias="additionalSpeedTiers", + description="Deprecated: use `serviceTiers` instead.", + ), ] = [] availability_nux: Annotated[ ModelAvailabilityNux | None, Field(alias="availabilityNux") @@ -6336,6 +6182,9 @@ class Model(BaseModel): ] = ["text", "image"] is_default: Annotated[bool, Field(alias="isDefault")] model: str + service_tiers: Annotated[ + list[ModelServiceTier] | None, Field(alias="serviceTiers") + ] = [] supported_reasoning_efforts: Annotated[ list[ReasoningEffortOption], Field(alias="supportedReasoningEfforts") ] @@ -6406,48 +6255,66 @@ class PermissionProfileFileSystemPermissions( ) -class PluginDetail(BaseModel): +class PluginSharePrincipal(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - apps: list[AppSummary] - description: str | None = None - marketplace_name: Annotated[str, Field(alias="marketplaceName")] - marketplace_path: Annotated[ - AbsolutePathBuf | None, Field(alias="marketplacePath") - ] = None - mcp_servers: Annotated[list[str], Field(alias="mcpServers")] - skills: list[SkillSummary] - summary: PluginSummary - - -class PluginMarketplaceEntry(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - interface: MarketplaceInterface | None = None name: str - path: Annotated[ - AbsolutePathBuf | None, + principal_id: Annotated[str, Field(alias="principalId")] + principal_type: Annotated[PluginSharePrincipalType, Field(alias="principalType")] + role: PluginSharePrincipalRole + + +class PluginShareTarget(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + principal_id: Annotated[str, Field(alias="principalId")] + principal_type: Annotated[PluginSharePrincipalType, Field(alias="principalType")] + role: PluginShareTargetRole + + +class PluginShareUpdateTargetsParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + discoverability: PluginShareUpdateDiscoverability + remote_plugin_id: Annotated[str, Field(alias="remotePluginId")] + share_targets: Annotated[list[PluginShareTarget], Field(alias="shareTargets")] + + +class PluginShareUpdateTargetsResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + discoverability: PluginShareDiscoverability + principals: list[PluginSharePrincipal] + + +class ProcessOutputDeltaNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + cap_reached: Annotated[ + bool, Field( - description="Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." + alias="capReached", + description="True on the final streamed chunk for this stream when output was truncated by `outputBytesCap`.", ), - ] = None - plugins: list[PluginSummary] - - -class PluginReadResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - plugin: PluginDetail - - -class PluginShareListResponse(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - data: list[PluginSummary] + ] + delta_base64: Annotated[ + str, Field(alias="deltaBase64", description="Base64-encoded output bytes.") + ] + process_handle: Annotated[ + str, + Field( + alias="processHandle", + description="Client-supplied, connection-scoped `processHandle` from `process/spawn`.", + ), + ] + stream: Annotated[ + ProcessOutputStream, Field(description="Output stream this chunk belongs to.") + ] class RateLimitSnapshot(BaseModel): @@ -6606,6 +6473,17 @@ class CommandExecOutputDeltaServerNotification(BaseModel): params: CommandExecOutputDeltaNotification +class ProcessOutputDeltaServerNotification(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + method: Annotated[ + Literal["process/outputDelta"], + Field(title="Process/outputDeltaNotificationMethod"), + ] + params: ProcessOutputDeltaNotification + + class ItemCommandExecutionTerminalInteractionServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -6826,6 +6704,44 @@ class SubAgentSource( root: SubAgentSourceValue | ThreadSpawnSubAgentSource | OtherSubAgentSource +class ThreadForkParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + approval_policy: Annotated[AskForApproval | None, Field(alias="approvalPolicy")] = ( + None + ) + approvals_reviewer: Annotated[ + ApprovalsReviewer | None, + Field( + alias="approvalsReviewer", + description="Override where approval requests are routed for review on this thread and subsequent turns.", + ), + ] = None + base_instructions: Annotated[str | None, Field(alias="baseInstructions")] = None + config: dict[str, Any] | None = None + cwd: str | None = None + developer_instructions: Annotated[ + str | None, Field(alias="developerInstructions") + ] = None + ephemeral: bool | None = None + model: Annotated[ + str | None, + Field(description="Configuration overrides for the forked thread, if any."), + ] = None + model_provider: Annotated[str | None, Field(alias="modelProvider")] = None + sandbox: SandboxMode | None = None + service_tier: Annotated[str | None, Field(alias="serviceTier")] = None + thread_id: Annotated[str, Field(alias="threadId")] + thread_source: Annotated[ + ThreadSource | None, + Field( + alias="threadSource", + description="Optional client-supplied analytics source classification for this forked thread.", + ), + ] = None + + class ThreadGoal(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -7087,10 +7003,17 @@ class ThreadStartParams(BaseModel): personality: Personality | None = None sandbox: SandboxMode | None = None service_name: Annotated[str | None, Field(alias="serviceName")] = None - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + service_tier: Annotated[str | None, Field(alias="serviceTier")] = None session_start_source: Annotated[ ThreadStartSource | None, Field(alias="sessionStartSource") ] = None + thread_source: Annotated[ + ThreadSource | None, + Field( + alias="threadSource", + description="Optional client-supplied analytics source classification for this thread.", + ), + ] = None class ThreadTokenUsage(BaseModel): @@ -7213,7 +7136,7 @@ class TurnStartParams(BaseModel): ), ] = None service_tier: Annotated[ - ServiceTier | None, + str | None, Field( alias="serviceTier", description="Override the service tier for this turn and subsequent turns.", @@ -7336,6 +7259,15 @@ class ThreadStartRequest(BaseModel): params: ThreadStartParams +class ThreadForkRequest(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: RequestId + method: Annotated[Literal["thread/fork"], Field(title="Thread/forkRequestMethod")] + params: ThreadForkParams + + class ThreadListRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -7345,15 +7277,16 @@ class ThreadListRequest(BaseModel): params: ThreadListParams -class DeviceKeyCreateRequest(BaseModel): +class PluginShareUpdateTargetsRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, ) id: RequestId method: Annotated[ - Literal["device/key/create"], Field(title="Device/key/createRequestMethod") + Literal["plugin/share/updateTargets"], + Field(title="Plugin/share/updateTargetsRequestMethod"), ] - params: DeviceKeyCreateParams + params: PluginShareUpdateTargetsParams class TurnStartRequest(BaseModel): @@ -7466,14 +7399,6 @@ class ConfigWriteResponse(BaseModel): version: str -class DeviceKeySignParams(BaseModel): - model_config = ConfigDict( - populate_by_name=True, - ) - key_id: Annotated[str, Field(alias="keyId")] - payload: DeviceKeySignPayload - - class ErrorNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -7549,6 +7474,13 @@ class ItemCompletedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, ) + completed_at_ms: Annotated[ + int, + Field( + alias="completedAtMs", + description="Unix timestamp (in milliseconds) when this item lifecycle completed.", + ), + ] item: ThreadItem thread_id: Annotated[str, Field(alias="threadId")] turn_id: Annotated[str, Field(alias="turnId")] @@ -7559,6 +7491,13 @@ class ItemStartedNotification(BaseModel): populate_by_name=True, ) item: ThreadItem + started_at_ms: Annotated[ + int, + Field( + alias="startedAtMs", + description="Unix timestamp (in milliseconds) when this item lifecycle started.", + ), + ] thread_id: Annotated[str, Field(alias="threadId")] turn_id: Annotated[str, Field(alias="turnId")] @@ -7601,17 +7540,58 @@ class PermissionProfile( ) -class PluginListResponse(BaseModel): +class PluginShareContext(BaseModel): model_config = ConfigDict( populate_by_name=True, ) - featured_plugin_ids: Annotated[ - list[str] | None, Field(alias="featuredPluginIds") - ] = [] - marketplace_load_errors: Annotated[ - list[MarketplaceLoadErrorInfo] | None, Field(alias="marketplaceLoadErrors") - ] = [] - marketplaces: list[PluginMarketplaceEntry] + creator_account_user_id: Annotated[ + str | None, Field(alias="creatorAccountUserId") + ] = None + creator_name: Annotated[str | None, Field(alias="creatorName")] = None + discoverability: PluginShareDiscoverability | None = None + remote_plugin_id: Annotated[str, Field(alias="remotePluginId")] + share_principals: Annotated[ + list[PluginSharePrincipal] | None, Field(alias="sharePrincipals") + ] = None + share_url: Annotated[str | None, Field(alias="shareUrl")] = None + + +class PluginShareSaveParams(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + discoverability: PluginShareDiscoverability | None = None + plugin_path: Annotated[AbsolutePathBuf, Field(alias="pluginPath")] + remote_plugin_id: Annotated[str | None, Field(alias="remotePluginId")] = None + share_targets: Annotated[ + list[PluginShareTarget] | None, Field(alias="shareTargets") + ] = None + + +class PluginSummary(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + auth_policy: Annotated[PluginAuthPolicy, Field(alias="authPolicy")] + availability: Annotated[ + PluginAvailability | None, + Field(description="Availability state for installing and using the plugin."), + ] = "AVAILABLE" + enabled: bool + id: str + install_policy: Annotated[PluginInstallPolicy, Field(alias="installPolicy")] + installed: bool + interface: PluginInterface | None = None + keywords: list[str] | None = [] + name: str + share_context: Annotated[ + PluginShareContext | None, + Field( + alias="shareContext", + description="Remote sharing context associated with this plugin when available.", + ), + ] = None + source: PluginSource class ProfileV2(BaseModel): @@ -7632,7 +7612,7 @@ class ProfileV2(BaseModel): model_reasoning_effort: ReasoningEffort | None = None model_reasoning_summary: ReasoningSummary | None = None model_verbosity: Verbosity | None = None - service_tier: ServiceTier | None = None + service_tier: str | None = None tools: ToolsV2 | None = None web_search: WebSearchMode | None = None @@ -7687,6 +7667,7 @@ class ResponseItem( | WebSearchCallResponseItem | ImageGenerationCallResponseItem | CompactionResponseItem + | ContextCompactionResponseItem | OtherResponseItem ] ): @@ -7706,6 +7687,7 @@ class ResponseItem( | WebSearchCallResponseItem | ImageGenerationCallResponseItem | CompactionResponseItem + | ContextCompactionResponseItem | OtherResponseItem ) @@ -7865,10 +7847,15 @@ class Turn(BaseModel): id: str items: Annotated[ list[ThreadItem], - Field( - description="Only populated on a `thread/resume` or `thread/fork` response. For all other responses and notifications returning a Turn, the items field will be an empty list." - ), + Field(description="Thread items currently included in this turn payload."), ] + items_view: Annotated[ + TurnItemsView | None, + Field( + alias="itemsView", + description="Describes how much of `items` has been loaded for this turn.", + ), + ] = "full" started_at: Annotated[ int | None, Field( @@ -7902,15 +7889,15 @@ class TurnStartedNotification(BaseModel): turn: Turn -class DeviceKeySignRequest(BaseModel): +class PluginShareSaveRequest(BaseModel): model_config = ConfigDict( populate_by_name=True, ) id: RequestId method: Annotated[ - Literal["device/key/sign"], Field(title="Device/key/signRequestMethod") + Literal["plugin/share/save"], Field(title="Plugin/share/saveRequestMethod") ] - params: DeviceKeySignParams + params: PluginShareSaveParams class ConfigBatchWriteRequest(BaseModel): @@ -7954,7 +7941,7 @@ class Config(BaseModel): review_model: str | None = None sandbox_mode: SandboxMode | None = None sandbox_workspace_write: SandboxWorkspaceWrite | None = None - service_tier: ServiceTier | None = None + service_tier: str | None = None tools: ToolsV2 | None = None web_search: WebSearchMode | None = None @@ -8024,11 +8011,25 @@ class ItemGuardianApprovalReviewCompletedNotification(BaseModel): populate_by_name=True, ) action: GuardianApprovalReviewAction + completed_at_ms: Annotated[ + int, + Field( + alias="completedAtMs", + description="Unix timestamp (in milliseconds) when this review completed.", + ), + ] decision_source: Annotated[AutoReviewDecisionSource, Field(alias="decisionSource")] review: GuardianApprovalReview review_id: Annotated[ str, Field(alias="reviewId", description="Stable identifier for this review.") ] + started_at_ms: Annotated[ + int, + Field( + alias="startedAtMs", + description="Unix timestamp (in milliseconds) when this review started.", + ), + ] target_item_id: Annotated[ str | None, Field( @@ -8049,6 +8050,13 @@ class ItemGuardianApprovalReviewStartedNotification(BaseModel): review_id: Annotated[ str, Field(alias="reviewId", description="Stable identifier for this review.") ] + started_at_ms: Annotated[ + int, + Field( + alias="startedAtMs", + description="Unix timestamp (in milliseconds) when this review started.", + ), + ] target_item_id: Annotated[ str | None, Field( @@ -8060,6 +8068,61 @@ class ItemGuardianApprovalReviewStartedNotification(BaseModel): turn_id: Annotated[str, Field(alias="turnId")] +class PluginDetail(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + apps: list[AppSummary] + description: str | None = None + hooks: list[PluginHookSummary] + marketplace_name: Annotated[str, Field(alias="marketplaceName")] + marketplace_path: Annotated[ + AbsolutePathBuf | None, Field(alias="marketplacePath") + ] = None + mcp_servers: Annotated[list[str], Field(alias="mcpServers")] + skills: list[SkillSummary] + summary: PluginSummary + + +class PluginMarketplaceEntry(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + interface: MarketplaceInterface | None = None + name: str + path: Annotated[ + AbsolutePathBuf | None, + Field( + description="Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." + ), + ] = None + plugins: list[PluginSummary] + + +class PluginReadResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + plugin: PluginDetail + + +class PluginShareListItem(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + local_plugin_path: Annotated[ + AbsolutePathBuf | None, Field(alias="localPluginPath") + ] = None + plugin: PluginSummary + + +class PluginShareListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + data: list[PluginShareListItem] + + class RawResponseItemCompletedNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -8200,6 +8263,13 @@ class Thread(BaseModel): description="Usually the first user message in the thread, if available." ), ] + session_id: Annotated[ + str, + Field( + alias="sessionId", + description="Session id shared by threads that belong to the same session tree.", + ), + ] source: Annotated[ SessionSource, Field( @@ -8209,6 +8279,13 @@ class Thread(BaseModel): status: Annotated[ ThreadStatus, Field(description="Current runtime status for the thread.") ] + thread_source: Annotated[ + ThreadSource | None, + Field( + alias="threadSource", + description="Optional analytics source classification for this thread.", + ), + ] = None turns: Annotated[ list[Turn], Field( @@ -8255,7 +8332,7 @@ class ThreadForkResponse(BaseModel): description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." ), ] - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + service_tier: Annotated[str | None, Field(alias="serviceTier")] = None thread: Thread @@ -8325,7 +8402,7 @@ class ThreadResumeResponse(BaseModel): description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." ), ] - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + service_tier: Annotated[str | None, Field(alias="serviceTier")] = None thread: Thread @@ -8372,7 +8449,7 @@ class ThreadStartResponse(BaseModel): description="Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions." ), ] - service_tier: Annotated[ServiceTier | None, Field(alias="serviceTier")] = None + service_tier: Annotated[str | None, Field(alias="serviceTier")] = None thread: Thread @@ -8430,12 +8507,10 @@ class ClientRequest( | PluginReadRequest | PluginSkillReadRequest | PluginShareSaveRequest + | PluginShareUpdateTargetsRequest | PluginShareListRequest | PluginShareDeleteRequest | AppListRequest - | DeviceKeyCreateRequest - | DeviceKeyPublicRequest - | DeviceKeySignRequest | FsReadFileRequest | FsWriteFileRequest | FsCreateDirectoryRequest @@ -8462,6 +8537,7 @@ class ClientRequest( | McpServerResourceReadRequest | McpServerToolCallRequest | WindowsSandboxSetupStartRequest + | WindowsSandboxReadinessRequest | AccountLoginStartRequest | AccountLoginCancelRequest | AccountLogoutRequest @@ -8512,12 +8588,10 @@ class ClientRequest( | PluginReadRequest | PluginSkillReadRequest | PluginShareSaveRequest + | PluginShareUpdateTargetsRequest | PluginShareListRequest | PluginShareDeleteRequest | AppListRequest - | DeviceKeyCreateRequest - | DeviceKeyPublicRequest - | DeviceKeySignRequest | FsReadFileRequest | FsWriteFileRequest | FsCreateDirectoryRequest @@ -8544,6 +8618,7 @@ class ClientRequest( | McpServerResourceReadRequest | McpServerToolCallRequest | WindowsSandboxSetupStartRequest + | WindowsSandboxReadinessRequest | AccountLoginStartRequest | AccountLoginCancelRequest | AccountLogoutRequest @@ -8568,6 +8643,19 @@ class ClientRequest( ] +class PluginListResponse(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + featured_plugin_ids: Annotated[ + list[str] | None, Field(alias="featuredPluginIds") + ] = [] + marketplace_load_errors: Annotated[ + list[MarketplaceLoadErrorInfo] | None, Field(alias="marketplaceLoadErrors") + ] = [] + marketplaces: list[PluginMarketplaceEntry] + + class ThreadStartedServerNotification(BaseModel): model_config = ConfigDict( populate_by_name=True, @@ -8604,6 +8692,8 @@ class ServerNotification( | ItemAgentMessageDeltaServerNotification | ItemPlanDeltaServerNotification | CommandExecOutputDeltaServerNotification + | ProcessOutputDeltaServerNotification + | ProcessExitedServerNotification | ItemCommandExecutionOutputDeltaServerNotification | ItemCommandExecutionTerminalInteractionServerNotification | ItemFileChangeOutputDeltaServerNotification @@ -8671,6 +8761,8 @@ class ServerNotification( | ItemAgentMessageDeltaServerNotification | ItemPlanDeltaServerNotification | CommandExecOutputDeltaServerNotification + | ProcessOutputDeltaServerNotification + | ProcessExitedServerNotification | ItemCommandExecutionOutputDeltaServerNotification | ItemCommandExecutionTerminalInteractionServerNotification | ItemFileChangeOutputDeltaServerNotification diff --git a/sdk/python/tests/test_artifact_workflow_and_binaries.py b/sdk/python/tests/test_artifact_workflow_and_binaries.py index 98710fdce2..ff77d77016 100644 --- a/sdk/python/tests/test_artifact_workflow_and_binaries.py +++ b/sdk/python/tests/test_artifact_workflow_and_binaries.py @@ -52,7 +52,8 @@ def test_generate_types_wires_all_generation_steps() -> None: ( node for node in tree.body - if isinstance(node, ast.FunctionDef) and node.name == "generate_types" + if isinstance(node, ast.FunctionDef) + and node.name == "generate_types_from_schema_dir" ), None, ) @@ -72,19 +73,17 @@ def test_generate_types_wires_all_generation_steps() -> None: ] -def test_schema_normalization_only_flattens_string_literal_oneofs() -> None: +def _load_runtime_schema_bundle(tmp_path: Path) -> dict: script = _load_update_script_module() - schema = json.loads( - ( - ROOT.parent.parent - / "codex-rs" - / "app-server-protocol" - / "schema" - / "json" - / "codex_app_server_protocol.v2.schemas.json" - ).read_text() - ) + schema_dir = script.generate_schema_from_pinned_runtime(tmp_path / "schema") + return json.loads(script.schema_bundle_path(schema_dir).read_text()) + +def test_schema_normalization_only_flattens_string_literal_oneofs( + tmp_path: Path, +) -> None: + script = _load_update_script_module() + schema = _load_runtime_schema_bundle(tmp_path) definitions = schema["definitions"] flattened = [ name @@ -94,27 +93,22 @@ def test_schema_normalization_only_flattens_string_literal_oneofs() -> None: ] assert flattened == [ - "AuthMode", - "CommandExecOutputStream", - "ExperimentalFeatureStage", - "InputModality", "MessagePhase", + "TurnItemsView", + "PluginAvailability", + "AuthMode", + "InputModality", + "ExperimentalFeatureStage", + "CommandExecOutputStream", + "ProcessOutputStream", ] -def test_python_codegen_schema_annotation_adds_stable_variant_titles() -> None: +def test_python_codegen_schema_annotation_adds_stable_variant_titles( + tmp_path: Path, +) -> None: script = _load_update_script_module() - schema = json.loads( - ( - ROOT.parent.parent - / "codex-rs" - / "app-server-protocol" - / "schema" - / "json" - / "codex_app_server_protocol.v2.schemas.json" - ).read_text() - ) - + schema = _load_runtime_schema_bundle(tmp_path) script._annotate_schema(schema) definitions = schema["definitions"] @@ -186,6 +180,25 @@ def test_runtime_distribution_name_is_consistent() -> None: ) +def test_source_sdk_package_pins_published_runtime() -> None: + """The source package metadata should pin the runtime wheel that ships schemas.""" + script = _load_update_script_module() + pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text()) + + assert { + "sdk_version": pyproject["project"]["version"], + "runtime_pin": script.pinned_runtime_version(), + "dependencies": pyproject["project"]["dependencies"], + } == { + "sdk_version": "0.131.0a4", + "runtime_pin": "0.131.0a4", + "dependencies": [ + "pydantic>=2.12", + "openai-codex-cli-bin==0.131.0a4", + ], + } + + def test_release_metadata_retries_without_invalid_auth( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -212,22 +225,6 @@ def test_release_metadata_retries_without_invalid_auth( assert authorizations == ["Bearer invalid-token", None] -def test_source_sdk_package_pins_published_runtime() -> None: - """The source package metadata should pin the runtime wheel that ships schemas.""" - pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text()) - - assert { - "sdk_version": pyproject["project"]["version"], - "dependencies": pyproject["project"]["dependencies"], - } == { - "sdk_version": "0.131.0a4", - "dependencies": [ - "pydantic>=2.12", - "openai-codex-cli-bin==0.131.0a4", - ], - } - - 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.""" runtime_setup = _load_runtime_setup_module() @@ -422,9 +419,7 @@ def test_runtime_resource_binaries_are_included_by_wheel_config( pyproject = tomllib.loads((staged / "pyproject.toml").read_text()) assert { "include": pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["include"], - "helper": ( - staged / "src" / "codex_cli_bin" / "bin" / "helper" - ).read_text(), + "helper": (staged / "src" / "codex_cli_bin" / "bin" / "helper").read_text(), } == { "include": ["src/codex_cli_bin/bin/**"], "helper": "fake helper\n", diff --git a/sdk/python/tests/test_client_rpc_methods.py b/sdk/python/tests/test_client_rpc_methods.py index a049960b07..7dc5ad4d86 100644 --- a/sdk/python/tests/test_client_rpc_methods.py +++ b/sdk/python/tests/test_client_rpc_methods.py @@ -66,6 +66,7 @@ def test_thread_resume_response_accepts_auto_review_reviewer() -> None: "id": "thread-1", "modelProvider": "openai", "preview": "", + "sessionId": "session-1", "source": "cli", "status": {"type": "idle"}, "turns": [], diff --git a/sdk/python/tests/test_contract_generation.py b/sdk/python/tests/test_contract_generation.py index bb5ec18bbc..f01b58c884 100644 --- a/sdk/python/tests/test_contract_generation.py +++ b/sdk/python/tests/test_contract_generation.py @@ -1,5 +1,6 @@ from __future__ import annotations +import importlib.metadata import os import subprocess import sys @@ -29,15 +30,19 @@ def _snapshot_target(root: Path, rel_path: Path) -> dict[str, bytes] | bytes | N def _snapshot_targets(root: Path) -> dict[str, dict[str, bytes] | bytes | None]: return { - str(rel_path): _snapshot_target(root, rel_path) for rel_path in GENERATED_TARGETS + str(rel_path): _snapshot_target(root, rel_path) + for rel_path in GENERATED_TARGETS } def test_generated_files_are_up_to_date(): before = _snapshot_targets(ROOT) - # Regenerate contract artifacts via single maintenance entrypoint. + # 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" env = os.environ.copy() + env.pop("CODEX_EXEC_PATH", None) python_bin = str(Path(sys.executable).parent) env["PATH"] = f"{python_bin}{os.pathsep}{env.get('PATH', '')}" diff --git a/sdk/python/tests/test_public_api_runtime_behavior.py b/sdk/python/tests/test_public_api_runtime_behavior.py index 3cf18c4e47..58b8ad43fe 100644 --- a/sdk/python/tests/test_public_api_runtime_behavior.py +++ b/sdk/python/tests/test_public_api_runtime_behavior.py @@ -93,6 +93,7 @@ def _item_completed_notification( method="item/completed", payload=ItemCompletedNotification.model_validate( { + "completedAtMs": 1, "item": item, "threadId": thread_id, "turnId": turn_id, diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index b432a6c33f..e72160e169 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -70,6 +70,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "service_name", "service_tier", "session_start_source", + "thread_source", ], Codex.thread_list: [ "archived", @@ -108,6 +109,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "model_provider", "sandbox", "service_tier", + "thread_source", ], Thread.turn: [ "approval_policy", @@ -148,6 +150,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "service_name", "service_tier", "session_start_source", + "thread_source", ], AsyncCodex.thread_list: [ "archived", @@ -186,6 +189,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "model_provider", "sandbox", "service_tier", + "thread_source", ], AsyncThread.turn: [ "approval_policy",