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
2 changes: 1 addition & 1 deletion .changeset/minor-add-cli-proxy-feature-flag.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions actions/setup/md/cli_proxy_prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<gh-cli>
The `gh` CLI is pre-authenticated via the CLI proxy sidecar. Use `gh` commands for all GitHub reads: listing and searching issues, pull requests, discussions, labels, milestones; reading workflow runs, jobs, and artifacts; accessing repository contents, code, and metadata. No GitHub MCP server is available.
</gh-cli>
3 changes: 3 additions & 0 deletions actions/setup/md/cli_proxy_with_safeoutputs_prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<gh-cli>
The `gh` CLI is pre-authenticated via the CLI proxy sidecar. Use `gh` commands for all GitHub reads: listing and searching issues, pull requests, discussions, labels, milestones; reading workflow runs, jobs, and artifacts; accessing repository contents, code, and metadata. Use safeoutputs tools for GitHub writes and completion signaling. No GitHub MCP server is available.
</gh-cli>
13 changes: 0 additions & 13 deletions pkg/constants/feature_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,4 @@ const (
// features:
// cli-proxy: true
CliProxyFeatureFlag FeatureFlag = "cli-proxy"
// CliProxyWritableFeatureFlag enables write operations on the AWF CLI proxy sidecar.
// By default, the CLI proxy sidecar is read-only. When this flag is enabled,
// --cli-proxy-writable is injected into the AWF command, allowing write operations
// such as creating issues or merging PRs via gh CLI.
//
// Requires CliProxyFeatureFlag to also be enabled.
//
// Workflow frontmatter usage:
//
// features:
// cli-proxy: true
// cli-proxy-writable: true
CliProxyWritableFeatureFlag FeatureFlag = "cli-proxy-writable"
)
6 changes: 0 additions & 6 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,6 @@ func BuildAWFArgs(config AWFCommandConfig) []string {
awfArgs = append(awfArgs, "--enable-cli-proxy")
awfHelpersLog.Print("Added --enable-cli-proxy for gh CLI proxy sidecar")

// Allow write operations when cli-proxy-writable feature flag is also set
if isFeatureEnabled(constants.CliProxyWritableFeatureFlag, config.WorkflowData) {
awfArgs = append(awfArgs, "--cli-proxy-writable")
awfHelpersLog.Print("Added --cli-proxy-writable for write access via gh CLI proxy")
}

// Generate and pass the guard policy JSON for the cli-proxy.
// Reuses getDIFCProxyPolicyJSON() to build the static policy from tools.github config
// (min-integrity and repos fields), matching the DIFC proxy guard policy semantics.
Expand Down
45 changes: 3 additions & 42 deletions pkg/workflow/awf_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,8 +730,8 @@ func TestAWFSupportsExcludeEnv(t *testing.T) {
}
}

// TestBuildAWFArgsCliProxy tests that BuildAWFArgs correctly injects --enable-cli-proxy,
// --cli-proxy-writable, and --cli-proxy-policy based on the cli-proxy feature flags.
// TestBuildAWFArgsCliProxy tests that BuildAWFArgs correctly injects --enable-cli-proxy
// and --cli-proxy-policy based on the cli-proxy feature flag.
func TestBuildAWFArgsCliProxy(t *testing.T) {
baseWorkflow := func(features map[string]any, tools map[string]any) *WorkflowData {
return &WorkflowData{
Expand All @@ -758,7 +758,6 @@ func TestBuildAWFArgsCliProxy(t *testing.T) {
argsStr := strings.Join(args, " ")

assert.NotContains(t, argsStr, "--enable-cli-proxy", "Should not include --enable-cli-proxy when feature flag is absent")
assert.NotContains(t, argsStr, "--cli-proxy-writable", "Should not include --cli-proxy-writable when feature flag is absent")
assert.NotContains(t, argsStr, "--cli-proxy-policy", "Should not include --cli-proxy-policy when feature flag is absent")
})

Expand All @@ -776,42 +775,6 @@ func TestBuildAWFArgsCliProxy(t *testing.T) {
argsStr := strings.Join(args, " ")

assert.Contains(t, argsStr, "--enable-cli-proxy", "Should include --enable-cli-proxy when cli-proxy feature flag is enabled")
assert.NotContains(t, argsStr, "--cli-proxy-writable", "Should not include --cli-proxy-writable when cli-proxy-writable feature flag is absent")
})

t.Run("includes --cli-proxy-writable when cli-proxy-writable feature flag is enabled", func(t *testing.T) {
config := AWFCommandConfig{
EngineName: "copilot",
WorkflowData: baseWorkflow(
map[string]any{"cli-proxy": true, "cli-proxy-writable": true},
nil,
),
AllowedDomains: "github.com",
}

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

assert.Contains(t, argsStr, "--enable-cli-proxy", "Should include --enable-cli-proxy")
assert.Contains(t, argsStr, "--cli-proxy-writable", "Should include --cli-proxy-writable when feature flag is enabled")
})

t.Run("does not include --cli-proxy-writable without --enable-cli-proxy", func(t *testing.T) {
// cli-proxy-writable alone (without cli-proxy) should not inject any cli-proxy flags
config := AWFCommandConfig{
EngineName: "copilot",
WorkflowData: baseWorkflow(
map[string]any{"cli-proxy-writable": true},
nil,
),
AllowedDomains: "github.com",
}

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

assert.NotContains(t, argsStr, "--enable-cli-proxy", "Should not include --enable-cli-proxy when cli-proxy flag is absent")
assert.NotContains(t, argsStr, "--cli-proxy-writable", "Should not include --cli-proxy-writable when cli-proxy flag is absent")
})

t.Run("includes --cli-proxy-policy with guard policy when tools.github has min-integrity", func(t *testing.T) {
Expand Down Expand Up @@ -894,8 +857,7 @@ func TestBuildAWFArgsCliProxy(t *testing.T) {
},
},
Features: map[string]any{
"cli-proxy": true,
"cli-proxy-writable": true,
"cli-proxy": true,
},
Tools: map[string]any{
"github": map[string]any{
Expand All @@ -914,7 +876,6 @@ func TestBuildAWFArgsCliProxy(t *testing.T) {
argsStr := strings.Join(args, " ")

assert.NotContains(t, argsStr, "--enable-cli-proxy", "Should not include --enable-cli-proxy for AWF < v0.25.14")
assert.NotContains(t, argsStr, "--cli-proxy-writable", "Should not include --cli-proxy-writable for AWF < v0.25.14")
assert.NotContains(t, argsStr, "--cli-proxy-policy", "Should not include --cli-proxy-policy for AWF < v0.25.14")
})
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/mcp_setup_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any,
}
// Standard MCP tools
if toolName == "github" || toolName == "playwright" || toolName == "cache-memory" || toolName == "agentic-workflows" {
// When cli-proxy is enabled, agents use the pre-authenticated gh CLI for GitHub
// reads instead of the GitHub MCP server. Skip registering the GitHub MCP server
// so it is not configured with the gateway.
if toolName == "github" && isFeatureEnabled(constants.CliProxyFeatureFlag, workflowData) {
mcpSetupGeneratorLog.Print("Skipping GitHub MCP server registration: cli-proxy feature flag is enabled")
continue
}
mcpTools = append(mcpTools, toolName)
} else if mcpConfig, ok := toolValue.(map[string]any); ok {
// Check if it's explicitly marked as MCP type in the new format
Expand Down
2 changes: 2 additions & 0 deletions pkg/workflow/prompt_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
agenticWorkflowsGuideFile = "agentic_workflows_guide.md"
githubMCPToolsPromptFile = "github_mcp_tools_prompt.md"
githubMCPToolsWithSafeOutputsPromptFile = "github_mcp_tools_with_safeoutputs_prompt.md"
cliProxyPromptFile = "cli_proxy_prompt.md"
cliProxyWithSafeOutputsPromptFile = "cli_proxy_with_safeoutputs_prompt.md"
)

// GitHub context prompt is kept embedded because it contains GitHub Actions expressions
Expand Down
17 changes: 17 additions & 0 deletions pkg/workflow/unified_prompt_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,24 @@ func (c *Compiler) collectPromptSections(data *WorkflowData) []PromptSection {
EnvVars: envVars,
})
}
}

// 9b. GitHub tool-use guidance: directs the model to the correct mechanism for
// GitHub reads (and writes when safe-outputs is also enabled).
// When cli-proxy is enabled, the agent uses the pre-authenticated gh CLI for reads
// instead of a GitHub MCP server (which is not registered). Otherwise, the GitHub
// MCP server is used for reads.
if isFeatureEnabled(constants.CliProxyFeatureFlag, data) {
unifiedPromptLog.Print("Adding cli-proxy tool-use guidance (gh CLI for reads, no GitHub MCP server)")
cliProxyFile := cliProxyPromptFile
if HasSafeOutputsEnabled(data.SafeOutputs) {
cliProxyFile = cliProxyWithSafeOutputsPromptFile
}
sections = append(sections, PromptSection{
Content: cliProxyFile,
IsFile: true,
})
} else if hasGitHubTool(data.ParsedTools) {
// GitHub MCP tool-use guidance: clarifies that the MCP server is read-only and
// directs the model to use it for GitHub reads. When safe-outputs is also enabled,
// the guidance explicitly separates reads (GitHub MCP) from writes (safeoutputs) so
Expand Down
124 changes: 124 additions & 0 deletions pkg/workflow/unified_prompt_step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,127 @@ func TestCollectPromptSections_GitHubMCPAndSafeOutputsConsistency(t *testing.T)
}
})
}

// TestCollectPromptSections_CliProxy tests that when the cli-proxy feature flag is
// enabled, the cli-proxy prompt is used instead of the GitHub MCP tools prompt,
// and that the GitHub MCP server guidance is never injected.
func TestCollectPromptSections_CliProxy(t *testing.T) {
t.Run("cli-proxy enabled without safe-outputs uses cli_proxy_prompt", func(t *testing.T) {
compiler := &Compiler{}

data := &WorkflowData{
ParsedTools: NewTools(map[string]any{"github": true}),
Features: map[string]any{"cli-proxy": true},
SafeOutputs: nil,
}

sections := compiler.collectPromptSections(data)
require.NotEmpty(t, sections, "Should collect sections")

// Should include cli-proxy prompt
var cliProxySection *PromptSection
for i := range sections {
if sections[i].IsFile && sections[i].Content == cliProxyPromptFile {
cliProxySection = &sections[i]
break
}
}
require.NotNil(t, cliProxySection, "Should include cli_proxy_prompt.md when cli-proxy is enabled")

// Should NOT include GitHub MCP tools prompt
for _, section := range sections {
assert.NotEqual(t, githubMCPToolsPromptFile, section.Content,
"Should not include github_mcp_tools_prompt.md when cli-proxy is enabled")
assert.NotEqual(t, githubMCPToolsWithSafeOutputsPromptFile, section.Content,
"Should not include github_mcp_tools_with_safeoutputs_prompt.md when cli-proxy is enabled")
}
})

t.Run("cli-proxy enabled with safe-outputs uses cli_proxy_with_safeoutputs_prompt", func(t *testing.T) {
compiler := &Compiler{}

data := &WorkflowData{
ParsedTools: NewTools(map[string]any{"github": true}),
Features: map[string]any{"cli-proxy": true},
SafeOutputs: &SafeOutputsConfig{
MissingData: &MissingDataConfig{},
NoOp: &NoOpConfig{},
},
}

sections := compiler.collectPromptSections(data)
require.NotEmpty(t, sections, "Should collect sections")

// Should include the with-safeoutputs cli-proxy prompt
var cliProxySection *PromptSection
for i := range sections {
if sections[i].IsFile && sections[i].Content == cliProxyWithSafeOutputsPromptFile {
cliProxySection = &sections[i]
break
}
}
require.NotNil(t, cliProxySection, "Should include cli_proxy_with_safeoutputs_prompt.md when cli-proxy and safe-outputs are both enabled")

// Should NOT include GitHub MCP tools prompt
for _, section := range sections {
assert.NotEqual(t, githubMCPToolsPromptFile, section.Content,
"Should not include github_mcp_tools_prompt.md when cli-proxy is enabled")
assert.NotEqual(t, githubMCPToolsWithSafeOutputsPromptFile, section.Content,
"Should not include github_mcp_tools_with_safeoutputs_prompt.md when cli-proxy is enabled")
}
})

t.Run("cli-proxy enabled without github tool still adds cli-proxy prompt", func(t *testing.T) {
compiler := &Compiler{}

data := &WorkflowData{
ParsedTools: NewTools(map[string]any{}),
Features: map[string]any{"cli-proxy": true},
SafeOutputs: nil,
}

sections := compiler.collectPromptSections(data)

// Should include cli-proxy prompt even without github tool configured
var cliProxySection *PromptSection
for i := range sections {
if sections[i].IsFile && sections[i].Content == cliProxyPromptFile {
cliProxySection = &sections[i]
break
}
}
require.NotNil(t, cliProxySection, "Should include cli_proxy_prompt.md when cli-proxy is enabled regardless of tools.github")
})

t.Run("cli-proxy disabled uses GitHub MCP tools prompt when github tool is enabled", func(t *testing.T) {
compiler := &Compiler{}

data := &WorkflowData{
ParsedTools: NewTools(map[string]any{"github": true}),
Features: nil,
SafeOutputs: nil,
}

sections := compiler.collectPromptSections(data)

// Should include the standard GitHub MCP tools prompt
var githubMCPSection *PromptSection
for i := range sections {
if sections[i].IsFile && strings.Contains(sections[i].Content, "github_mcp_tools") {
githubMCPSection = &sections[i]
break
}
}
require.NotNil(t, githubMCPSection, "Should include github_mcp_tools file when cli-proxy is not enabled")
assert.Equal(t, githubMCPToolsPromptFile, githubMCPSection.Content,
"Should use the standard github_mcp_tools_prompt when cli-proxy is not enabled")

// Should NOT include cli-proxy prompt
for _, section := range sections {
assert.NotEqual(t, cliProxyPromptFile, section.Content,
"Should not include cli_proxy_prompt.md when cli-proxy is not enabled")
assert.NotEqual(t, cliProxyWithSafeOutputsPromptFile, section.Content,
"Should not include cli_proxy_with_safeoutputs_prompt.md when cli-proxy is not enabled")
}
})
}
Loading