From f752b25fc43d5e79a4f1cd1878a51702e7fea977 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Tue, 2 Jun 2026 15:12:07 -0700 Subject: [PATCH 1/3] Revert "Use environment secrets for Azure signing" (#25948) Reverts openai/codex#24859 --- .github/workflows/rust-release-windows.yml | 16 +++++++++++++--- .github/workflows/rust-release.yml | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index 0d8cc29697..fc14088ef1 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -6,6 +6,19 @@ on: release-lto: required: true type: string + secrets: + AZURE_TRUSTED_SIGNING_CLIENT_ID: + required: true + AZURE_TRUSTED_SIGNING_TENANT_ID: + required: true + AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID: + required: true + AZURE_TRUSTED_SIGNING_ENDPOINT: + required: true + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: + required: true + AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME: + required: true # Cargo's libgit2 transport has been flaky when fetching git dependencies with # nested submodules. Prefer the system git CLI across every Cargo invocation. @@ -151,9 +164,6 @@ jobs: - build-windows-binaries name: Build - ${{ matrix.runner }} - ${{ matrix.target }} runs-on: ${{ matrix.runs_on }} - environment: - name: azure-artifact-signing - deployment: false timeout-minutes: 90 permissions: contents: read diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 6b0814dae5..364fe78902 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -865,6 +865,7 @@ jobs: uses: ./.github/workflows/rust-release-windows.yml with: release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }} + secrets: inherit argument-comment-lint-release-assets: if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} From bec21c71142ed44d8eb7d7ca5719c87d961044d0 Mon Sep 17 00:00:00 2001 From: Won Park Date: Tue, 2 Jun 2026 15:27:52 -0700 Subject: [PATCH 2/3] Expose standalone image generation in code mode (#25923) ## Why Standalone image generation remained top-level-only in code-mode sessions. ## What changed - Change imagegen exposure from `DirectModelOnly` to `Direct`. - Keep direct-mode access while enabling nested code-mode access. - Add a focused regression test for the exposure contract. ## Validation - `just test -p codex-image-generation-extension` --- ...ge_generation.rs => imagegen_extension.rs} | 152 ++++++++++++++---- codex-rs/app-server/tests/suite/v2/mod.rs | 2 +- codex-rs/ext/image-generation/src/tool.rs | 4 +- 3 files changed, 125 insertions(+), 33 deletions(-) rename codex-rs/app-server/tests/suite/v2/{image_generation.rs => imagegen_extension.rs} (70%) diff --git a/codex-rs/app-server/tests/suite/v2/image_generation.rs b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs similarity index 70% rename from codex-rs/app-server/tests/suite/v2/image_generation.rs rename to codex-rs/app-server/tests/suite/v2/imagegen_extension.rs index e1f4cfbc9a..924c6c58ba 100644 --- a/codex-rs/app-server/tests/suite/v2/image_generation.rs +++ b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs @@ -30,6 +30,12 @@ use wiremock::matchers::path; const RESULT: &str = "cG5n"; +#[derive(Clone, Copy)] +enum ImagegenTestMode { + Direct, + CodeModeOnly, +} + // macOS and Windows Bazel CI can spend tens of seconds starting app-server // subprocesses or processing test RPCs under load. #[cfg(any(target_os = "macos", windows))] @@ -69,7 +75,7 @@ async fn standalone_image_generation_persists_image_and_returns_it_to_model() -> .await; let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri())?; + create_config_toml(codex_home.path(), &server.uri(), ImagegenTestMode::Direct)?; write_chatgpt_auth( codex_home.path(), ChatGptAuthFixture::new("access-chatgpt"), @@ -79,34 +85,7 @@ async fn standalone_image_generation_persists_image_and_returns_it_to_model() -> let mut mcp = TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - let thread_req = mcp - .send_thread_start_request(ThreadStartParams::default()) - .await?; - let thread_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; - - let turn_req = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id, - client_user_message_id: None, - input: vec![V2UserInput::Text { - text: "Generate an image".to_string(), - text_elements: Vec::new(), - }], - ..Default::default() - }) - .await?; - let turn_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), - ) - .await??; - let _turn: TurnStartResponse = to_response::(turn_resp)?; + start_image_generation_turn(&mut mcp).await?; let completed = timeout( DEFAULT_READ_TIMEOUT, @@ -157,6 +136,110 @@ async fn standalone_image_generation_persists_image_and_returns_it_to_model() -> Ok(()) } +#[cfg_attr(windows, ignore = "covered by Linux and macOS CI")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn standalone_image_generation_is_callable_from_code_mode_only() -> Result<()> { + let call_id = "code-mode-image-run-1"; + let server = responses::start_mock_server().await; + mount_image_response(&server).await; + + let response_mock = responses::mount_sse_sequence( + &server, + vec![ + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_custom_tool_call( + call_id, + "exec", + r#" +const result = await tools.image_gen__imagegen({ + action: "generate", + prompt: "paint a blue whale", +}); +image(result); +"#, + ), + responses::ev_completed("resp-1"), + ]), + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "Done"), + responses::ev_completed("resp-2"), + ]), + ], + ) + .await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + ImagegenTestMode::CodeModeOnly, + )?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("access-chatgpt"), + AuthCredentialsStoreMode::File, + )?; + + let mut mcp = + TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + start_image_generation_turn(&mut mcp).await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let requests = response_mock.requests(); + assert_eq!(requests.len(), 2); + assert!(requests[0].body_contains_text("image_gen__imagegen")); + let output = requests[1].custom_tool_call_output(call_id); + assert_eq!( + output["output"][1], + json!({ + "type": "input_image", + "image_url": format!("data:image/png;base64,{RESULT}"), + "detail": "high", + }) + ); + assert_eq!(output["output"].as_array().map(Vec::len), Some(2)); + + Ok(()) +} + +async fn start_image_generation_turn(mcp: &mut TestAppServer) -> Result<()> { + let thread_req = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + client_user_message_id: None, + input: vec![V2UserInput::Text { + text: "Generate an image".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + let _turn: TurnStartResponse = to_response::(turn_resp)?; + + Ok(()) +} + async fn wait_for_image_generation_completed( mcp: &mut TestAppServer, ) -> Result { @@ -187,7 +270,15 @@ async fn mount_image_response(server: &MockServer) { .await; } -fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> { +fn create_config_toml( + codex_home: &Path, + server_uri: &str, + mode: ImagegenTestMode, +) -> std::io::Result<()> { + let code_mode_only = match mode { + ImagegenTestMode::Direct => "", + ImagegenTestMode::CodeModeOnly => "code_mode_only = true", + }; std::fs::write( codex_home.join("config.toml"), format!( @@ -200,6 +291,7 @@ chatgpt_base_url = "{server_uri}" [features] imagegenext = true +{code_mode_only} [model_providers.openai-custom] name = "OpenAI" diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 950cccebb5..9aef40aade 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -17,7 +17,7 @@ mod experimental_feature_list; mod external_agent_config; mod fs; mod hooks_list; -mod image_generation; +mod imagegen_extension; mod initialize; mod marketplace_add; mod marketplace_remove; diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index 41d407d912..e4e8e13135 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -77,9 +77,9 @@ impl ToolExecutor for ImageGenerationTool { imagegen_tool_spec() } - /// Keeps this model-facing tool out of the nested code-mode tool surface. + /// Exposes image generation directly and through the nested code-mode tool surface. fn exposure(&self) -> ToolExposure { - ToolExposure::DirectModelOnly + ToolExposure::Direct } /// Executes the selected image operation and returns the completed image result. From 9ab5f3ca1b7c06f8742a41cdb1434410b01f73bd Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 2 Jun 2026 15:39:33 -0700 Subject: [PATCH 3/3] config: default untrusted projects to read-only permissions --- codex-rs/config/src/config_toml.rs | 30 +++---- .../core/src/config/config_loader_tests.rs | 6 +- codex-rs/core/src/config/config_tests.rs | 87 ++++++++++++------- codex-rs/core/src/config/permissions.rs | 2 +- 4 files changed, 70 insertions(+), 55 deletions(-) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 0311482cf2..4bfba5c8ff 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -728,29 +728,23 @@ impl ConfigToml { active_project: Option<&ProjectConfig>, permission_profile_constraint: Option<&crate::Constrained>, ) -> PermissionProfile { - let sandbox_mode_was_explicit = - sandbox_mode_override.is_some() || self.sandbox_mode.is_some(); - let resolved_sandbox_mode = sandbox_mode_override - .or(self.sandbox_mode) - .or(if sandbox_mode_was_explicit { - None - } else { - // If no sandbox_mode is set but this directory has a trust decision, - // default to workspace-write except on unsandboxed Windows where we + let configured_sandbox_mode = sandbox_mode_override.or(self.sandbox_mode); + let resolved_sandbox_mode = configured_sandbox_mode + .or_else(|| { + // If no sandbox_mode is set for a trusted directory, default + // to workspace-write except on unsandboxed Windows where we // default to read-only. - active_project.and_then(|p| { - if p.is_trusted() || p.is_untrusted() { + active_project + .filter(|project| project.is_trusted()) + .map(|_| { if cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled { - Some(SandboxMode::ReadOnly) + SandboxMode::ReadOnly } else { - Some(SandboxMode::WorkspaceWrite) + SandboxMode::WorkspaceWrite } - } else { - None - } - }) + }) }) .unwrap_or_default(); let effective_sandbox_mode = if cfg!(target_os = "windows") @@ -788,7 +782,7 @@ impl ConfigToml { }, SandboxMode::DangerFullAccess => PermissionProfile::Disabled, }; - if !sandbox_mode_was_explicit + if configured_sandbox_mode.is_none() && let Some(constraint) = permission_profile_constraint && let Err(err) = constraint.can_set(&permission_profile) { diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 5edecebec0..01c23f5408 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1423,11 +1423,7 @@ async fn system_allowed_permissions_keep_builtin_permission_fallbacks() -> anyho ), ( Some(TrustLevel::Untrusted), - if cfg!(target_os = "windows") { - BUILT_IN_PERMISSION_PROFILE_READ_ONLY - } else { - BUILT_IN_PERMISSION_PROFILE_WORKSPACE - }, + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, ), (None, BUILT_IN_PERMISSION_PROFILE_READ_ONLY), ] { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 192e449ff8..c493cd8e86 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2548,6 +2548,50 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std:: Ok(()) } +#[tokio::test] +async fn empty_config_defaults_to_builtin_read_only_for_untrusted_project() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let project_key = cwd.path().to_string_lossy().to_string(); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + projects: Some(HashMap::from([( + project_key, + ProjectConfig { + trust_level: Some(TrustLevel::Untrusted), + }, + )])), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert_eq!( + config + .permissions + .active_permission_profile() + .as_ref() + .map(|active| active.id.as_str()), + Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY) + ); + assert!( + policy.can_read_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to allow reads, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected untrusted project fallback to use :read-only, policy: {policy:?}" + ); + Ok(()) +} + #[tokio::test] async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_settings() -> std::io::Result<()> { @@ -8533,7 +8577,7 @@ async fn test_load_config_rejects_legacy_ollama_chat_provider_with_helpful_error } #[tokio::test] -async fn test_untrusted_project_gets_workspace_write_sandbox() -> anyhow::Result<()> { +async fn test_untrusted_project_gets_read_only_sandbox() -> anyhow::Result<()> { let config_with_untrusted = r#" [projects."/tmp/test"] trust_level = "untrusted" @@ -8554,18 +8598,10 @@ trust_level = "untrusted" ) .await; - // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) - if cfg!(target_os = "windows") { - assert!( - matches!(resolution, SandboxPolicy::ReadOnly { .. }), - "Expected ReadOnly on Windows, got {resolution:?}" - ); - } else { - assert!( - matches!(resolution, SandboxPolicy::WorkspaceWrite { .. }), - "Expected WorkspaceWrite for untrusted project, got {resolution:?}" - ); - } + assert!( + matches!(resolution, SandboxPolicy::ReadOnly { .. }), + "Expected ReadOnly for untrusted project, got {resolution:?}" + ); Ok(()) } @@ -8922,24 +8958,13 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow: "Expected UnlessTrusted approval policy for untrusted project" ); - // Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows) - if cfg!(target_os = "windows") { - assert!( - matches!( - &config.legacy_sandbox_policy(), - SandboxPolicy::ReadOnly { .. } - ), - "Expected ReadOnly on Windows" - ); - } else { - assert!( - matches!( - &config.legacy_sandbox_policy(), - SandboxPolicy::WorkspaceWrite { .. } - ), - "Expected WorkspaceWrite sandbox for untrusted project" - ); - } + assert!( + matches!( + &config.legacy_sandbox_policy(), + SandboxPolicy::ReadOnly { .. } + ), + "Expected ReadOnly sandbox for untrusted project" + ); Ok(()) } diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index f683d9c7eb..f338d2d61d 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -49,7 +49,7 @@ pub(crate) fn default_builtin_permission_profile_name( active_project: &ProjectConfig, windows_sandbox_level: WindowsSandboxLevel, ) -> &'static str { - if (active_project.is_trusted() || active_project.is_untrusted()) + if active_project.is_trusted() && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled) { BUILT_IN_WORKSPACE_PROFILE