diff --git a/docs/adr/29431-version-gated-engine-specific-awf-flags-for-opencode.md b/docs/adr/29431-version-gated-engine-specific-awf-flags-for-opencode.md new file mode 100644 index 0000000000..70e80c64f4 --- /dev/null +++ b/docs/adr/29431-version-gated-engine-specific-awf-flags-for-opencode.md @@ -0,0 +1,77 @@ +# ADR-29431: Version-Gated Engine-Specific AWF Flags for OpenCode + +**Date**: 2026-05-01 +**Status**: Draft +**Deciders**: lpcox, copilot-swe-agent + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +The `gh-aw` compiler translates workflow definitions into AWF (Agentic Workflow Firewall) CLI invocations. Different engines require different AWF flags and open network ports. The OpenCode engine introduces a dynamic provider routing proxy that listens on port 10004 and is activated by the AWF flag `--enable-opencode`. AWF itself did not support this flag before v0.25.30; passing an unrecognized flag to an older AWF version causes the run to fail at startup with an "unknown flag" error. An existing version-gating mechanism (`awfSupportsAllowHostPorts`) already handles the same problem for `--allow-host-ports`, establishing a pattern the codebase can follow. + +### Decision + +We will emit `--enable-opencode` and append port 10004 to `--allow-host-ports` in `BuildAWFArgs()` when (a) the workflow engine is `opencode` and (b) the effective AWF version is ≥ v0.25.30. Both conditions must be true simultaneously, enforced through a dedicated version-gate function `awfSupportsEnableOpenCode`. The `AWFEnableOpenCodeMinVersion` constant is introduced in `pkg/constants/version_constants.go` to make the threshold explicit and testable. Because `AWFEnableOpenCodeMinVersion` (v0.25.30) is greater than `AWFAllowHostPortsMinVersion` (v0.25.24), when the OpenCode flag is enabled, `--allow-host-ports` support is always already guaranteed. + +### Alternatives Considered + +#### Alternative 1: Unconditional flag emission (no version gate) + +Emit `--enable-opencode` and port 10004 for every OpenCode workflow regardless of AWF version. This is simpler and eliminates the version-gate bookkeeping. It was rejected because OpenCode workflows that pin an older AWF image version (e.g., in a workflow's `firewall.version` field) would receive an unknown flag and fail immediately at AWF startup — a hard runtime error with no graceful fallback. + +#### Alternative 2: Dedicated firewall config field for OpenCode ports + +Add a first-class field (e.g., `firewall.opencodePorts`) to the workflow's sandbox configuration so operators can declare the port list explicitly rather than deriving it from the engine name. This was considered because it separates the network port decision from the engine-name string, making each workflow's intent explicit. It was rejected because no existing workflow uses such a field, the engine name is already the canonical signal of intent in this codebase, and adding a new config field would require schema changes and documentation with no benefit for the current use case. + +### Consequences + +#### Positive +- OpenCode workflows on supported AWF versions correctly activate the API proxy listener on port 10004, enabling dynamic provider routing. +- The solution follows the established version-gating pattern (`awfSupportsAllowHostPorts`), making the codebase consistent and the pattern easy to discover for future contributors. +- The `AWFEnableOpenCodeMinVersion` constant gives operators a single place to look up the minimum AWF version required for OpenCode support. + +#### Negative +- Each new AWF feature flag now requires a minimum-version constant, a version-gate function, and associated tests. The pattern scales linearly with the number of features, adding maintenance surface. +- The `opencodeEnabled` pre-computation in `BuildAWFArgs` couples engine-name logic with firewall-version logic in one function, making that function responsible for more decisions than before. + +#### Neutral +- The `"latest"` AWF version string is treated as always meeting the minimum version requirement, which is a conservative assumption already established by the existing pattern. +- Non-semver version strings (e.g., branch names used in development) are treated as too old (returns false), erring on the side of not emitting the flag. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Flag Emission + +1. Implementations **MUST** emit `--enable-opencode` in the AWF argument list when the workflow engine is `opencode` and the effective AWF version is greater than or equal to `AWFEnableOpenCodeMinVersion` (v0.25.30). +2. Implementations **MUST NOT** emit `--enable-opencode` when the workflow engine is not `opencode`. +3. Implementations **MUST NOT** emit `--enable-opencode` when the effective AWF version is less than `AWFEnableOpenCodeMinVersion`, even if the engine is `opencode`. +4. Implementations **MUST** treat the string `"latest"` as satisfying the minimum version requirement for `--enable-opencode`. +5. Implementations **MUST** treat non-semver version strings (e.g., branch names) as not satisfying the minimum version requirement (conservative default). + +### Port Allowance + +1. Implementations **MUST** append port 10004 to the `--allow-host-ports` argument if and only if `--enable-opencode` would also be emitted (same condition: engine is `opencode` and AWF version ≥ v0.25.30). +2. Implementations **MUST NOT** include port 10004 in `--allow-host-ports` for any engine other than `opencode`. +3. Implementations **MUST NOT** include port 10004 in `--allow-host-ports` for `opencode` workflows using an AWF version below `AWFEnableOpenCodeMinVersion`. +4. Implementations **SHOULD** compute the `opencodeEnabled` boolean once (pre-computation) rather than repeating the engine-name and version checks at each emission site within `BuildAWFArgs`. + +### Version Constant Management + +1. Implementations **MUST** define the minimum AWF version threshold for `--enable-opencode` as the named constant `AWFEnableOpenCodeMinVersion` in `pkg/constants/version_constants.go`. +2. Implementations **MUST NOT** embed the version string `"v0.25.30"` as a magic literal at call sites; the named constant **MUST** be used instead. +3. Implementations **SHOULD** document the reason the minimum version was introduced (the AWF PR or issue that added the flag) in the constant's comment. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/25201347429) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* diff --git a/pkg/constants/version_constants.go b/pkg/constants/version_constants.go index d30b190f31..d959badbe1 100644 --- a/pkg/constants/version_constants.go +++ b/pkg/constants/version_constants.go @@ -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" + // 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. diff --git a/pkg/workflow/awf_config.go b/pkg/workflow/awf_config.go index bea6266353..585cf332d1 100644 --- a/pkg/workflow/awf_config.go +++ b/pkg/workflow/awf_config.go @@ -19,6 +19,7 @@ // }, // "apiProxy": { // "enabled": true, +// "enableOpenCode": true, // "targets": { // "openai": { "host": "api.openai.com" }, // "anthropic": { "host": "api.anthropic.com" }, @@ -47,6 +48,8 @@ import ( "encoding/json" "fmt" "strings" + + "github.com/github/gh-aw/pkg/constants" ) // AWFConfigFile represents the AWF configuration file schema. @@ -79,12 +82,18 @@ type AWFNetworkConfig struct { } // AWFAPIProxyConfig is the "apiProxy" section of the AWF config file. -// It maps to the --enable-api-proxy and --*-api-target CLI flags. +// It maps to the --enable-api-proxy, --enable-opencode, and --*-api-target CLI flags. type AWFAPIProxyConfig struct { // Enabled enables the API proxy sidecar for LLM gateway credential isolation. // Maps to: --enable-api-proxy Enabled bool `json:"enabled"` + // EnableOpenCode enables the OpenCode API proxy listener on port 10004 + // (dynamic provider routing). Only emitted when true (omitempty). + // Maps to: --enable-opencode + // Requires: Enabled == true and AWF >= v0.25.30 + EnableOpenCode bool `json:"enableOpenCode,omitempty"` + // Targets holds per-provider API target overrides. // Supported keys: "openai", "anthropic", "copilot", "gemini" Targets map[string]*AWFAPITargetConfig `json:"targets,omitempty"` @@ -137,6 +146,14 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { Enabled: true, } + // Enable the OpenCode API proxy listener on port 10004 for the opencode engine. + // Expressed in the config file as apiProxy.enableOpenCode so it is auditable + // alongside the other apiProxy settings, mirroring the --enable-opencode CLI flag. + firewallConfig := getFirewallConfig(config.WorkflowData) + if config.EngineName == string(constants.OpenCodeEngine) && awfSupportsEnableOpenCode(firewallConfig) { + apiProxy.EnableOpenCode = true + } + targets := map[string]*AWFAPITargetConfig{} if openaiTarget := extractAPITargetHost(config.WorkflowData, "OPENAI_BASE_URL"); openaiTarget != "" { @@ -158,7 +175,6 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) { awfConfig.APIProxy = apiProxy // ── Container section ───────────────────────────────────────────────────── - firewallConfig := getFirewallConfig(config.WorkflowData) awfImageTag := buildAWFImageTagWithDigests(getAWFImageTag(firewallConfig), config.WorkflowData) if awfImageTag != "" { awfConfig.Container = &AWFContainerConfig{ diff --git a/pkg/workflow/awf_config_test.go b/pkg/workflow/awf_config_test.go index 0bf2ffdc3e..4423687496 100644 --- a/pkg/workflow/awf_config_test.go +++ b/pkg/workflow/awf_config_test.go @@ -195,6 +195,71 @@ func TestBuildAWFConfigJSON(t *testing.T) { assert.NotContains(t, jsonStr, "\n", "JSON output should not contain newlines (must be compact)") assert.NotContains(t, jsonStr, " ", "JSON output should not contain indentation") }) + + t.Run("enableOpenCode is set in apiProxy for opencode engine with supported AWF version", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "opencode", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "opencode"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + Version: "v0.25.30", + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + + assert.Contains(t, jsonStr, `"enableOpenCode":true`, "apiProxy should include enableOpenCode:true for opencode engine") + }) + + t.Run("enableOpenCode is not set for non-opencode engines", func(t *testing.T) { + for _, engine := range []string{"copilot", "claude", "codex", "gemini"} { + config := AWFCommandConfig{ + EngineName: engine, + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: engine}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + Version: "v0.25.30", + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + + assert.NotContains(t, jsonStr, `"enableOpenCode"`, "apiProxy should not include enableOpenCode for engine %s", engine) + } + }) + + t.Run("enableOpenCode is not set for opencode engine with old AWF version", func(t *testing.T) { + config := AWFCommandConfig{ + EngineName: "opencode", + AllowedDomains: "github.com", + WorkflowData: &WorkflowData{ + EngineConfig: &EngineConfig{ID: "opencode"}, + NetworkPermissions: &NetworkPermissions{ + Firewall: &FirewallConfig{ + Enabled: true, + Version: "v0.25.29", + }, + }, + }, + } + + jsonStr, err := BuildAWFConfigJSON(config) + require.NoError(t, err) + + assert.NotContains(t, jsonStr, `"enableOpenCode"`, "apiProxy should not include enableOpenCode for AWF version below minimum") + }) } // TestBuildAWFConfigJSON_DomainDeduplication verifies that duplicate domain entries diff --git a/pkg/workflow/awf_helpers.go b/pkg/workflow/awf_helpers.go index 9a16ee5ae6..10402bac17 100644 --- a/pkg/workflow/awf_helpers.go +++ b/pkg/workflow/awf_helpers.go @@ -231,6 +231,7 @@ func BuildAWFCommand(config AWFCommandConfig) string { // BuildAWFCommand and are therefore not emitted here: // - --allow-domains / --block-domains → network.allowDomains / network.blockDomains // - --enable-api-proxy → apiProxy.enabled +// - --enable-opencode (opencode engine) → apiProxy.enableOpenCode (AWF v0.25.30+) // - --image-tag → container.imageTag // - --openai-api-target → apiProxy.targets.openai.host // - --anthropic-api-target → apiProxy.targets.anthropic.host @@ -316,6 +317,11 @@ 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 && @@ -323,12 +329,22 @@ func BuildAWFArgs(config AWFCommandConfig) []string { 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") @@ -684,3 +700,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 +} diff --git a/pkg/workflow/awf_helpers_test.go b/pkg/workflow/awf_helpers_test.go index 0d5a49f431..1c7a6d9d95 100644 --- a/pkg/workflow/awf_helpers_test.go +++ b/pkg/workflow/awf_helpers_test.go @@ -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) { diff --git a/pkg/workflow/enable_api_proxy_test.go b/pkg/workflow/enable_api_proxy_test.go index 95935f6fde..7b779fa800 100644 --- a/pkg/workflow/enable_api_proxy_test.go +++ b/pkg/workflow/enable_api_proxy_test.go @@ -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") + } + }) }