Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions pkg/constants/version_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ const AWFCliProxyMinVersion Version = "v0.25.17"
// --allow-host-ports or the run will fail at startup with an unknown flag error.
const AWFAllowHostPortsMinVersion Version = "v0.25.24"

// AWFEnableOpenCodeMinVersion is the minimum AWF version that supports the
// --enable-opencode flag. This flag enables the OpenCode API proxy listener
// on port 10004 (dynamic provider routing). Workflows pinning an older AWF
// version must not emit --enable-opencode or the run will fail at startup.
// Introduced in gh-aw-firewall PR #2337.
const AWFEnableOpenCodeMinVersion Version = "v0.25.30"

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 use of a named constant for the version threshold. One suggestion: consider adding a link to the specific AWF release notes or changelog entry (not just the PR number) so future maintainers can quickly verify what changed in that version without needing access to the internal firewall repo.


// CopilotNoAskUserMinVersion is the minimum Copilot CLI version that supports the --no-ask-user
// flag, which enables fully autonomous agentic runs by suppressing interactive prompts.
// Workflows using an older Copilot CLI version must not emit --no-ask-user or the run will fail.
Expand Down
46 changes: 46 additions & 0 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,19 +316,34 @@ func BuildAWFArgs(config AWFCommandConfig) []string {
// AWF's --enable-host-access defaults to ports 80,443. The MCP gateway now
// listens on port 8080 (non-privileged), so we must explicitly allow it
// when AWF supports --allow-host-ports.
// For OpenCode engine, also include port 10004 (OpenCode dynamic provider routing)
// and emit --enable-opencode when AWF supports it (v0.25.30+).
// AWFEnableOpenCodeMinVersion > AWFAllowHostPortsMinVersion, so when opencode support
// is available, --allow-host-ports support is also guaranteed.
opencodeEnabled := config.EngineName == string(constants.OpenCodeEngine) && awfSupportsEnableOpenCode(firewallConfig)
if awfSupportsAllowHostPorts(firewallConfig) {
mcpGatewayPort := int(DefaultMCPGatewayPort)
if config.WorkflowData != nil && config.WorkflowData.SandboxConfig != nil &&
config.WorkflowData.SandboxConfig.MCP != nil && config.WorkflowData.SandboxConfig.MCP.Port > 0 {
mcpGatewayPort = config.WorkflowData.SandboxConfig.MCP.Port
}
hostPorts := fmt.Sprintf("80,443,%d", mcpGatewayPort)
if opencodeEnabled {
hostPorts += ",10004"
}
awfArgs = append(awfArgs, "--allow-host-ports", hostPorts)
awfHelpersLog.Printf("Added --allow-host-ports %s for MCP gateway access", hostPorts)
} else {
awfHelpersLog.Printf("Skipping --allow-host-ports: AWF version %q requires at least %s", getAWFImageTag(firewallConfig), constants.AWFAllowHostPortsMinVersion)
}

if opencodeEnabled {
awfArgs = append(awfArgs, "--enable-opencode")
awfHelpersLog.Print("Added --enable-opencode and port 10004 for OpenCode API proxy listener")
} else if config.EngineName == string(constants.OpenCodeEngine) {
awfHelpersLog.Printf("Skipping --enable-opencode: AWF version %q is older than minimum %s", getAWFImageTag(firewallConfig), constants.AWFEnableOpenCodeMinVersion)
}

// Skip pulling images since they are pre-downloaded
awfArgs = append(awfArgs, "--skip-pull")
awfHelpersLog.Print("Using --skip-pull since images are pre-downloaded")
Expand Down Expand Up @@ -684,3 +699,34 @@ func awfSupportsAllowHostPorts(firewallConfig *FirewallConfig) bool {
minVersion := string(constants.AWFAllowHostPortsMinVersion)
return semverutil.Compare(versionStr, minVersion) >= 0
}

// awfSupportsEnableOpenCode returns true when the effective AWF version supports
// --enable-opencode.
//
// The --enable-opencode flag enables the OpenCode API proxy listener on port 10004
// (dynamic provider routing). It was introduced in AWF v0.25.30 (gh-aw-firewall PR #2337).
// Any workflow that pins an explicit version older than v0.25.30 must not emit
// --enable-opencode or the run will fail at startup.
//
// Special cases:
// - No version override (firewallConfig is nil or has no Version): use DefaultFirewallVersion
// and compare against AWFEnableOpenCodeMinVersion.
// - "latest": always returns true (latest is always a new release).
// - Any semver string ≥ AWFEnableOpenCodeMinVersion: returns true.
// - Any semver string < AWFEnableOpenCodeMinVersion: returns false.
// - Non-semver string (e.g. a branch name): returns false (conservative).
func awfSupportsEnableOpenCode(firewallConfig *FirewallConfig) bool {
var versionStr string
if firewallConfig != nil && firewallConfig.Version != "" {
versionStr = firewallConfig.Version
} else {
versionStr = string(constants.DefaultFirewallVersion)
}

if strings.EqualFold(versionStr, "latest") {
return true
}

minVersion := string(constants.AWFEnableOpenCodeMinVersion)
return semverutil.Compare(versionStr, minVersion) >= 0
}
186 changes: 186 additions & 0 deletions pkg/workflow/awf_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,192 @@ func TestBuildAWFArgsAllowHostPorts(t *testing.T) {
})
}

// TestBuildAWFArgsEnableOpenCode tests that BuildAWFArgs emits --enable-opencode and
// includes port 10004 in --allow-host-ports when engine=opencode and AWF supports it.
func TestBuildAWFArgsEnableOpenCode(t *testing.T) {
t.Run("emits --enable-opencode and port 10004 for opencode engine with default AWF version", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "opencode",
WorkflowData: &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{ID: "opencode"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
Version: "v0.25.30",
},
},
},
AllowedDomains: "github.com",
}

args := BuildAWFArgs(config)
argsStr := strings.Join(args, " ")

assert.Contains(t, argsStr, "--enable-opencode", "Should include --enable-opencode for opencode engine")
assert.Contains(t, argsStr, "10004", "Should include port 10004 in --allow-host-ports for opencode engine")
assert.Contains(t, argsStr, "80,443,8080,10004", "Should include all required ports for opencode engine")
})

t.Run("does not emit --enable-opencode for non-opencode engines", func(t *testing.T) {
for _, engine := range []string{"copilot", "claude", "codex", "gemini"} {
config := AWFCommandConfig{
EngineName: engine,
WorkflowData: &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{ID: engine},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
Version: "v0.25.30",
},
},
},
AllowedDomains: "github.com",
}

args := BuildAWFArgs(config)
argsStr := strings.Join(args, " ")

assert.NotContains(t, argsStr, "--enable-opencode", "Should not include --enable-opencode for engine %s", engine)
assert.NotContains(t, argsStr, "10004", "Should not include port 10004 for engine %s", engine)
}
})

t.Run("skips --enable-opencode when AWF version is too old", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "opencode",
WorkflowData: &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{ID: "opencode"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
Version: "v0.25.29",
},
},
},
AllowedDomains: "github.com",
}

args := BuildAWFArgs(config)
argsStr := strings.Join(args, " ")

assert.NotContains(t, argsStr, "--enable-opencode", "Should skip --enable-opencode for AWF versions below minimum")
assert.NotContains(t, argsStr, "10004", "Should skip port 10004 for AWF versions below minimum")
})

t.Run("emits --enable-opencode for opencode engine with latest AWF version", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "opencode",
WorkflowData: &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{ID: "opencode"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
Version: "latest",
},
},
},
AllowedDomains: "github.com",
}

args := BuildAWFArgs(config)
argsStr := strings.Join(args, " ")

assert.Contains(t, argsStr, "--enable-opencode", "Should include --enable-opencode for opencode engine with latest AWF version")
assert.Contains(t, argsStr, "10004", "Should include port 10004 for opencode engine with latest AWF version")
})

t.Run("port 10004 respects custom MCP gateway port", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "opencode",
WorkflowData: &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{ID: "opencode"},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
Version: "v0.25.30",
},
},
SandboxConfig: &SandboxConfig{
MCP: &MCPGatewayRuntimeConfig{Port: 9090},
},
},
AllowedDomains: "github.com",
}

args := BuildAWFArgs(config)
argsStr := strings.Join(args, " ")

assert.Contains(t, argsStr, "80,443,9090,10004", "Should use custom MCP port and include port 10004")
})
}

// TestAWFSupportsEnableOpenCode tests the awfSupportsEnableOpenCode version gate function.
func TestAWFSupportsEnableOpenCode(t *testing.T) {
tests := []struct {
name string
firewallConfig *FirewallConfig
want bool
}{
{
name: "nil firewall config returns false (default version v0.25.29 < v0.25.30)",
firewallConfig: nil,
want: false,
},
{
name: "empty version returns false (default version v0.25.29 < v0.25.30)",
firewallConfig: &FirewallConfig{},
want: false,
},
{
name: "latest returns true",
firewallConfig: &FirewallConfig{Version: "latest"},
want: true,
},
{
name: "v0.25.30 supports --enable-opencode (exact minimum version)",
firewallConfig: &FirewallConfig{Version: "v0.25.30"},
want: true,
},
{
name: "v0.26.0 supports --enable-opencode",
firewallConfig: &FirewallConfig{Version: "v0.26.0"},
want: true,
},
{
name: "v0.25.29 does not support --enable-opencode",
firewallConfig: &FirewallConfig{Version: "v0.25.29"},
want: false,
},
{
name: "v0.25.24 does not support --enable-opencode",
firewallConfig: &FirewallConfig{Version: "v0.25.24"},
want: false,
},
{
name: "v0.1.0 does not support --enable-opencode",
firewallConfig: &FirewallConfig{Version: "v0.1.0"},
want: false,
},
{
name: "non-semver branch name returns false (conservative)",
firewallConfig: &FirewallConfig{Version: "feature-branch"},
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := awfSupportsEnableOpenCode(tt.firewallConfig)
assert.Equal(t, tt.want, got, "awfSupportsEnableOpenCode result")
})
}
}

// TestBuildAWFArgsDiagnosticLogs tests that BuildAWFArgs includes --diagnostic-logs
// only when features.awf-diagnostic-logs is enabled.
func TestBuildAWFArgsDiagnosticLogs(t *testing.T) {
Expand Down
28 changes: 28 additions & 0 deletions pkg/workflow/enable_api_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,32 @@ func TestEngineAWFEnableApiProxy(t *testing.T) {
t.Error("Expected Gemini AWF command to contain apiProxy enabled in config JSON")
}
})

t.Run("OpenCode AWF command includes apiProxy enabled in config file", func(t *testing.T) {
workflowData := &WorkflowData{
Name: "test-workflow",
EngineConfig: &EngineConfig{
ID: "opencode",
},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{
Enabled: true,
},
},
}

engine := NewOpenCodeEngine()
steps := engine.GetExecutionSteps(workflowData, "test.log")

if len(steps) < 2 {
t.Fatal("Expected at least two execution steps (config + execution)")
}

// steps[0] = Write OpenCode Config, steps[1] = Execute OpenCode CLI
stepContent := strings.Join(steps[1], "\n")

if !strings.Contains(stepContent, `"enabled":true`) {
t.Error("Expected OpenCode AWF command to contain apiProxy enabled in config JSON")
}
})
}
Loading