diff --git a/guards/github-guard/rust-guard/src/labels/mod.rs b/guards/github-guard/rust-guard/src/labels/mod.rs index 804dcc13..61ee4ee6 100644 --- a/guards/github-guard/rust-guard/src/labels/mod.rs +++ b/guards/github-guard/rust-guard/src/labels/mod.rs @@ -4770,6 +4770,40 @@ mod tests { } } + #[test] + fn test_apply_tool_labels_set_issue_fields_writer_integrity() { + let ctx = default_ctx(); + let repo_id = "github/copilot"; + let tool_args = json!({ + "owner": "github", + "repo": "copilot", + "issue_number": 1, + "fields": [ + { + "field_id": "PVTSSF_example", + "text_value": "In progress" + } + ] + }); + + let (secrecy, integrity, _desc) = apply_tool_labels( + "set_issue_fields", + &tool_args, + repo_id, + vec![], + vec![], + String::new(), + &ctx, + ); + + assert_eq!(secrecy, vec![] as Vec, "set_issue_fields secrecy mismatch"); + assert_eq!( + integrity, + writer_integrity(repo_id, &ctx), + "set_issue_fields should have writer integrity" + ); + } + #[test] fn test_apply_tool_labels_granular_sub_issue_tools_writer_integrity() { let ctx = default_ctx(); 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 6ba98578..d0948e5b 100644 --- a/guards/github-guard/rust-guard/src/labels/tool_rules.rs +++ b/guards/github-guard/rust-guard/src/labels/tool_rules.rs @@ -533,6 +533,15 @@ pub fn apply_tool_labels( integrity = writer_integrity(repo_id, ctx); } + // === Issue custom fields mutation (repo-scoped write) === + "set_issue_fields" => { + // Field definitions are organization-level, but the mutation targets a specific + // issue in owner/repo and returns issue-scoped metadata. + // S = S(repo); I = writer + secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); + integrity = writer_integrity(repo_id, ctx); + } + // === Granular repo-scoped write operations === // Covers granular issue PATCH tools, sub-issue management, granular PR PATCH tools, // and PR review tools. All follow: S = S(repo), I = writer. diff --git a/guards/github-guard/rust-guard/src/tools.rs b/guards/github-guard/rust-guard/src/tools.rs index 375a6673..2bd68f54 100644 --- a/guards/github-guard/rust-guard/src/tools.rs +++ b/guards/github-guard/rust-guard/src/tools.rs @@ -100,6 +100,9 @@ pub const READ_WRITE_OPERATIONS: &[&str] = &[ "update_issue_title", // PATCH — modifies issue title "update_issue_type", // PATCH — modifies issue type + // Issue custom field mutation (field definitions are org-level; target issue is repo-scoped) + "set_issue_fields", // GraphQL — sets custom field values on a specific repository issue + // Sub-issue management tools (alongside sub_issue_write composite) "add_sub_issue", // POST /repos/.../issues/{number}/sub_issues "remove_sub_issue", // DELETE/POST — remove sub-issue link @@ -362,6 +365,21 @@ mod tests { } } + #[test] + fn test_set_issue_fields_is_read_write_operation() { + let op = "set_issue_fields"; + assert!( + is_read_write_operation(op), + "{} must be classified as a read-write operation", + op + ); + assert!( + !is_write_operation(op), + "{} should not be in WRITE_OPERATIONS (it is in READ_WRITE_OPERATIONS)", + op + ); + } + #[test] fn test_sub_issue_management_tools_are_read_write_operations() { for op in &["add_sub_issue", "remove_sub_issue", "reprioritize_sub_issue"] {