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 602110c2..9daa3384 100644 --- a/guards/github-guard/rust-guard/src/labels/tool_rules.rs +++ b/guards/github-guard/rust-guard/src/labels/tool_rules.rs @@ -591,7 +591,9 @@ pub fn apply_tool_labels( } // === Projects write operations (org-scoped) === - "projects_write" => { + "projects_write" + // Deprecated aliases that map to projects_write + | "add_project_item" | "update_project_item" | "delete_project_item" => { // Projects are org-scoped; write responses carry the same labels as reads. // I = approved: if !owner.is_empty() { @@ -609,7 +611,9 @@ pub fn apply_tool_labels( } // === Actions: Workflow run triggers === - "actions_run_trigger" => { + "actions_run_trigger" + // Deprecated aliases that map to actions_run_trigger + | "run_workflow" | "delete_workflow_run_logs" => { // Triggering a workflow run returns repo-scoped metadata. // S = S(repo); I = writer secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); @@ -670,6 +674,16 @@ pub fn apply_tool_labels( integrity = writer_integrity(repo_id, ctx); } + // === Dynamic toolset enablement (capability expansion) === + "enable_toolset" => { + // Enabling a toolset expands the agent's runtime capability set. + // Requires writer-level integrity to prevent low-trust agents from + // self-escalating by enabling additional tool groups. + // S = public (empty — no repository-scoped data); I = writer (global) + baseline_scope = "github".to_string(); + integrity = writer_integrity("github", ctx); + } + // === Star/unstar operations (public metadata) === "star_repository" | "unstar_repository" => { // Starring is a public action; response is minimal metadata. @@ -679,6 +693,33 @@ pub fn apply_tool_labels( integrity = project_github_label(ctx); } + // === Issue/PR comment editing/deletion (pre-emptive) === + "update_issue_comment" | "delete_issue_comment" => { + // Editing or deleting an issue/PR comment is a repo-scoped write. + // S = S(repo); I = writer + secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); + integrity = writer_integrity(repo_id, ctx); + } + + // === Release management (pre-emptive) === + "create_release" | "edit_release" | "delete_release" => { + // Release operations are repo-scoped writes. + // S = S(repo); I = writer + secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx); + integrity = writer_integrity(repo_id, ctx); + } + + // === Gist deletion (pre-emptive) === + "delete_gist" => { + // Gist deletion is a write on user-scoped content. + // Conservatively treat gists as private/user-scoped, consistent with + // other gist operations that may target secret gists. + // S = private_user; I = writer(user) + secrecy = private_user_label(); + baseline_scope = "user".to_string(); + integrity = writer_integrity("user", ctx); + } + _ => { // Default: inherit provided labels } diff --git a/guards/github-guard/rust-guard/src/tools.rs b/guards/github-guard/rust-guard/src/tools.rs index 91e18a87..8dcd7859 100644 --- a/guards/github-guard/rust-guard/src/tools.rs +++ b/guards/github-guard/rust-guard/src/tools.rs @@ -59,6 +59,20 @@ pub const WRITE_OPERATIONS: &[&str] = &[ // Pre-emptive: gh repo deploy-key add/delete — SSH key with optional write access "add_deploy_key", "delete_deploy_key", + // Deprecated alias coverage (guard sees alias name before backend resolves it) + "run_workflow", // deprecated alias for actions_run_trigger (POST workflow dispatch) + "delete_workflow_run_logs", // deprecated alias for actions_run_trigger (DELETE run logs) + "add_project_item", // deprecated alias for projects_write (addProjectV2ItemById) + "delete_project_item", // deprecated alias for projects_write (deleteProjectV2Item) + // Pre-emptive: issue/PR comment editing/deletion (gh issue/pr comment --edit/--delete) + "update_issue_comment", // PATCH /repos/.../issues/comments/{id} + "delete_issue_comment", // DELETE /repos/.../issues/comments/{id} + // Pre-emptive: release management (gh release create/edit/delete) + "create_release", // POST /repos/.../releases + "edit_release", // PATCH /repos/.../releases/{id} + "delete_release", // DELETE /repos/.../releases/{id} + // Pre-emptive: gist deletion (gh gist delete) + "delete_gist", // DELETE /gists/{gist_id} ]; /// Read-write operations that both read and modify data @@ -73,6 +87,8 @@ pub const READ_WRITE_OPERATIONS: &[&str] = &[ // Pre-emptive entries for anticipated future MCP tools (no equivalent tool today) // gh agent-task create — creates a Copilot coding-agent job (branch + PR); blocked as unsupported "create_agent_task", + // Deprecated alias coverage + "update_project_item", // deprecated alias for projects_write (updateProjectV2ItemFieldValue) ]; /// Check if a tool is a write operation @@ -249,4 +265,46 @@ mod tests { "create_agent_task should not be in WRITE_OPERATIONS (it is in READ_WRITE_OPERATIONS)" ); } + + #[test] + fn test_deprecated_alias_write_operations() { + for op in &[ + "run_workflow", + "delete_workflow_run_logs", + "add_project_item", + "delete_project_item", + ] { + assert!( + is_write_operation(op), + "{} (deprecated alias) must be classified as a write operation", + op + ); + } + } + + #[test] + fn test_deprecated_alias_read_write_operations() { + assert!( + is_read_write_operation("update_project_item"), + "update_project_item (deprecated alias) must be classified as a read-write operation" + ); + } + + #[test] + fn test_preemptive_cli_write_operations() { + for op in &[ + "update_issue_comment", + "delete_issue_comment", + "create_release", + "edit_release", + "delete_release", + "delete_gist", + ] { + assert!( + is_write_operation(op), + "{} (pre-emptive CLI) must be classified as a write operation", + op + ); + } + } }