diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 62a8f55d1ba..d7631473c94 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -29,6 +29,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { priority, upgrade: preset.upgrade.as_ref().map(|u| u.into()), base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index cc471ae8901..03c758c6b4f 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -77,6 +77,7 @@ async fn models_client_hits_models_endpoint() { priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/src/guardian.rs b/codex-rs/core/src/guardian.rs index 2bdca0da016..29a41a80d2d 100644 --- a/codex-rs/core/src/guardian.rs +++ b/codex-rs/core/src/guardian.rs @@ -538,11 +538,19 @@ async fn run_guardian_subagent( }; (turn.model_info.slug.clone(), reasoning_effort) }; + let guardian_model_info = session + .services + .models_manager + .get_model_info(&guardian_model, turn.config.as_ref()) + .await; let guardian_config = build_guardian_subagent_config( turn.config.as_ref(), live_network_config, guardian_model.as_str(), guardian_reasoning_effort, + guardian_model_info + .guardian_developer_instructions + .as_deref(), )?; // Reuse the standard interactive subagent runner so we can seed inherited @@ -608,11 +616,12 @@ fn build_guardian_subagent_config( live_network_config: Option, active_model: &str, reasoning_effort: Option, + guardian_prompt_override: Option<&str>, ) -> anyhow::Result { let mut guardian_config = parent_config.clone(); guardian_config.model = Some(active_model.to_string()); guardian_config.model_reasoning_effort = reasoning_effort; - guardian_config.developer_instructions = Some(guardian_policy_prompt()); + guardian_config.developer_instructions = Some(guardian_policy_prompt(guardian_prompt_override)); guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); guardian_config.permissions.sandbox_policy = Constrained::allow_only(SandboxPolicy::new_read_only_policy()); @@ -818,8 +827,10 @@ fn guardian_output_contract_prompt() -> &'static str { /// Keep the prompt in a dedicated markdown file so reviewers can audit prompt /// changes directly without diffing through code. The output contract is /// appended from code so it stays near `guardian_output_schema()`. -fn guardian_policy_prompt() -> String { - let prompt = include_str!("guardian_prompt.md").trim_end(); +fn guardian_policy_prompt(prompt_override: Option<&str>) -> String { + let prompt = prompt_override + .unwrap_or(include_str!("guardian_prompt.md")) + .trim_end(); format!("{prompt}\n\n{}\n", guardian_output_contract_prompt()) } diff --git a/codex-rs/core/src/guardian_tests.rs b/codex-rs/core/src/guardian_tests.rs index 8122e085461..08bff5d4665 100644 --- a/codex-rs/core/src/guardian_tests.rs +++ b/codex-rs/core/src/guardian_tests.rs @@ -232,6 +232,7 @@ fn guardian_subagent_config_preserves_parent_network_proxy() { None, "parent-active-model", Some(codex_protocol::openai_models::ReasoningEffort::Low), + None, ) .expect("guardian config"); @@ -278,6 +279,7 @@ fn guardian_subagent_config_uses_live_network_proxy_state() { Some(live_network.clone()), "active-model", None, + None, ) .expect("guardian config"); @@ -308,7 +310,7 @@ fn guardian_subagent_config_rejects_pinned_collab_feature() { ) .expect("managed features"); - let err = build_guardian_subagent_config(&parent_config, None, "active-model", None) + let err = build_guardian_subagent_config(&parent_config, None, "active-model", None, None) .expect_err("guardian config should fail when collab is pinned on"); assert!( @@ -323,8 +325,27 @@ fn guardian_subagent_config_uses_parent_active_model_instead_of_hardcoded_slug() parent_config.model = Some("configured-model".to_string()); let guardian_config = - build_guardian_subagent_config(&parent_config, None, "active-model", None) + build_guardian_subagent_config(&parent_config, None, "active-model", None, None) .expect("guardian config"); assert_eq!(guardian_config.model, Some("active-model".to_string())); } + +#[test] +fn guardian_subagent_config_prefers_model_prompt_override() { + let guardian_config = build_guardian_subagent_config( + &test_config(), + None, + "active-model", + None, + Some("override prompt"), + ) + .expect("guardian config"); + + let instructions = guardian_config + .developer_instructions + .expect("guardian instructions"); + + assert!(instructions.starts_with("override prompt")); + assert!(instructions.contains("\"risk_level\": \"low\" | \"medium\" | \"high\"")); +} diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index 3664a526609..3a47b25c8bf 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -73,6 +73,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), + guardian_developer_instructions: None, model_messages: local_personality_messages_for_slug(slug), supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 937de8d5360..e1e41f968d9 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -58,6 +58,7 @@ fn test_model_info( priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, @@ -678,6 +679,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index e8f9cbf7fef..a1fe70a7172 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -335,6 +335,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { priority, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 754c46ebfbf..26011a30e97 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -633,6 +633,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: Some(ModelMessages { instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()), instructions_variables: Some(ModelInstructionsVariables { @@ -748,6 +749,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: Some(ModelMessages { instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()), instructions_variables: Some(ModelInstructionsVariables { diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 4610bec096e..a7f4553301d 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -294,6 +294,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, @@ -536,6 +537,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { priority: 1, upgrade: None, base_instructions: remote_base.to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, @@ -1002,6 +1004,7 @@ fn test_remote_model_with_policy( priority, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 4139a582a89..3b6d27eb3a5 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -400,6 +400,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 3a3c89ac758..429efb1c50f 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -996,6 +996,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), + guardian_developer_instructions: None, model_messages: None, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 3d668c44771..6fb9feaade8 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -246,6 +246,8 @@ pub struct ModelInfo { pub upgrade: Option, pub base_instructions: String, #[serde(default, skip_serializing_if = "Option::is_none")] + pub guardian_developer_instructions: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub model_messages: Option, pub supports_reasoning_summaries: bool, #[serde(default)] @@ -521,6 +523,7 @@ mod tests { availability_nux: None, upgrade: None, base_instructions: "base".to_string(), + guardian_developer_instructions: None, model_messages: spec, supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto,