Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
13 changes: 13 additions & 0 deletions .github/workflows/ai-moderator.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/changeset.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/codex-github-remote-mcp-test.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/daily-fact.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/daily-observability-report.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/duplicate-code-detector.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/grumpy-reviewer.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/issue-arborist.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/schema-feature-coverage.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/smoke-call-workflow.lock.yml

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

13 changes: 13 additions & 0 deletions .github/workflows/smoke-codex.lock.yml

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

28 changes: 24 additions & 4 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,7 @@ func (e *CodexEngine) expandNeutralToolsToCodexToolsFromMap(tools map[string]any
return result.ToMap()
}

// renderShellEnvironmentPolicy generates the [shell_environment_policy] section for config.toml
// This controls which environment variables are passed through to MCP servers for security
func (e *CodexEngine) renderShellEnvironmentPolicy(yaml *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) {
func (e *CodexEngine) getShellEnvironmentPolicyVars(tools map[string]any, mcpTools []string) []string {
// Collect all environment variables needed by MCP servers
envVars := make(map[string]bool)

Expand Down Expand Up @@ -509,13 +507,20 @@ func (e *CodexEngine) renderShellEnvironmentPolicy(yaml *strings.Builder, tools
}
}

// Sort environment variable names for consistent output
var sortedEnvVars []string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactoring of getShellEnvironmentPolicyVars to return []string is clean and improves testability. Consider adding a brief comment explaining what the returned slice contains (sorted env var names needed by MCP servers) since the comment was moved to the outer function.

for envVar := range envVars {
sortedEnvVars = append(sortedEnvVars, envVar)
}
sort.Strings(sortedEnvVars)

return sortedEnvVars
}

// renderShellEnvironmentPolicy generates the [shell_environment_policy] section for config.toml
// This controls which environment variables are passed through to MCP servers for security
func (e *CodexEngine) renderShellEnvironmentPolicy(yaml *strings.Builder, tools map[string]any, mcpTools []string) {
sortedEnvVars := e.getShellEnvironmentPolicyVars(tools, mcpTools)

// Render [shell_environment_policy] section
yaml.WriteString(" \n")
yaml.WriteString(" [shell_environment_policy]\n")
Expand All @@ -530,6 +535,21 @@ func (e *CodexEngine) renderShellEnvironmentPolicy(yaml *strings.Builder, tools
yaml.WriteString("]\n")
}

func (e *CodexEngine) renderShellEnvironmentPolicyToml(yaml *strings.Builder, tools map[string]any, mcpTools []string, indent string) {
sortedEnvVars := e.getShellEnvironmentPolicyVars(tools, mcpTools)

yaml.WriteString(indent + "[shell_environment_policy]\n")
yaml.WriteString(indent + "inherit = \"core\"\n")
yaml.WriteString(indent + "include_only = [")
for i, envVar := range sortedEnvVars {
if i > 0 {
yaml.WriteString(", ")
}
yaml.WriteString("\"" + envVar + "\"")
}
yaml.WriteString("]\n")
}

// RenderMCPConfig is implemented in codex_mcp.go

// renderCodexMCPConfig is implemented in codex_mcp.go
Expand Down
13 changes: 13 additions & 0 deletions pkg/workflow/codex_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,19 @@ func TestCodexEngineRenderMCPConfig(t *testing.T) {
"}",
"}",
"GH_AW_MCP_CONFIG_NORM_EOF",
"",
"# Sync converter output to writable CODEX_HOME for Codex",
"mkdir -p /tmp/gh-aw/mcp-config",
"cat > \"/tmp/gh-aw/mcp-config/config.toml\" << GH_AW_CODEX_SHELL_POLICY_NORM_EOF",
"[shell_environment_policy]",
"inherit = \"core\"",
"include_only = [\"CODEX_API_KEY\", \"GITHUB_PERSONAL_ACCESS_TOKEN\", \"HOME\", \"OPENAI_API_KEY\", \"PATH\"]",
"GH_AW_CODEX_SHELL_POLICY_NORM_EOF",
"cat \"${RUNNER_TEMP}/gh-aw/mcp-config/config.toml\" >> \"/tmp/gh-aw/mcp-config/config.toml\"",
"chmod 600 \"/tmp/gh-aw/mcp-config/config.toml\"",
"mkdir -p \"${CODEX_HOME}\"",
"cp \"/tmp/gh-aw/mcp-config/config.toml\" \"${CODEX_HOME}/config.toml\"",
"chmod 600 \"${CODEX_HOME}/config.toml\"",
},
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition of test cases for the CODEX_HOME sync logic. The test covers the shell environment policy normalization. It would be helpful to also include a test case where CODEX_HOME is unset to verify graceful handling of that edge case.

Expand Down
39 changes: 36 additions & 3 deletions pkg/workflow/codex_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an

// Add shell environment policy to control which environment variables are passed through
// This is a security feature to prevent accidental exposure of secrets
e.renderShellEnvironmentPolicy(yaml, tools, mcpTools, workflowData)
e.renderShellEnvironmentPolicy(yaml, tools, mcpTools)

// Expand neutral tools (like playwright: null) to include the copilot agent tools
expandedTools := e.expandNeutralToolsToCodexToolsFromMap(tools)
Expand Down Expand Up @@ -99,11 +99,44 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an
yaml.WriteString(" # Generate JSON config for MCP gateway\n")

// Gateway uses JSON format without Copilot-specific fields and multi-line args
return renderStandardJSONMCPConfig(yaml, tools, mcpTools, workflowData,
if err := renderStandardJSONMCPConfig(yaml, tools, mcpTools, workflowData,
"${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json", false, false,
func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error {
return e.renderCodexJSONMCPConfigWithContext(yaml, toolName, toolConfig, isLast, workflowData)
}, nil)
}, nil); err != nil {
return err
}

// start_mcp_gateway.cjs converts the gateway output and writes Codex config to
// ${RUNNER_TEMP}/gh-aw/mcp-config/config.toml. Codex reads config from
// $CODEX_HOME/config.toml, so copy the converted config into writable CODEX_HOME
// and prepend shell policy (converter output does not include this section).
yaml.WriteString(" \n")
yaml.WriteString(" # Sync converter output to writable CODEX_HOME for Codex\n")
yaml.WriteString(" mkdir -p /tmp/gh-aw/mcp-config\n")

shellPolicyDelimiter := GenerateHeredocDelimiterFromSeed("CODEX_SHELL_POLICY", workflowData.FrontmatterHash)
yaml.WriteString(" cat > \"/tmp/gh-aw/mcp-config/config.toml\" << " + shellPolicyDelimiter + "\n")
e.renderShellEnvironmentPolicyToml(yaml, tools, mcpTools, " ")
yaml.WriteString(" " + shellPolicyDelimiter + "\n")
yaml.WriteString(" cat \"${RUNNER_TEMP}/gh-aw/mcp-config/config.toml\" >> \"/tmp/gh-aw/mcp-config/config.toml\"\n")
Comment thread
lpcox marked this conversation as resolved.
if workflowData.EngineConfig != nil && strings.TrimSpace(workflowData.EngineConfig.Config) != "" {
customConfigDelimiter := GenerateHeredocDelimiterFromSeed("CODEX_CUSTOM_CONFIG", workflowData.FrontmatterHash)
yaml.WriteString(" \n")
yaml.WriteString(" # Append engine-level custom Codex config\n")
yaml.WriteString(" cat >> \"/tmp/gh-aw/mcp-config/config.toml\" << " + customConfigDelimiter + "\n")
yaml.WriteString(workflowData.EngineConfig.Config)
if !strings.HasSuffix(workflowData.EngineConfig.Config, "\n") {
yaml.WriteString("\n")
}
yaml.WriteString(" " + customConfigDelimiter + "\n")
}
yaml.WriteString(" chmod 600 \"/tmp/gh-aw/mcp-config/config.toml\"\n")
Comment thread
lpcox marked this conversation as resolved.
yaml.WriteString(" mkdir -p \"${CODEX_HOME}\"\n")
yaml.WriteString(" cp \"/tmp/gh-aw/mcp-config/config.toml\" \"${CODEX_HOME}/config.toml\"\n")
yaml.WriteString(" chmod 600 \"${CODEX_HOME}/config.toml\"\n")

return nil
}

// renderCodexMCPConfigWithContext generates custom MCP server configuration for a single tool in codex workflow config.toml
Expand Down
Loading