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
1 change: 1 addition & 0 deletions docs/src/content/docs/agent-factory-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn,
| [Daily Workflow Updater](https://github.com/github/gh-aw/blob/main/.github/workflows/daily-workflow-updater.md) | copilot | [![Daily Workflow Updater](https://github.com/github/gh-aw/actions/workflows/daily-workflow-updater.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/daily-workflow-updater.lock.yml) | - | - |
| [DeepReport - Intelligence Gathering Agent](https://github.com/github/gh-aw/blob/main/.github/workflows/deep-report.md) | codex | [![DeepReport - Intelligence Gathering Agent](https://github.com/github/gh-aw/actions/workflows/deep-report.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/deep-report.lock.yml) | `0 15 * * 1-5` | - |
| [Delight](https://github.com/github/gh-aw/blob/main/.github/workflows/delight.md) | copilot | [![Delight](https://github.com/github/gh-aw/actions/workflows/delight.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/delight.lock.yml) | - | - |
| [Dependabot Burner](https://github.com/github/gh-aw/blob/main/.github/workflows/dependabot-burner.md) | copilot | [![Dependabot Burner](https://github.com/github/gh-aw/actions/workflows/dependabot-burner.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dependabot-burner.lock.yml) | - | - |
| [Dependabot Dependency Checker](https://github.com/github/gh-aw/blob/main/.github/workflows/dependabot-go-checker.md) | copilot | [![Dependabot Dependency Checker](https://github.com/github/gh-aw/actions/workflows/dependabot-go-checker.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dependabot-go-checker.lock.yml) | `0 9 * * 1,3,5` | - |
| [Dependabot Project Manager](https://github.com/github/gh-aw/blob/main/.github/workflows/dependabot-project-manager.md) | copilot | [![Dependabot Project Manager](https://github.com/github/gh-aw/actions/workflows/dependabot-project-manager.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dependabot-project-manager.lock.yml) | - | - |
| [Dev](https://github.com/github/gh-aw/blob/main/.github/workflows/dev.md) | copilot | [![Dev](https://github.com/github/gh-aw/actions/workflows/dev.lock.yml/badge.svg)](https://github.com/github/gh-aw/actions/workflows/dev.lock.yml) | - | - |
Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ type CapabilityProvider interface {
// SupportsFirewall returns true if this engine supports network firewalling/sandboxing
// When true, the engine can enforce network restrictions defined in the workflow
SupportsFirewall() bool

// SupportsPlugins returns true if this engine supports plugin installation
// When true, plugins can be installed using the engine's plugin install command
SupportsPlugins() bool
Comment on lines 125 to +131
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface hierarchy comment block above lists CapabilityProvider methods but doesn't mention SupportsPlugins, even though it is now part of the interface. Please update that documentation list to keep the file-level architecture docs accurate.

Copilot uses AI. Check for mistakes.
}

// WorkflowExecutor handles workflow compilation and execution
Expand Down Expand Up @@ -199,6 +203,7 @@ type BaseEngine struct {
supportsWebFetch bool
supportsWebSearch bool
supportsFirewall bool
supportsPlugins bool
}

func (e *BaseEngine) GetID() string {
Expand Down Expand Up @@ -241,6 +246,10 @@ func (e *BaseEngine) SupportsFirewall() bool {
return e.supportsFirewall
}

func (e *BaseEngine) SupportsPlugins() bool {
return e.supportsPlugins
}

// GetDeclaredOutputFiles returns an empty list by default (engines can override)
func (e *BaseEngine) GetDeclaredOutputFiles() []string {
return []string{}
Expand Down
6 changes: 6 additions & 0 deletions pkg/workflow/compiler_orchestrator_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ func (c *Compiler) processToolsAndMarkdown(result *parser.FrontmatterResult, cle
orchestratorToolsLog.Printf("Merged plugins: %d total unique plugins", len(pluginInfo.Plugins))
}

// Validate plugin support for the current engine
if err := c.validatePluginSupport(pluginInfo, agenticEngine); err != nil {
orchestratorToolsLog.Printf("Plugin support validation failed: %v", err)
return nil, err
}

// Add MCP fetch server if needed (when web-fetch is requested but engine doesn't support it)
tools, _ = AddMCPFetchServerIfNeeded(tools, agenticEngine)

Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func NewCopilotEngine() *CopilotEngine {
supportsWebFetch: true, // Copilot CLI has built-in web-fetch support
supportsWebSearch: false, // Copilot CLI does not have built-in web-search support
supportsFirewall: true, // Copilot supports network firewalling via AWF
supportsPlugins: true, // Copilot supports plugin installation
},
}
}
Expand Down
19 changes: 19 additions & 0 deletions pkg/workflow/copilot_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func TestCopilotEngine(t *testing.T) {
t.Error("Expected copilot engine to not support max-turns yet")
}

if !engine.SupportsPlugins() {
t.Error("Expected copilot engine to support plugins")
}

// Test declared output files (session files are copied to logs folder)
outputFiles := engine.GetDeclaredOutputFiles()
if len(outputFiles) != 1 {
Expand Down Expand Up @@ -84,6 +88,21 @@ func TestOtherEnginesNoDefaultDetectionModel(t *testing.T) {
}
}

func TestOtherEnginesNoPluginSupport(t *testing.T) {
// Test that only Copilot engine supports plugins
engines := []CodingAgentEngine{
NewClaudeEngine(),
NewCodexEngine(),
NewCustomEngine(),
}

for _, engine := range engines {
if engine.SupportsPlugins() {
t.Errorf("Expected engine '%s' to not support plugins, but it does", engine.GetID())
}
}
Comment on lines +91 to +103
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test codifies that Claude/Codex/Custom engines do not support plugins, but other parts of the workflow package currently generate plugin installation steps for Claude and Codex. If plugins are truly Copilot-only, those engine implementations (and any tests expecting plugin installs for them) should be updated/removed; otherwise this test should be updated to reflect actual supported engines.

Copilot uses AI. Check for mistakes.
}

func TestCopilotEngineInstallationSteps(t *testing.T) {
engine := NewCopilotEngine()

Expand Down
24 changes: 24 additions & 0 deletions pkg/workflow/engine_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,27 @@ func (c *Compiler) validateSingleEngineSpecification(mainEngineSetting string, i

return "", fmt.Errorf("invalid engine configuration in included file, missing or invalid 'id' field. Expected string or object with 'id' field.\n\nExample (string):\nengine: copilot\n\nExample (object):\nengine:\n id: copilot\n model: gpt-4\n\nSee: %s", constants.DocsEnginesURL)
}

// validatePluginSupport validates that plugins are only used with engines that support them
func (c *Compiler) validatePluginSupport(pluginInfo *PluginInfo, agenticEngine CodingAgentEngine) error {
// No plugins specified, validation passes
if pluginInfo == nil || len(pluginInfo.Plugins) == 0 {
return nil
}

engineValidationLog.Printf("Validating plugin support for engine: %s", agenticEngine.GetID())

// Check if the engine supports plugins
if !agenticEngine.SupportsPlugins() {
// Build error message listing the plugins that were specified
pluginsList := strings.Join(pluginInfo.Plugins, ", ")

return fmt.Errorf("engine '%s' does not support plugins. The following plugins cannot be installed: %s\n\nOnly the 'copilot' engine currently supports plugin installation.\n\nTo fix this, either:\n1. Remove the 'plugins' field from your workflow\n2. Change to an engine that supports plugins (e.g., engine: copilot)\n\nSee: %s",
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validatePluginSupport currently hard-codes that only the 'copilot' engine supports plugin installation and returns an error for any other engine with plugins. This conflicts with the existing behavior where Claude and Codex engines still generate plugin installation steps (e.g., in their GetInstallationSteps implementations), and there are tests/workflows that compile plugins with those engines. Please align the codebase in one direction: either (a) mark Claude/Codex as SupportsPlugins=true and update the error message to list supported engines dynamically, or (b) remove/disable plugin-install step generation (and related tests/docs) for Claude/Codex so validation matches actual support.

Suggested change
return fmt.Errorf("engine '%s' does not support plugins. The following plugins cannot be installed: %s\n\nOnly the 'copilot' engine currently supports plugin installation.\n\nTo fix this, either:\n1. Remove the 'plugins' field from your workflow\n2. Change to an engine that supports plugins (e.g., engine: copilot)\n\nSee: %s",
return fmt.Errorf("engine '%s' does not support plugins. The following plugins cannot be installed: %s\n\nTo fix this, either:\n1. Remove the 'plugins' field from your workflow\n2. Change to an engine that supports plugins (see the documentation for supported engines)\n\nSee: %s",

Copilot uses AI. Check for mistakes.
agenticEngine.GetID(),
pluginsList,
constants.DocsEnginesURL)
}

engineValidationLog.Printf("Engine %s supports plugins: %d plugins to install", agenticEngine.GetID(), len(pluginInfo.Plugins))
return nil
}
121 changes: 121 additions & 0 deletions pkg/workflow/engine_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,124 @@ func TestValidateEngineDidYouMean(t *testing.T) {
})
}
}

// TestValidatePluginSupport tests the validatePluginSupport function
func TestValidatePluginSupport(t *testing.T) {
tests := []struct {
name string
pluginInfo *PluginInfo
engineID string
expectError bool
errorMsg string
}{
{
name: "no plugins with copilot engine",
pluginInfo: nil,
engineID: "copilot",
expectError: false,
},
{
name: "plugins with copilot engine (supported)",
pluginInfo: &PluginInfo{
Plugins: []string{"org/plugin1", "org/plugin2"},
},
engineID: "copilot",
expectError: false,
},
{
name: "plugins with claude engine (not supported)",
pluginInfo: &PluginInfo{
Plugins: []string{"org/plugin1"},
},
engineID: "claude",
expectError: true,
errorMsg: "does not support plugins",
},
{
name: "plugins with codex engine (not supported)",
pluginInfo: &PluginInfo{
Plugins: []string{"org/plugin1", "org/plugin2"},
},
engineID: "codex",
expectError: true,
errorMsg: "does not support plugins",
},
{
name: "plugins with custom engine (not supported)",
pluginInfo: &PluginInfo{
Plugins: []string{"org/plugin1"},
},
engineID: "custom",
expectError: true,
errorMsg: "does not support plugins",
},
Comment on lines +421 to +447
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests assume plugins are only supported for the 'copilot' engine. That assumption currently conflicts with existing plugin compilation/import tests and engine installation logic that include 'claude plugin install' / 'codex plugin install' steps. Please reconcile: either update SupportsPlugins defaults for those engines and adjust validation/tests, or remove non-copilot plugin installation support everywhere so this validation behavior is consistent.

Copilot uses AI. Check for mistakes.
{
name: "empty plugin list",
pluginInfo: &PluginInfo{Plugins: []string{}},
engineID: "claude",
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
compiler := NewCompiler()
engine, err := compiler.engineRegistry.GetEngine(tt.engineID)
if err != nil {
t.Fatalf("Failed to get engine: %v", err)
}

err = compiler.validatePluginSupport(tt.pluginInfo, engine)

if tt.expectError && err == nil {
t.Error("Expected validation to fail but it succeeded")
} else if !tt.expectError && err != nil {
t.Errorf("Expected validation to succeed but it failed: %v", err)
} else if tt.expectError && err != nil && tt.errorMsg != "" {
if !strings.Contains(err.Error(), tt.errorMsg) {
t.Errorf("Expected error containing '%s', got '%s'", tt.errorMsg, err.Error())
}
}
})
}
}

// TestValidatePluginSupportErrorMessage verifies the plugin validation error message quality
func TestValidatePluginSupportErrorMessage(t *testing.T) {
compiler := NewCompiler()
claudeEngine, err := compiler.engineRegistry.GetEngine("claude")
if err != nil {
t.Fatalf("Failed to get claude engine: %v", err)
}

pluginInfo := &PluginInfo{
Plugins: []string{"org/plugin1", "org/plugin2"},
}

err = compiler.validatePluginSupport(pluginInfo, claudeEngine)
if err == nil {
t.Fatal("Expected validation to fail for plugins with claude engine")
}

errorMsg := err.Error()

// Error should mention the engine name
if !strings.Contains(errorMsg, "claude") {
t.Errorf("Error message should mention the engine name, got: %s", errorMsg)
}

// Error should list the plugins that can't be installed
if !strings.Contains(errorMsg, "org/plugin1") || !strings.Contains(errorMsg, "org/plugin2") {
t.Errorf("Error message should list the plugins, got: %s", errorMsg)
}

// Error should mention copilot as the supported engine
if !strings.Contains(errorMsg, "copilot") {
t.Errorf("Error message should mention copilot as supported engine, got: %s", errorMsg)
}

// Error should provide actionable fixes
if !strings.Contains(errorMsg, "To fix this") {
t.Errorf("Error message should provide actionable fixes, got: %s", errorMsg)
}
}
Loading