Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions guards/github-guard/rust-guard/src/labels/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
let parse_owner_repo = |path: &str| {
let mut parts = path.split('/').filter(|segment| !segment.is_empty());
Expand Down
24 changes: 2 additions & 22 deletions guards/github-guard/rust-guard/src/labels/response_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 2 additions & 22 deletions guards/github-guard/rust-guard/src/labels/response_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 9 additions & 16 deletions guards/github-guard/rust-guard/src/labels/tool_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading