Use model catalog approval messages (#31312)

## Why

Approval guidance is currently assembled entirely by the client. Model
Messages V2 needs model catalogs to provide model-specific `on_request`
guidance for both user-reviewed and auto-reviewed approval flows while
retaining the existing generated prompt as a compatibility fallback.

## What changed

- add nullable `on_request` and `on_request_auto_review` catalog
messages
- select the message matching the active approvals reviewer for
`on_request` policies
- replace the complete legacy approval section when the selected catalog
value exists, including support for an empty string that suppresses the
section
- retain legacy rendering when the object or selected key is absent, and
for non-`on_request` policies
- preserve approval messages when base-instruction or personality
overrides clear instruction templates
- refresh permissions instructions when the active model changes
- pass catalog messages through initial and incremental permissions
construction

## Relationship to reviewer persistence

PR #31309 independently persists the approvals reviewer in turn context
and refreshes permissions when that reviewer changes. This PR is based
directly on `main` and does not duplicate that rollout migration; once
both land, reviewer switches will also select and append the new catalog
variant.

## Testing

- `just test -p codex-protocol`
- `just test -p codex-prompts`
- `just test -p codex-models-manager`
- `just test -p codex-core permissions_messages`
This commit is contained in:
Dylan Hurd
2026-07-07 10:52:38 -07:00
committed by GitHub
parent 1345c16dd7
commit 358575465c
13 changed files with 419 additions and 20 deletions
+1
View File
@@ -62,6 +62,7 @@ pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProce
pub(crate) use model_switch_instructions::ModelSwitchInstructions;
pub(crate) use multi_agent_mode_instructions::MultiAgentModeInstructions;
pub(crate) use network_rule_saved::NetworkRuleSaved;
pub use permissions_instructions::ApprovalPromptContext;
pub use permissions_instructions::PermissionsInstructions;
pub(crate) use personality_spec_instructions::PersonalitySpecInstructions;
pub(crate) use plugin_instructions::PluginInstructions;
@@ -1 +1,2 @@
pub use codex_prompts::ApprovalPromptContext;
pub use codex_prompts::PermissionsInstructions;
+9 -1
View File
@@ -1,3 +1,4 @@
use crate::context::ApprovalPromptContext;
use crate::context::CollaborationModeInstructions;
use crate::context::ContextualUserFragment;
use crate::context::ModelSwitchInstructions;
@@ -30,6 +31,7 @@ fn build_permissions_update_item(
let prev = previous?;
if prev.permission_profile() == next.permission_profile()
&& prev.approval_policy == next.approval_policy.value()
&& prev.model == next.model_info.slug
{
return None;
}
@@ -38,7 +40,13 @@ fn build_permissions_update_item(
PermissionsInstructions::from_permission_profile(
&next.permission_profile,
next.approval_policy.value(),
next.config.approvals_reviewer,
ApprovalPromptContext::new(
next.config.approvals_reviewer,
next.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.approvals.as_ref()),
),
exec_policy,
#[allow(deprecated)]
&next.cwd,
+9 -1
View File
@@ -21,6 +21,7 @@ use crate::build_available_skills;
use crate::compact;
use crate::config::ManagedFeatures;
use crate::config::resolve_tool_suggest_config_from_layer_stack;
use crate::context::ApprovalPromptContext;
use crate::context::ApprovedCommandPrefixSaved;
use crate::context::AvailableSkillsInstructions;
use crate::context::CollaborationModeInstructions;
@@ -3206,7 +3207,14 @@ impl Session {
PermissionsInstructions::from_permission_profile(
&turn_context.permission_profile,
turn_context.approval_policy.value(),
turn_context.config.approvals_reviewer,
ApprovalPromptContext::new(
turn_context.config.approvals_reviewer,
turn_context
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.approvals.as_ref()),
),
self.services.exec_policy.current().as_ref(),
#[allow(deprecated)]
&turn_context.cwd,
+6 -11
View File
@@ -9041,18 +9041,13 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
#[tokio::test]
async fn record_context_updates_and_set_reference_context_item_persists_baseline_without_emitting_diffs()
{
let (mut session, previous_context) = make_session_and_context().await;
let next_model = if previous_context.model_info.slug == "gpt-5.4" {
"gpt-5.2"
} else {
"gpt-5.4"
};
let turn_context = previous_context
.with_model(next_model.to_string(), &session.services.models_manager)
.await;
let previous_context_item = previous_context.to_turn_context_item();
let previous_context = Arc::new(previous_context);
let (mut session, turn_context) = make_session_and_context().await;
let previous_context_item = turn_context.to_turn_context_item();
let previous_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &previous_context).await;
let mut turn_context = Arc::try_unwrap(previous_context)
.unwrap_or_else(|_| panic!("previous turn context should have no remaining references"));
turn_context.sub_id = format!("{}-next", turn_context.sub_id);
{
let mut state = session.state.lock().await;
state.set_reference_context_item(Some(previous_context_item.clone()));
@@ -2,10 +2,15 @@ use anyhow::Result;
use codex_config::ConfigLayerStack;
use codex_core::ForkSnapshot;
use codex_core::config::Constrained;
use codex_core::context::ApprovalPromptContext;
use codex_core::context::ContextualUserFragment;
use codex_core::context::PermissionsInstructions;
use codex_core::load_exec_policy;
use codex_models_manager::model_info::model_info_from_slug;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelsResponse;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -33,6 +38,129 @@ fn permissions_texts(request: &ResponsesRequest) -> Vec<String> {
.collect()
}
fn model_with_approval_messages(
slug: &str,
on_request: &str,
on_request_auto_review: &str,
) -> codex_protocol::openai_models::ModelInfo {
let mut model = model_info_from_slug(slug);
model.model_messages = Some(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: Some(ApprovalMessages {
on_request: Some(on_request.to_string()),
on_request_auto_review: Some(on_request_auto_review.to_string()),
}),
});
model
}
async fn submit_text_turn(
test: &core_test_support::test_codex::TestCodex,
text: &str,
) -> Result<()> {
test.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: text.to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn catalog_approval_message_is_sent_in_initial_permissions() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let req = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let model_slug = "catalog-approvals-model";
let model = model_with_approval_messages(
model_slug,
"catalog user approval instructions",
"catalog auto-review approval instructions",
);
let mut builder = test_codex()
.with_model(model_slug)
.with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
});
let test = builder.build(&server).await?;
submit_text_turn(&test, "hello").await?;
let permissions = permissions_texts(&req.single_request());
assert_eq!(permissions.len(), 1);
assert!(permissions[0].contains("catalog user approval instructions"));
assert!(permissions[0].contains("Filesystem sandboxing defines"));
assert!(!permissions[0].contains("How to request escalation"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn model_change_appends_new_catalog_approval_message() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let _req1 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let req2 = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let first_slug = "catalog-approvals-model-a";
let second_slug = "catalog-approvals-model-b";
let first = model_with_approval_messages(first_slug, "model A approvals", "model A auto");
let second = model_with_approval_messages(second_slug, "model B approvals", "model B auto");
let mut builder = test_codex()
.with_model(first_slug)
.with_config(move |config| {
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
config.model_catalog = Some(ModelsResponse {
models: vec![first, second],
});
});
let test = builder.build(&server).await?;
submit_text_turn(&test, "first").await?;
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(second_slug.to_string()),
..Default::default()
},
)
.await?;
submit_text_turn(&test, "second").await?;
let permissions = permissions_texts(&req2.single_request());
assert_eq!(permissions.len(), 2);
assert!(
permissions
.last()
.is_some_and(|text| text.contains("model B approvals"))
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn permissions_message_sent_once_on_start() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -563,7 +691,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let expected = PermissionsInstructions::from_permission_profile(
&permission_profile,
AskForApproval::OnRequest,
test.config.approvals_reviewer,
ApprovalPromptContext::new(test.config.approvals_reviewer, /*messages*/ None),
&exec_policy,
test.config.cwd.as_path(),
/*exec_permission_approvals_enabled*/ false,
+2
View File
@@ -573,6 +573,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
personality_friendly: Some(friendly_personality_message.to_string()),
personality_pragmatic: Some("Pragmatic variant".to_string()),
}),
approvals: None,
}),
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
@@ -689,6 +690,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
personality_friendly: Some(remote_friendly_message.to_string()),
personality_pragmatic: Some(remote_pragmatic_message.to_string()),
}),
approvals: None,
}),
include_skills_usage_instructions: false,
supports_reasoning_summaries: false,
+13 -2
View File
@@ -54,14 +54,24 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig)
if let Some(base_instructions) = &config.base_instructions {
model.base_instructions = base_instructions.clone();
model.model_messages = None;
clear_instruction_messages(&mut model);
} else if !config.personality_enabled {
model.model_messages = None;
clear_instruction_messages(&mut model);
}
model
}
fn clear_instruction_messages(model: &mut ModelInfo) {
if let Some(model_messages) = &mut model.model_messages {
model_messages.instructions_template = None;
model_messages.instructions_variables = None;
if model_messages.approvals.is_none() {
model.model_messages = None;
}
}
}
/// Build a minimal fallback model descriptor for missing/unknown slugs.
pub fn model_info_from_slug(slug: &str) -> ModelInfo {
warn!("Unknown model {slug} is used. This will use fallback model metadata.");
@@ -119,6 +129,7 @@ fn local_personality_messages_for_slug(slug: &str) -> Option<ModelMessages> {
personality_friendly: Some(LOCAL_FRIENDLY_TEMPLATE.to_string()),
personality_pragmatic: Some(LOCAL_PRAGMATIC_TEMPLATE.to_string()),
}),
approvals: None,
}),
_ => None,
}
@@ -1,5 +1,6 @@
use super::*;
use crate::ModelsManagerConfig;
use codex_protocol::openai_models::ApprovalMessages;
use pretty_assertions::assert_eq;
#[test]
@@ -44,6 +45,68 @@ fn reasoning_summaries_override_false_is_noop_when_model_is_false() {
assert_eq!(updated, model);
}
#[test]
fn base_instruction_override_preserves_catalog_approval_messages() {
let mut model = model_info_from_slug("unknown-model");
let approvals = ApprovalMessages {
on_request: Some("user approvals".to_string()),
on_request_auto_review: Some("auto approvals".to_string()),
};
model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()),
instructions_variables: Some(ModelInstructionsVariables {
personality_default: Some("default".to_string()),
personality_friendly: Some("friendly".to_string()),
personality_pragmatic: Some("pragmatic".to_string()),
}),
approvals: Some(approvals.clone()),
});
let config = ModelsManagerConfig {
base_instructions: Some("override".to_string()),
..Default::default()
};
let updated = with_config_overrides(model, &config);
assert_eq!(
updated.model_messages,
Some(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: Some(approvals),
})
);
}
#[test]
fn disabled_personality_preserves_catalog_approval_messages() {
let mut model = model_info_from_slug("unknown-model");
let approvals = ApprovalMessages {
on_request: Some("user approvals".to_string()),
on_request_auto_review: None,
};
model.model_messages = Some(ModelMessages {
instructions_template: Some("template".to_string()),
instructions_variables: None,
approvals: Some(approvals.clone()),
});
let config = ModelsManagerConfig {
personality_enabled: false,
..Default::default()
};
let updated = with_config_overrides(model, &config);
assert_eq!(
updated.model_messages,
Some(ModelMessages {
instructions_template: None,
instructions_variables: None,
approvals: Some(approvals),
})
);
}
#[test]
fn model_context_window_override_clamps_to_max_context_window() {
let mut model = model_info_from_slug("unknown-model");
+1
View File
@@ -12,6 +12,7 @@ pub use compact::SUMMARY_PREFIX;
pub use goals::budget_limit_prompt;
pub use goals::continuation_prompt;
pub use goals::objective_updated_prompt;
pub use permissions_instructions::ApprovalPromptContext;
pub use permissions_instructions::PermissionsInstructions;
pub use realtime::BACKEND_PROMPT;
pub use realtime::END_INSTRUCTIONS;
@@ -4,6 +4,7 @@ use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::format_allow_prefixes;
use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
@@ -47,6 +48,7 @@ static SANDBOX_MODE_READ_ONLY_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
struct PermissionsPromptConfig<'a> {
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
approval_messages: Option<&'a ApprovalMessages>,
exec_policy: &'a Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
@@ -58,12 +60,24 @@ pub struct PermissionsInstructions {
text: String,
}
#[derive(Debug, Clone, Copy)]
pub struct ApprovalPromptContext<'a> {
reviewer: ApprovalsReviewer,
messages: Option<&'a ApprovalMessages>,
}
impl<'a> ApprovalPromptContext<'a> {
pub fn new(reviewer: ApprovalsReviewer, messages: Option<&'a ApprovalMessages>) -> Self {
Self { reviewer, messages }
}
}
impl PermissionsInstructions {
/// Builds permissions instructions from the effective permission profile and approval policy.
pub fn from_permission_profile(
permission_profile: &PermissionProfile,
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
approval_context: ApprovalPromptContext<'_>,
exec_policy: &Policy,
cwd: &Path,
exec_permission_approvals_enabled: bool,
@@ -78,7 +92,8 @@ impl PermissionsInstructions {
network_access_from_policy(permission_profile.network_sandbox_policy()),
PermissionsPromptConfig {
approval_policy,
approvals_reviewer,
approvals_reviewer: approval_context.reviewer,
approval_messages: approval_context.messages,
exec_policy,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
@@ -122,6 +137,7 @@ impl PermissionsInstructions {
&approval_text(
config.approval_policy,
config.approvals_reviewer,
config.approval_messages,
config.exec_policy,
config.exec_permission_approvals_enabled,
config.request_permissions_tool_enabled,
@@ -192,10 +208,23 @@ fn append_section(text: &mut String, section: &str) {
fn approval_text(
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
approval_messages: Option<&ApprovalMessages>,
exec_policy: &Policy,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> String {
if approval_policy == AskForApproval::OnRequest
&& let Some(approval_messages) = approval_messages
{
let selected = match approvals_reviewer {
ApprovalsReviewer::User => approval_messages.on_request.as_ref(),
ApprovalsReviewer::AutoReview => approval_messages.on_request_auto_review.as_ref(),
};
if let Some(selected) = selected {
return selected.clone();
}
}
let with_request_permissions_tool = |text: &str| {
if request_permissions_tool_enabled {
format!("{text}\n\n{}", request_permissions_tool_prompt_section())
@@ -36,6 +36,7 @@ fn builds_permissions_with_network_access_override() {
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: None,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
@@ -72,7 +73,7 @@ fn builds_permissions_from_profile() {
let instructions = PermissionsInstructions::from_permission_profile(
&permission_profile,
AskForApproval::UnlessTrusted,
ApprovalsReviewer::User,
ApprovalPromptContext::new(ApprovalsReviewer::User, /*messages*/ None),
&Policy::empty(),
&cwd,
/*exec_permission_approvals_enabled*/ false,
@@ -117,7 +118,7 @@ fn builds_permissions_from_profile_with_denied_reads() {
let instructions = PermissionsInstructions::from_permission_profile(
&permission_profile,
AskForApproval::OnRequest,
ApprovalsReviewer::AutoReview,
ApprovalPromptContext::new(ApprovalsReviewer::AutoReview, /*messages*/ None),
&Policy::empty(),
&cwd,
/*exec_permission_approvals_enabled*/ false,
@@ -142,6 +143,7 @@ fn includes_request_rule_instructions_for_on_request() {
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: None,
exec_policy: &exec_policy,
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: false,
@@ -163,6 +165,7 @@ fn includes_request_permissions_tool_instructions_for_unless_trusted_when_enable
PermissionsPromptConfig {
approval_policy: AskForApproval::UnlessTrusted,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: None,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
@@ -183,6 +186,7 @@ fn includes_request_permission_rule_instructions_for_on_request_when_enabled() {
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: None,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: false,
@@ -203,6 +207,7 @@ fn includes_request_permissions_tool_instructions_for_on_request_when_tool_is_en
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: None,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: false,
request_permissions_tool_enabled: true,
@@ -223,6 +228,7 @@ fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_b
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: None,
exec_policy: &Policy::empty(),
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: true,
@@ -235,11 +241,108 @@ fn on_request_includes_tool_guidance_alongside_inline_permission_guidance_when_b
assert!(text.contains("# request_permissions Tool"));
}
#[test]
fn catalog_approval_messages_select_reviewer_variant() {
let messages = ApprovalMessages {
on_request: Some("user catalog approvals".to_string()),
on_request_auto_review: Some("auto-review catalog approvals".to_string()),
};
for (reviewer, expected) in [
(ApprovalsReviewer::User, "user catalog approvals"),
(
ApprovalsReviewer::AutoReview,
"auto-review catalog approvals",
),
] {
assert_eq!(
approval_text(
AskForApproval::OnRequest,
reviewer,
Some(&messages),
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
),
expected
);
}
}
#[test]
fn empty_catalog_approval_message_suppresses_legacy_approval_section() {
let messages = ApprovalMessages {
on_request: Some(String::new()),
on_request_auto_review: None,
};
let mut exec_policy = Policy::empty();
exec_policy
.add_prefix_rule(&["git".to_string(), "pull".to_string()], Decision::Allow)
.expect("add rule");
let writable_root =
AbsolutePathBuf::from_absolute_path(test_path_buf("/tmp/repo")).expect("absolute path");
let instructions = PermissionsInstructions::from_permissions_with_network(
SandboxMode::WorkspaceWrite,
NetworkAccess::Restricted,
PermissionsPromptConfig {
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::User,
approval_messages: Some(&messages),
exec_policy: &exec_policy,
exec_permission_approvals_enabled: true,
request_permissions_tool_enabled: true,
},
Some(vec![WritableRoot {
root: writable_root.clone(),
read_only_subpaths: Vec::new(),
protected_metadata_names: Vec::new(),
}]),
);
let text = instructions.body();
assert!(text.contains("`sandbox_mode` is `workspace-write`"));
assert!(text.contains("Network access is restricted."));
assert!(text.contains(writable_root.to_string_lossy().as_ref()));
assert!(!text.contains("How to request escalation"));
assert!(!text.contains("request_permissions Tool"));
assert!(!text.contains("Approved command prefixes"));
}
#[test]
fn missing_catalog_key_and_non_on_request_policy_use_legacy_approval_text() {
let messages = ApprovalMessages {
on_request: None,
on_request_auto_review: Some("unused catalog approvals".to_string()),
};
let on_request = approval_text(
AskForApproval::OnRequest,
ApprovalsReviewer::User,
Some(&messages),
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
let never = approval_text(
AskForApproval::Never,
ApprovalsReviewer::User,
Some(&messages),
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
);
assert!(on_request.contains("How to request escalation"));
assert_eq!(never, APPROVAL_POLICY_NEVER);
}
#[test]
fn auto_review_approvals_append_auto_review_specific_guidance() {
let text = approval_text(
AskForApproval::OnRequest,
ApprovalsReviewer::AutoReview,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
@@ -255,6 +358,7 @@ fn auto_review_approvals_omit_auto_review_specific_guidance_when_approval_is_nev
let text = approval_text(
AskForApproval::Never,
ApprovalsReviewer::AutoReview,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
@@ -307,6 +411,7 @@ fn granular_policy_lists_prompted_and_rejected_categories_separately() {
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
@@ -344,6 +449,7 @@ fn granular_policy_includes_command_permission_instructions_when_sandbox_approva
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
@@ -376,6 +482,7 @@ fn granular_policy_omits_shell_permission_instructions_when_inline_requests_are_
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
@@ -408,6 +515,7 @@ fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_s
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
@@ -423,6 +531,7 @@ fn granular_policy_includes_request_permissions_tool_only_when_that_prompt_can_s
mcp_elicitations: true,
}),
ApprovalsReviewer::User,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ true,
@@ -441,6 +550,7 @@ fn granular_policy_lists_request_permissions_category_without_tool_section_when_
mcp_elicitations: false,
}),
ApprovalsReviewer::User,
/*approval_messages*/ None,
&Policy::empty(),
/*exec_permission_approvals_enabled*/ true,
/*request_permissions_tool_enabled*/ false,
+42
View File
@@ -485,6 +485,13 @@ impl ModelInfo {
pub struct ModelMessages {
pub instructions_template: Option<String>,
pub instructions_variables: Option<ModelInstructionsVariables>,
pub approvals: Option<ApprovalMessages>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ApprovalMessages {
pub on_request: Option<String>,
pub on_request_auto_review: Option<String>,
}
impl ModelMessages {
@@ -704,6 +711,37 @@ mod tests {
}
}
#[test]
fn model_messages_deserialize_without_approvals() {
let messages: ModelMessages =
from_str(r#"{"instructions_template":null,"instructions_variables":null}"#)
.expect("model messages should deserialize");
assert_eq!(messages.approvals, None);
}
#[test]
fn approval_messages_preserve_missing_and_empty_values() {
let messages: ModelMessages = from_str(
r#"{
"instructions_template": null,
"instructions_variables": null,
"approvals": {
"on_request": ""
}
}"#,
)
.expect("approval messages should deserialize");
assert_eq!(
messages.approvals,
Some(ApprovalMessages {
on_request: Some(String::new()),
on_request_auto_review: None,
})
);
}
#[test]
fn reasoning_effort_accepts_known_and_custom_values() {
let custom = ReasoningEffort::Custom("future".to_string());
@@ -775,6 +813,7 @@ mod tests {
let model = test_model(Some(ModelMessages {
instructions_template: Some("Hello {{ personality }}".to_string()),
instructions_variables: Some(personality_variables()),
approvals: None,
}));
let instructions = model.get_model_instructions(Some(Personality::Friendly));
@@ -791,6 +830,7 @@ mod tests {
personality_friendly: Some("friendly".to_string()),
personality_pragmatic: None,
}),
approvals: None,
}));
assert_eq!(
model.get_model_instructions(Some(Personality::Friendly)),
@@ -816,6 +856,7 @@ mod tests {
personality_friendly: None,
personality_pragmatic: None,
}),
approvals: None,
}));
assert_eq!(
model_no_personality.get_model_instructions(Some(Personality::Friendly)),
@@ -844,6 +885,7 @@ mod tests {
personality_friendly: None,
personality_pragmatic: None,
}),
approvals: None,
}));
let instructions = model.get_model_instructions(Some(Personality::Friendly));