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
5 changes: 5 additions & 0 deletions .changeset/patch-separate-github-tools-lists.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

80 changes: 78 additions & 2 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@ const ActivatedOutput = "activated"

var AgenticEngines = []string{"claude", "codex", "copilot"}

// DefaultGitHubTools defines the default read-only GitHub MCP tools
var DefaultGitHubTools = []string{
// DefaultGitHubToolsLocal defines the default read-only GitHub MCP tools for local (Docker) mode
var DefaultGitHubToolsLocal = []string{
// actions
"download_workflow_run_artifact",
"get_job_logs",
Expand Down Expand Up @@ -217,6 +217,82 @@ var DefaultGitHubTools = []string{
"list_sub_issues",
}

// DefaultGitHubToolsRemote defines the default read-only GitHub MCP tools for remote (hosted) mode
var DefaultGitHubToolsRemote = []string{
// actions
"download_workflow_run_artifact",
"get_job_logs",
"get_workflow_run",
"get_workflow_run_logs",
"get_workflow_run_usage",
"list_workflow_jobs",
"list_workflow_run_artifacts",
"list_workflow_runs",
"list_workflows",
// code security
"get_code_scanning_alert",
"list_code_scanning_alerts",
// context
"get_me",
// dependabot
"get_dependabot_alert",
"list_dependabot_alerts",
// discussions
"get_discussion",
"get_discussion_comments",
"list_discussion_categories",
"list_discussions",
// issues
"get_issue",
"get_issue_comments",
"list_issues",
"search_issues",
// notifications
"get_notification_details",
"list_notifications",
// organizations
"search_orgs",
// labels
"get_label",
"list_label",
// prs
"get_pull_request",
"get_pull_request_comments",
"get_pull_request_diff",
"get_pull_request_files",
"get_pull_request_reviews",
"get_pull_request_status",
"list_pull_requests",
"pull_request_read",
"search_pull_requests",
// repos
"get_commit",
"get_file_contents",
"get_tag",
"list_branches",
"list_commits",
"list_tags",
"search_code",
"search_repositories",
// secret protection
"get_secret_scanning_alert",
"list_secret_scanning_alerts",
// users
"search_users",
// additional unique tools (previously duplicated block extras)
"get_latest_release",
"get_pull_request_review_comments",
"get_release_by_tag",
"list_issue_types",
"list_releases",
"list_starred_repositories",
"list_sub_issues",
}

// DefaultGitHubTools is deprecated. Use DefaultGitHubToolsLocal or DefaultGitHubToolsRemote instead.
// Kept for backward compatibility and defaults to local mode tools.
var DefaultGitHubTools = DefaultGitHubToolsLocal

// DefaultBashTools defines basic bash commands that should be available by default when bash is enabled
var DefaultBashTools = []string{
"echo",
Expand Down
11 changes: 9 additions & 2 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,15 @@ func (e *ClaudeEngine) computeAllowedClaudeToolsString(tools map[string]any, saf
}
}
} else if toolName == "github" {
// For GitHub tools without explicit allowed list, use default GitHub tools
for _, defaultTool := range constants.DefaultGitHubTools {
// For GitHub tools without explicit allowed list, use appropriate default GitHub tools based on mode
githubMode := getGitHubType(mcpConfig)
var defaultTools []string
if githubMode == "remote" {
defaultTools = constants.DefaultGitHubToolsRemote
} else {
defaultTools = constants.DefaultGitHubToolsLocal
}
for _, defaultTool := range defaultTools {
allowedTools = append(allowedTools, fmt.Sprintf("mcp__github__%s", defaultTool))
}
}
Expand Down
11 changes: 10 additions & 1 deletion pkg/workflow/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1544,11 +1544,20 @@ func (c *Compiler) applyDefaultTools(tools map[string]any, safeOutputs *SafeOutp
}
}

// Determine which default tools list to use based on mode
githubMode := getGitHubType(githubTool)
var defaultTools []string
if githubMode == "remote" {
defaultTools = constants.DefaultGitHubToolsRemote
} else {
defaultTools = constants.DefaultGitHubToolsLocal
}

// Add default GitHub tools that aren't already present
newAllowed := make([]any, len(existingAllowed))
copy(newAllowed, existingAllowed)

for _, defaultTool := range constants.DefaultGitHubTools {
for _, defaultTool := range defaultTools {
if !existingToolsSet[defaultTool] {
newAllowed = append(newAllowed, defaultTool)
}
Expand Down
128 changes: 128 additions & 0 deletions pkg/workflow/github_tools_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package workflow

import (
"testing"

"github.com/githubnext/gh-aw/pkg/constants"
)

// TestGitHubToolsModeSeparation verifies that local and remote GitHub tools lists are properly separated
func TestGitHubToolsModeSeparation(t *testing.T) {
// Verify both lists exist and are not empty
if len(constants.DefaultGitHubToolsLocal) == 0 {
t.Error("DefaultGitHubToolsLocal should not be empty")
}

if len(constants.DefaultGitHubToolsRemote) == 0 {
t.Error("DefaultGitHubToolsRemote should not be empty")
}

// Verify backward compatibility - DefaultGitHubTools should point to local
if len(constants.DefaultGitHubTools) == 0 {
t.Error("DefaultGitHubTools should not be empty (backward compatibility)")
}

// Verify DefaultGitHubTools points to the same data as DefaultGitHubToolsLocal
if len(constants.DefaultGitHubTools) != len(constants.DefaultGitHubToolsLocal) {
t.Errorf("DefaultGitHubTools should have same length as DefaultGitHubToolsLocal for backward compatibility")
}

// Verify they contain expected core tools
expectedCoreTools := []string{
"get_issue",
"list_issues",
"get_commit",
"get_file_contents",
"search_repositories",
}

// Check local tools
localToolsMap := make(map[string]bool)
for _, tool := range constants.DefaultGitHubToolsLocal {
localToolsMap[tool] = true
}

for _, expectedTool := range expectedCoreTools {
if !localToolsMap[expectedTool] {
t.Errorf("Expected core tool '%s' not found in DefaultGitHubToolsLocal", expectedTool)
}
}

// Check remote tools
remoteToolsMap := make(map[string]bool)
for _, tool := range constants.DefaultGitHubToolsRemote {
remoteToolsMap[tool] = true
}

for _, expectedTool := range expectedCoreTools {
if !remoteToolsMap[expectedTool] {
t.Errorf("Expected core tool '%s' not found in DefaultGitHubToolsRemote", expectedTool)
}
}
}

// TestApplyDefaultToolsUsesCorrectMode verifies that applyDefaultTools uses the correct tool list based on mode
func TestApplyDefaultToolsUsesCorrectMode(t *testing.T) {
compiler := NewCompiler(false, "", "test")

tests := []struct {
name string
tools map[string]any
expectedList string // "local" or "remote"
}{
{
name: "Local mode (default)",
tools: map[string]any{
"github": map[string]any{},
},
expectedList: "local",
},
{
name: "Explicit local mode",
tools: map[string]any{
"github": map[string]any{
"mode": "local",
},
},
expectedList: "local",
},
{
name: "Remote mode",
tools: map[string]any{
"github": map[string]any{
"mode": "remote",
},
},
expectedList: "remote",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := compiler.applyDefaultTools(tt.tools, nil)

// Get the allowed tools from the github configuration
githubConfig, ok := result["github"].(map[string]any)
if !ok {
t.Fatal("Expected github configuration to be a map")
}

allowed, ok := githubConfig["allowed"].([]any)
if !ok {
t.Fatal("Expected allowed to be a slice")
}

// Verify that the number of tools matches the expected list
var expectedCount int
if tt.expectedList == "local" {
expectedCount = len(constants.DefaultGitHubToolsLocal)
} else {
expectedCount = len(constants.DefaultGitHubToolsRemote)
}

if len(allowed) != expectedCount {
t.Errorf("Expected %d tools for %s mode, got %d", expectedCount, tt.expectedList, len(allowed))
}
})
}
}
Loading