mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
Add model catalog fields for granular approval templates (#57325)
## What changed
Add optional `approvals.granular` catalog fields for the policy introduction and prompted and rejected category lists. Preserve template strings verbatim in `GranularApprovalMessages`, leaving validation and rendering to consumers.
Document the 500-byte template limit, required `{{ policy }}` and `{{ categories }}` markers, and fallback rules.
## Testing
Extend serialization tests to cover null, empty, and populated granular templates, and verify their preservation through model responses and base instruction overrides.
GitOrigin-RevId: ce32e41a9b37e2e7c45ac9b079708a640c99326c
This commit is contained in:
@@ -263,6 +263,7 @@ fn permissions_state(
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
};
|
||||
let permission_messages = PermissionMessages {
|
||||
danger_full_access: Some("Full access.".to_string()),
|
||||
|
||||
@@ -49,6 +49,7 @@ async fn catalog_permission_message_loaded_from_remote_models_is_sent(
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
}),
|
||||
collaboration_modes: None,
|
||||
auto_review: None,
|
||||
|
||||
@@ -89,6 +89,7 @@ fn model_with_approval_messages(
|
||||
on_request_auto_review: Some(on_request_auto_review.to_string()),
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -223,6 +224,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
|
||||
on_request_auto_review: None,
|
||||
never: Some("catalog never approval instructions".to_string()),
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
},
|
||||
"catalog never approval instructions",
|
||||
"Approval policy is currently never",
|
||||
@@ -234,6 +236,7 @@ async fn catalog_non_on_request_approval_messages_are_sent_in_initial_permission
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: Some("catalog unless-trusted approval instructions".to_string()),
|
||||
granular: None,
|
||||
},
|
||||
"catalog unless-trusted approval instructions",
|
||||
"`approval_policy` is `unless-trusted`",
|
||||
|
||||
@@ -137,6 +137,7 @@ async fn protected_model_settings_use_the_proposed_permissions(
|
||||
on_request_auto_review: Some(REVIEW_INSTRUCTIONS.to_string()),
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
});
|
||||
messages.permissions = Some(PermissionMessages {
|
||||
danger_full_access: None,
|
||||
|
||||
@@ -1103,6 +1103,7 @@ async fn active_model_switch_updates_core_context_from_captured_settings(
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
});
|
||||
messages.token_budget =
|
||||
(initial_model || destination_has_guidance).then(|| ModelTokenBudgetConfig {
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_protocol::openai_models::ApprovalMessages;
|
||||
use codex_protocol::openai_models::AutoReviewMessages;
|
||||
use codex_protocol::openai_models::CollaborationModeMessages;
|
||||
use codex_protocol::openai_models::ConfirmationPolicies;
|
||||
use codex_protocol::openai_models::GranularApprovalMessages;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::ModelTokenBudgetConfig;
|
||||
use codex_protocol::openai_models::MultiAgentMessages;
|
||||
@@ -35,6 +36,11 @@ fn base_instruction_override_is_literal_and_preserves_catalog_messages() {
|
||||
on_request_auto_review: Some("auto approvals".to_string()),
|
||||
never: Some("never approvals".to_string()),
|
||||
unless_trusted: Some("unless-trusted approvals".to_string()),
|
||||
granular: Some(GranularApprovalMessages {
|
||||
intro: Some("Catalog: {{ policy }}".to_string()),
|
||||
prompted_categories: Some("Allowed: {{ categories }}".to_string()),
|
||||
rejected_categories: Some("Denied: {{ categories }}".to_string()),
|
||||
}),
|
||||
};
|
||||
let collaboration_modes = CollaborationModeMessages {
|
||||
default: Some("default instructions".to_string()),
|
||||
@@ -167,6 +173,7 @@ fn personality_none_strips_catalog_instruction_sources_through_the_next_h1() {
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -426,6 +426,7 @@ fn catalog_approval_messages_select_reviewer_variant() {
|
||||
on_request_auto_review: Some("auto-review catalog approvals".to_string()),
|
||||
never: Some("never catalog approvals".to_string()),
|
||||
unless_trusted: Some("unless-trusted catalog approvals".to_string()),
|
||||
granular: None,
|
||||
};
|
||||
|
||||
for (approval_policy, reviewer, expected) in [
|
||||
@@ -474,6 +475,7 @@ fn empty_catalog_approval_message_suppresses_legacy_approval_section() {
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
};
|
||||
let approved_command_prefixes = vec![vec!["git".to_string(), "pull".to_string()]];
|
||||
let writable_root = "/tmp/repo".to_string();
|
||||
@@ -514,6 +516,7 @@ fn missing_catalog_key_uses_legacy_approval_text() {
|
||||
on_request_auto_review: Some("unused catalog approvals".to_string()),
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
@@ -523,6 +526,7 @@ fn missing_catalog_key_uses_legacy_approval_text() {
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
},
|
||||
),
|
||||
] {
|
||||
@@ -566,6 +570,7 @@ fn empty_catalog_non_on_request_approval_messages_suppress_legacy_approval_text(
|
||||
on_request_auto_review: None,
|
||||
never: Some(String::new()),
|
||||
unless_trusted: Some(String::new()),
|
||||
granular: None,
|
||||
};
|
||||
|
||||
for approval_policy in [AskForApproval::Never, AskForApproval::UnlessTrusted] {
|
||||
@@ -734,6 +739,7 @@ fn preserves_supplied_path_spellings_and_order() {
|
||||
on_request_auto_review: None,
|
||||
never: None,
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
};
|
||||
let permission_messages = PermissionMessages {
|
||||
danger_full_access: None,
|
||||
|
||||
@@ -775,6 +775,27 @@ pub struct ApprovalMessages {
|
||||
pub on_request_auto_review: Option<String>,
|
||||
pub never: Option<String>,
|
||||
pub unless_trusted: Option<String>,
|
||||
/// Optional catalog templates for harness-owned granular approval facts.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub granular: Option<GranularApprovalMessages>,
|
||||
}
|
||||
|
||||
/// Catalog presentation for granular approval policy; the harness owns the policy and lists.
|
||||
/// This wire type preserves templates verbatim; validation and rendering are consumer concerns.
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
pub struct GranularApprovalMessages {
|
||||
/// Rendering contract: at most 500 UTF-8 bytes with exactly one `{{ policy }}` marker.
|
||||
/// Consumers must use bundled text for missing, null, empty, invalid, or oversized templates.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub intro: Option<String>,
|
||||
/// Presentation around `{{ categories }}`, including the fixed prompted-bucket label.
|
||||
/// Uses the same byte limit and fallback rules as `intro`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prompted_categories: Option<String>,
|
||||
/// Presentation around `{{ categories }}`, including the fixed rejected-bucket label.
|
||||
/// Uses the same byte limit and fallback rules as `intro`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rejected_categories: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
|
||||
@@ -1277,8 +1298,51 @@ mod tests {
|
||||
on_request_auto_review: None,
|
||||
never: Some(String::new()),
|
||||
unless_trusted: None,
|
||||
granular: None,
|
||||
})
|
||||
);
|
||||
|
||||
for (granular, expected) in [
|
||||
(serde_json::json!(null), None),
|
||||
(serde_json::json!({}), Some(serde_json::json!({}))),
|
||||
(
|
||||
serde_json::json!({"intro": null, "prompted_categories": null, "rejected_categories": null}),
|
||||
Some(serde_json::json!({})),
|
||||
),
|
||||
(
|
||||
serde_json::json!({"intro": ""}),
|
||||
Some(serde_json::json!({"intro": ""})),
|
||||
),
|
||||
(
|
||||
serde_json::json!({
|
||||
"intro": "Catalog: {{ policy }}",
|
||||
"prompted_categories": "Allowed: {{ categories }}",
|
||||
"rejected_categories": "Denied: {{ categories }}",
|
||||
}),
|
||||
Some(serde_json::json!({
|
||||
"intro": "Catalog: {{ policy }}",
|
||||
"prompted_categories": "Allowed: {{ categories }}",
|
||||
"rejected_categories": "Denied: {{ categories }}",
|
||||
})),
|
||||
),
|
||||
] {
|
||||
let wire = serde_json::json!({"approvals": {
|
||||
"on_request": "existing override", "granular": granular,
|
||||
}});
|
||||
let messages: ModelMessages =
|
||||
serde_json::from_value(wire).expect("sparse granular messages");
|
||||
let roundtrip = serde_json::to_value(messages.approvals).expect("serialize approvals");
|
||||
let mut expected_approvals = serde_json::json!({
|
||||
"on_request": "existing override",
|
||||
"on_request_auto_review": null,
|
||||
"never": null,
|
||||
"unless_trusted": null,
|
||||
});
|
||||
if let Some(expected) = expected {
|
||||
expected_approvals["granular"] = expected;
|
||||
}
|
||||
assert_eq!(roundtrip, expected_approvals);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1605,6 +1669,11 @@ mod tests {
|
||||
on_request_auto_review: None,
|
||||
never: Some("never approval".to_string()),
|
||||
unless_trusted: Some("unless-trusted approval".to_string()),
|
||||
granular: Some(GranularApprovalMessages {
|
||||
intro: Some("Catalog: {{ policy }}".to_string()),
|
||||
prompted_categories: Some("Allowed: {{ categories }}".to_string()),
|
||||
rejected_categories: Some("Denied: {{ categories }}".to_string()),
|
||||
}),
|
||||
}),
|
||||
collaboration_modes: Some(CollaborationModeMessages {
|
||||
default: Some("default collaboration".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user