From 70402c131c6756b385c963e1421deb3542fc3e0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:02:32 +0000 Subject: [PATCH 1/3] Initial plan From a1f05dfa79d25a962f47b8139ace9f85a87c5519 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:08:56 +0000 Subject: [PATCH 2/3] Add pre-emptive guard entries for 3 CLI write operations (edit_repository, revert_pull_request, add_deploy_key, delete_deploy_key) Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/17c13f5c-2bc5-46db-8a22-089994e14c83 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../github-guard/rust-guard/src/labels/mod.rs | 80 +++++++++++++++++++ .../rust-guard/src/labels/tool_rules.rs | 25 ++++++ guards/github-guard/rust-guard/src/tools.rs | 23 ++++++ 3 files changed, 128 insertions(+) diff --git a/guards/github-guard/rust-guard/src/labels/mod.rs b/guards/github-guard/rust-guard/src/labels/mod.rs index 0adfc0f94..588826480 100644 --- a/guards/github-guard/rust-guard/src/labels/mod.rs +++ b/guards/github-guard/rust-guard/src/labels/mod.rs @@ -4712,4 +4712,84 @@ mod tests { assert_eq!(integrity, writer_integrity(repo_id, &ctx), "fork_repository should have writer integrity"); } + + #[test] + fn test_apply_tool_labels_edit_repository_writer_integrity() { + let ctx = default_ctx(); + let repo_id = "github/copilot"; + let tool_args = json!({ + "owner": "github", + "repo": "copilot", + }); + + let (_secrecy, integrity, _desc) = apply_tool_labels( + "edit_repository", + &tool_args, + repo_id, + vec![], + vec![], + String::new(), + &ctx, + ); + + assert_eq!(integrity, writer_integrity(repo_id, &ctx), "edit_repository should have writer integrity"); + } + + #[test] + fn test_apply_tool_labels_revert_pull_request_writer_integrity() { + let ctx = default_ctx(); + let repo_id = "github/copilot"; + let tool_args = json!({ + "owner": "github", + "repo": "copilot", + "pullNumber": 42, + }); + + let (_secrecy, integrity, _desc) = apply_tool_labels( + "revert_pull_request", + &tool_args, + repo_id, + vec![], + vec![], + String::new(), + &ctx, + ); + + assert_eq!(integrity, writer_integrity(repo_id, &ctx), "revert_pull_request should have writer integrity"); + } + + #[test] + fn test_apply_tool_labels_deploy_key_operations_private_secrecy() { + let ctx = default_ctx(); + let repo_id = "github/copilot"; + let tool_args = json!({ + "owner": "github", + "repo": "copilot", + }); + + for tool_name in &["add_deploy_key", "delete_deploy_key"] { + let (secrecy, integrity, _desc) = apply_tool_labels( + tool_name, + &tool_args, + repo_id, + vec![], + vec![], + String::new(), + &ctx, + ); + + assert_eq!( + secrecy, + super::helpers::policy_private_scope_label("github", "copilot", repo_id, &ctx), + "{} should have private-scoped secrecy", + tool_name + ); + assert_eq!( + integrity, + writer_integrity(repo_id, &ctx), + "{} should have writer integrity", + tool_name + ); + } + } } 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 7943d11bc..f5f5c713c 100644 --- a/guards/github-guard/rust-guard/src/labels/tool_rules.rs +++ b/guards/github-guard/rust-guard/src/labels/tool_rules.rs @@ -644,6 +644,31 @@ pub fn apply_tool_labels( integrity = writer_integrity(repo_id, ctx); } + // === Repository settings edit (can change visibility) === + "edit_repository" => { + // Can change repo visibility, security settings, default branch. + // S = S(repo); I = writer (requires admin access) + secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); + integrity = writer_integrity(repo_id, ctx); + } + + // === PR revert (creates revert branch + PR) === + "revert_pull_request" => { + // Creates a new branch + PR reverting a merged PR. + // S = S(repo); I = writer + secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); + integrity = writer_integrity(repo_id, ctx); + } + + // === Deploy key management (SSH key with optional write access) === + "add_deploy_key" | "delete_deploy_key" => { + // Manages SSH deploy keys — `add_deploy_key` may grant persistent write access. + // S = private:owner/repo (deploy key secrets should be restricted) + // I = writer (requires admin access) + secrecy = policy_private_scope_label(&owner, &repo, repo_id, ctx); + integrity = writer_integrity(repo_id, ctx); + } + // === Star/unstar operations (public metadata) === "star_repository" | "unstar_repository" => { // Starring is a public action; response is minimal metadata. diff --git a/guards/github-guard/rust-guard/src/tools.rs b/guards/github-guard/rust-guard/src/tools.rs index c0185df7b..5018ac7ef 100644 --- a/guards/github-guard/rust-guard/src/tools.rs +++ b/guards/github-guard/rust-guard/src/tools.rs @@ -51,6 +51,13 @@ pub const WRITE_OPERATIONS: &[&str] = &[ "rerun_workflow_run", // gh run rerun — reruns a completed workflow run "rerun_failed_jobs", // gh run rerun --failed — reruns only failed jobs "rerun_workflow_job", // gh run rerun --job — reruns a specific job + // Pre-emptive: gh repo edit (PATCH /repos/{owner}/{repo}) — can change visibility, security settings + "edit_repository", + // Pre-emptive: gh pr revert (GraphQL revertPullRequest) — creates revert branch + PR + "revert_pull_request", + // Pre-emptive: gh repo deploy-key add/delete — SSH key with optional write access + "add_deploy_key", + "delete_deploy_key", ]; /// Read-write operations that both read and modify data @@ -210,6 +217,22 @@ mod tests { } } + #[test] + fn test_cli_gap_operations_are_write_operations() { + for op in &[ + "edit_repository", + "revert_pull_request", + "add_deploy_key", + "delete_deploy_key", + ] { + assert!( + is_write_operation(op), + "{} must be classified as a write operation", + op + ); + } + } + #[test] fn test_create_agent_task_is_read_write_and_blocked() { assert!( From 6c001472706a0abb953bb12d440d0184658de8e9 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sat, 11 Apr 2026 14:21:52 -0700 Subject: [PATCH 3/3] Fix comment: deploy key secrecy scope is policy-dependent The comment claimed secrecy was always private:owner/repo, but policy_private_scope_label() returns a scope that depends on the cached policy scope kind (unscoped, owner-scoped, or repo-scoped). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- guards/github-guard/rust-guard/src/labels/tool_rules.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f5f5c713c..fd66280e2 100644 --- a/guards/github-guard/rust-guard/src/labels/tool_rules.rs +++ b/guards/github-guard/rust-guard/src/labels/tool_rules.rs @@ -663,7 +663,7 @@ pub fn apply_tool_labels( // === Deploy key management (SSH key with optional write access) === "add_deploy_key" | "delete_deploy_key" => { // Manages SSH deploy keys — `add_deploy_key` may grant persistent write access. - // S = private:owner/repo (deploy key secrets should be restricted) + // S = at least private; scope is policy-dependent (may be unscoped, owner-scoped, or repo-scoped) // I = writer (requires admin access) secrecy = policy_private_scope_label(&owner, &repo, repo_id, ctx); integrity = writer_integrity(repo_id, ctx);