Skip to content

Commit 7c09e3b

Browse files
charley-oaicodex
andcommitted
feat(core): allow guardian prompt overrides from model metadata
Co-authored-by: Codex <noreply@openai.com>
1 parent 04f5f35 commit 7c09e3b

12 files changed

Lines changed: 53 additions & 5 deletions

File tree

codex-rs/app-server/tests/common/models_cache.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
2929
priority,
3030
upgrade: preset.upgrade.as_ref().map(|u| u.into()),
3131
base_instructions: "base instructions".to_string(),
32+
guardian_developer_instructions: None,
3233
model_messages: None,
3334
supports_reasoning_summaries: false,
3435
default_reasoning_summary: ReasoningSummary::Auto,

codex-rs/codex-api/tests/models_integration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ async fn models_client_hits_models_endpoint() {
7777
priority: 1,
7878
upgrade: None,
7979
base_instructions: "base instructions".to_string(),
80+
guardian_developer_instructions: None,
8081
model_messages: None,
8182
supports_reasoning_summaries: false,
8283
default_reasoning_summary: ReasoningSummary::Auto,

codex-rs/core/src/guardian.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,11 +577,19 @@ async fn run_guardian_subagent(
577577
};
578578
(turn.model_info.slug.clone(), reasoning_effort)
579579
};
580+
let guardian_model_info = session
581+
.services
582+
.models_manager
583+
.get_model_info(&guardian_model, turn.config.as_ref())
584+
.await;
580585
let guardian_config = build_guardian_subagent_config(
581586
turn.config.as_ref(),
582587
live_network_config,
583588
guardian_model.as_str(),
584589
guardian_reasoning_effort,
590+
guardian_model_info
591+
.guardian_developer_instructions
592+
.as_deref(),
585593
)?;
586594

587595
// Reuse the standard interactive subagent runner so we can seed inherited
@@ -647,11 +655,12 @@ fn build_guardian_subagent_config(
647655
live_network_config: Option<codex_network_proxy::NetworkProxyConfig>,
648656
active_model: &str,
649657
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
658+
guardian_prompt_override: Option<&str>,
650659
) -> anyhow::Result<Config> {
651660
let mut guardian_config = parent_config.clone();
652661
guardian_config.model = Some(active_model.to_string());
653662
guardian_config.model_reasoning_effort = reasoning_effort;
654-
guardian_config.developer_instructions = Some(guardian_policy_prompt());
663+
guardian_config.developer_instructions = Some(guardian_policy_prompt(guardian_prompt_override));
655664
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
656665
guardian_config.permissions.sandbox_policy =
657666
Constrained::allow_only(SandboxPolicy::new_read_only_policy());
@@ -861,8 +870,10 @@ fn guardian_output_contract_prompt() -> &'static str {
861870
/// Keep the prompt in a dedicated markdown file so reviewers can audit prompt
862871
/// changes directly without diffing through code. The output contract is
863872
/// appended from code so it stays near `guardian_output_schema()`.
864-
fn guardian_policy_prompt() -> String {
865-
let prompt = include_str!("guardian_prompt.md").trim_end();
873+
fn guardian_policy_prompt(prompt_override: Option<&str>) -> String {
874+
let prompt = prompt_override
875+
.unwrap_or(include_str!("guardian_prompt.md"))
876+
.trim_end();
866877
format!("{prompt}\n\n{}\n", guardian_output_contract_prompt())
867878
}
868879

codex-rs/core/src/guardian_tests.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ fn guardian_subagent_config_preserves_parent_network_proxy() {
232232
None,
233233
"parent-active-model",
234234
Some(codex_protocol::openai_models::ReasoningEffort::Low),
235+
None,
235236
)
236237
.expect("guardian config");
237238

@@ -278,6 +279,7 @@ fn guardian_subagent_config_uses_live_network_proxy_state() {
278279
Some(live_network.clone()),
279280
"active-model",
280281
None,
282+
None,
281283
)
282284
.expect("guardian config");
283285

@@ -308,7 +310,7 @@ fn guardian_subagent_config_rejects_pinned_collab_feature() {
308310
)
309311
.expect("managed features");
310312

311-
let err = build_guardian_subagent_config(&parent_config, None, "active-model", None)
313+
let err = build_guardian_subagent_config(&parent_config, None, "active-model", None, None)
312314
.expect_err("guardian config should fail when collab is pinned on");
313315

314316
assert!(
@@ -323,8 +325,27 @@ fn guardian_subagent_config_uses_parent_active_model_instead_of_hardcoded_slug()
323325
parent_config.model = Some("configured-model".to_string());
324326

325327
let guardian_config =
326-
build_guardian_subagent_config(&parent_config, None, "active-model", None)
328+
build_guardian_subagent_config(&parent_config, None, "active-model", None, None)
327329
.expect("guardian config");
328330

329331
assert_eq!(guardian_config.model, Some("active-model".to_string()));
330332
}
333+
334+
#[test]
335+
fn guardian_subagent_config_prefers_model_prompt_override() {
336+
let guardian_config = build_guardian_subagent_config(
337+
&test_config(),
338+
None,
339+
"active-model",
340+
None,
341+
Some("override prompt"),
342+
)
343+
.expect("guardian config");
344+
345+
let instructions = guardian_config
346+
.developer_instructions
347+
.expect("guardian instructions");
348+
349+
assert!(instructions.starts_with("override prompt"));
350+
assert!(instructions.contains("\"risk_level\": \"low\" | \"medium\" | \"high\""));
351+
}

codex-rs/core/src/models_manager/model_info.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo {
7373
availability_nux: None,
7474
upgrade: None,
7575
base_instructions: BASE_INSTRUCTIONS.to_string(),
76+
guardian_developer_instructions: None,
7677
model_messages: local_personality_messages_for_slug(slug),
7778
supports_reasoning_summaries: false,
7879
default_reasoning_summary: ReasoningSummary::Auto,

codex-rs/core/tests/suite/model_switching.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fn test_model_info(
5858
priority: 1,
5959
upgrade: None,
6060
base_instructions: "base instructions".to_string(),
61+
guardian_developer_instructions: None,
6162
model_messages: None,
6263
supports_reasoning_summaries: false,
6364
default_reasoning_summary: ReasoningSummary::Auto,
@@ -678,6 +679,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
678679
priority: 1,
679680
upgrade: None,
680681
base_instructions: "base instructions".to_string(),
682+
guardian_developer_instructions: None,
681683
model_messages: None,
682684
supports_reasoning_summaries: false,
683685
default_reasoning_summary: ReasoningSummary::Auto,

codex-rs/core/tests/suite/models_cache_ttl.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
335335
priority,
336336
upgrade: None,
337337
base_instructions: "base instructions".to_string(),
338+
guardian_developer_instructions: None,
338339
model_messages: None,
339340
supports_reasoning_summaries: false,
340341
default_reasoning_summary: ReasoningSummary::Auto,

codex-rs/core/tests/suite/personality.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
633633
priority: 1,
634634
upgrade: None,
635635
base_instructions: "base instructions".to_string(),
636+
guardian_developer_instructions: None,
636637
model_messages: Some(ModelMessages {
637638
instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()),
638639
instructions_variables: Some(ModelInstructionsVariables {
@@ -748,6 +749,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
748749
priority: 1,
749750
upgrade: None,
750751
base_instructions: "base instructions".to_string(),
752+
guardian_developer_instructions: None,
751753
model_messages: Some(ModelMessages {
752754
instructions_template: Some("Base instructions\n{{ personality }}\n".to_string()),
753755
instructions_variables: Some(ModelInstructionsVariables {

codex-rs/core/tests/suite/remote_models.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
294294
priority: 1,
295295
upgrade: None,
296296
base_instructions: "base instructions".to_string(),
297+
guardian_developer_instructions: None,
297298
model_messages: None,
298299
supports_reasoning_summaries: false,
299300
default_reasoning_summary: ReasoningSummary::Auto,
@@ -536,6 +537,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
536537
priority: 1,
537538
upgrade: None,
538539
base_instructions: remote_base.to_string(),
540+
guardian_developer_instructions: None,
539541
model_messages: None,
540542
supports_reasoning_summaries: false,
541543
default_reasoning_summary: ReasoningSummary::Auto,
@@ -1002,6 +1004,7 @@ fn test_remote_model_with_policy(
10021004
priority,
10031005
upgrade: None,
10041006
base_instructions: "base instructions".to_string(),
1007+
guardian_developer_instructions: None,
10051008
model_messages: None,
10061009
supports_reasoning_summaries: false,
10071010
default_reasoning_summary: ReasoningSummary::Auto,

codex-rs/core/tests/suite/rmcp_client.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
400400
priority: 1,
401401
upgrade: None,
402402
base_instructions: "base instructions".to_string(),
403+
guardian_developer_instructions: None,
403404
model_messages: None,
404405
supports_reasoning_summaries: false,
405406
default_reasoning_summary: ReasoningSummary::Auto,

0 commit comments

Comments
 (0)