Allow configuring the Guardian prompt template (#45516)

## What changed

Add `auto_review.experimental_policy_template` to override the Guardian prompt template in `config.toml`. Trim the configured value and ignore it when empty. Prefer the override over the model catalog template, retaining the bundled template as the final fallback.

The template's `{{ tenant_policy_config }}` placeholder is replaced with the resolved Guardian policy.

## Testing

Extend tests to cover template deserialization, trimming, precedence over the catalog template, and rendered policy text in Guardian inference requests.

GitOrigin-RevId: 85b4a8fc193a42735354894203ccbd1f738a3b58
This commit is contained in:
Won Park
2026-09-14 21:06:36 +00:00
committed by copyberry
parent fd5bf3b059
commit 520e13a4bc
8 changed files with 63 additions and 13 deletions
+2
View File
@@ -553,6 +553,8 @@ pub enum ThreadStoreToml {
pub struct AutoReviewToml {
/// Additional policy instructions inserted into the guardian prompt.
pub policy: Option<String>,
/// Experimental full Guardian prompt template containing the tenant policy placeholder.
pub experimental_policy_template: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
+5 -1
View File
@@ -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"
}
}
+25 -8
View File
@@ -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::<ConfigToml>(
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()
};
+14
View File
@@ -683,6 +683,11 @@ pub struct Config {
/// guardian developer prompt.
pub guardian_policy_config: Option<String>,
/// 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<String>,
/// Whether to inject the `<permissions instructions>` 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,
@@ -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,
))
);
}
@@ -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,
@@ -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();
@@ -213,6 +213,7 @@ fn new_config(model: Option<String>, 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,