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
10 changes: 6 additions & 4 deletions pkg/workflow/threat_detection_external.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package workflow

import (
"fmt"
"net"
"slices"
"strconv"
"strings"
Expand All @@ -25,9 +24,12 @@ func (c *Compiler) buildPrepareDetectionEngineConfigForExternalDetectorStep(data
if data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil && data.SafeOutputs.ThreatDetection.Model != "" {
detectionData.Model = data.SafeOutputs.ThreatDetection.Model
}
provider := NewCodexEngine().ResolveLLMProvider(detectionData)
profile := llmProviderProfileFor(provider)
codexAPIBase := "http://" + net.JoinHostPort(constants.AWFAPIProxyContainerIP, strconv.Itoa(profile.gatewayPort))
// Reuse the agent job's provider endpoint resolution so detection never pins
// Codex at a non-OpenAI api-proxy ingress (e.g. the Anthropic port 10001,
// which rejects Codex requests with 403 "Credentials for Anthropic ... are
// not configured"). Codex speaks the OpenAI Responses wire API, so only the
// OpenAI (10000) or Copilot (10002) ingress can serve it.
codexAPIBase := NewCodexEngine().getOpenAIProxyProviderBaseURL(detectionData)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b83aff6. External Codex detection now normalizes every non-GitHub provider to OpenAI before execution, and the regression test asserts the OpenAI credential expression while rejecting the Anthropic one.

codexWSSBase := codexProxyWebsocketBaseURL(codexAPIBase)
codexConfig := buildExternalDetectorCodexConfig(codexAPIBase, codexWSSBase)
codexConfigDelimiter := GenerateHeredocDelimiterFromContent("CODEX_DETECTION_CONFIG", codexConfig)
Expand Down
3 changes: 3 additions & 0 deletions pkg/workflow/threat_detection_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ func buildExternalDetectorWorkflowData(data *WorkflowData, engineID string) *Wor
"bash": []any{"*"},
}
d.EngineConfig = resolveExternalDetectorEngineConfig(data, engineID)
if engineID == "codex" && NewCodexEngine().ResolveLLMProvider(d) != LLMProviderGitHub {
d.EngineConfig.LLMProvider = LLMProviderOpenAI
}
d.EngineConfig.Env = mergeThreatDetectionEngineEnv(data, d.EngineConfig.Env)
if d.EngineConfig.APITarget == "" && data.EngineConfig != nil {
d.EngineConfig.APITarget = data.EngineConfig.APITarget
Expand Down
79 changes: 79 additions & 0 deletions pkg/workflow/threat_detection_isolation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,85 @@ Test workflow`
}
}

// TestExternalDetectorCodexConfigUsesOpenAIProxyPort verifies that the detection
// config.toml pins Codex to an ingress that speaks the OpenAI Responses wire API.
// Pointing it at the Anthropic ingress (port 10001) makes every detection request
// fail with 403 "Credentials for Anthropic (port 10001) are not configured", so the
// engine never produces a verdict and the detection job fails with ERR_SYSTEM.
func TestExternalDetectorCodexConfigUsesOpenAIProxyPort(t *testing.T) {
tests := []struct {
name string
engineYAML string
}{
{
name: "default provider",
engineYAML: "engine:\n id: codex\n",
},
{
name: "anthropic provider override",
engineYAML: "engine:\n id: codex\n model-provider: anthropic\n",
},
}

openAIBaseURL := "http://" + net.JoinHostPort(constants.AWFAPIProxyContainerIP, strconv.Itoa(constants.CodexLLMGatewayPort))
anthropicHostPort := net.JoinHostPort(constants.AWFAPIProxyContainerIP, strconv.Itoa(constants.ClaudeLLMGatewayPort))

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

tmpDir := testutil.TempDir(t, "test-external-detector-codex-port-*")
workflowPath := filepath.Join(tmpDir, "test-codex-port.md")

workflowContent := `---
on: push
` + tt.engineYAML + `safe-outputs:
create-issue:
features:
gh-aw-detection: true
---
Test workflow`

if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}

result, err := os.ReadFile(stringutil.MarkdownToLockFile(workflowPath))
if err != nil {
t.Fatalf("Failed to read compiled workflow: %v", err)
}

detectionSection := extractJobSection(string(result), "detection")
if detectionSection == "" {
t.Fatal("Detection job not found in compiled workflow")
}

for _, key := range []string{"base_url", "api_base"} {
expected := key + ` = "` + openAIBaseURL + `"`
if !strings.Contains(detectionSection, expected) {
t.Errorf("Codex detection config must set %s to the OpenAI ingress (%s)", key, openAIBaseURL)
}
}
expectedWSS := `wss_base = "ws://` + net.JoinHostPort(constants.AWFAPIProxyContainerIP, strconv.Itoa(constants.CodexLLMGatewayPort)) + `"`
if !strings.Contains(detectionSection, expectedWSS) {
t.Errorf("Codex detection config must set wss_base to the OpenAI ingress (%s)", expectedWSS)
}
if strings.Contains(detectionSection, `base_url = "http://`+anthropicHostPort+`"`) {
t.Errorf("Codex detection config must never point at the Anthropic ingress (%s)", anthropicHostPort)
}
if !strings.Contains(detectionSection, `CODEX_API_KEY: ${{ secrets.CODEX_API_KEY || secrets.OPENAI_API_KEY }}`) {
t.Error("Codex detection execution must use the OpenAI credential expression")
}
if strings.Contains(detectionSection, `CODEX_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}`) {
t.Error("Codex detection execution must never use the Anthropic credential expression")
}
})
}
}

// TestExternalDetectorCodexConfigModelProviderAtRoot verifies that the top-level
// model_provider selector is emitted before any TOML table header ([history],
// [model_providers.*]). If it appears after [history], TOML parses it as
Expand Down
Loading