diff --git a/pkg/workflow/compiler_difc_proxy.go b/pkg/workflow/compiler_difc_proxy.go index 07249547f1..9b6c509bed 100644 --- a/pkg/workflow/compiler_difc_proxy.go +++ b/pkg/workflow/compiler_difc_proxy.go @@ -357,6 +357,14 @@ func (c *Compiler) generateStartCliProxyStep(yaml *strings.Builder, data *Workfl } } +// defaultCliProxyPolicyJSON is the fallback guard policy for the CLI proxy when no +// guard policy is explicitly configured in the workflow frontmatter. +// The DIFC proxy requires a --policy flag to forward requests; without it, all API +// calls return HTTP 503 with body "proxy enforcement not configured". +// This default allows all repos with no integrity filtering — the most permissive +// policy that still satisfies the proxy's requirement. +const defaultCliProxyPolicyJSON = `{"allow-only":{"repos":"all","min-integrity":"none"}}` + // buildStartCliProxyStepYAML returns the YAML for the "Start CLI proxy" step, // or an empty string if the proxy cannot be configured. func (c *Compiler) buildStartCliProxyStepYAML(data *WorkflowData) string { @@ -368,10 +376,15 @@ func (c *Compiler) buildStartCliProxyStepYAML(data *WorkflowData) string { customGitHubToken := getGitHubToken(githubTool) effectiveToken := getEffectiveGitHubToken(customGitHubToken) - // Build the guard policy JSON (static fields only) + // Build the guard policy JSON (static fields only). + // The CLI proxy requires a policy to forward requests — without one, all API + // calls return HTTP 503 ("proxy enforcement not configured"). Use the default + // permissive policy when no guard policy is configured in the frontmatter. policyJSON := getDIFCProxyPolicyJSON(githubTool) - // An empty policy is acceptable — the proxy still provides gh CLI routing - // without guard filtering when no min-integrity is configured. + if policyJSON == "" { + policyJSON = defaultCliProxyPolicyJSON + difcProxyLog.Print("No guard policy configured, using default CLI proxy policy") + } // Resolve the container image from the MCP gateway configuration ensureDefaultMCPGatewayConfig(data) @@ -389,9 +402,7 @@ func (c *Compiler) buildStartCliProxyStepYAML(data *WorkflowData) string { sb.WriteString(" env:\n") fmt.Fprintf(&sb, " GH_TOKEN: %s\n", effectiveToken) sb.WriteString(" GITHUB_SERVER_URL: ${{ github.server_url }}\n") - if policyJSON != "" { - fmt.Fprintf(&sb, " CLI_PROXY_POLICY: '%s'\n", policyJSON) - } + fmt.Fprintf(&sb, " CLI_PROXY_POLICY: '%s'\n", policyJSON) fmt.Fprintf(&sb, " CLI_PROXY_IMAGE: '%s'\n", containerImage) sb.WriteString(" run: |\n") sb.WriteString(" bash \"${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh\"\n") diff --git a/pkg/workflow/compiler_difc_proxy_test.go b/pkg/workflow/compiler_difc_proxy_test.go index 2e56cc0436..b1c32f9d9c 100644 --- a/pkg/workflow/compiler_difc_proxy_test.go +++ b/pkg/workflow/compiler_difc_proxy_test.go @@ -693,3 +693,68 @@ func TestGenerateSetGHRepoAfterDIFCProxyStep(t *testing.T) { assert.NotContains(t, result, "GH_HOST", "should not touch GH_HOST") }) } + +// TestBuildStartCliProxyStepYAML verifies that the CLI proxy step always emits +// CLI_PROXY_POLICY, using the default permissive policy when no guard policy is +// configured in the frontmatter. +func TestBuildStartCliProxyStepYAML(t *testing.T) { + c := &Compiler{} + + t.Run("emits default policy when no guard policy is configured", func(t *testing.T) { + data := &WorkflowData{ + Tools: map[string]any{ + "github": map[string]any{"toolsets": []string{"default"}}, + }, + } + + result := c.buildStartCliProxyStepYAML(data) + require.NotEmpty(t, result, "should emit CLI proxy step even without guard policy") + assert.Contains(t, result, "CLI_PROXY_POLICY", "should always emit CLI_PROXY_POLICY") + assert.Contains(t, result, `"allow-only"`, "default policy should contain allow-only") + assert.Contains(t, result, `"repos":"all"`, "default policy should allow all repos") + assert.Contains(t, result, `"min-integrity":"none"`, "default policy should have min-integrity none") + }) + + t.Run("emits default policy when github tool is nil", func(t *testing.T) { + data := &WorkflowData{ + Tools: map[string]any{}, + } + + result := c.buildStartCliProxyStepYAML(data) + require.NotEmpty(t, result, "should emit CLI proxy step even without github tool") + assert.Contains(t, result, "CLI_PROXY_POLICY", "should always emit CLI_PROXY_POLICY") + assert.Contains(t, result, `"min-integrity":"none"`, "should use default min-integrity") + }) + + t.Run("uses configured guard policy when present", func(t *testing.T) { + data := &WorkflowData{ + Tools: map[string]any{ + "github": map[string]any{ + "min-integrity": "approved", + "allowed-repos": "owner/*", + }, + }, + } + + result := c.buildStartCliProxyStepYAML(data) + require.NotEmpty(t, result, "should emit CLI proxy step") + assert.Contains(t, result, "CLI_PROXY_POLICY", "should emit CLI_PROXY_POLICY") + assert.Contains(t, result, `"min-integrity":"approved"`, "should use configured min-integrity") + assert.Contains(t, result, `"repos":"owner/*"`, "should use configured repos") + }) + + t.Run("emits correct step structure", func(t *testing.T) { + data := &WorkflowData{ + Tools: map[string]any{ + "github": map[string]any{"toolsets": []string{"default"}}, + }, + } + + result := c.buildStartCliProxyStepYAML(data) + assert.Contains(t, result, "name: Start CLI proxy", "should have correct step name") + assert.Contains(t, result, "GH_TOKEN:", "should include GH_TOKEN") + assert.Contains(t, result, "GITHUB_SERVER_URL:", "should include GITHUB_SERVER_URL") + assert.Contains(t, result, "CLI_PROXY_IMAGE:", "should include CLI_PROXY_IMAGE") + assert.Contains(t, result, "start_cli_proxy.sh", "should reference the start script") + }) +}