diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 0645ae01ec8..56e7d3b60d5 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -74,6 +74,9 @@ const ExpressionBreakThreshold LineLength = 100 // for MCP servers, gateway services, and validation ranges. const ( + // AWFAPIProxyContainerIP is the fixed api-proxy sidecar address inside the AWF sandbox network. + AWFAPIProxyContainerIP = "172.30.0.30" + // DefaultMCPGatewayPort is the default port for the MCP gateway HTTP service DefaultMCPGatewayPort = 8080 diff --git a/pkg/workflow/codex_engine_test.go b/pkg/workflow/codex_engine_test.go index d939a76db60..751695691ee 100644 --- a/pkg/workflow/codex_engine_test.go +++ b/pkg/workflow/codex_engine_test.go @@ -310,6 +310,73 @@ func TestCodexEngineRenderMCPConfig(t *testing.T) { } } +func TestCodexEngineRenderMCPConfigOpenAIProxyProvider(t *testing.T) { + engine := NewCodexEngine() + + t.Run("injects openai-proxy provider when firewall is enabled", func(t *testing.T) { + tools := map[string]any{} + mcpTools := []string{} + var yaml strings.Builder + workflowData := &WorkflowData{ + Name: "test-workflow", + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{Enabled: true}, + }, + } + + if err := engine.RenderMCPConfig(&yaml, tools, mcpTools, workflowData); err != nil { + t.Fatalf("RenderMCPConfig returned unexpected error: %v", err) + } + + result := yaml.String() + expectedLines := []string{ + "model_provider = \"openai-proxy\"", + "[model_providers.openai-proxy]", + "name = \"OpenAI AWF proxy\"", + fmt.Sprintf("base_url = \"http://%s:%d\"", constants.AWFAPIProxyContainerIP, constants.ClaudeLLMGatewayPort), + "env_key = \"OPENAI_API_KEY\"", + "supports_websockets = false", + } + + for _, expected := range expectedLines { + if !strings.Contains(result, expected) { + t.Errorf("Expected MCP config to contain %q, got:\n%s", expected, result) + } + } + if !strings.Contains(result, "awk '") { + t.Errorf("Expected firewall-enabled config append to use awk filtering, got:\n%s", result) + } + }) + + t.Run("does not inject openai-proxy provider when firewall is disabled", func(t *testing.T) { + tools := map[string]any{} + mcpTools := []string{} + var yaml strings.Builder + workflowData := &WorkflowData{Name: "test-workflow"} + + if err := engine.RenderMCPConfig(&yaml, tools, mcpTools, workflowData); err != nil { + t.Fatalf("RenderMCPConfig returned unexpected error: %v", err) + } + + result := yaml.String() + if strings.Contains(result, "model_provider = \"openai-proxy\"") { + t.Errorf("Did not expect openai-proxy provider when firewall is disabled, got:\n%s", result) + } + if strings.Contains(result, "awk '") { + t.Errorf("Did not expect awk filtering when firewall is disabled, got:\n%s", result) + } + }) +} + +func TestCodexEngineOpenAIProxyProviderBaseURL(t *testing.T) { + engine := NewCodexEngine() + expected := fmt.Sprintf("http://%s:%d", constants.AWFAPIProxyContainerIP, constants.ClaudeLLMGatewayPort) + + if actual := engine.getOpenAIProxyProviderBaseURL(); actual != expected { + t.Errorf("Expected OpenAI proxy provider base URL %q, got %q", expected, actual) + } +} + func TestCodexEngineExecutionAddsMountedMCPCLIPathSetup(t *testing.T) { engine := NewCodexEngine() workflowData := &WorkflowData{ diff --git a/pkg/workflow/codex_mcp.go b/pkg/workflow/codex_mcp.go index 939006c6e3e..896ea3a4c01 100644 --- a/pkg/workflow/codex_mcp.go +++ b/pkg/workflow/codex_mcp.go @@ -2,13 +2,21 @@ package workflow import ( "fmt" + "net" + "strconv" "strings" + "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" ) var codexMCPLog = logger.New("workflow:codex_mcp") +const ( + codexOpenAIProxyProviderID = "openai-proxy" + codexOpenAIProxyProviderName = "OpenAI AWF proxy" +) + // RenderMCPConfig generates MCP server configuration for Codex func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) error { if codexMCPLog.Enabled() { @@ -118,8 +126,15 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an shellPolicyDelimiter := GenerateHeredocDelimiterFromSeed("CODEX_SHELL_POLICY", workflowData.FrontmatterHash) yaml.WriteString(" cat > \"/tmp/gh-aw/mcp-config/config.toml\" << " + shellPolicyDelimiter + "\n") e.renderShellEnvironmentPolicyToml(yaml, tools, mcpTools, " ") + if isFirewallEnabled(workflowData) { + e.renderOpenAIProxyProviderToml(yaml, " ") + } yaml.WriteString(" " + shellPolicyDelimiter + "\n") - yaml.WriteString(" cat \"${RUNNER_TEMP}/gh-aw/mcp-config/config.toml\" >> \"/tmp/gh-aw/mcp-config/config.toml\"\n") + if isFirewallEnabled(workflowData) { + e.renderAppendConvertedConfigWithoutOpenAIProxy(yaml) + } else { + yaml.WriteString(" cat \"${RUNNER_TEMP}/gh-aw/mcp-config/config.toml\" >> \"/tmp/gh-aw/mcp-config/config.toml\"\n") + } if workflowData.EngineConfig != nil && strings.TrimSpace(workflowData.EngineConfig.Config) != "" { customConfigDelimiter := GenerateHeredocDelimiterFromSeed("CODEX_CUSTOM_CONFIG", workflowData.FrontmatterHash) yaml.WriteString(" \n") @@ -139,6 +154,31 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an return nil } +func (e *CodexEngine) renderOpenAIProxyProviderToml(yaml *strings.Builder, indent string) { + yaml.WriteString("\n") + yaml.WriteString(indent + "model_provider = \"" + codexOpenAIProxyProviderID + "\"\n") + yaml.WriteString("\n") + yaml.WriteString(indent + "[model_providers." + codexOpenAIProxyProviderID + "]\n") + yaml.WriteString(indent + "name = \"" + codexOpenAIProxyProviderName + "\"\n") + yaml.WriteString(indent + "base_url = \"" + e.getOpenAIProxyProviderBaseURL() + "\"\n") + yaml.WriteString(indent + "env_key = \"OPENAI_API_KEY\"\n") + yaml.WriteString(indent + "supports_websockets = false\n") +} + +func (e *CodexEngine) getOpenAIProxyProviderBaseURL() string { + return "http://" + net.JoinHostPort(constants.AWFAPIProxyContainerIP, strconv.Itoa(constants.ClaudeLLMGatewayPort)) +} + +func (e *CodexEngine) renderAppendConvertedConfigWithoutOpenAIProxy(yaml *strings.Builder) { + yaml.WriteString(" awk '\n") + yaml.WriteString(" BEGIN { skip_openai_proxy = 0 }\n") + yaml.WriteString(" /^[[:space:]]*model_provider[[:space:]]*=/ { next }\n") + yaml.WriteString(" /^\\[model_providers\\.openai-proxy\\][[:space:]]*$/ { skip_openai_proxy = 1; next }\n") + yaml.WriteString(" /^\\[/ { skip_openai_proxy = 0 }\n") + yaml.WriteString(" !skip_openai_proxy { print }\n") + yaml.WriteString(" ' \"${RUNNER_TEMP}/gh-aw/mcp-config/config.toml\" >> \"/tmp/gh-aw/mcp-config/config.toml\"\n") +} + // renderCodexMCPConfigWithContext generates custom MCP server configuration for a single tool in codex workflow config.toml // This version includes workflowData to determine if localhost URLs should be rewritten func (e *CodexEngine) renderCodexMCPConfigWithContext(yaml *strings.Builder, toolName string, toolConfig map[string]any, workflowData *WorkflowData) error {