persist untruncated approve rule list

This commit is contained in:
Roy Han
2026-03-17 17:22:53 -07:00
parent 1a9555eda9
commit 71f1c6ed79
17 changed files with 120 additions and 21 deletions
+55 -21
View File
@@ -778,13 +778,18 @@ const MAX_RENDERED_PREFIXES: usize = 100;
const MAX_ALLOW_PREFIX_TEXT_BYTES: usize = 5000;
const TRUNCATED_MARKER: &str = "...\n[Some commands were truncated]";
pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
let mut truncated = false;
if prefixes.len() > MAX_RENDERED_PREFIXES {
truncated = true;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AllowPrefixRenderSummary {
pub rendered: String,
pub prompt_truncated: bool,
pub total_count: usize,
pub prompt_visible_count: usize,
}
pub fn summarize_allow_prefixes(mut prefixes: Vec<Vec<String>>) -> AllowPrefixRenderSummary {
let total_count = prefixes.len();
let mut truncated = total_count > MAX_RENDERED_PREFIXES;
let mut prefixes = prefixes;
prefixes.sort_by(|a, b| {
a.len()
.cmp(&b.len())
@@ -792,15 +797,15 @@ pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
.then_with(|| a.cmp(b))
});
let full_text = prefixes
let rendered_prefixes = prefixes
.into_iter()
.take(MAX_RENDERED_PREFIXES)
.map(|prefix| format!("- {}", render_command_prefix(&prefix)))
.collect::<Vec<_>>()
.join("\n");
.collect::<Vec<_>>();
let mut output = rendered_prefixes.join("\n");
let mut prompt_visible_count = rendered_prefixes.len();
// truncate to last UTF8 char
let mut output = full_text;
let byte_idx = output
.char_indices()
.nth(MAX_ALLOW_PREFIX_TEXT_BYTES)
@@ -808,13 +813,27 @@ pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
if let Some(byte_idx) = byte_idx {
truncated = true;
output = output[..byte_idx].to_string();
prompt_visible_count = if output.is_empty() {
0
} else {
output.lines().count()
};
}
if truncated {
Some(format!("{output}{TRUNCATED_MARKER}"))
} else {
Some(output)
output = format!("{output}{TRUNCATED_MARKER}");
}
AllowPrefixRenderSummary {
rendered: output,
prompt_truncated: truncated,
total_count,
prompt_visible_count,
}
}
pub fn format_allow_prefixes(prefixes: Vec<Vec<String>>) -> Option<String> {
Some(summarize_allow_prefixes(prefixes).rendered)
}
fn prefix_combined_str_len(prefix: &[String]) -> usize {
@@ -2280,10 +2299,11 @@ mod tests {
.map(|i| vec![format!("{i:03}")])
.collect::<Vec<_>>();
let output = format_allow_prefixes(prefixes).expect("rendered list");
assert_eq!(output.ends_with(TRUNCATED_MARKER), true);
eprintln!("output: {output}");
assert_eq!(output.lines().count(), MAX_RENDERED_PREFIXES + 1);
let summary = summarize_allow_prefixes(prefixes);
assert!(summary.rendered.ends_with(TRUNCATED_MARKER));
assert_eq!(summary.total_count, MAX_RENDERED_PREFIXES + 5);
assert_eq!(summary.prompt_visible_count, MAX_RENDERED_PREFIXES);
assert_eq!(summary.rendered.lines().count(), MAX_RENDERED_PREFIXES + 1);
}
#[test]
@@ -2298,12 +2318,26 @@ mod tests {
.expect("add rule");
}
let output =
format_allow_prefixes(exec_policy.get_allowed_prefixes()).expect("formatted prefixes");
let summary = summarize_allow_prefixes(exec_policy.get_allowed_prefixes());
assert!(
output.len() <= MAX_ALLOW_PREFIX_TEXT_BYTES + TRUNCATED_MARKER.len(),
"output length exceeds expected limit: {output}",
summary.rendered.len() <= MAX_ALLOW_PREFIX_TEXT_BYTES + TRUNCATED_MARKER.len(),
"output length exceeds expected limit: {}",
summary.rendered,
);
assert!(summary.prompt_truncated);
assert!(summary.prompt_visible_count < summary.total_count);
}
#[test]
fn summarize_allow_prefixes_reports_untruncated_counts() {
let summary = summarize_allow_prefixes(vec![
vec!["git".to_string(), "fetch".to_string()],
vec!["cargo".to_string(), "test".to_string()],
]);
assert_eq!(summary.total_count, 2);
assert_eq!(summary.prompt_visible_count, 2);
assert!(!summary.prompt_truncated);
}
#[test]
+11
View File
@@ -2344,6 +2344,14 @@ impl fmt::Display for SubAgentSource {
/// NOTE: There used to be an `instructions` field here, which stored user_instructions, but we
/// now save that on TurnContext. base_instructions stores the base instructions for the session,
/// and should be used when there is no config override.
#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
pub struct ApprovedCommandPrefixesSnapshot {
pub prefixes: Vec<Vec<String>>,
pub prompt_truncated: bool,
pub total_count: usize,
pub prompt_visible_count: usize,
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct SessionMeta {
pub id: ThreadId,
@@ -2370,6 +2378,8 @@ pub struct SessionMeta {
pub dynamic_tools: Option<Vec<DynamicToolSpec>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved_command_prefixes: Option<ApprovedCommandPrefixesSnapshot>,
}
impl Default for SessionMeta {
@@ -2388,6 +2398,7 @@ impl Default for SessionMeta {
base_instructions: None,
dynamic_tools: None,
memory_mode: None,
approved_command_prefixes: None,
}
}
}