diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 655ad2d0adcd..c5043ef52256 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -325,7 +325,7 @@ impl From for AskForApproval { pub enum ApprovalsReviewer { #[serde(rename = "user")] User, - #[serde(rename = "auto_review", alias = "guardian_subagent")] + #[serde(rename = "guardian_subagent", alias = "auto_review")] AutoReview, } @@ -7483,7 +7483,7 @@ mod tests { ); assert_eq!( serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"), - "\"auto_review\"" + "\"guardian_subagent\"" ); for value in ["user", "auto_review", "guardian_subagent"] { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 820cd7e8ba6e..3dddde05d101 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -6610,6 +6610,28 @@ async fn feature_requirements_normalize_effective_feature_values() -> std::io::R Ok(()) } +#[tokio::test] +async fn feature_requirements_auto_review_disables_guardian_approval() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .cloud_requirements(CloudRequirementsLoader::new(async { + Ok(Some(crate::config_loader::ConfigRequirementsToml { + feature_requirements: Some(crate::config_loader::FeatureRequirementsToml { + entries: BTreeMap::from([("auto_review".to_string(), false)]), + }), + ..Default::default() + })) + })) + .build() + .await?; + + assert!(!config.features.enabled(Feature::GuardianApproval)); + + Ok(()) +} + #[tokio::test] async fn browser_feature_requirements_are_valid() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/managed_features.rs b/codex-rs/core/src/config/managed_features.rs index 2ebd4749a342..fd3032920d97 100644 --- a/codex-rs/core/src/config/managed_features.rs +++ b/codex-rs/core/src/config/managed_features.rs @@ -199,6 +199,11 @@ fn parse_feature_requirements( ) -> BTreeMap { let mut pinned_features = BTreeMap::new(); for (key, enabled) in feature_requirements.entries { + if key == "auto_review" { + pinned_features.insert(Feature::GuardianApproval, enabled); + continue; + } + if let Some(feature) = canonical_feature_for_key(&key) { pinned_features.insert(feature, enabled); continue; diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 06484c64417e..2be5c6f1242e 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -87,8 +87,8 @@ pub enum ApprovalsReviewer { #[default] #[serde(rename = "user")] User, - #[serde(rename = "auto_review", alias = "guardian_subagent")] - #[strum(serialize = "auto_review")] + #[serde(rename = "guardian_subagent", alias = "auto_review")] + #[strum(serialize = "guardian_subagent")] AutoReview, } @@ -609,7 +609,7 @@ mod tests { ); assert_eq!( serde_json::to_string(&ApprovalsReviewer::AutoReview).expect("serialize reviewer"), - "\"auto_review\"" + "\"guardian_subagent\"" ); for value in ["user", "auto_review", "guardian_subagent"] { diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index f473804cf267..bb6f8dbe6639 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1675,7 +1675,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; assert!(config.contains("guardian_approval = true")); - assert!(config.contains("approvals_reviewer = \"auto_review\"")); + assert!(config.contains("approvals_reviewer = \"guardian_subagent\"")); assert!(config.contains("approval_policy = \"on-request\"")); assert!(config.contains("sandbox_mode = \"workspace-write\"")); Ok(()) @@ -1835,7 +1835,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review ); let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(config.contains("approvals_reviewer = \"auto_review\"")); + assert!(config.contains("approvals_reviewer = \"guardian_subagent\"")); assert!(config.contains("guardian_approval = true")); assert!(config.contains("approval_policy = \"on-request\"")); assert!(config.contains("sandbox_mode = \"workspace-write\"")); @@ -1969,7 +1969,7 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev ); assert_eq!( profile_config.get("approvals_reviewer"), - Some(&TomlValue::String("auto_review".to_string())) + Some(&TomlValue::String("guardian_subagent".to_string())) ); Ok(()) } diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index cbf40d0d7541..d33df0f681f1 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -108,6 +108,7 @@ mod tests { approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: None, cwd: cwd.abs(), instruction_source_paths: Vec::new(), reasoning_effort: None, @@ -155,7 +156,7 @@ mod tests { .insert(side_thread_id, SideThreadState::new(main_thread_id)); app.config.permissions.approval_policy = codex_config::Constrained::allow_any(AskForApproval::OnRequest); - app.config.approvals_reviewer = ApprovalsReviewer::GuardianSubagent; + app.config.approvals_reviewer = ApprovalsReviewer::AutoReview; app.config.permissions.sandbox_policy = codex_config::Constrained::allow_any(SandboxPolicy::new_workspace_write_policy()); @@ -164,7 +165,7 @@ mod tests { let expected_main_session = ThreadSessionState { approval_policy: AskForApproval::OnRequest, - approvals_reviewer: ApprovalsReviewer::GuardianSubagent, + approvals_reviewer: ApprovalsReviewer::AutoReview, sandbox_policy: SandboxPolicy::new_workspace_write_policy(), ..main_session }; diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 1502db16f75c..1c48c4491832 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -724,7 +724,7 @@ mod tests { rendered.contains("allowed_approval_policies: on-request (source: cloud requirements)") ); assert!(rendered.contains( - "allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))" + "allowed_approvals_reviewers: guardian_subagent (source: MDM managed_config.toml (legacy))" )); assert!( rendered.contains( @@ -779,7 +779,7 @@ mod tests { let rendered = render_to_text(&render_debug_config_lines(&stack)); assert!(rendered.contains( - "allowed_approvals_reviewers: auto_review (source: MDM managed_config.toml (legacy))" + "allowed_approvals_reviewers: guardian_subagent (source: MDM managed_config.toml (legacy))" )); assert!(!rendered.contains("Requirements:\n ")); } 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 c75b3e89af35..fac98223d77c 100644 --- a/sdk/python/src/codex_app_server/generated/v2_all.py +++ b/sdk/python/src/codex_app_server/generated/v2_all.py @@ -146,6 +146,7 @@ class AppToolsConfig(BaseModel): class ApprovalsReviewer(Enum): user = "user" + auto_review = "auto_review" guardian_subagent = "guardian_subagent" diff --git a/sdk/python/tests/test_client_rpc_methods.py b/sdk/python/tests/test_client_rpc_methods.py index 274218056699..f1dd353b1fb8 100644 --- a/sdk/python/tests/test_client_rpc_methods.py +++ b/sdk/python/tests/test_client_rpc_methods.py @@ -4,7 +4,12 @@ from typing import Any from codex_app_server.client import AppServerClient, _params_dict -from codex_app_server.generated.v2_all import ThreadListParams, ThreadTokenUsageUpdatedNotification +from codex_app_server.generated.v2_all import ( + ApprovalsReviewer, + ThreadListParams, + ThreadResumeResponse, + ThreadTokenUsageUpdatedNotification, +) from codex_app_server.models import UnknownNotification ROOT = Path(__file__).resolve().parents[1] @@ -40,6 +45,34 @@ def test_generated_v2_bundle_has_single_shared_plan_type_definition() -> None: assert source.count("class PlanType(") == 1 +def test_thread_resume_response_accepts_auto_review_reviewer() -> None: + response = ThreadResumeResponse.model_validate( + { + "approvalPolicy": "on-request", + "approvalsReviewer": "auto_review", + "cwd": "/tmp", + "model": "gpt-5", + "modelProvider": "openai", + "sandbox": {"type": "dangerFullAccess"}, + "thread": { + "cliVersion": "1.0.0", + "createdAt": 1, + "cwd": "/tmp", + "ephemeral": False, + "id": "thread-1", + "modelProvider": "openai", + "preview": "", + "source": "cli", + "status": {"type": "idle"}, + "turns": [], + "updatedAt": 1, + }, + } + ) + + assert response.approvals_reviewer is ApprovalsReviewer.auto_review + + def test_notifications_are_typed_with_canonical_v2_methods() -> None: client = AppServerClient() event = client._coerce_notification( @@ -89,7 +122,9 @@ def test_unknown_notifications_fall_back_to_unknown_payloads() -> None: def test_invalid_notification_payload_falls_back_to_unknown() -> None: client = AppServerClient() - event = client._coerce_notification("thread/tokenUsage/updated", {"threadId": "missing"}) + event = client._coerce_notification( + "thread/tokenUsage/updated", {"threadId": "missing"} + ) assert event.method == "thread/tokenUsage/updated" assert isinstance(event.payload, UnknownNotification)