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
5 changes: 5 additions & 0 deletions pkg/parser/content_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ func extractBotsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "bots", "[]")
}

// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
func extractPluginsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "plugins", "[]")
Comment on lines +148 to +150
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

extractPluginsFromContent returns the raw JSON for the plugins frontmatter field. Since plugins also supports the object form ({ repos: [...], github-token: ... }), callers that expect a JSON array (as in import processing) will silently fail to extract any plugins when the object form is used. Consider making this helper return a normalized JSON array of repos (matching the array form) or introducing a dedicated extractor that always yields []string repos regardless of whether the source was array or object format.

Suggested change
// extractPluginsFromContent extracts plugins section from frontmatter as JSON string
func extractPluginsFromContent(content string) (string, error) {
return extractFrontmatterField(content, "plugins", "[]")
// extractPluginsFromContent extracts plugins section from frontmatter as a JSON array of repos.
// It normalizes both the array form:
// plugins: [ "owner1/repo1", "owner2/repo2" ]
// and the object form:
// plugins:
// repos: [ "owner1/repo1", "owner2/repo2" ]
// github-token: ...
// into a JSON array of strings: ["owner1/repo1","owner2/repo2"].
func extractPluginsFromContent(content string) (string, error) {
contentExtractorLog.Printf("Extracting plugins")
result, err := ExtractFrontmatterFromContent(content)
if err != nil {
contentExtractorLog.Printf("Failed to extract frontmatter for plugins: %v", err)
return "[]", nil
}
rawPlugins, exists := result.Frontmatter["plugins"]
if !exists {
contentExtractorLog.Printf("Plugins field not found in frontmatter")
return "[]", nil
}
// Helper to convert various slice types to []string.
toStringSlice := func(v interface{}) []string {
switch vv := v.(type) {
case []string:
return vv
case []interface{}:
out := make([]string, 0, len(vv))
for _, item := range vv {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
default:
return nil
}
}
var repos []string
switch v := rawPlugins.(type) {
case []interface{}, []string:
// Already an array form; normalize to []string.
repos = toStringSlice(v)
case map[string]interface{}:
if rv, ok := v["repos"]; ok {
repos = toStringSlice(rv)
}
case map[interface{}]interface{}:
// Handle maps with non-string keys from YAML parsing.
for key, value := range v {
if ks, ok := key.(string); ok && ks == "repos" {
repos = toStringSlice(value)
break
}
}
default:
contentExtractorLog.Printf("Plugins field has unsupported type %T", rawPlugins)
}
if len(repos) == 0 {
contentExtractorLog.Printf("No repos found in plugins configuration")
return "[]", nil
}
pluginsJSON, err := json.Marshal(repos)
if err != nil {
contentExtractorLog.Printf("Failed to marshal plugins repos to JSON: %v", err)
return "[]", nil
}
contentExtractorLog.Printf("Successfully extracted plugins: count=%d", len(repos))
return strings.TrimSpace(string(pluginsJSON)), nil

Copilot uses AI. Check for mistakes.
}

// extractPostStepsFromContent extracts post-steps section from frontmatter as YAML string
func extractPostStepsFromContent(content string) (string, error) {
result, err := ExtractFrontmatterFromContent(content)
Expand Down
19 changes: 19 additions & 0 deletions pkg/parser/import_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type ImportsResult struct {
MergedPermissions string // Merged permissions configuration from all imports
MergedSecretMasking string // Merged secret-masking steps from all imports
MergedBots []string // Merged bots list from all imports (union of bot names)
MergedPlugins []string // Merged plugins list from all imports (union of plugin repos)
MergedPostSteps string // Merged post-steps configuration from all imports (appended in order)
MergedLabels []string // Merged labels from all imports (union of label names)
MergedCaches []string // Merged cache configurations from all imports (appended in order)
Expand Down Expand Up @@ -180,6 +181,8 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
var safeInputs []string
var bots []string // Track unique bot names
botsSet := make(map[string]bool) // Set for deduplicating bots
var plugins []string // Track unique plugin repos
pluginsSet := make(map[string]bool) // Set for deduplicating plugins
var labels []string // Track unique labels
labelsSet := make(map[string]bool) // Set for deduplicating labels
var caches []string // Track cache configurations (appended in order)
Expand Down Expand Up @@ -544,6 +547,21 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
}
}

// Extract plugins from imported file (merge into set to avoid duplicates)
pluginsContent, err := extractPluginsFromContent(string(content))
if err == nil && pluginsContent != "" && pluginsContent != "[]" {
// Parse plugins JSON array
var importedPlugins []string
if jsonErr := json.Unmarshal([]byte(pluginsContent), &importedPlugins); jsonErr == nil {
for _, plugin := range importedPlugins {
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
Comment on lines +553 to +560
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Imported plugin extraction assumes plugins frontmatter is a JSON array and unmarshals into []string. If an imported workflow uses the supported object format (plugins: { repos: [...], github-token: ... }), json.Unmarshal will fail and the plugins will be dropped with no error, so imports won’t behave the same as top-level parsing. Consider parsing the imported file’s frontmatter plugins value as either []any or map[string]any (like extractPluginsFromFrontmatter does) and extracting repos accordingly (and decide how to handle/merge an imported github-token if present).

Suggested change
// Parse plugins JSON array
var importedPlugins []string
if jsonErr := json.Unmarshal([]byte(pluginsContent), &importedPlugins); jsonErr == nil {
for _, plugin := range importedPlugins {
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
// Parse plugins JSON which may be either:
// - a JSON array of strings: ["owner/repo", ...]
// - a JSON object: { "repos": ["owner/repo", ...], "github-token": "..." }
var importedPluginsRaw any
if jsonErr := json.Unmarshal([]byte(pluginsContent), &importedPluginsRaw); jsonErr == nil {
switch v := importedPluginsRaw.(type) {
case []any:
for _, item := range v {
plugin, ok := item.(string)
if !ok {
continue
}
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
}
case map[string]any:
if reposRaw, ok := v["repos"]; ok {
if reposSlice, ok := reposRaw.([]any); ok {
for _, item := range reposSlice {
plugin, ok := item.(string)
if !ok {
continue
}
if !pluginsSet[plugin] {
pluginsSet[plugin] = true
plugins = append(plugins, plugin)
}
}
}
}
// Note: imported "github-token" (if present) is intentionally ignored here
// to avoid changing existing token-handling behavior.

Copilot uses AI. Check for mistakes.
}
}
}

// Extract post-steps from imported file (append in order)
postStepsContent, err := extractPostStepsFromContent(string(content))
if err == nil && postStepsContent != "" {
Expand Down Expand Up @@ -593,6 +611,7 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
MergedPermissions: permissionsBuilder.String(),
MergedSecretMasking: secretMaskingBuilder.String(),
MergedBots: bots,
MergedPlugins: plugins,
MergedPostSteps: postStepsBuilder.String(),
MergedLabels: labels,
MergedCaches: caches,
Expand Down
29 changes: 29 additions & 0 deletions pkg/workflow/compiler_orchestrator_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,35 @@ func (c *Compiler) processToolsAndMarkdown(result *parser.FrontmatterResult, cle
orchestratorToolsLog.Printf("Extracted %d plugins from frontmatter (custom_token=%v)", len(plugins), pluginsToken != "")
}

// Merge plugins from imports with top-level plugins
if len(importsResult.MergedPlugins) > 0 {
orchestratorToolsLog.Printf("Merging %d plugins from imports", len(importsResult.MergedPlugins))
// Create a set to track unique plugins
pluginsSet := make(map[string]bool)

// Add imported plugins first (imports have lower priority)
for _, plugin := range importsResult.MergedPlugins {
pluginsSet[plugin] = true
}

// Add top-level plugins (these override/supplement imports)
for _, plugin := range plugins {
pluginsSet[plugin] = true
}

// Convert set back to slice
mergedPlugins := make([]string, 0, len(pluginsSet))
for plugin := range pluginsSet {
mergedPlugins = append(mergedPlugins, plugin)
}

// Sort for deterministic output
sort.Strings(mergedPlugins)
plugins = mergedPlugins

orchestratorToolsLog.Printf("Merged plugins: %d total unique plugins", len(plugins))
}

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

Expand Down
Loading
Loading