Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/skills/agentic-workflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ After loading the matching workflow prompt or skill, follow it directly:
- Choose workflow architecture and patterns: `.github/aw/patterns.md`
- Optimize token usage and cost: `.github/aw/token-optimization.md`
- Design long-running multi-agent research workflows: `.github/aw/multi-agent-research.md`
- Maintaining a repository, consuming backlog: `.githbub/aw/maintainer.md`

When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill.

7 changes: 4 additions & 3 deletions .github/workflows/shared/aider.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ engine:
display-name: Aider
description: Aider AI pair programming CLI running in scripting (non-interactive) mode
experimental: true
mcp: false
provider:
name: github
behaviors:
Expand Down Expand Up @@ -111,7 +112,7 @@ from `OPENAI_API_BASE`.

Aider runs in scripting mode: the generated prompt file is passed with
`--message-file` and all confirmations are auto-accepted (`--yes-always`).
Aider has no MCP client, so MCP-backed tools are unavailable. Disable the
GitHub MCP server with `tools: github: false` and rely on `bash:` tools, file
edits, and safe outputs written to `$GH_AW_SAFE_OUTPUTS` instead.
Aider has no MCP client, so the compiler exposes MCP-backed tools through
`cli-proxy` and GitHub access through `gh-proxy`. Both proxies are enabled
automatically and cannot be disabled for this engine.
-->
53 changes: 36 additions & 17 deletions .github/workflows/smoke-aider.lock.yml

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion .github/workflows/smoke-aider.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ network:
- defaults
- github
tools:
github: false
edit:
bash:
- "*"
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/data/agentic_workflows_fallback_aw_files.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"llms.md",
"loop.md",
"lsp.md",
"maintainer.md",
"mcp-clis.md",
"memory-stateful-patterns.md",
"memory.md",
Expand Down
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13116,6 +13116,11 @@
"type": "boolean",
"description": "Marks the engine as experimental so compiled workflows can surface an explicit warning."
},
"mcp": {
"type": "boolean",
"description": "Whether the engine supports MCP. When false, the compiler automatically enables gh-proxy and cli-proxy and rejects attempts to disable either proxy.",
"default": true
},
"runtime-id": {
"type": "string",
"description": "Runtime adapter identifier. Maps to the CodingAgentEngine registered in the engine registry. Defaults to id when omitted."
Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ type EngineCapabilities struct {
// ToolsAllowlist reports whether the engine supports MCP tool allow-listing.
ToolsAllowlist bool

// MCP reports whether the engine supports MCP servers directly.
MCP bool

// MaxTurns reports whether the engine supports the max-turns feature.
MaxTurns bool

Expand Down Expand Up @@ -150,6 +153,13 @@ type CapabilityProvider interface {
GetCapabilities() EngineCapabilities
}

// MCPProxyEngine provides the identity and MCP capability information needed to
// configure CLI proxy tools.
type MCPProxyEngine interface {
Engine
CapabilityProvider
}

// WorkflowExecutor handles workflow compilation and execution
// All engines must implement this to generate GitHub Actions steps
type WorkflowExecutor interface {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/antigravity_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func NewAntigravityEngine() *AntigravityEngine {
ghSkillAgentName: "antigravity",
capabilities: EngineCapabilities{
ToolsAllowlist: true,
MCP: true,
MaxTurns: true,
MaxContinuations: false, // Antigravity CLI does not support --max-autopilot-continues-style continuation mode
WebSearch: false,
Expand Down
8 changes: 7 additions & 1 deletion pkg/workflow/behavior_defined_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ func NewBehaviorDefinedEngine(def *EngineDefinition) (*BehaviorDefinedEngine, er
if def.Behaviors == nil {
return nil, fmt.Errorf("engine definition %q is missing behaviors", def.ID)
}
capabilities := def.Behaviors.Capabilities.ToRuntimeCapabilities()
capabilities.MCP = true
if def.MCP != nil {
capabilities.MCP = *def.MCP
}

engine := &BehaviorDefinedEngine{
UniversalLLMConsumerEngine: UniversalLLMConsumerEngine{
BaseEngine: BaseEngine{
Expand All @@ -47,7 +53,7 @@ func NewBehaviorDefinedEngine(def *EngineDefinition) (*BehaviorDefinedEngine, er
description: def.Description,
experimental: def.Experimental,
ghSkillAgentName: def.GHSkillAgentName,
capabilities: def.Behaviors.Capabilities.ToRuntimeCapabilities(),
capabilities: capabilities,
},
},
definition: def,
Expand Down
16 changes: 16 additions & 0 deletions pkg/workflow/behavior_defined_engine_harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ func TestBehaviorDefinedEngineHarnessScript(t *testing.T) {
})
}

func TestBehaviorDefinedEngineMCPCapability(t *testing.T) {
t.Run("defaults to supported", func(t *testing.T) {
engine, err := NewBehaviorDefinedEngine(newHarnessEngineDefinition())
require.NoError(t, err)
assert.True(t, engine.GetCapabilities().MCP)
})

t.Run("uses engine definition", func(t *testing.T) {
def := newHarnessEngineDefinition()
def.MCP = boolPtr(false)
engine, err := NewBehaviorDefinedEngine(def)
require.NoError(t, err)
assert.False(t, engine.GetCapabilities().MCP)
})
}

// TestBehaviorDefinedEngineNoHarnessScript verifies that engines without harness-script
// continue to use the direct command execution path (inline prompt substitution).
func TestBehaviorDefinedEngineNoHarnessScript(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func NewClaudeEngine() *ClaudeEngine {
ghSkillAgentName: "claude-code",
capabilities: EngineCapabilities{
ToolsAllowlist: true,
MCP: true,
MaxTurns: true, // Claude supports max-turns feature
MaxContinuations: false, // Claude Code does not support --max-autopilot-continues-style continuation
WebSearch: true, // Claude has built-in WebSearch support
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/codex_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func NewCodexEngine() *CodexEngine {
ghSkillAgentName: "codex",
capabilities: EngineCapabilities{
ToolsAllowlist: true,
MCP: true,
MaxTurns: true, // AWF max-turns is supported for Codex runs
MaxContinuations: false, // Codex does not support --max-autopilot-continues-style continuation mode
WebSearch: true, // Codex has built-in web-search support
Expand Down
52 changes: 51 additions & 1 deletion pkg/workflow/compiler_orchestrator_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (c *Compiler) processToolsAndMarkdown(result *parser.FrontmatterResult, cle
if err != nil {
return nil, err
}
toolsData, err := c.resolveToolsConfiguration(result, effectiveMarkdown, markdownDir, importsResult, agenticEngine)
toolsData, err := c.resolveToolsConfiguration(result, effectiveMarkdown, markdownDir, importsResult, agenticEngine, engineSetting)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -174,6 +174,7 @@ func (c *Compiler) resolveToolsConfiguration(
markdownDir string,
importsResult *parser.ImportsResult,
agenticEngine CodingAgentEngine,
engineSetting string,
) (*mergedToolsData, error) {
topTools := extractToolsMapFromFrontmatter(result.Frontmatter)
if err := ValidateToolsSection(topTools); err != nil {
Expand Down Expand Up @@ -206,7 +207,15 @@ func (c *Compiler) resolveToolsConfiguration(
orchestratorToolsLog.Printf("MCP configuration validation failed: %v", err)
return nil, err
}
tools, err = enforceMCPProxyTools(agenticEngine, tools)
if err != nil {
return nil, err
}
tools = c.adjustToolsForEngineCapabilities(result.Frontmatter, agenticEngine, tools)
tools, err = enforceMCPProxyTools(agenticEngine, tools)
if err != nil {
return nil, err
}
if err := c.validateEngineToolRequirements(result.Frontmatter, agenticEngine, tools); err != nil {
return nil, err
}
Expand All @@ -220,6 +229,47 @@ func (c *Compiler) resolveToolsConfiguration(
}, nil
}

// enforceMCPProxyTools exposes MCP-backed tools through CLI proxies for engines
// that do not have an MCP client.
func enforceMCPProxyTools(engine MCPProxyEngine, tools map[string]any) (map[string]any, error) {
if engine == nil || engine.GetCapabilities().MCP {
return tools, nil
}

if githubValue, exists := tools["github"]; exists {
switch github := githubValue.(type) {
case bool:
if !github {
return nil, fmt.Errorf("engine '%s' does not support MCP; tools.github cannot be disabled because gh-proxy is required", engine.GetID())
}
case map[string]any:
if modeValue, hasMode := github["mode"]; hasMode {
mode, ok := modeValue.(string)
if !ok || (mode != string(GitHubMCPModeGHProxy) && mode != string(GitHubMCPModeCLI)) {
return nil, fmt.Errorf("engine '%s' does not support MCP; tools.github.mode must be gh-proxy", engine.GetID())
}
}
github["mode"] = string(GitHubMCPModeGHProxy)
case nil, string:
tools["github"] = map[string]any{"mode": string(GitHubMCPModeGHProxy)}
}
}

if _, exists := tools["github"]; !exists {
tools["github"] = map[string]any{"mode": string(GitHubMCPModeGHProxy)}
} else if enabled, ok := tools["github"].(bool); ok && enabled {
tools["github"] = map[string]any{"mode": string(GitHubMCPModeGHProxy)}
}
Comment on lines +258 to +262

if cliProxy, exists := tools["cli-proxy"]; exists {
if enabled, ok := cliProxy.(bool); ok && !enabled {
return nil, fmt.Errorf("engine '%s' does not support MCP; tools.cli-proxy cannot be disabled", engine.GetID())
}
}
tools["cli-proxy"] = true
return tools, nil
}

func nonEmptyStrings(values ...string) []string {
out := make([]string, 0, len(values))
for _, value := range values {
Expand Down
67 changes: 67 additions & 0 deletions pkg/workflow/compiler_orchestrator_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,70 @@ func TestWarnDeprecatedFrontmatterFields_SafeOutputsDeprecatedAliases(t *testing
assert.Contains(t, stderr, "required-title-prefix", "warning should mention required-title-prefix replacement")
assert.Equal(t, 1, compiler.warningCount, "one warning for the deprecated alias")
}

func TestEnforceMCPProxyTools(t *testing.T) {
engine := &BaseEngine{
id: "custom-engine",
capabilities: EngineCapabilities{MCP: false},
}

t.Run("enables required proxies", func(t *testing.T) {
tools, err := enforceMCPProxyTools(engine, map[string]any{})
require.NoError(t, err)
assert.Equal(t, true, tools["cli-proxy"])
assert.Equal(t, map[string]any{"mode": "gh-proxy"}, tools["github"])
})

t.Run("preserves github configuration", func(t *testing.T) {
tools, err := enforceMCPProxyTools(engine, map[string]any{
"github": map[string]any{"toolsets": []any{"repos"}},
})
require.NoError(t, err)
assert.Equal(t, map[string]any{
"mode": "gh-proxy",
"toolsets": []any{"repos"},
}, tools["github"])
assert.Equal(t, true, tools["cli-proxy"])
})

t.Run("normalizes default-enabled github forms", func(t *testing.T) {
for name, github := range map[string]any{
"nil": nil,
"string": "enabled",
} {
t.Run(name, func(t *testing.T) {
tools, err := enforceMCPProxyTools(engine, map[string]any{"github": github})
require.NoError(t, err)
assert.Equal(t, map[string]any{"mode": "gh-proxy"}, tools["github"])
assert.Equal(t, true, tools["cli-proxy"])
})
}
})

t.Run("rejects disabled github proxy", func(t *testing.T) {
_, err := enforceMCPProxyTools(engine, map[string]any{"github": false})
require.ErrorContains(t, err, "tools.github cannot be disabled")
})

t.Run("rejects non proxy github mode", func(t *testing.T) {
_, err := enforceMCPProxyTools(engine, map[string]any{
"github": map[string]any{"mode": "remote"},
})
require.ErrorContains(t, err, "tools.github.mode must be gh-proxy")
})

t.Run("rejects disabled cli proxy", func(t *testing.T) {
_, err := enforceMCPProxyTools(engine, map[string]any{"cli-proxy": false})
require.ErrorContains(t, err, "tools.cli-proxy cannot be disabled")
})

t.Run("leaves MCP engines unchanged", func(t *testing.T) {
tools := map[string]any{"github": false, "cli-proxy": false}
actual, err := enforceMCPProxyTools(&BaseEngine{
id: "mcp-engine",
capabilities: EngineCapabilities{MCP: true},
}, tools)
require.NoError(t, err)
assert.Equal(t, tools, actual)
})
}
1 change: 1 addition & 0 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func NewCopilotEngine() *CopilotEngine {
ghSkillAgentName: "github-copilot",
capabilities: EngineCapabilities{
ToolsAllowlist: true,
MCP: true,
MaxTurns: true, // AWF max-turns is supported for Copilot runs
MaxContinuations: true, // Copilot CLI supports --autopilot with --max-autopilot-continues
WebSearch: false, // Copilot CLI does not have built-in web-search support
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/data/engines/pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ engine:
display-name: Pi
description: Pi AI coding agent (experimental)
runtime-id: pi
mcp: false
provider:
name: github
---
Expand Down
20 changes: 15 additions & 5 deletions pkg/workflow/engine_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,15 @@ type EngineCapabilitiesDefinition struct {
// ToRuntimeCapabilities converts the declarative capabilities definition into the
// runtime EngineCapabilities struct used by CodingAgentEngine implementations.
func (d EngineCapabilitiesDefinition) ToRuntimeCapabilities() EngineCapabilities {
return EngineCapabilities(d)
return EngineCapabilities{
ToolsAllowlist: d.ToolsAllowlist,
MaxTurns: d.MaxTurns,
WebSearch: d.WebSearch,
MaxContinuations: d.MaxContinuations,
NativeAgentFile: d.NativeAgentFile,
BareMode: d.BareMode,
BashCommandAllowlist: d.BashCommandAllowlist,
}
}

// EngineManifestDefinition describes engine-specific files and folders that alter
Expand Down Expand Up @@ -274,10 +282,12 @@ func (a *AuthDefinition) RequiredSecretNames() []string {
// It is separate from the runtime adapter (CodingAgentEngine) to allow the catalog
// layer to carry identity and provider information without coupling to implementation.
type EngineDefinition struct {
ID string `yaml:"id"`
DisplayName string `yaml:"display-name,omitempty"`
Description string `yaml:"description,omitempty"`
Experimental bool `yaml:"experimental,omitempty"`
ID string `yaml:"id"`
DisplayName string `yaml:"display-name,omitempty"`
Description string `yaml:"description,omitempty"`
Experimental bool `yaml:"experimental,omitempty"`
// MCP indicates whether the engine supports MCP. Nil defaults to supported.
MCP *bool `yaml:"mcp,omitempty"`
GHSkillAgentName string `yaml:"gh-skill-agent-name,omitempty"`
// RuntimeID maps to the CodingAgentEngine registered in EngineRegistry.
// Defaults to ID when omitted.
Expand Down
12 changes: 7 additions & 5 deletions pkg/workflow/engine_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ func TestNewEngineCatalog_BuiltIns(t *testing.T) {
engineID string
displayName string
provider string
mcp *bool
}{
{"claude", "Claude Code", "anthropic"},
{"codex", "Codex", "openai"},
{"copilot", "GitHub Copilot CLI", "github"},
{"gemini", "Google Gemini CLI", "google"},
{"pi", "Pi", "github"},
{"claude", "Claude Code", "anthropic", nil},
{"codex", "Codex", "openai", nil},
{"copilot", "GitHub Copilot CLI", "github", nil},
{"gemini", "Google Gemini CLI", "google", nil},
{"pi", "Pi", "github", boolPtr(false)},
}

for _, tt := range tests {
Expand All @@ -38,6 +39,7 @@ func TestNewEngineCatalog_BuiltIns(t *testing.T) {
assert.Equal(t, tt.displayName, resolved.Definition.DisplayName, "Definition.DisplayName should match")
assert.Equal(t, tt.provider, resolved.Definition.Provider.Name, "Definition.Provider.Name should match")
assert.Equal(t, tt.engineID, resolved.Runtime.GetID(), "Runtime.GetID() should match engine ID")
assert.Equal(t, tt.mcp, resolved.Definition.MCP, "Definition.MCP should match")
})
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/gemini_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func NewGeminiEngine() *GeminiEngine {
ghSkillAgentName: "gemini-cli",
capabilities: EngineCapabilities{
ToolsAllowlist: true,
MCP: true,
MaxTurns: true,
MaxContinuations: false, // Gemini CLI does not support --max-autopilot-continues-style continuation mode
WebSearch: false,
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/pi_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func NewPiEngine() *PiEngine {
ghSkillAgentName: "pi",
capabilities: EngineCapabilities{
ToolsAllowlist: true,
MCP: false,
MaxTurns: true,
MaxContinuations: false,
WebSearch: false,
Expand Down
Loading
Loading