diff --git a/guards/github-guard/rust-guard/src/labels/helpers.rs b/guards/github-guard/rust-guard/src/labels/helpers.rs index 3cb25097..bf1a68ce 100644 --- a/guards/github-guard/rust-guard/src/labels/helpers.rs +++ b/guards/github-guard/rust-guard/src/labels/helpers.rs @@ -865,6 +865,24 @@ pub fn extract_repo_info_from_search_query(query: &str) -> (String, String, Stri (String::new(), String::new(), String::new()) } +/// Extract (owner, repo, repo_id) from tool_args, falling back to the +/// `query` field's `repo:` qualifier when the explicit fields are absent. +/// This is the canonical resolution for tools that accept either explicit +/// owner/repo args OR a free-text search query with a `repo:` scope. +pub(crate) fn extract_repo_scope_with_query_fallback( + tool_args: &Value, +) -> (String, String, String) { + let (owner, repo, repo_id) = extract_repo_info(tool_args); + if owner.is_empty() || repo.is_empty() { + let query = tool_args.get("query").and_then(|v| v.as_str()).unwrap_or(""); + let (q_owner, q_repo, q_repo_id) = extract_repo_info_from_search_query(query); + if !q_repo_id.is_empty() { + return (q_owner, q_repo, q_repo_id); + } + } + (owner, repo, repo_id) +} + pub(crate) fn extract_repo_from_github_url(url: &str) -> Option { let parse_owner_repo = |path: &str| { let mut parts = path.split('/').filter(|segment| !segment.is_empty()); 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 42f4d0ba..e5c86cde 100644 --- a/guards/github-guard/rust-guard/src/labels/response_items.rs +++ b/guards/github-guard/rust-guard/src/labels/response_items.rs @@ -136,17 +136,7 @@ pub fn label_response_items( if !items.is_empty() { let items_to_process = limit_items_with_log(items, "list_pull_requests"); - let (mut arg_owner, mut arg_repo, mut arg_repo_full) = extract_repo_info(tool_args); - // For search operations, extract repo from query when tool_args lacks owner/repo - if arg_owner.is_empty() || arg_repo.is_empty() { - let query = tool_args.get("query").and_then(|v| v.as_str()).unwrap_or(""); - let (q_owner, q_repo, q_repo_id) = extract_repo_info_from_search_query(query); - if !q_repo_id.is_empty() { - arg_owner = q_owner; - arg_repo = q_repo; - arg_repo_full = q_repo_id; - } - } + let (arg_owner, arg_repo, arg_repo_full) = extract_repo_scope_with_query_fallback(tool_args); let default_repo_private = if !arg_owner.is_empty() && !arg_repo.is_empty() { super::backend::is_repo_private(&arg_owner, &arg_repo).unwrap_or(false) } else { @@ -259,17 +249,7 @@ pub fn label_response_items( let items_limited = limit_items_with_log(all_items.as_slice(), "list_issues"); // Get owner/repo from tool_args for contributor verification - let (mut arg_owner, mut arg_repo, mut default_repo_full_name) = extract_repo_info(tool_args); - // For search operations, extract repo from query when tool_args lacks owner/repo - if arg_owner.is_empty() || arg_repo.is_empty() { - let query = tool_args.get("query").and_then(|v| v.as_str()).unwrap_or(""); - let (q_owner, q_repo, q_repo_id) = extract_repo_info_from_search_query(query); - if !q_repo_id.is_empty() { - arg_owner = q_owner; - arg_repo = q_repo; - default_repo_full_name = q_repo_id; - } - } + let (arg_owner, arg_repo, default_repo_full_name) = extract_repo_scope_with_query_fallback(tool_args); let default_repo_private = if !arg_owner.is_empty() && !arg_repo.is_empty() { super::backend::is_repo_private(&arg_owner, &arg_repo).unwrap_or(false) } else { diff --git a/guards/github-guard/rust-guard/src/labels/response_paths.rs b/guards/github-guard/rust-guard/src/labels/response_paths.rs index 2059bda3..6f2fdd21 100644 --- a/guards/github-guard/rust-guard/src/labels/response_paths.rs +++ b/guards/github-guard/rust-guard/src/labels/response_paths.rs @@ -128,17 +128,7 @@ pub fn label_response_paths( return None; } // Try tool_args first, fall back to extracting from first item - let (mut arg_owner, mut arg_repo, mut arg_repo_full) = extract_repo_info(tool_args); - // For search operations, extract repo from query when tool_args lacks owner/repo - if arg_owner.is_empty() || arg_repo.is_empty() { - let query = tool_args.get("query").and_then(|v| v.as_str()).unwrap_or(""); - let (q_owner, q_repo, q_repo_id) = extract_repo_info_from_search_query(query); - if !q_repo_id.is_empty() { - arg_owner = q_owner; - arg_repo = q_repo; - arg_repo_full = q_repo_id; - } - } + let (arg_owner, arg_repo, arg_repo_full) = extract_repo_scope_with_query_fallback(tool_args); let default_repo_private = if !arg_owner.is_empty() && !arg_repo.is_empty() { super::backend::is_repo_private(&arg_owner, &arg_repo).unwrap_or(false) } else { @@ -250,17 +240,7 @@ pub fn label_response_paths( return None; } // Try tool_args first, fall back to extracting from first item - let (mut arg_owner, mut arg_repo, mut arg_repo_full) = extract_repo_info(tool_args); - // For search operations, extract repo from query when tool_args lacks owner/repo - if arg_owner.is_empty() || arg_repo.is_empty() { - let query = tool_args.get("query").and_then(|v| v.as_str()).unwrap_or(""); - let (q_owner, q_repo, q_repo_id) = extract_repo_info_from_search_query(query); - if !q_repo_id.is_empty() { - arg_owner = q_owner; - arg_repo = q_repo; - arg_repo_full = q_repo_id; - } - } + let (arg_owner, arg_repo, arg_repo_full) = extract_repo_scope_with_query_fallback(tool_args); let default_repo_private = if !arg_owner.is_empty() && !arg_repo.is_empty() { super::backend::is_repo_private(&arg_owner, &arg_repo).unwrap_or(false) } else { 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 9daa3384..a0c756e7 100644 --- a/guards/github-guard/rust-guard/src/labels/tool_rules.rs +++ b/guards/github-guard/rust-guard/src/labels/tool_rules.rs @@ -207,22 +207,15 @@ pub fn apply_tool_labels( integrity = writer_integrity(repo_id, ctx); } - // === Repository Transfer (blocked: irreversible ownership change) === - "transfer_repository" => { - // Repository transfers are irreversible and cannot be allowed by agents. - // Blocking is enforced in label_resource via is_blocked_tool(); this arm - // applies repo-visibility secrecy so the resource is at least correctly - // classified before the integrity override happens in label_resource. - secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); - } - - // === Modifying Repository Operations (blocked: unsupported gh repo operations) === - "archive_repository" | "unarchive_repository" | "rename_repository" => { - // All modifying `gh repo` operations (archive, unarchive, rename) are treated as - // unsupported for automated agents — the same policy as transfer_repository. - // Blocking is enforced in label_resource via is_blocked_tool(); this arm applies - // repo-visibility secrecy so the resource is correctly classified before the - // integrity override happens in label_resource. + // === Blocked repository operations === + // Applies repo-visibility secrecy before label_resource enforces the unconditional + // block via is_blocked_tool(). Covers: irreversible ownership changes + // (transfer_repository) and unsupported gh-repo operations (archive, unarchive, + // rename). + "transfer_repository" + | "archive_repository" + | "unarchive_repository" + | "rename_repository" => { secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); }