Compare commits

...

1 Commits

Author SHA1 Message Date
Charles Cunningham
c5fdd79a42 feat(core): allow guardian prompt overrides from model metadata
Co-authored-by: Codex <noreply@openai.com>
2026-03-07 16:43:46 -08:00
12 changed files with 53 additions and 5 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<codex_network_proxy::NetworkProxyConfig>,
active_model: &str,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
guardian_prompt_override: Option<&str>,
) -> anyhow::Result<Config> {
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())
}

View File

@@ -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\""));
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -246,6 +246,8 @@ pub struct ModelInfo {
pub upgrade: Option<ModelInfoUpgrade>,
pub base_instructions: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guardian_developer_instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_messages: Option<ModelMessages>,
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,