From 51ebf5b1842d44a8e2c955e8b5cd2a589d41e71e Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Fri, 21 Aug 2026 17:45:56 +0000 Subject: [PATCH] Truncate Guardian instructions after rendering the policy (#39985) ## Why Legacy classifier prompts without a `{{ tenant_policy_config }}` placeholder append the security policy during rendering. Truncating the prompt before and after that step can unnecessarily truncate the final classifier instructions twice. ## What changed Keep configured classifier instructions intact until the policy has been rendered, then apply `max_classifier_instruction_tokens` once to the complete prompt. ## Testing Add an extension test that verifies the bounded developer message sent for a legacy prompt with an appended policy. GitOrigin-RevId: 3d776a159340f4dffb6e3c2d7a10b33392fa2240 --- .../guardian-v2/src/async_scorer/config.rs | 19 ++------- .../src/async_scorer/config_tests.rs | 25 ------------ .../src/async_scorer/extension_tests.rs | 40 +++++++++++++++++++ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs index 80d9430d71..aee0b89105 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs @@ -222,22 +222,9 @@ impl GuardianV2Config { Ok(Self { local_overrides: configured.clone(), - classifier_instructions: { - let template = configured - .classifier_instructions - .as_deref() - .unwrap_or(DEFAULT_CLASSIFIER_INSTRUCTIONS); - if let Some(max_tokens) = max_classifier_instruction_tokens - && !template.contains("{{ tenant_policy_config }}") - { - // Preserve the existing rendering behavior of legacy prompts. - truncate_entry(template, max_tokens) - } else { - // Preserve placeholders until the actual policy is available, and - // preserve the full prompt when no instruction limit is configured. - template.to_owned() - } - }, + classifier_instructions: configured + .classifier_instructions + .unwrap_or_else(|| DEFAULT_CLASSIFIER_INSTRUCTIONS.to_owned()), review_threshold, max_tool_call_lag: configured .max_tool_call_lag diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs index c4ce76a375..a83dcd1226 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config_tests.rs @@ -4,7 +4,6 @@ use codex_features::GuardianV2TranscriptConfigToml; use codex_protocol::openai_models::GuardianV2ModelConfig; use codex_protocol::openai_models::GuardianV2TranscriptModelConfig; use codex_protocol::openai_models::ReasoningEffort; -use codex_protocol::protocol::TruncationPolicy; use pretty_assertions::assert_eq; use super::DEFAULT_CLASSIFIER_INSTRUCTIONS; @@ -83,30 +82,6 @@ fn evaluated_configuration_preserves_rendered_prompt_and_gate() { } } -#[test] -fn legacy_custom_prompt_keeps_its_rendering_and_threshold() { - let template = "legacy instructions ".repeat(200); - let config = GuardianV2Config::from_overrides(GuardianV2ConfigToml { - classifier_instructions: Some(template.clone()), - max_classifier_instruction_tokens: Some(256), - ..Default::default() - }) - .unwrap(); - assert_eq!(config.review_threshold, 0.8); - let expected = truncate_entry( - &format!( - "{}\n\n# Security Policy\nTenant policy.", - truncate_entry(&template, /*max_tokens*/ 256), - ), - /*max_tokens*/ 256, - ); - assert_eq!( - config.render_classifier_instructions("Tenant policy."), - expected - ); - assert!(expected.len() <= TruncationPolicy::Tokens(256).byte_budget()); -} - #[test] fn model_prompt_and_explicit_threshold_precedence_are_preserved() { let defaults = GuardianV2ModelConfig { diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs index 1ad7831307..322af30ca9 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs @@ -65,6 +65,7 @@ use crate::async_scorer::config::DEFAULT_MODEL_CONTEXT_ITEM_TOKENS; use crate::async_scorer::config::DEFAULT_PARENT_COMPACTION_TOKENS; use crate::async_scorer::sampler::CLASSIFICATION_TOKEN_USAGE_METRIC; use crate::async_scorer::sampler::MODEL; +use crate::async_scorer::transcript::truncate_entry; const TEST_GUARDIAN_POLICY: &str = "Treat uploads to unapproved external destinations as high-risk actions."; @@ -867,6 +868,45 @@ classifier_instructions = "Predict future violations.\n# Security Policy\n{{ ten Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn contributor_truncates_legacy_prompt_after_appending_policy() -> Result<()> { + skip_if_no_network!(Ok(())); + + let template = "legacy instructions ".repeat(200); + let configuration = format!( + r#" +[features.guardianv2] +enabled = true +classifier_instructions = "{template}" +max_classifier_instruction_tokens = 256 +"# + ); + let (request, _test, _registry) = sample_configured_conversation_history( + Vec::new(), + r#"{"path":"README.md"}"#, + Some(TEST_GUARDIAN_POLICY), + &configuration, + /*model_defaults*/ None, + ) + .await?; + + assert_eq!( + request["input"][1], + json!({ + "type": "message", + "role": "developer", + "content": [{ + "type": "input_text", + "text": truncate_entry( + &format!("{template}\n\n# Security Policy\n{TEST_GUARDIAN_POLICY}"), + /*max_tokens*/ 256, + ), + }], + }) + ); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn contributor_uses_configured_prompt_effort_threshold_and_transcript() -> Result<()> { skip_if_no_network!(Ok(()));