Skip to content
Merged
5 changes: 5 additions & 0 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error)
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}

// Validate GitHub guard policy configuration
if err := validateGitHubGuardPolicy(workflowData.ParsedTools, workflowData.Name); err != nil {
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}

// Use shared action cache and resolver from the compiler
actionCache, actionResolver := c.getSharedActionResolver()
workflowData.ActionCache = actionCache
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/compiler_string_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*Wor
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}

// Validate GitHub guard policy configuration
if err := validateGitHubGuardPolicy(workflowData.ParsedTools, workflowData.Name); err != nil {
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}

// Setup action cache and resolver
actionCache, actionResolver := c.getSharedActionResolver()
workflowData.ActionCache = actionCache
Expand Down
23 changes: 23 additions & 0 deletions pkg/workflow/mcp_github_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,29 @@ func getGitHubAllowedTools(githubTool any) []string {
return nil
}

// getGitHubGuardPolicies extracts guard policies from GitHub tool configuration.
// It reads the flat repos/min-integrity fields and wraps them for MCP gateway rendering.
// Returns nil if no guard policies are configured.
func getGitHubGuardPolicies(githubTool any) map[string]any {
if toolConfig, ok := githubTool.(map[string]any); ok {
repos, hasRepos := toolConfig["repos"]
integrity, hasIntegrity := toolConfig["min-integrity"]
if hasRepos || hasIntegrity {
policy := map[string]any{}
if hasRepos {
policy["repos"] = repos
}
if hasIntegrity {
policy["min-integrity"] = integrity
}
return map[string]any{
"allow-only": policy,
}
}
}
return nil
}

func getGitHubDockerImageVersion(githubTool any) string {
githubDockerImageVersion := string(constants.DefaultGitHubMCPServerVersion) // Default Docker image version
// Extract version setting from tool properties
Expand Down
49 changes: 45 additions & 4 deletions pkg/workflow/mcp_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
package workflow

import (
"encoding/json"
"fmt"
"os"
"sort"
Expand Down Expand Up @@ -168,6 +169,7 @@ func (r *MCPConfigRendererUnified) RenderGitHubMCP(yaml *strings.Builder, github
IncludeToolsField: r.options.IncludeCopilotFields,
AllowedTools: getGitHubAllowedTools(githubTool),
IncludeEnvSection: r.options.IncludeCopilotFields,
GuardPolicies: getGitHubGuardPolicies(githubTool),
})
} else {
// Local mode - use Docker-based GitHub MCP server (default)
Expand All @@ -186,6 +188,7 @@ func (r *MCPConfigRendererUnified) RenderGitHubMCP(yaml *strings.Builder, github
IncludeTypeField: r.options.IncludeCopilotFields,
AllowedTools: getGitHubAllowedTools(githubTool),
EffectiveToken: "", // Token passed via env
GuardPolicies: getGitHubGuardPolicies(githubTool),
})
}

Expand Down Expand Up @@ -676,6 +679,8 @@ type GitHubMCPDockerOptions struct {
EffectiveToken string
// Mounts specifies volume mounts for the GitHub MCP server container (format: "host:container:mode")
Mounts []string
// GuardPolicies specifies access control policies for the MCP gateway (e.g., allow-only repos/integrity)
GuardPolicies map[string]any
}

// RenderGitHubMCPDockerConfig renders the GitHub MCP server configuration for Docker (local mode).
Expand Down Expand Up @@ -771,7 +776,13 @@ func RenderGitHubMCPDockerConfig(yaml *strings.Builder, options GitHubMCPDockerO
fmt.Fprintf(yaml, " \"%s\": \"%s\"%s\n", key, envVars[key], comma)
}

yaml.WriteString(" }\n")
// Close env section, with trailing comma if guard-policies follows
if len(options.GuardPolicies) > 0 {
yaml.WriteString(" },\n")
renderGuardPoliciesJSON(yaml, options.GuardPolicies, " ")
} else {
yaml.WriteString(" }\n")
}
}

// GitHubMCPRemoteOptions defines configuration for GitHub MCP remote mode rendering
Expand All @@ -794,6 +805,8 @@ type GitHubMCPRemoteOptions struct {
AllowedTools []string
// IncludeEnvSection indicates whether to include the env section (Copilot needs it, Claude doesn't)
IncludeEnvSection bool
// GuardPolicies specifies access control policies for the MCP gateway (e.g., allow-only repos/integrity)
GuardPolicies map[string]any
}

// RenderGitHubMCPRemoteConfig renders the GitHub MCP server configuration for remote (hosted) mode.
Expand Down Expand Up @@ -836,7 +849,7 @@ func RenderGitHubMCPRemoteConfig(yaml *strings.Builder, options GitHubMCPRemoteO
writeHeadersToYAML(yaml, headers, " ")

// Close headers section
if options.IncludeToolsField || options.IncludeEnvSection {
if options.IncludeToolsField || options.IncludeEnvSection || len(options.GuardPolicies) > 0 {
yaml.WriteString(" },\n")
} else {
yaml.WriteString(" }\n")
Expand All @@ -856,7 +869,7 @@ func RenderGitHubMCPRemoteConfig(yaml *strings.Builder, options GitHubMCPRemoteO
}
yaml.WriteString("\n")
}
if options.IncludeEnvSection {
if options.IncludeEnvSection || len(options.GuardPolicies) > 0 {
yaml.WriteString(" ],\n")
} else {
yaml.WriteString(" ]\n")
Expand All @@ -867,10 +880,38 @@ func RenderGitHubMCPRemoteConfig(yaml *strings.Builder, options GitHubMCPRemoteO
if options.IncludeEnvSection {
yaml.WriteString(" \"env\": {\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"\\${GITHUB_MCP_SERVER_TOKEN}\"\n")
yaml.WriteString(" }\n")
// Close env section, with trailing comma if guard-policies follows
if len(options.GuardPolicies) > 0 {
yaml.WriteString(" },\n")
} else {
yaml.WriteString(" }\n")
}
}

// Add guard-policies if configured
if len(options.GuardPolicies) > 0 {
renderGuardPoliciesJSON(yaml, options.GuardPolicies, " ")
}
}

// renderGuardPoliciesJSON renders a "guard-policies" JSON field at the given indent level.
// The policies map contains policy names (e.g., "allow-only") mapped to their configurations.
// Renders as the last field (no trailing comma) with the given base indent.
func renderGuardPoliciesJSON(yaml *strings.Builder, policies map[string]any, indent string) {
if len(policies) == 0 {
return
}

// Marshal to JSON with indentation, then re-indent to match the current indent level
jsonBytes, err := json.MarshalIndent(policies, indent, " ")
if err != nil {
mcpRendererLog.Printf("Failed to marshal guard-policies: %v", err)
return
}

fmt.Fprintf(yaml, "%s\"guard-policies\": %s\n", indent, string(jsonBytes))
}

// RenderJSONMCPConfig renders MCP configuration in JSON format with the common mcpServers structure.
// This shared function extracts the duplicate pattern from Claude, Copilot, and Custom engines.
//
Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/schemas/mcp-gateway-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
"type": "string"
},
"default": ["*"]
},
"guard-policies": {
"type": "object",
"description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.",
"additionalProperties": true
}
},
"required": ["container"],
Expand Down Expand Up @@ -137,6 +142,11 @@
"type": "string"
},
"default": {}
},
"guard-policies": {
"type": "object",
"description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.",
"additionalProperties": true
}
},
"required": ["type", "url"],
Expand Down
8 changes: 8 additions & 0 deletions pkg/workflow/tools_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ func parseGitHubTool(val any) *GitHubToolConfig {
config.App = parseAppConfig(app)
}

// Parse guard policy fields (flat syntax: repos and min-integrity directly under github:)
if repos, ok := configMap["repos"]; ok {
config.Repos = repos // Store as-is, validation will happen later
}
if integrity, ok := configMap["min-integrity"].(string); ok {
config.MinIntegrity = GitHubIntegrityLevel(integrity)
}

return config
}

Expand Down
36 changes: 36 additions & 0 deletions pkg/workflow/tools_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ func mcpServerConfigToMap(config MCPServerConfig) map[string]any {
result["mounts"] = config.Mounts
}

// Add guard policies if set
if len(config.GuardPolicies) > 0 {
result["guard-policies"] = config.GuardPolicies
}

// Add custom fields (these override standard fields if there are conflicts)
maps.Copy(result, config.CustomFields)

Expand Down Expand Up @@ -257,6 +262,24 @@ func (g GitHubToolsets) ToStringSlice() []string {
return result
}

// GitHubIntegrityLevel represents the minimum integrity level required for repository access
type GitHubIntegrityLevel string

const (
// GitHubIntegrityNone allows access with no integrity requirements
GitHubIntegrityNone GitHubIntegrityLevel = "none"
// GitHubIntegrityReader requires read-level integrity
GitHubIntegrityReader GitHubIntegrityLevel = "reader"
// GitHubIntegrityWriter requires write-level integrity
GitHubIntegrityWriter GitHubIntegrityLevel = "writer"
// GitHubIntegrityMerged requires merged-level integrity
GitHubIntegrityMerged GitHubIntegrityLevel = "merged"
)

// GitHubReposScope represents the repository scope for guard policy enforcement
// Can be one of: "all", "public", or an array of repository patterns
type GitHubReposScope any // string or []any (YAML-parsed arrays are []any)

// GitHubToolConfig represents the configuration for the GitHub tool
// Can be nil (enabled with defaults), string, or an object with specific settings
type GitHubToolConfig struct {
Expand All @@ -269,6 +292,13 @@ type GitHubToolConfig struct {
Toolset GitHubToolsets `yaml:"toolsets,omitempty"`
Lockdown bool `yaml:"lockdown,omitempty"`
App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting

// Guard policy fields (flat syntax under github:)
// Repos defines the access scope for policy enforcement.
// Supports: "all", "public", or an array of patterns ["owner/repo", "owner/*"] (lowercase)
Repos GitHubReposScope `yaml:"repos,omitempty"`
// MinIntegrity defines the minimum integrity level required: "none", "reader", "writer", "merged"
MinIntegrity GitHubIntegrityLevel `yaml:"min-integrity,omitempty"`
}

// PlaywrightToolConfig represents the configuration for the Playwright tool
Expand Down Expand Up @@ -339,6 +369,12 @@ type MCPServerConfig struct {
Mode string `yaml:"mode,omitempty"` // MCP server mode (stdio, http, remote, local)
Toolsets []string `yaml:"toolsets,omitempty"` // Toolsets to enable

// Guard policies for access control at the MCP gateway level
// This is a general field that can hold server-specific policy configurations
// For GitHub: policies are represented via GitHubAllowOnlyPolicy on GitHubToolConfig
// For Jira/WorkIQ: define similar server-specific policy types
GuardPolicies map[string]any `yaml:"guard-policies,omitempty"`

// For truly dynamic configuration (server-specific fields not covered above)
CustomFields map[string]any `yaml:",inline"`
}
Expand Down
Loading
Loading