magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

pub(super) fn sanitize_tool_label(call: &ToolCall, label: &str) -> String {
    if is_tool(&call.name, ToolCapability::Read) {
        sanitize_read_target(label)
    } else {
        sanitize_display_text(label)
    }
}

pub(super) fn sanitize_read_target(text: &str) -> String {
    sanitize_display_text(text)
        .chars()
        .filter(|character| !character.is_control())
        .collect()
}

pub(super) fn sanitize_tool_params(call: &ToolCall) -> Value {
    if ToolCapability::from_dispatch_name(&call.name) == Some(ToolCapability::Read) {
        sanitize_read_params(&call.arguments)
    } else {
        sanitize_detail_json(&call.arguments)
    }
}

fn sanitize_read_params(value: &Value) -> Value {
    match value {
        Value::Object(object) => {
            let mut sanitized = serde_json::Map::new();
            for (key, value) in object {
                let key = sanitize_read_target(key);
                let value = if is_credential_like_key(&key) {
                    Value::String("<redacted>".to_string())
                } else {
                    sanitize_read_params(value)
                };
                let key = unique_sanitized_key(&sanitized, key);
                sanitized.insert(key, value);
            }
            Value::Object(sanitized)
        }
        Value::Array(values) => Value::Array(values.iter().map(sanitize_read_params).collect()),
        Value::String(text) => Value::String(sanitize_read_target(text)),
        other => other.clone(),
    }
}

pub(super) fn sanitize_read_metadata(value: &Value) -> Value {
    sanitize_read_metadata_value(value, None)
}

fn sanitize_read_metadata_value(value: &Value, parent_key: Option<&str>) -> Value {
    match value {
        Value::Object(object) => {
            let mut sanitized = serde_json::Map::new();
            for (key, value) in object {
                let key = sanitize_display_text(key);
                let value = if is_credential_like_key(&key) {
                    Value::String("<redacted>".to_string())
                } else {
                    sanitize_read_metadata_value(value, Some(&key))
                };
                let key = unique_sanitized_key(&sanitized, key);
                sanitized.insert(key, value);
            }
            Value::Object(sanitized)
        }
        Value::Array(values) => Value::Array(
            values
                .iter()
                .map(|value| sanitize_read_metadata_value(value, parent_key))
                .collect(),
        ),
        Value::String(text) => match parent_key {
            Some("path" | "paths" | "source" | "requested_path" | "url") => {
                Value::String(sanitize_read_target(text))
            }
            Some("error") => Value::String(sanitize_read_target(text)),
            _ => Value::String(sanitize_detail_output(text)),
        },
        other => other.clone(),
    }
}

pub(super) fn sanitize_detail_json(value: &Value) -> Value {
    match value {
        Value::Object(object) => {
            let mut sanitized = serde_json::Map::new();
            for (key, value) in object {
                let key = sanitize_display_text(key);
                let value = if is_credential_like_key(&key) {
                    Value::String("<redacted>".to_string())
                } else {
                    sanitize_detail_json(value)
                };
                let key = unique_sanitized_key(&sanitized, key);
                sanitized.insert(key, value);
            }
            Value::Object(sanitized)
        }
        Value::Array(values) => Value::Array(values.iter().map(sanitize_detail_json).collect()),
        Value::String(text) => Value::String(sanitize_detail_output(text)),
        other => other.clone(),
    }
}

fn unique_sanitized_key(object: &serde_json::Map<String, Value>, key: String) -> String {
    if !object.contains_key(&key) {
        return key;
    }
    let mut index = 2usize;
    loop {
        let candidate = format!("{key} [{index}]");
        if !object.contains_key(&candidate) {
            return candidate;
        }
        index += 1;
    }
}

pub(super) fn sanitize_detail_output(text: &str) -> String {
    sanitize_detail_text(text, 0)
}

fn sanitize_detail_text(text: &str, depth: usize) -> String {
    let mut replacements = Vec::<(usize, usize, String)>::new();
    collect_detail_replacements(text, depth, &mut replacements);
    if replacements.is_empty() {
        return sanitize_display_text(text);
    }
    let mut output = String::with_capacity(text.len());
    let mut copied_through = 0;
    for (start, end, replacement) in replacements {
        if start < copied_through {
            continue;
        }
        output.push_str(&sanitize_display_text(&text[copied_through..start]));
        output.push_str(&replacement);
        copied_through = end;
    }
    output.push_str(&sanitize_display_text(&text[copied_through..]));
    output
}

fn collect_detail_replacements(
    text: &str,
    depth: usize,
    replacements: &mut Vec<(usize, usize, String)>,
) {
    const MAX_JSON_STRING_DEPTH: usize = 32;
    if depth >= MAX_JSON_STRING_DEPTH {
        replacements.push((0, text.len(), "<redacted>".to_string()));
        return;
    }

    let mut scan_from = 0;
    while scan_from < text.len() {
        let Some(relative_start) = text[scan_from..].find('"') else {
            break;
        };
        let start = scan_from + relative_start;
        let Some((token_end, decoded)) = parse_json_string_at(text, start) else {
            scan_from = start + 1;
            continue;
        };
        let after_token = skip_json_whitespace(text, token_end);
        if text.as_bytes().get(after_token) == Some(&b':') {
            let key = sanitize_display_text(&decoded);
            if is_credential_like_key(&key) {
                let value_start = skip_diff_prefixed_value_start(text, start, after_token + 1);
                let value_end = json_value_end(text, value_start).unwrap_or(text.len());
                let mut replacement = sanitize_display_controls(&text[start..value_start]);
                replacement.push_str("\"<redacted>\"");
                replacements.push((start, value_end, replacement));
                scan_from = value_end.max(value_start + 1).min(text.len());
            } else {
                scan_from = token_end;
            }
            continue;
        }

        let mut nested = Vec::new();
        collect_detail_replacements(&decoded, depth + 1, &mut nested);
        if !nested.is_empty()
            && let Some(boundaries) = json_string_boundary_map(&text[start..token_end], &decoded)
        {
            for (nested_start, nested_end, replacement) in nested {
                let Ok(start_index) =
                    boundaries.binary_search_by_key(&nested_start, |(decoded, _)| *decoded)
                else {
                    continue;
                };
                let Ok(end_index) =
                    boundaries.binary_search_by_key(&nested_end, |(decoded, _)| *decoded)
                else {
                    continue;
                };
                replacements.push((
                    start + boundaries[start_index].1,
                    start + boundaries[end_index].1,
                    encode_json_string_fragment(&replacement),
                ));
            }
        }
        scan_from = token_end;
    }
}

pub(super) fn sanitize_read_block_output(result: &ToolResult) -> String {
    if !result.success {
        return sanitize_read_target(&result.content);
    }
    let results = result
        .metadata
        .get("results")
        .and_then(Value::as_array)
        .map(Vec::as_slice)
        .unwrap_or_default();
    let mut next_result = 0usize;
    let mut failed_section = false;
    result
        .content
        .lines()
        .map(|line| {
            if let Some(item) = results.get(next_result)
                && let Some(requested_path) = item.get("requested_path").and_then(Value::as_str)
                && line == format!("--- FILE: {requested_path} ---")
            {
                failed_section = item.get("success").and_then(Value::as_bool) == Some(false);
                next_result += 1;
                return sanitize_read_target(line);
            }
            if failed_section {
                sanitize_read_target(line)
            } else {
                sanitize_display_text(line)
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}