diff --git a/guards/github-guard/rust-guard/src/labels/helpers.rs b/guards/github-guard/rust-guard/src/labels/helpers.rs index 45d5b4734..854081a7e 100644 --- a/guards/github-guard/rust-guard/src/labels/helpers.rs +++ b/guards/github-guard/rust-guard/src/labels/helpers.rs @@ -660,6 +660,23 @@ pub fn extract_items_array(response: &Value) -> (Option<&Vec>, String) { (None, String::new()) } +/// Collect items from a response that is either a JSON array or a single object. +/// +/// Returns a `Vec<&Value>` of items to process. Wrappers like MCP text envelopes +/// and search-result metadata objects are excluded from single-object promotion. +pub(crate) fn collect_items_simple(response: &Value) -> Vec<&Value> { + if let Some(arr) = response.as_array() { + arr.iter().collect() + } else if response.is_object() + && !is_search_result_wrapper(response) + && !is_mcp_text_wrapper(response) + { + vec![response] + } else { + vec![] + } +} + /// GraphQL collection fields under data.repository and their JSON Pointer paths. const GRAPHQL_COLLECTION_FIELDS: &[(&str, &str)] = &[ ("issues", "/data/repository/issues/nodes"), diff --git a/guards/github-guard/rust-guard/src/labels/response_items.rs b/guards/github-guard/rust-guard/src/labels/response_items.rs index 5ff56fa51..b022d1c89 100644 --- a/guards/github-guard/rust-guard/src/labels/response_items.rs +++ b/guards/github-guard/rust-guard/src/labels/response_items.rs @@ -316,16 +316,7 @@ pub fn label_response_items( // === File Contents - repo-scoped secrecy === "get_file_contents" => { - let all_items: Vec<&Value> = if actual_response.is_array() { - actual_response - .as_array() - .map(|arr| arr.iter().collect()) - .unwrap_or_default() - } else if actual_response.is_object() && !is_search_result_wrapper(&actual_response) && !is_mcp_text_wrapper(&actual_response) { - vec![&actual_response] - } else { - vec![] - }; + let all_items = collect_items_simple(&actual_response); let items_limited = limit_items_with_log(all_items.as_slice(), "get_file_contents"); let (arg_owner, arg_repo, repo_full_name) = extract_repo_info(tool_args); @@ -351,16 +342,7 @@ pub fn label_response_items( // === Commits - label by branch (default branch = merged) === "list_commits" | "get_commit" => { - let all_items: Vec<&Value> = if actual_response.is_array() { - actual_response - .as_array() - .map(|arr| arr.iter().collect()) - .unwrap_or_default() - } else if actual_response.is_object() && !is_search_result_wrapper(&actual_response) && !is_mcp_text_wrapper(&actual_response) { - vec![&actual_response] - } else { - vec![] - }; + let all_items = collect_items_simple(&actual_response); // Limit items to prevent WASM memory exhaustion let items_limited = limit_items_with_log(all_items.as_slice(), "list_commits"); @@ -403,16 +385,7 @@ pub fn label_response_items( // === Gists - label by visibility === "list_gists" | "get_gist" => { - let all_items: Vec<&Value> = if actual_response.is_array() { - actual_response - .as_array() - .map(|arr| arr.iter().collect()) - .unwrap_or_default() - } else if actual_response.is_object() && !is_search_result_wrapper(&actual_response) && !is_mcp_text_wrapper(&actual_response) { - vec![&actual_response] - } else { - vec![] - }; + let all_items = collect_items_simple(&actual_response); // Limit items to prevent WASM memory exhaustion let items_limited = limit_items_with_log(all_items.as_slice(), "list_gists"); @@ -460,16 +433,7 @@ pub fn label_response_items( // === Releases - merged-level integrity (endorsed) === "list_releases" | "get_latest_release" | "get_release_by_tag" => { - let all_items: Vec<&Value> = if actual_response.is_array() { - actual_response - .as_array() - .map(|arr| arr.iter().collect()) - .unwrap_or_default() - } else if actual_response.is_object() && !is_search_result_wrapper(&actual_response) && !is_mcp_text_wrapper(&actual_response) { - vec![&actual_response] - } else { - vec![] - }; + let all_items = collect_items_simple(&actual_response); // Limit items to prevent WASM memory exhaustion let items_limited = limit_items_with_log(all_items.as_slice(), "list_releases"); diff --git a/guards/github-guard/rust-guard/src/labels/tool_rules.rs b/guards/github-guard/rust-guard/src/labels/tool_rules.rs index dd5fea4f3..34ab5fe2e 100644 --- a/guards/github-guard/rust-guard/src/labels/tool_rules.rs +++ b/guards/github-guard/rust-guard/src/labels/tool_rules.rs @@ -9,9 +9,10 @@ use super::constants::{field_names, SENSITIVE_FILE_KEYWORDS, SENSITIVE_FILE_PATT use super::helpers::{ author_association_floor_from_str, ensure_integrity_baseline, extract_number_as_string, extract_repo_info, extract_repo_info_from_search_query, format_repo_id, - is_default_branch_commit_context, is_default_branch_ref, is_trusted_first_party_bot, - max_integrity, merged_integrity, policy_private_scope_label, private_user_label, - project_github_label, reader_integrity, secret_label, writer_integrity, PolicyContext, + is_configured_trusted_bot, is_default_branch_commit_context, is_default_branch_ref, + is_trusted_first_party_bot, max_integrity, merged_integrity, policy_private_scope_label, + private_user_label, project_github_label, reader_integrity, secret_label, writer_integrity, + PolicyContext, }; fn apply_repo_visibility_secrecy( @@ -127,7 +128,9 @@ pub fn apply_tool_labels( ); // Elevate trusted first-party bots to approved if let Some(ref login) = info.author_login { - if is_trusted_first_party_bot(login) { + if is_trusted_first_party_bot(login) + || is_configured_trusted_bot(login, ctx) + { floor = max_integrity( repo_id, floor, @@ -185,7 +188,9 @@ pub fn apply_tool_labels( // Elevate trusted first-party bots to approved if let Some(ref login) = facts.author_login { - if is_trusted_first_party_bot(login) { + if is_trusted_first_party_bot(login) + || is_configured_trusted_bot(login, ctx) + { integrity = max_integrity( repo_id, integrity,