Support model catalog permission messages (#33147)

## What changed

- Add per-sandbox-mode permission messages to `ModelMessages` and preserve them when applying model configuration overrides.
- Use the selected catalog message in permission instructions, substituting `{{ network_access }}` with the active network policy. Fall back to the existing sandbox text when no override is provided, and allow an empty override to omit only the sandbox section.
- Apply catalog permission messages when a session starts and when its model changes.

## Testing

- Cover catalog deserialization, mode selection, network substitution, fallback and empty-message behavior, remote model catalogs, and model changes.

GitOrigin-RevId: cd5ed3aee3155dca0e7b2358c0f09bae73236856
This commit is contained in:
rhan-oai authored and copyberry committed 2026-07-14 20:14:29 +00:00
1 parent f90e7deea6
commit eb80df0317
12 files changed
+540 -11

No files matched your search

+36
View File
@@ -506,6 +506,7 @@ pub struct ModelMessages {
pub instructions_variables: Option<ModelInstructionsVariables>,
pub approvals: Option<ApprovalMessages>,
pub auto_review: Option<AutoReviewMessages>,
pub permissions: Option<PermissionMessages>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
@@ -519,6 +520,13 @@ pub struct AutoReviewMessages {
pub policy: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct PermissionMessages {
pub danger_full_access: Option<String>,
pub workspace_write: Option<String>,
pub read_only: Option<String>,
}
impl ModelMessages {
fn has_personality_placeholder(&self) -> bool {
self.instructions_template
@@ -744,6 +752,7 @@ mod tests {
.expect("model messages should deserialize");
assert_eq!(messages.approvals, None);
assert_eq!(messages.permissions, None);
}
#[test]
@@ -768,6 +777,29 @@ mod tests {
);
}
#[test]
fn permission_messages_preserve_missing_and_empty_values() {
let messages: ModelMessages = from_str(
r#"{
"instructions_template": null,
"instructions_variables": null,
"permissions": {
"workspace_write": ""
}
}"#,
)
.expect("permission messages should deserialize");
assert_eq!(
messages.permissions,
Some(PermissionMessages {
danger_full_access: None,
workspace_write: Some(String::new()),
read_only: None,
})
);
}
#[test]
fn reasoning_effort_accepts_known_and_custom_values() {
let custom = ReasoningEffort::Custom("future".to_string());
@@ -841,6 +873,7 @@ mod tests {
instructions_variables: Some(personality_variables()),
approvals: None,
auto_review: None,
permissions: None,
}));
let instructions = model.get_model_instructions(Some(Personality::Friendly));
@@ -859,6 +892,7 @@ mod tests {
}),
approvals: None,
auto_review: None,
permissions: None,
}));
assert_eq!(
model.get_model_instructions(Some(Personality::Friendly)),
@@ -886,6 +920,7 @@ mod tests {
}),
approvals: None,
auto_review: None,
permissions: None,
}));
assert_eq!(
model_no_personality.get_model_instructions(Some(Personality::Friendly)),
@@ -916,6 +951,7 @@ mod tests {
}),
approvals: None,
auto_review: None,
permissions: None,
}));
let instructions = model.get_model_instructions(Some(Personality::Friendly));