diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index ffc748f7ab..d881c2d693 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -553,6 +553,8 @@ pub enum ThreadStoreToml { pub struct AutoReviewToml { /// Additional policy instructions inserted into the guardian prompt. pub policy: Option, + /// Experimental full Guardian prompt template containing the tenant policy placeholder. + pub experimental_policy_template: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 0091c9f67a..72fdd44907 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -383,6 +383,10 @@ }, "AutoReviewToml": { "properties": { + "experimental_policy_template": { + "description": "Experimental full Guardian prompt template containing the tenant policy placeholder.", + "type": "string" + }, "policy": { "description": "Additional policy instructions inserted into the guardian prompt.", "type": "string" @@ -7208,4 +7212,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 31fcbfc906..cafb0ff6fe 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8232,29 +8232,38 @@ async fn load_config_uses_requirements_guardian_policy_config() -> std::io::Resu } #[test] -fn config_toml_deserializes_auto_review_policy() { +fn config_toml_deserializes_auto_review_policy_and_template() { let cfg = toml::from_str::( r#" [auto_review] policy = "Use the user-configured guardian policy." +experimental_policy_template = "Configured template: {{ tenant_policy_config }}" "#, ) .expect("TOML deserialization should succeed"); + let auto_review = cfg.auto_review.as_ref().expect("auto-review config"); assert_eq!( - cfg.auto_review - .as_ref() - .and_then(|auto_review| auto_review.policy.as_deref()), - Some("Use the user-configured guardian policy.") + ( + auto_review.policy.as_deref(), + auto_review.experimental_policy_template.as_deref(), + ), + ( + Some("Use the user-configured guardian policy."), + Some("Configured template: {{ tenant_policy_config }}"), + ) ); } #[tokio::test] -async fn load_config_uses_auto_review_guardian_policy_config() -> std::io::Result<()> { +async fn load_config_uses_auto_review_guardian_policy_config_and_template() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cfg = ConfigToml { auto_review: Some(AutoReviewToml { policy: Some(" Use the user-configured guardian policy. ".to_string()), + experimental_policy_template: Some( + " Configured template: {{ tenant_policy_config }} ".to_string(), + ), }), ..Default::default() }; @@ -8270,8 +8279,14 @@ async fn load_config_uses_auto_review_guardian_policy_config() -> std::io::Resul .await?; assert_eq!( - config.guardian_policy_config.as_deref(), - Some("Use the user-configured guardian policy.") + ( + config.guardian_policy_config.as_deref(), + config.guardian_policy_template.as_deref(), + ), + ( + Some("Use the user-configured guardian policy."), + Some("Configured template: {{ tenant_policy_config }}"), + ) ); Ok(()) @@ -8292,6 +8307,7 @@ async fn requirements_guardian_policy_beats_auto_review() -> std::io::Result<()> let cfg = ConfigToml { auto_review: Some(AutoReviewToml { policy: Some("Use the user-configured guardian policy.".to_string()), + experimental_policy_template: None, }), ..Default::default() }; @@ -8322,6 +8338,7 @@ async fn load_config_ignores_empty_auto_review_guardian_policy_config() -> std:: let cfg = ConfigToml { auto_review: Some(AutoReviewToml { policy: Some(" ".to_string()), + experimental_policy_template: None, }), ..Default::default() }; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 0e36b5fd1a..12a1472ac4 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -683,6 +683,11 @@ pub struct Config { /// guardian developer prompt. pub guardian_policy_config: Option, + /// Guardian prompt template override from config.toml. + /// The resolved policy config replaces its `{{ tenant_policy_config }}` + /// placeholder when a review session is built. + pub guardian_policy_template: Option, + /// Whether to inject the `` developer block. pub include_permissions_instructions: bool, @@ -3910,6 +3915,14 @@ impl Config { auto_review.policy.as_deref(), )) }); + let guardian_policy_template = cfg + .auto_review + .as_ref() + .and_then(|auto_review| { + normalize_guardian_policy_config( + auto_review.experimental_policy_template.as_deref(), + ) + }); let personality = personality.or(cfg.personality); let experimental_compact_prompt_path = cfg.experimental_compact_prompt_file.as_ref(); @@ -4253,6 +4266,7 @@ impl Config { .or(show_raw_agent_reasoning) .unwrap_or(false), guardian_policy_config, + guardian_policy_template, model_reasoning_effort: cfg.model_reasoning_effort, plan_mode_reasoning_effort: cfg.plan_mode_reasoning_effort, model_reasoning_summary: cfg.model_reasoning_summary, diff --git a/codex-rs/core/src/guardian/review_session_tests.rs b/codex-rs/core/src/guardian/review_session_tests.rs index cda680ac5e..37e38b4e9c 100644 --- a/codex-rs/core/src/guardian/review_session_tests.rs +++ b/codex-rs/core/src/guardian/review_session_tests.rs @@ -626,11 +626,13 @@ async fn guardian_review_session_config_disables_skill_instructions() { } #[tokio::test] -async fn guardian_review_session_config_prefers_managed_policy_and_uses_catalog_template() { +async fn guardian_review_session_config_prefers_configured_policy_and_template() { let mut parent_config = crate::config::test_config().await; let managed_policy = "Use the managed Guardian policy."; + let configured_template = "Configured Guardian template:\n{{ tenant_policy_config }}"; let catalog_template = "Catalog Guardian template:\n{{ tenant_policy_config }}"; parent_config.guardian_policy_config = Some(managed_policy.to_string()); + parent_config.guardian_policy_template = Some(configured_template.to_string()); let model_messages = ModelMessages { persistent_instructions: None, tools: None, @@ -667,7 +669,7 @@ async fn guardian_review_session_config_prefers_managed_policy_and_uses_catalog_ guardian_config.base_instructions, Some(guardian_policy_prompt_with_config_and_template( managed_policy, - catalog_template, + configured_template, )) ); } diff --git a/codex-rs/core/src/guardian/reviewer_config.rs b/codex-rs/core/src/guardian/reviewer_config.rs index a79ef12bc9..cd346cb45f 100644 --- a/codex-rs/core/src/guardian/reviewer_config.rs +++ b/codex-rs/core/src/guardian/reviewer_config.rs @@ -47,8 +47,10 @@ pub fn build_guardian_review_session_config( } let catalog_auto_review = model_messages.and_then(|messages| messages.auto_review.as_ref()); let tenant_policy_config = parent_config.resolve_guardian_policy(model_messages); - let policy_template = catalog_auto_review - .and_then(|messages| messages.policy_template.as_deref()) + let policy_template = parent_config + .guardian_policy_template + .as_deref() + .or_else(|| catalog_auto_review.and_then(|messages| messages.policy_template.as_deref())) .unwrap_or(BUNDLED_GUARDIAN_POLICY_TEMPLATE); guardian_config.base_instructions = Some(guardian_policy_prompt_with_config_and_template( tenant_policy_config, diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index ba4b572616..0040bfe9e5 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -153,6 +153,8 @@ async fn guardian_session_inherits_parent_http_fallback( ) -> Result<()> { skip_if_no_network!(Ok(())); + let configured_policy = "Use the task-configured Guardian policy."; + let configured_template = "You are judging one planned coding-agent action.\nConfigured template: {{ tenant_policy_config }}"; let server = start_mock_server().await; let websocket_fallback = Mock::given(method("GET")) .and(path_regex(".*/responses$")) @@ -205,6 +207,8 @@ async fn guardian_session_inherits_parent_http_fallback( config.model_provider.stream_max_retries = Some(1); config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); config.approvals_reviewer = ApprovalsReviewer::User; + config.guardian_policy_config = Some(configured_policy.to_string()); + config.guardian_policy_template = Some(configured_template.to_string()); }); let test = builder.build_with_auto_env(&server).await?; @@ -247,6 +251,10 @@ async fn guardian_session_inherits_parent_http_fallback( request.body_json()["client_metadata"]["x-openai-subagent"].as_str() == Some("guardian") }) .expect("Guardian reviewer inference request"); + assert!( + guardian_request + .body_contains_text("Configured template: Use the task-configured Guardian policy.") + ); assert_eq!(guardian_request.path(), expected_guardian_path); let credits_enabled = expected_guardian_path.ends_with("/guardian"); let body = guardian_request.body_json(); diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index dfe19337af..ca0a84bf5e 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -213,6 +213,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R base_instructions_provenance: None, developer_instructions: None, guardian_policy_config: None, + guardian_policy_template: None, include_permissions_instructions: false, include_apps_instructions: false, include_collaboration_mode_instructions: false,