Skip to content

Commit 4163393

Browse files
authored
Add GitHubMCPMode string enum and replace githubTool any with map[string]any (#38945)
1 parent c1b02b8 commit 4163393

29 files changed

Lines changed: 276 additions & 283 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ADR-38945: Type GitHub MCP Mode as an Enum and Narrow Tool Config to `map[string]any`
2+
3+
**Date**: 2026-06-12
4+
**Status**: Draft
5+
6+
## Context
7+
8+
The GitHub MCP configuration code in `pkg/workflow/mcp_github_config.go` had grown around two weakly-typed conventions. First, roughly 19 helper functions accepted the GitHub tool config as `githubTool any` and each immediately re-asserted it to `map[string]any` internally, scattering the same `.(map[string]any)` type assertion (and its failure handling) across nearly every function and its callers. Second, the MCP "mode" of the GitHub server (`local`, `remote`, `gh-proxy`, `cli`) was modelled as a bare `string`, so the compiler relied on string literals like `== "remote"` and `!= "gh-proxy"` with no compile-time protection against typos or invalid values. Both patterns made the code harder to read and gave the type system no leverage to catch mistakes in a security-sensitive area (proxy policy, lockdown, token selection).
9+
10+
## Decision
11+
12+
We will introduce a named `GitHubMCPMode` string type in `tools_types.go` with constants `GitHubMCPModeLocal`, `GitHubMCPModeRemote`, `GitHubMCPModeGHProxy`, and `GitHubMCPModeCLI`, change `GitHubToolConfig.Mode` from `string` to `GitHubMCPMode`, and cast the parsed value once at the single ingestion point in `tools_parser.go`. We will also narrow every `githubTool any` parameter across `mcp_github_config.go` and its callers to `map[string]any`, performing the single `.(map[string]any)` assertion once at each call site (when reading `tools["github"]`) rather than inside every helper. `normalizeGitHubType`/`getGitHubType` return `GitHubMCPMode`, and all bare string comparisons are replaced with the typed constants. We chose this because pushing the type assertion to the boundary and naming the mode values makes the security-relevant configuration paths self-documenting and compiler-checked, with no behavioral change (nil maps remain safe since Go returns zero values on reads).
13+
14+
## Alternatives Considered
15+
16+
### Alternative 1: Keep `githubTool any` and the bare-string mode
17+
Leave the existing signatures unchanged and continue asserting to `map[string]any` inside each helper. Rejected because it perpetuates ~19 duplicated assertions and offers no compile-time defense for mode values; the duplication and stringly-typed comparisons are exactly the maintenance and correctness hazard this PR exists to remove.
18+
19+
### Alternative 2: Model the tool config as a concrete struct end-to-end
20+
Replace the `map[string]any` representation entirely with a fully-typed `GitHubToolConfig` struct threaded through all helpers. Rejected (for now) as too large a change: the tool config is parsed from open-ended frontmatter and shared with generic MCP rendering paths that operate on `map[string]any`, so a full struct migration would ripple far beyond this PR. Narrowing to `map[string]any` plus a typed mode enum captures most of the type-safety benefit at a fraction of the blast radius.
21+
22+
## Consequences
23+
24+
### Positive
25+
- The single `.(map[string]any)` assertion moves to each call site, eliminating ~19 duplicated assertions and their inconsistent failure handling inside helpers.
26+
- `GitHubMCPMode` constants give the compiler authority over mode values, preventing typo'd or invalid mode strings in security-sensitive logic (proxy, lockdown, token selection).
27+
- Helper signatures (`map[string]any`, `GitHubMCPMode`) now document intent directly, improving readability for future contributors.
28+
29+
### Negative
30+
- Wide, mechanical change surface: ~30 non-test files plus 11 test files touched, increasing review effort and rebase/merge-conflict risk against concurrent work in `pkg/workflow`.
31+
- Test inputs that previously exercised non-map values (e.g. `"invalid"`, `false`, `"not a map"`) are replaced with `nil`/empty maps, so the "wrong type passed in" branch is no longer covered at the helper level — that contract now lives at the call-site assertion.
32+
- The type assertion at each call site silently yields a `nil` map on mismatch (`, _`), so a malformed `tools["github"]` value degrades to zero-value behavior rather than an explicit error.
33+
34+
### Neutral
35+
- `getGitHubReadOnly()` loses its unused parameter and now takes no arguments, since it unconditionally returns `true`.
36+
- The change is intended to be behavior-preserving; the GitHub MCP server remains unconditionally read-only and nil-map reads continue to return zero values.
37+
38+
---
39+
40+
*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/27446718826) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*

pkg/workflow/agent_validation.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ func (c *Compiler) validatePiEngineRequirements(tools *ToolsConfig, engine Codin
199199
return nil
200200
}
201201

202-
if tools == nil || tools.GitHub == nil || tools.GitHub.Mode != "gh-proxy" {
202+
if tools == nil || tools.GitHub == nil ||
203+
(tools.GitHub.Mode != GitHubMCPModeGHProxy && tools.GitHub.Mode != GitHubMCPModeCLI) {
203204
return errors.New("engine 'pi' requires tools.github.mode: gh-proxy")
204205
}
205206

pkg/workflow/claude_tools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func appendGitHubMCPTools(allowedTools []string, toolName string, toolValue any,
322322
}
323323
githubMode := getGitHubType(mcpConfig)
324324
defaultTools := constants.DefaultGitHubToolsLocal
325-
if githubMode == "remote" {
325+
if githubMode == GitHubMCPModeRemote {
326326
defaultTools = constants.DefaultGitHubToolsRemote
327327
}
328328
for _, defaultTool := range defaultTools {

pkg/workflow/codex_mcp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an
5656
renderer := createRenderer(false) // isLast is always false in TOML format
5757
switch toolName {
5858
case "github":
59-
githubTool := expandedTools["github"]
59+
githubTool, _ := expandedTools["github"].(map[string]any)
6060
renderer.RenderGitHubMCP(&mcpConfigContent, githubTool, workflowData)
6161
case "playwright":
6262
playwrightTool := expandedTools["playwright"]

pkg/workflow/compiler_difc_proxy.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ func hasDIFCGuardsConfigured(data *WorkflowData) bool {
8383
if !hasGitHub || githubTool == false {
8484
return false
8585
}
86-
return len(getGitHubGuardPolicies(githubTool)) > 0
86+
toolConfig, _ := githubTool.(map[string]any)
87+
return len(getGitHubGuardPolicies(toolConfig)) > 0
8788
}
8889

8990
// isIntegrityProxyEnabled returns true unless the user has explicitly disabled the DIFC proxy
@@ -167,20 +168,15 @@ func hasPreAgentStepsWithGHToken(data *WorkflowData) bool {
167168
// endorser-min-integrity) are also included in the proxy policy.
168169
//
169170
// Returns an empty string if no guard policy fields are found.
170-
func getDIFCProxyPolicyJSON(githubTool any, data *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig) string {
171-
toolConfig, ok := githubTool.(map[string]any)
172-
if !ok {
173-
return ""
174-
}
175-
171+
func getDIFCProxyPolicyJSON(githubTool map[string]any, data *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig) string {
176172
policy := make(map[string]any)
177173

178174
// Support both 'allowed-repos' (preferred) and deprecated 'repos'
179-
repos, hasRepos := toolConfig["allowed-repos"]
175+
repos, hasRepos := githubTool["allowed-repos"]
180176
if !hasRepos {
181-
repos, hasRepos = toolConfig["repos"]
177+
repos, hasRepos = githubTool["repos"]
182178
}
183-
integrity, hasIntegrity := toolConfig["min-integrity"]
179+
integrity, hasIntegrity := githubTool["min-integrity"]
184180

185181
if !hasRepos && !hasIntegrity {
186182
return ""
@@ -198,7 +194,7 @@ func getDIFCProxyPolicyJSON(githubTool any, data *WorkflowData, gatewayConfig *M
198194
}
199195

200196
// Inject reaction fields when the feature flag is enabled and MCPG supports it.
201-
injectIntegrityReactionFields(policy, toolConfig, data, gatewayConfig)
197+
injectIntegrityReactionFields(policy, githubTool, data, gatewayConfig)
202198

203199
guardPolicy := map[string]any{
204200
"allow-only": policy,
@@ -229,16 +225,16 @@ func resolveProxyContainerImage(gatewayConfig *MCPGatewayRuntimeConfig) string {
229225
func (c *Compiler) buildStartDIFCProxyStepYAML(data *WorkflowData) string {
230226
difcProxyLog.Print("Building Start DIFC proxy step YAML")
231227

232-
githubTool := data.Tools["github"]
228+
githubToolConfig, _ := data.Tools["github"].(map[string]any)
233229

234230
// Get MCP server token (same token the gateway uses for the GitHub MCP server)
235-
customGitHubToken := getGitHubToken(githubTool)
231+
customGitHubToken := getGitHubToken(githubToolConfig)
236232
effectiveToken := getEffectiveGitHubToken(customGitHubToken)
237233

238234
// Build the simplified guard policy JSON (static fields only)
239235
// (plus reaction fields when integrity-reactions feature flag is enabled)
240236
ensureDefaultMCPGatewayConfig(data)
241-
policyJSON := getDIFCProxyPolicyJSON(githubTool, data, data.SandboxConfig.MCP)
237+
policyJSON := getDIFCProxyPolicyJSON(githubToolConfig, data, data.SandboxConfig.MCP)
242238
if policyJSON == "" {
243239
difcProxyLog.Print("Could not build DIFC proxy policy JSON, skipping proxy start")
244240
return ""
@@ -498,18 +494,18 @@ const defaultCliProxyPolicyJSON = `{"allow-only":{"repos":"all","min-integrity":
498494
func (c *Compiler) buildStartCliProxyStepYAML(data *WorkflowData) string {
499495
difcProxyLog.Print("Building Start CLI proxy step YAML")
500496

501-
githubTool := data.Tools["github"]
497+
githubToolConfig, _ := data.Tools["github"].(map[string]any)
502498

503499
// Get token for the proxy
504-
customGitHubToken := getGitHubToken(githubTool)
500+
customGitHubToken := getGitHubToken(githubToolConfig)
505501
effectiveToken := getEffectiveGitHubToken(customGitHubToken)
506502

507503
// Build the guard policy JSON (static fields only, plus reaction fields when enabled).
508504
// The CLI proxy requires a policy to forward requests — without one, all API
509505
// calls return HTTP 503 ("proxy enforcement not configured"). Use the default
510506
// permissive policy when no guard policy is configured in the frontmatter.
511507
ensureDefaultMCPGatewayConfig(data)
512-
policyJSON := getDIFCProxyPolicyJSON(githubTool, data, data.SandboxConfig.MCP)
508+
policyJSON := getDIFCProxyPolicyJSON(githubToolConfig, data, data.SandboxConfig.MCP)
513509
if policyJSON == "" {
514510
policyJSON = defaultCliProxyPolicyJSON
515511
difcProxyLog.Print("No guard policy configured, using default CLI proxy policy")

pkg/workflow/compiler_difc_proxy_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func TestHasPreAgentStepsWithGHToken(t *testing.T) {
210210
func TestGetDIFCProxyPolicyJSON(t *testing.T) {
211211
tests := []struct {
212212
name string
213-
githubTool any
213+
githubTool map[string]any
214214
expectedContains []string
215215
expectedAbsent []string
216216
expectEmpty bool
@@ -221,8 +221,8 @@ func TestGetDIFCProxyPolicyJSON(t *testing.T) {
221221
expectEmpty: true,
222222
},
223223
{
224-
name: "non-map tool",
225-
githubTool: false,
224+
name: "empty map tool",
225+
githubTool: map[string]any{},
226226
expectEmpty: true,
227227
},
228228
{

pkg/workflow/compiler_github_mcp_steps.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ func (c *Compiler) generateGitHubMCPLockdownDetectionStep(yaml *strings.Builder,
2626
githubConfigLog.Print("Skipping GitHub MCP lockdown detection step: GitHub tool not enabled")
2727
return
2828
}
29+
githubToolMap, _ := githubTool.(map[string]any)
2930

3031
// Skip when guard policy is already fully configured in the workflow.
3132
// The step is only needed to auto-configure guard policies for public repos.
32-
if len(getGitHubGuardPolicies(githubTool)) > 0 {
33+
if len(getGitHubGuardPolicies(githubToolMap)) > 0 {
3334
githubConfigLog.Print("Guard policy already configured in workflow, skipping automatic guard policy determination")
3435
return
3536
}
@@ -168,9 +169,10 @@ func (c *Compiler) generateParseGuardVarsStep(yaml *strings.Builder, data *Workf
168169
githubConfigLog.Print("Skipping parse-guard-vars step: GitHub tool not enabled")
169170
return
170171
}
172+
githubToolMap, _ := githubTool.(map[string]any)
171173

172174
// Only generate the step when guard policies are configured.
173-
if len(getGitHubGuardPolicies(githubTool)) == 0 {
175+
if len(getGitHubGuardPolicies(githubToolMap)) == 0 {
174176
githubConfigLog.Print("Skipping parse-guard-vars step: no explicit guard policies configured")
175177
return
176178
}

pkg/workflow/compiler_yaml.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -933,12 +933,15 @@ func (c *Compiler) generateCreateAwInfo(yaml *strings.Builder, data *WorkflowDat
933933
// Include lockdown validation env vars when lockdown is explicitly enabled.
934934
// validateLockdownRequirements is called from generate_aw_info.cjs and uses these vars.
935935
githubTool, hasGitHub := data.Tools["github"]
936-
if hasGitHub && githubTool != false && hasGitHubLockdownExplicitlySet(githubTool) && getGitHubLockdown(githubTool) {
937-
yaml.WriteString(" GITHUB_MCP_LOCKDOWN_EXPLICIT: \"true\"\n")
938-
yaml.WriteString(" GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}\n")
939-
yaml.WriteString(" GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}\n")
940-
if customToken := getGitHubToken(githubTool); customToken != "" {
941-
fmt.Fprintf(yaml, " CUSTOM_GITHUB_TOKEN: %s\n", customToken)
936+
if hasGitHub && githubTool != false {
937+
toolConfig, _ := githubTool.(map[string]any)
938+
if hasGitHubLockdownExplicitlySet(toolConfig) && getGitHubLockdown(toolConfig) {
939+
yaml.WriteString(" GITHUB_MCP_LOCKDOWN_EXPLICIT: \"true\"\n")
940+
yaml.WriteString(" GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}\n")
941+
yaml.WriteString(" GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}\n")
942+
if customToken := getGitHubToken(toolConfig); customToken != "" {
943+
fmt.Fprintf(yaml, " CUSTOM_GITHUB_TOKEN: %s\n", customToken)
944+
}
942945
}
943946
}
944947
// Embed custom token weights only when custom model multipliers are configured.

pkg/workflow/copilot_engine_execution.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,12 +604,14 @@ touch %s
604604
if workflowData.ParsedTools != nil && workflowData.ParsedTools.GitHub != nil && workflowData.ParsedTools.GitHub.GitHubApp != nil {
605605
tokenExpression := "${{ steps.github-mcp-app-token.outputs.token }}"
606606
if workflowData.ParsedTools.GitHub.GitHubApp.shouldIgnoreMissingKey() {
607-
customGitHubToken := getGitHubToken(workflowData.Tools["github"])
607+
githubToolConfig, _ := workflowData.Tools["github"].(map[string]any)
608+
customGitHubToken := getGitHubToken(githubToolConfig)
608609
tokenExpression = combineTokenExpressions(tokenExpression, getEffectiveGitHubToken(customGitHubToken))
609610
}
610611
env["GITHUB_MCP_SERVER_TOKEN"] = tokenExpression
611612
} else {
612-
customGitHubToken := getGitHubToken(workflowData.Tools["github"])
613+
githubToolConfig, _ := workflowData.Tools["github"].(map[string]any)
614+
customGitHubToken := getGitHubToken(githubToolConfig)
613615
// Use effective token with precedence: custom > default
614616
effectiveToken := getEffectiveGitHubToken(customGitHubToken)
615617
env["GITHUB_MCP_SERVER_TOKEN"] = effectiveToken

pkg/workflow/copilot_engine_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,7 @@ func TestCopilotEnginePromptFilePath(t *testing.T) {
14441444
func TestCopilotEngineRenderGitHubMCPConfig(t *testing.T) {
14451445
tests := []struct {
14461446
name string
1447-
githubTool any
1447+
githubTool map[string]any
14481448
isLast bool
14491449
expectedStrs []string
14501450
}{

0 commit comments

Comments
 (0)