Skip to content

Commit df3bb25

Browse files
authored
[rust-guard] Move test-only function out of production code; add missing field_names constants (#3096)
Two small code quality improvements to the Rust guard: dead production code inflating the WASM binary, and JSON field strings scattered as raw literals instead of using the existing `field_names` convention. ## Changes ### 1. `is_forked_pull_request_with_callback` → `#[cfg(test)]` only (`backend.rs`) The function was `pub` in production code with `#[allow(dead_code)]` suppressing the warning, but was only ever called from tests. Moved it into the `#[cfg(test)] mod tests` block as a private `fn`, removing the suppression annotation and eliminating the dead WASM binary code. ```rust // Before: production code #[allow(dead_code)] pub fn is_forked_pull_request_with_callback(...) -> Option<bool> { ... } // After: test-only #[cfg(test)] mod tests { fn is_forked_pull_request_with_callback(...) -> Option<bool> { ... } } ``` ### 2. New `field_names` constants + replacement of raw literals (`constants.rs`, `helpers.rs`, `response_items.rs`, `response_paths.rs`) Added `FULL_NAME`, `NUMBER`, `PRIVATE`, and `LOGIN` to the `field_names` module alongside the existing `OWNER`, `REPO`, `SHA`, etc. Replaced all production-code JSON field string lookups with the new constants across the four affected files. Added `use super::constants::field_names;` to `response_items.rs` and `response_paths.rs` which previously lacked the import. Note: `"private".to_string()` in secrecy label constructions (not field lookups) are intentionally left as-is. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build3562526043/b336/launcher.test /tmp/go-build3562526043/b336/launcher.test -test.testlogfile=/tmp/go-build3562526043/b336/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net 64/src/mime/encodedword.go x_amd64/vet -p crypto/internal/-atomic -lang=go1.25 x_amd64/vet -o ify@v1.11.1/asse-errorsas -trimpath` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3562526043/b318/config.test /tmp/go-build3562526043/b318/config.test -test.testlogfile=/tmp/go-build3562526043/b318/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/runtime/cgo 2zQN/vEak0ZJ9yzSgbLLs2zQN x_amd64/vet guard.3t4ldemkr1/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet mcpgodebug /home/REDACTED/.ru-unreachable=false x_amd64/vet 1226�� 122696/b119/_pkg_.a nn_n/bFgeOALOdCKszpT-nn_n x_amd64/vet /home/REDACTED/wor/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet /home/REDACTED/wor-unsafeptr=false /home/REDACTED/wor-unreachable=false x_amd64/vet` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build3562526043/b336/launcher.test /tmp/go-build3562526043/b336/launcher.test -test.testlogfile=/tmp/go-build3562526043/b336/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net 64/src/mime/encodedword.go x_amd64/vet -p crypto/internal/-atomic -lang=go1.25 x_amd64/vet -o ify@v1.11.1/asse-errorsas -trimpath` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build3562526043/b336/launcher.test /tmp/go-build3562526043/b336/launcher.test -test.testlogfile=/tmp/go-build3562526043/b336/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net 64/src/mime/encodedword.go x_amd64/vet -p crypto/internal/-atomic -lang=go1.25 x_amd64/vet -o ify@v1.11.1/asse-errorsas -trimpath` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3562526043/b345/mcp.test /tmp/go-build3562526043/b345/mcp.test -test.testlogfile=/tmp/go-build3562526043/b345/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true _.a 58122696/b151//_-ifaceassert x_amd64/vet go1.25.8 ternal/engine/wa--version 122696/b151/ x_amd64/vet ctor�� .cfg 122696/b151/ x_amd64/vet --gdwarf-5 ut-1903621372.c -o x_amd64/vet` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details>
2 parents 0dda6f3 + aab26d1 commit df3bb25

5 files changed

Lines changed: 98 additions & 91 deletions

File tree

guards/github-guard/rust-guard/src/labels/backend.rs

Lines changed: 69 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -264,75 +264,6 @@ pub fn is_repo_org_owned(owner: &str, repo: &str) -> Option<bool> {
264264
get_cached_owner_is_org(&repo_id)
265265
}
266266

267-
/// Determine whether a pull request is from a fork.
268-
///
269-
/// This helper calls `pull_request_read` through the provided backend callback,
270-
/// extracts `base.repo.full_name` and `head.repo.full_name`, and returns:
271-
/// - `Some(true)` if the PR is from a fork (head repo differs from base repo)
272-
/// - `Some(false)` if the PR is direct (same repository)
273-
/// - `None` if the result cannot be determined
274-
#[allow(dead_code)]
275-
pub fn is_forked_pull_request_with_callback(
276-
callback: GithubMcpCallback,
277-
owner: &str,
278-
repo: &str,
279-
pull_number: &str,
280-
) -> Option<bool> {
281-
if owner.is_empty() || repo.is_empty() || pull_number.is_empty() {
282-
return None;
283-
}
284-
285-
let args = serde_json::json!({
286-
"owner": owner,
287-
"repo": repo,
288-
"pullNumber": pull_number,
289-
"method": "get",
290-
});
291-
292-
let args_str = args.to_string();
293-
let mut result_buffer = vec![0u8; SMALL_BUFFER_SIZE];
294-
295-
crate::log_debug(&format!(
296-
"Checking PR origin for {}/{}#{}",
297-
owner, repo, pull_number
298-
));
299-
300-
let len = match callback("pull_request_read", &args_str, &mut result_buffer) {
301-
Ok(len) if len > 0 => len,
302-
Ok(_) => return None,
303-
Err(code) => {
304-
crate::log_warn(&format!(
305-
"Failed to check PR origin for {}/{}#{}: error code {}",
306-
owner, repo, pull_number, code
307-
));
308-
return None;
309-
}
310-
};
311-
312-
let response_str = std::str::from_utf8(&result_buffer[..len]).ok()?;
313-
let response = serde_json::from_str::<Value>(response_str).ok()?;
314-
let pr = super::extract_mcp_response(&response);
315-
316-
let base_full_name = pr
317-
.get("base")
318-
.and_then(|b| b.get("repo"))
319-
.and_then(|r| r.get("full_name"))
320-
.and_then(|v| v.as_str());
321-
322-
let head_full_name = pr
323-
.get("head")
324-
.and_then(|h| h.get("repo"))
325-
.and_then(|r| r.get("full_name"))
326-
.and_then(|v| v.as_str());
327-
328-
match (base_full_name, head_full_name) {
329-
(Some(base), Some(head)) if !base.is_empty() && !head.is_empty() => {
330-
Some(!base.eq_ignore_ascii_case(head))
331-
}
332-
_ => None,
333-
}
334-
}
335-
336267
/// Fetch pull request facts used for integrity derivation.
337268
pub fn get_pull_request_facts_with_callback(
338269
callback: GithubMcpCallback,
@@ -674,9 +605,78 @@ fn extract_rate_reset_seconds(error_text: &str) -> Option<u64> {
674605

675606
#[cfg(test)]
676607
mod tests {
608+
use super::super::constants::field_names;
677609
use super::*;
678610
use std::sync::atomic::{AtomicUsize, Ordering};
679611

612+
/// Determine whether a pull request is from a fork.
613+
///
614+
/// This helper calls `pull_request_read` through the provided backend callback,
615+
/// extracts `base.repo.full_name` and `head.repo.full_name`, and returns:
616+
/// - `Some(true)` if the PR is from a fork (head repo differs from base repo)
617+
/// - `Some(false)` if the PR is direct (same repository)
618+
/// - `None` if the result cannot be determined
619+
fn is_forked_pull_request_with_callback(
620+
callback: GithubMcpCallback,
621+
owner: &str,
622+
repo: &str,
623+
pull_number: &str,
624+
) -> Option<bool> {
625+
if owner.is_empty() || repo.is_empty() || pull_number.is_empty() {
626+
return None;
627+
}
628+
629+
let args = serde_json::json!({
630+
"owner": owner,
631+
"repo": repo,
632+
"pullNumber": pull_number,
633+
"method": "get",
634+
});
635+
636+
let args_str = args.to_string();
637+
let mut result_buffer = vec![0u8; SMALL_BUFFER_SIZE];
638+
639+
crate::log_debug(&format!(
640+
"Checking PR origin for {}/{}#{}",
641+
owner, repo, pull_number
642+
));
643+
644+
let len = match callback("pull_request_read", &args_str, &mut result_buffer) {
645+
Ok(len) if len > 0 => len,
646+
Ok(_) => return None,
647+
Err(code) => {
648+
crate::log_warn(&format!(
649+
"Failed to check PR origin for {}/{}#{}: error code {}",
650+
owner, repo, pull_number, code
651+
));
652+
return None;
653+
}
654+
};
655+
656+
let response_str = std::str::from_utf8(&result_buffer[..len]).ok()?;
657+
let response = serde_json::from_str::<Value>(response_str).ok()?;
658+
let pr = crate::labels::extract_mcp_response(&response);
659+
660+
let base_full_name = pr
661+
.get("base")
662+
.and_then(|b| b.get("repo"))
663+
.and_then(|r| r.get(field_names::FULL_NAME))
664+
.and_then(|v| v.as_str());
665+
666+
let head_full_name = pr
667+
.get("head")
668+
.and_then(|h| h.get("repo"))
669+
.and_then(|r| r.get(field_names::FULL_NAME))
670+
.and_then(|v| v.as_str());
671+
672+
match (base_full_name, head_full_name) {
673+
(Some(base), Some(head)) if !base.is_empty() && !head.is_empty() => {
674+
Some(!base.eq_ignore_ascii_case(head))
675+
}
676+
_ => None,
677+
}
678+
}
679+
680680
fn copy_payload(payload: Value, buffer: &mut [u8]) -> Result<usize, i32> {
681681
let serialized = payload.to_string();
682682
let bytes = serialized.as_bytes();

guards/github-guard/rust-guard/src/labels/constants.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ pub mod field_names {
3939
pub const SHA: &str = "sha";
4040
pub const MERGED_AT: &str = "merged_at";
4141
pub const MERGED: &str = "merged";
42+
// Commonly accessed response fields
43+
pub const FULL_NAME: &str = "full_name";
44+
pub const NUMBER: &str = "number";
45+
pub const PRIVATE: &str = "private";
46+
pub const LOGIN: &str = "login";
4247
}
4348

4449
/// Sensitive file patterns for detecting secret-containing files

guards/github-guard/rust-guard/src/labels/helpers.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use super::constants::{field_names, label_constants};
1212
/// number segment from `html_url` or `url` (e.g. `.../issues/123` → `123`).
1313
/// Returns "unknown" (with a log warning) if no number can be determined.
1414
pub(crate) fn extract_resource_number(item: &Value, resource_type: &str, repo: &str) -> String {
15-
if let Some(n) = item.get("number").and_then(|v| v.as_u64()) {
15+
if let Some(n) = item.get(field_names::NUMBER).and_then(|v| v.as_u64()) {
1616
return n.to_string();
1717
}
1818
// Fallback: extract trailing number from html_url or url
@@ -298,7 +298,7 @@ fn apply_approval_label_promotion(
298298
ctx: &PolicyContext,
299299
) -> Vec<String> {
300300
if let Some(label) = first_matching_approval_label(item, ctx) {
301-
let number = item.get("number").and_then(|v| v.as_u64()).unwrap_or(0);
301+
let number = item.get(field_names::NUMBER).and_then(|v| v.as_u64()).unwrap_or(0);
302302
crate::log_info(&format!(
303303
"[integrity] {}:{}#{} promoted to approved (label '{}' in approval-labels)",
304304
resource_type, repo_full_name, number, label
@@ -603,13 +603,13 @@ pub(crate) fn extract_repo_from_github_url(url: &str) -> Option<String> {
603603
/// Returns empty string if no repo info found
604604
pub fn extract_repo_from_item(item: &Value) -> String {
605605
// Direct full_name (repositories)
606-
if let Some(name) = item.get("full_name").and_then(|v| v.as_str()) {
606+
if let Some(name) = item.get(field_names::FULL_NAME).and_then(|v| v.as_str()) {
607607
return name.to_string();
608608
}
609609
// repository.full_name (issues, PRs with repo info)
610610
if let Some(name) = item
611611
.get("repository")
612-
.and_then(|r| r.get("full_name"))
612+
.and_then(|r| r.get(field_names::FULL_NAME))
613613
.and_then(|v| v.as_str())
614614
{
615615
return name.to_string();
@@ -618,7 +618,7 @@ pub fn extract_repo_from_item(item: &Value) -> String {
618618
if let Some(name) = item
619619
.get("base")
620620
.and_then(|b| b.get("repo"))
621-
.and_then(|r| r.get("full_name"))
621+
.and_then(|r| r.get(field_names::FULL_NAME))
622622
.and_then(|v| v.as_str())
623623
{
624624
return name.to_string();
@@ -627,7 +627,7 @@ pub fn extract_repo_from_item(item: &Value) -> String {
627627
if let Some(name) = item
628628
.get("head")
629629
.and_then(|h| h.get("repo"))
630-
.and_then(|r| r.get("full_name"))
630+
.and_then(|r| r.get(field_names::FULL_NAME))
631631
.and_then(|v| v.as_str())
632632
{
633633
return name.to_string();
@@ -925,12 +925,12 @@ pub fn author_association_floor_from_str(
925925
/// Returns empty string if no login found.
926926
fn extract_author_login(item: &Value) -> &str {
927927
// Issues and PRs use user.login
928-
let login = get_nested_str(item, "user", "login");
928+
let login = get_nested_str(item, "user", field_names::LOGIN);
929929
if !login.is_empty() {
930930
return login;
931931
}
932932
// Commits use author.login
933-
get_nested_str(item, "author", "login")
933+
get_nested_str(item, "author", field_names::LOGIN)
934934
}
935935

936936
/// Check whether an item contains an `author_association` (or `authorAssociation`) field.
@@ -1036,7 +1036,7 @@ pub fn pr_integrity(
10361036
// Step 1: Check if author is in blocked_users — takes precedence over all other rules.
10371037
let author_login = extract_author_login(item);
10381038
if !author_login.is_empty() && is_blocked_user(author_login, ctx) {
1039-
let number = item.get("number").and_then(|v| v.as_u64()).unwrap_or(0);
1039+
let number = item.get(field_names::NUMBER).and_then(|v| v.as_u64()).unwrap_or(0);
10401040
crate::log_info(&format!(
10411041
"[integrity] pr:{}#{} → blocked (author '{}' in blocked-users)",
10421042
repo_full_name, number, author_login
@@ -1062,7 +1062,7 @@ pub fn pr_integrity(
10621062
// and merge status.
10631063
if integrity.is_empty() && !has_author_association(item) && !repo_private {
10641064
let number_opt = item
1065-
.get("number")
1065+
.get(field_names::NUMBER)
10661066
.and_then(|v| v.as_u64())
10671067
.map(|n| n.to_string())
10681068
.or_else(|| extract_number_from_url(item));
@@ -1176,7 +1176,7 @@ pub fn issue_integrity(
11761176
// Step 1: Check if author is in blocked_users — takes precedence over all other rules.
11771177
let author_login = extract_author_login(item);
11781178
if !author_login.is_empty() && is_blocked_user(author_login, ctx) {
1179-
let number = item.get("number").and_then(|v| v.as_u64()).unwrap_or(0);
1179+
let number = item.get(field_names::NUMBER).and_then(|v| v.as_u64()).unwrap_or(0);
11801180
crate::log_info(&format!(
11811181
"[integrity] issue:{}#{} → blocked (author '{}' in blocked-users)",
11821182
repo_full_name, number, author_login
@@ -1192,7 +1192,7 @@ pub fn issue_integrity(
11921192
// incorrectly assigning "none" integrity to members/collaborators.
11931193
if integrity.is_empty() && !has_author_association(item) && !repo_private {
11941194
let number_opt = item
1195-
.get("number")
1195+
.get(field_names::NUMBER)
11961196
.and_then(|v| v.as_u64())
11971197
.map(|n| n.to_string())
11981198
.or_else(|| extract_number_from_url(item));

guards/github-guard/rust-guard/src/labels/response_items.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! Use path-based labeling (`label_response_paths`) when possible for better
1111
//! performance with large result sets.
1212
13+
use super::constants::field_names;
1314
use super::extract_mcp_response;
1415
use super::helpers::*;
1516
use crate::{LabeledItem, ResourceLabels};
@@ -53,8 +54,8 @@ pub fn label_response_items(
5354

5455
let mut private_count = 0;
5556
for (i, item) in items_to_process.iter().enumerate() {
56-
let is_private = get_bool_or(item, "private", false);
57-
let full_name = get_str_or(item, "full_name", "unknown");
57+
let is_private = get_bool_or(item, field_names::PRIVATE, false);
58+
let full_name = get_str_or(item, field_names::FULL_NAME, "unknown");
5859

5960
// Repository metadata has approved-level integrity (endorsed by maintainers)
6061
let integrity = writer_integrity(full_name, ctx);
@@ -165,12 +166,12 @@ pub fn label_response_items(
165166
let base_head_repo = item
166167
.get("base")
167168
.and_then(|b| b.get("repo"))
168-
.and_then(|r| r.get("full_name"))
169+
.and_then(|r| r.get(field_names::FULL_NAME))
169170
.and_then(|v| v.as_str())
170171
.or_else(|| {
171172
item.get("head")
172173
.and_then(|h| h.get("repo"))
173-
.and_then(|r| r.get("full_name"))
174+
.and_then(|r| r.get(field_names::FULL_NAME))
174175
.and_then(|v| v.as_str())
175176
})
176177
.unwrap_or("");
@@ -192,12 +193,12 @@ pub fn label_response_items(
192193
let is_forked = item
193194
.get("base")
194195
.and_then(|b| b.get("repo"))
195-
.and_then(|r| r.get("full_name"))
196+
.and_then(|r| r.get(field_names::FULL_NAME))
196197
.and_then(|v| v.as_str())
197198
.zip(
198199
item.get("head")
199200
.and_then(|h| h.get("repo"))
200-
.and_then(|r| r.get("full_name"))
201+
.and_then(|r| r.get(field_names::FULL_NAME))
201202
.and_then(|v| v.as_str()),
202203
)
203204
.map(|(base, head)| !base.eq_ignore_ascii_case(head));

guards/github-guard/rust-guard/src/labels/response_paths.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! Returns JSON paths like `/items/0`, `/items/1` pointing to labeled objects
88
//! in the response, rather than cloning the entire data.
99
10+
use super::constants::field_names;
1011
use super::extract_mcp_response;
1112
use super::helpers::*;
1213
use serde_json::Value;
@@ -70,8 +71,8 @@ pub fn label_response_paths(
7071
let mut labeled_paths = Vec::with_capacity(limited_items.len());
7172

7273
for (i, item) in limited_items.iter().enumerate() {
73-
let is_private = get_bool_or(item, "private", false);
74-
let full_name = get_str_or(item, "full_name", "unknown");
74+
let is_private = get_bool_or(item, field_names::PRIVATE, false);
75+
let full_name = get_str_or(item, field_names::FULL_NAME, "unknown");
7576
let integrity = writer_integrity(full_name, ctx);
7677

7778
let secrecy = if is_private {
@@ -171,13 +172,13 @@ pub fn label_response_paths(
171172
let base_repo = item
172173
.get("base")
173174
.and_then(|b| b.get("repo"))
174-
.and_then(|r| r.get("full_name"))
175+
.and_then(|r| r.get(field_names::FULL_NAME))
175176
.and_then(|v| v.as_str())
176177
.unwrap_or("");
177178
let head_repo = item
178179
.get("head")
179180
.and_then(|h| h.get("repo"))
180-
.and_then(|r| r.get("full_name"))
181+
.and_then(|r| r.get(field_names::FULL_NAME))
181182
.and_then(|v| v.as_str())
182183
.unwrap_or("");
183184
let is_forked = if !base_repo.is_empty() && !head_repo.is_empty() {

0 commit comments

Comments
 (0)