From 62ff1a454ccf88d3835a29cda1b6c730daf94c31 Mon Sep 17 00:00:00 2001 From: sdcoffey Date: Mon, 9 Mar 2026 20:55:29 -0700 Subject: [PATCH] add ci check for generated python --- .github/workflows/rust-ci.yml | 24 +++++ .../codex_app_server_protocol/models.py | 31 +++++++ .../codex_app_server_protocol/v2/models.py | 25 ++++++ codex-rs/app-server-protocol/src/export.rs | 88 +++++++++---------- 4 files changed, 120 insertions(+), 48 deletions(-) diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 46de51bc69..ad1aa565f0 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -65,6 +65,26 @@ jobs: - name: cargo fmt run: cargo fmt -- --config imports_granularity=Item --check + schema_fixtures: + name: App-server schema fixtures + runs-on: ubuntu-24.04 + needs: changed + if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }} + defaults: + run: + working-directory: codex-rs + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - uses: astral-sh/setup-uv@v6 + - uses: dtolnay/rust-toolchain@1.93.0 + - name: Regenerate app-server schema fixtures + run: cargo run -p codex-app-server-protocol --bin write_schema_fixtures + - name: Check vendored schema fixtures are up to date + run: git diff --exit-code -- app-server-protocol/schema + cargo_shear: name: cargo shear runs-on: ubuntu-24.04 @@ -200,6 +220,10 @@ jobs: with: targets: ${{ matrix.target }} components: clippy + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - uses: astral-sh/setup-uv@v6 - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}} name: Use hermetic Cargo home (musl) diff --git a/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/models.py b/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/models.py index c6669e5ab6..9e06c4a8de 100644 --- a/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/models.py +++ b/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/models.py @@ -141,6 +141,7 @@ class AskForApproval1(StrEnum): class Reject(BaseModel): mcp_elicitations: bool + request_permissions: bool rules: bool sandbox_approval: bool @@ -324,6 +325,12 @@ class PluginInstallClientRequestMethod(RootModel[Literal["plugin/install"]]): ) +class PluginUninstallClientRequestMethod(RootModel[Literal["plugin/uninstall"]]): + root: Literal["plugin/uninstall"] = Field( + ..., title="Plugin/uninstallClientRequestMethod" + ) + + class TurnStartClientRequestMethod(RootModel[Literal["turn/start"]]): root: Literal["turn/start"] = Field(..., title="Turn/startClientRequestMethod") @@ -2378,6 +2385,11 @@ class PatchChangeKind( root: AddPatchChangeKind | DeletePatchChangeKind | UpdatePatchChangeKind +class PermissionGrantScope(StrEnum): + turn = "turn" + session = "session" + + class PermissionProfile(BaseModel): file_system: FileSystemPermissions | None = None macos: MacOsSeatbeltProfileExtensions | None = None @@ -2421,6 +2433,10 @@ class PluginListParams(BaseModel): ) +class PluginUninstallParams(BaseModel): + plugin_id: str = Field(..., alias="pluginId") + + class ProductSurface(StrEnum): chatgpt = "chatgpt" codex = "codex" @@ -2625,6 +2641,10 @@ class ReasoningTextDeltaNotification(BaseModel): class RejectConfig(BaseModel): mcp_elicitations: bool = Field(..., description="Reject MCP elicitation prompts.") + request_permissions: bool = Field( + ..., + description="Reject approval prompts related to built-in permission requests.", + ) rules: bool = Field( ..., description="Reject prompts triggered by execpolicy `prompt` rules." ) @@ -4699,6 +4719,14 @@ class PluginInstallClientRequest(BaseModel): params: PluginInstallParams +class PluginUninstallClientRequest(BaseModel): + id: RequestId + method: PluginUninstallClientRequestMethod = Field( + ..., title="Plugin/uninstallClientRequestMethod" + ) + params: PluginUninstallParams + + class TurnInterruptClientRequest(BaseModel): id: RequestId method: TurnInterruptClientRequestMethod = Field( @@ -5686,6 +5714,7 @@ class PermissionsRequestApprovalParams(BaseModel): class PermissionsRequestApprovalResponse(BaseModel): permissions: GrantedPermissionProfile + scope: PermissionGrantScope | None = "turn" class PlanItemArg(BaseModel): @@ -6972,6 +7001,7 @@ class ClientRequest( | AppListClientRequest | SkillsConfigWriteClientRequest | PluginInstallClientRequest + | PluginUninstallClientRequest | TurnStartClientRequest | TurnSteerClientRequest | TurnInterruptClientRequest @@ -7023,6 +7053,7 @@ class ClientRequest( | AppListClientRequest | SkillsConfigWriteClientRequest | PluginInstallClientRequest + | PluginUninstallClientRequest | TurnStartClientRequest | TurnSteerClientRequest | TurnInterruptClientRequest diff --git a/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/v2/models.py b/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/v2/models.py index 885e55da1a..04b6ffa8e1 100644 --- a/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/v2/models.py +++ b/codex-rs/app-server-protocol/schema/python/codex_app_server_protocol/v2/models.py @@ -179,6 +179,7 @@ class AskForApproval1(StrEnum): class Reject(BaseModel): mcp_elicitations: bool + request_permissions: bool rules: bool sandbox_approval: bool @@ -341,6 +342,12 @@ class PluginInstallClientRequestMethod(RootModel[Literal["plugin/install"]]): ) +class PluginUninstallClientRequestMethod(RootModel[Literal["plugin/uninstall"]]): + root: Literal["plugin/uninstall"] = Field( + ..., title="Plugin/uninstallClientRequestMethod" + ) + + class TurnStartClientRequestMethod(RootModel[Literal["turn/start"]]): root: Literal["turn/start"] = Field(..., title="Turn/startClientRequestMethod") @@ -2535,6 +2542,14 @@ class PluginSummary(BaseModel): source: PluginSource +class PluginUninstallParams(BaseModel): + plugin_id: str = Field(..., alias="pluginId") + + +class PluginUninstallResponse(BaseModel): + pass + + class ProductSurface(StrEnum): chatgpt = "chatgpt" codex = "codex" @@ -4796,6 +4811,14 @@ class PluginInstallClientRequest(BaseModel): params: PluginInstallParams +class PluginUninstallClientRequest(BaseModel): + id: RequestId + method: PluginUninstallClientRequestMethod = Field( + ..., title="Plugin/uninstallClientRequestMethod" + ) + params: PluginUninstallParams + + class TurnInterruptClientRequest(BaseModel): id: RequestId method: TurnInterruptClientRequestMethod = Field( @@ -6994,6 +7017,7 @@ class ClientRequest( | AppListClientRequest | SkillsConfigWriteClientRequest | PluginInstallClientRequest + | PluginUninstallClientRequest | TurnStartClientRequest | TurnSteerClientRequest | TurnInterruptClientRequest @@ -7045,6 +7069,7 @@ class ClientRequest( | AppListClientRequest | SkillsConfigWriteClientRequest | PluginInstallClientRequest + | PluginUninstallClientRequest | TurnStartClientRequest | TurnSteerClientRequest | TurnInterruptClientRequest diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 2f047a7bfb..fec47dd368 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -334,7 +334,7 @@ pub fn generate_python_with_options( } fn generate_python_models(schema_path: &Path, output_path: &Path) -> Result<()> { - let mut command = datamodel_codegen_command(); + let mut command = datamodel_codegen_command()?; command .arg("--input") .arg(schema_path) @@ -370,7 +370,7 @@ fn generate_python_models(schema_path: &Path, output_path: &Path) -> Result<()> if !status.success() { return Err(anyhow!( "Python model generator failed with status {status}. \ -Install `uv`/`uvx` or `datamodel-codegen` to regenerate app-server Python bindings." +Install `uv` to run the vendored Python codegen toolchain." )); } @@ -385,33 +385,28 @@ Install `uv`/`uvx` or `datamodel-codegen` to regenerate app-server Python bindin Ok(()) } -fn datamodel_codegen_command() -> Command { - if command_exists("uv") { - // Keep the uv-managed tool environment next to the protocol crate source, - // not under schema/python/, which is regenerated as a fixture artifact. - let python_codegen_project = Path::new(env!("CARGO_MANIFEST_DIR")).join("python"); - let mut command = Command::new("uv"); - command - .arg("run") - .arg("--project") - .arg(python_codegen_project) - .arg("--locked") - .arg("python") - .arg("-m") - .arg("datamodel_code_generator"); - return command; +fn datamodel_codegen_command() -> Result { + // Keep the uv-managed tool environment next to the protocol crate source, + // not under schema/python/, which is regenerated as a fixture artifact. + let python_codegen_project = Path::new(env!("CARGO_MANIFEST_DIR")).join("python"); + if !uv_exists() { + return Err(anyhow!( + "Python model generation requires `uv` to be installed so the vendored \ +Python codegen toolchain can run from {}.", + python_codegen_project.display() + )); } - if command_exists("uvx") { - let mut command = Command::new("uvx"); - command - .arg("--from") - .arg("datamodel-code-generator") - .arg("datamodel-codegen"); - return command; - } - - Command::new("datamodel-codegen") + let mut command = Command::new("uv"); + command + .arg("run") + .arg("--project") + .arg(python_codegen_project) + .arg("--locked") + .arg("python") + .arg("-m") + .arg("datamodel_code_generator"); + Ok(command) } fn run_ruff_on_python_files(ruff: Option<&Path>, out_dir: &Path) -> Result<()> { @@ -438,39 +433,36 @@ fn ruff_command(ruff: Option<&Path>) -> Result { return Ok(Command::new(ruff_bin)); } - if command_exists("uv") { - let python_codegen_project = Path::new(env!("CARGO_MANIFEST_DIR")).join("python"); - let mut command = Command::new("uv"); - command - .arg("run") - .arg("--project") - .arg(python_codegen_project) - .arg("--locked") - .arg("ruff"); - return Ok(command); + let python_codegen_project = Path::new(env!("CARGO_MANIFEST_DIR")).join("python"); + if !uv_exists() { + return Err(anyhow!( + "Ruff formatting requires `uv` to be installed so the vendored Python \ +codegen toolchain can run from {}, or pass `--ruff /path/to/ruff`.", + python_codegen_project.display() + )); } - if command_exists("ruff") { - return Ok(Command::new("ruff")); - } - - Err(anyhow!( - "Ruff was requested for Python generation, but no Ruff command is available. \ -Install `uv` for the vendored Python toolchain or pass `--ruff /path/to/ruff`." - )) + let mut command = Command::new("uv"); + command + .arg("run") + .arg("--project") + .arg(python_codegen_project) + .arg("--locked") + .arg("ruff"); + Ok(command) } -fn command_exists(command: &str) -> bool { +fn uv_exists() -> bool { std::env::var_os("PATH").is_some_and(|paths| { std::env::split_paths(&paths).any(|path| { - let candidate = path.join(command); + let candidate = path.join("uv"); if candidate.is_file() { return true; } #[cfg(windows)] { - let candidate = path.join(format!("{command}.exe")); + let candidate = path.join("uv.exe"); candidate.is_file() }