From fac5c0190e430f79a06b9fa26d9f07a051188788 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:00:31 +0000 Subject: [PATCH 01/11] Initial plan From c029693b1c985230905dfa7a9f20c01351528c69 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:06:56 +0000 Subject: [PATCH 02/11] Add guard policy types and schema support Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 10 +++ pkg/workflow/tools_types.go | 70 ++++++++++++++++--- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/pkg/workflow/schemas/mcp-gateway-config.schema.json b/pkg/workflow/schemas/mcp-gateway-config.schema.json index c5bc9bbda8a..5fc0eae654c 100644 --- a/pkg/workflow/schemas/mcp-gateway-config.schema.json +++ b/pkg/workflow/schemas/mcp-gateway-config.schema.json @@ -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"], @@ -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"], diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 12c30a6bfdb..8b39c692630 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -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) @@ -257,18 +262,59 @@ 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 []string + +// GitHubAllowOnlyPolicy represents the allowonly guard policy configuration +type GitHubAllowOnlyPolicy struct { + // Repos defines the access scope for policy enforcement + // Supports: + // - "all": all repositories + // - "public": public repositories only + // - array of patterns: ["owner/repo", "owner/*", "owner/re*"] (lowercase) + Repos GitHubReposScope `json:"repos" yaml:"repos"` + + // Integrity defines the minimum integrity level required + // Valid values: "none", "reader", "writer", "merged" + Integrity GitHubIntegrityLevel `json:"integrity" yaml:"integrity"` +} + +// GitHubGuardPolicy represents guard policy configuration for GitHub MCP server +// Guard policies enforce access control at the MCP gateway level +type GitHubGuardPolicy struct { + // AllowOnly policy restricts access based on repository scope and integrity level + AllowOnly *GitHubAllowOnlyPolicy `json:"allowonly,omitempty" yaml:"allowonly,omitempty"` +} + // GitHubToolConfig represents the configuration for the GitHub tool // Can be nil (enabled with defaults), string, or an object with specific settings type GitHubToolConfig struct { - Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` - Mode string `yaml:"mode,omitempty"` - Version string `yaml:"version,omitempty"` - Args []string `yaml:"args,omitempty"` - ReadOnly bool `yaml:"read-only,omitempty"` - GitHubToken string `yaml:"github-token,omitempty"` - Toolset GitHubToolsets `yaml:"toolsets,omitempty"` - Lockdown bool `yaml:"lockdown,omitempty"` - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting + Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` + Mode string `yaml:"mode,omitempty"` + Version string `yaml:"version,omitempty"` + Args []string `yaml:"args,omitempty"` + ReadOnly bool `yaml:"read-only,omitempty"` + GitHubToken string `yaml:"github-token,omitempty"` + Toolset GitHubToolsets `yaml:"toolsets,omitempty"` + Lockdown bool `yaml:"lockdown,omitempty"` + App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting + GuardPolicy *GitHubGuardPolicy `yaml:"guard-policy,omitempty"` // Guard policy configuration for access control + GuardPolicies *GitHubGuardPolicy `yaml:"guard-policies,omitempty"` // Alias for guard-policy (supports both singular and plural) } // PlaywrightToolConfig represents the configuration for the Playwright tool @@ -339,6 +385,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: use GitHubGuardPolicy + // 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"` } From faa60f8858ce0e5c306b968d209adf876ecf603a Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:08:12 +0000 Subject: [PATCH 03/11] Add guard policy parsing support in frontmatter Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/tools_parser.go | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 8998a390444..ba2206c1e30 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -234,6 +234,13 @@ func parseGitHubTool(val any) *GitHubToolConfig { config.App = parseAppConfig(app) } + // Parse guard policy configuration (support both singular and plural forms) + if guardPolicy, ok := configMap["guard-policy"].(map[string]any); ok { + config.GuardPolicy = parseGitHubGuardPolicy(guardPolicy) + } else if guardPolicies, ok := configMap["guard-policies"].(map[string]any); ok { + config.GuardPolicies = parseGitHubGuardPolicy(guardPolicies) + } + return config } @@ -242,6 +249,43 @@ func parseGitHubTool(val any) *GitHubToolConfig { } } +// parseGitHubGuardPolicy converts raw guard policy configuration to GitHubGuardPolicy +func parseGitHubGuardPolicy(policyMap map[string]any) *GitHubGuardPolicy { + if policyMap == nil { + return nil + } + + policy := &GitHubGuardPolicy{} + + // Parse allowonly policy + if allowOnly, ok := policyMap["allowonly"].(map[string]any); ok { + policy.AllowOnly = parseGitHubAllowOnlyPolicy(allowOnly) + } + + return policy +} + +// parseGitHubAllowOnlyPolicy parses the allowonly guard policy +func parseGitHubAllowOnlyPolicy(allowOnlyMap map[string]any) *GitHubAllowOnlyPolicy { + if allowOnlyMap == nil { + return nil + } + + policy := &GitHubAllowOnlyPolicy{} + + // Parse repos field - can be string ("all", "public") or array of patterns + if repos, ok := allowOnlyMap["repos"]; ok { + policy.Repos = repos // Store as-is, validation will happen later + } + + // Parse integrity field + if integrity, ok := allowOnlyMap["integrity"].(string); ok { + policy.Integrity = GitHubIntegrityLevel(integrity) + } + + return policy +} + // parseBashTool converts raw bash tool configuration to BashToolConfig func parseBashTool(val any) *BashToolConfig { if val == nil { From 8d682a8637b70caef93e8a2a2411d917c09cfc94 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:09:37 +0000 Subject: [PATCH 04/11] Add comprehensive guard policy validation Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- .../compiler_orchestrator_workflow.go | 5 + pkg/workflow/compiler_string_api.go | 5 + pkg/workflow/tools_validation.go | 166 ++++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/pkg/workflow/compiler_orchestrator_workflow.go b/pkg/workflow/compiler_orchestrator_workflow.go index 6d7b0653e7f..289e43fb881 100644 --- a/pkg/workflow/compiler_orchestrator_workflow.go +++ b/pkg/workflow/compiler_orchestrator_workflow.go @@ -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 diff --git a/pkg/workflow/compiler_string_api.go b/pkg/workflow/compiler_string_api.go index 30fe1c61c73..94f65371dc6 100644 --- a/pkg/workflow/compiler_string_api.go +++ b/pkg/workflow/compiler_string_api.go @@ -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 diff --git a/pkg/workflow/tools_validation.go b/pkg/workflow/tools_validation.go index 17a06f4efec..c62a40a287d 100644 --- a/pkg/workflow/tools_validation.go +++ b/pkg/workflow/tools_validation.go @@ -87,6 +87,172 @@ func validateGitHubToolConfig(tools *Tools, workflowName string) error { return nil } +// validateGitHubGuardPolicy validates the GitHub guard policy configuration +func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { + if tools == nil || tools.GitHub == nil { + return nil + } + + // Get the guard policy (check both singular and plural forms) + var guardPolicy *GitHubGuardPolicy + if tools.GitHub.GuardPolicy != nil { + guardPolicy = tools.GitHub.GuardPolicy + } else if tools.GitHub.GuardPolicies != nil { + guardPolicy = tools.GitHub.GuardPolicies + } + + if guardPolicy == nil { + return nil // No guard policy configured + } + + // Validate allowonly policy if present + if guardPolicy.AllowOnly != nil { + if err := validateGitHubAllowOnlyPolicy(guardPolicy.AllowOnly, workflowName); err != nil { + return err + } + } + + return nil +} + +// validateGitHubAllowOnlyPolicy validates the allowonly guard policy configuration +func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName string) error { + if policy == nil { + return nil + } + + // Validate repos field (required) + if policy.Repos == nil { + toolsValidationLog.Printf("Missing repos in allowonly policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allowonly.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*'])") + } + + // Validate repos format + if err := validateReposScope(policy.Repos, workflowName); err != nil { + return err + } + + // Validate integrity field (required) + if policy.Integrity == "" { + toolsValidationLog.Printf("Missing integrity in allowonly policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allowonly.integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") + } + + // Validate integrity value + validIntegrityLevels := map[GitHubIntegrityLevel]bool{ + GitHubIntegrityNone: true, + GitHubIntegrityReader: true, + GitHubIntegrityWriter: true, + GitHubIntegrityMerged: true, + } + + if !validIntegrityLevels[policy.Integrity] { + toolsValidationLog.Printf("Invalid integrity level '%s' in workflow: %s", policy.Integrity, workflowName) + return errors.New("invalid guard policy: 'allowonly.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(policy.Integrity) + "'") + } + + return nil +} + +// validateReposScope validates the repos field in allowonly policy +func validateReposScope(repos any, workflowName string) error { + // Case 1: String value ("all" or "public") + if reposStr, ok := repos.(string); ok { + if reposStr != "all" && reposStr != "public" { + toolsValidationLog.Printf("Invalid repos string '%s' in workflow: %s", reposStr, workflowName) + return errors.New("invalid guard policy: 'allowonly.repos' string must be 'all' or 'public'. Got: '" + reposStr + "'") + } + return nil + } + + // Case 2: Array of patterns + if reposArray, ok := repos.([]any); ok { + if len(reposArray) == 0 { + toolsValidationLog.Printf("Empty repos array in workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allowonly.repos' array cannot be empty. Provide at least one repository pattern") + } + + for i, item := range reposArray { + pattern, ok := item.(string) + if !ok { + toolsValidationLog.Printf("Non-string item in repos array at index %d in workflow: %s", i, workflowName) + return errors.New("invalid guard policy: 'allowonly.repos' array must contain only strings") + } + + if err := validateRepoPattern(pattern, workflowName); err != nil { + return err + } + } + + return nil + } + + // Invalid type + toolsValidationLog.Printf("Invalid repos type in workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allowonly.repos' must be 'all', 'public', or an array of repository patterns") +} + +// validateRepoPattern validates a single repository pattern +func validateRepoPattern(pattern string, workflowName string) error { + // Pattern must be lowercase + if strings.ToLower(pattern) != pattern { + toolsValidationLog.Printf("Repository pattern '%s' is not lowercase in workflow: %s", pattern, workflowName) + return errors.New("invalid guard policy: repository pattern '" + pattern + "' must be lowercase") + } + + // Check for valid pattern formats: + // 1. owner/repo (exact match) + // 2. owner/* (owner wildcard) + // 3. owner/re* (repository prefix wildcard) + parts := strings.Split(pattern, "/") + if len(parts) != 2 { + toolsValidationLog.Printf("Invalid repository pattern '%s' in workflow: %s", pattern, workflowName) + return errors.New("invalid guard policy: repository pattern '" + pattern + "' must be in format 'owner/repo', 'owner/*', or 'owner/prefix*'") + } + + owner := parts[0] + repo := parts[1] + + // Validate owner part (must be non-empty and contain only valid characters) + if owner == "" { + return errors.New("invalid guard policy: repository pattern '" + pattern + "' has empty owner") + } + + if !isValidOwnerOrRepo(owner) { + return errors.New("invalid guard policy: repository pattern '" + pattern + "' has invalid owner. Must contain only lowercase letters, numbers, hyphens, and underscores") + } + + // Validate repo part + if repo == "" { + return errors.New("invalid guard policy: repository pattern '" + pattern + "' has empty repository name") + } + + // Allow wildcard '*' or prefix with trailing '*' + if repo != "*" && !isValidOwnerOrRepo(strings.TrimSuffix(repo, "*")) { + return errors.New("invalid guard policy: repository pattern '" + pattern + "' has invalid repository name. Must contain only lowercase letters, numbers, hyphens, underscores, or be '*' or 'prefix*'") + } + + // Validate that wildcard is only at the end (not in the middle) + if strings.Contains(strings.TrimSuffix(repo, "*"), "*") { + return errors.New("invalid guard policy: repository pattern '" + pattern + "' has wildcard in the middle. Wildcards only allowed at the end (e.g., 'prefix*')") + } + + return nil +} + +// isValidOwnerOrRepo checks if a string contains only valid GitHub owner/repo characters +func isValidOwnerOrRepo(s string) bool { + if s == "" { + return false + } + for _, ch := range s { + if !((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_') { + return false + } + } + return true +} + // Note: validateGitToolForSafeOutputs was removed because git commands are automatically // injected by the compiler when safe-outputs needs them (see compiler_safe_outputs.go). // The validation was misleading - it would fail even though the compiler would add the From eeda7d56ed1ea988f68d19abcfc2bd44baf44e25 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:10:34 +0000 Subject: [PATCH 05/11] Add guard policies proposal document Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- GUARD_POLICIES_PROPOSAL.md | 332 +++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 GUARD_POLICIES_PROPOSAL.md diff --git a/GUARD_POLICIES_PROPOSAL.md b/GUARD_POLICIES_PROPOSAL.md new file mode 100644 index 00000000000..bfbf8c1b990 --- /dev/null +++ b/GUARD_POLICIES_PROPOSAL.md @@ -0,0 +1,332 @@ +# Guard Policies Integration Proposal + +## Executive Summary + +This document proposes an extensible guard policies framework for the MCP Gateway, starting with GitHub-specific policies. Guard policies enable fine-grained access control at the MCP gateway level, restricting which repositories and operations AI agents can access through MCP servers. + +## Problem Statement + +The user requested support for guard policies in the MCP gateway configuration, with the following requirements: + +1. Support GitHub-specific "allowonly" guard policies with: + - `repos` (scope): Repository access patterns + - `integrity` (minintegrity): Minimum integrity level required + +2. Design an extensible system that can support future MCP servers (Jira, WorkIQ) with different policy schemas + +3. Expose these parameters through workflow frontmatter in an intuitive way + +## Proposed Solution + +### 1. Type Hierarchy + +``` +GitHubGuardPolicy (server-specific) + └── GitHubAllowOnlyPolicy + ├── Repos: GitHubReposScope (string or []string) + └── Integrity: GitHubIntegrityLevel (enum) + +MCPServerConfig (general) + └── GuardPolicies: map[string]any (extensible for all servers) +``` + +### 2. GitHub Guard Policy Schema + +Based on the provided JSON schema, the implementation supports: + +**Repos Scope:** +- `"all"` - All repositories accessible by the token +- `"public"` - Public repositories only +- Array of patterns: + - `"owner/repo"` - Exact repository match + - `"owner/*"` - All repositories under owner + - `"owner/prefix*"` - Repositories with name prefix under owner + +**Integrity Levels:** +- `"none"` - No integrity requirements +- `"reader"` - Read-level integrity +- `"writer"` - Write-level integrity +- `"merged"` - Merged-level integrity + +### 3. Frontmatter Syntax + +**Minimal Example:** +```yaml +tools: + github: + mode: remote + toolsets: [default] + guard-policies: + allowonly: + repos: "all" + integrity: reader +``` + +**With Repository Patterns:** +```yaml +tools: + github: + mode: remote + toolsets: [default] + guard-policies: + allowonly: + repos: + - "myorg/*" + - "partner/shared-repo" + - "docs/api-*" + integrity: writer +``` + +**Singular Form (also supported):** +```yaml +tools: + github: + guard-policy: + allowonly: + repos: "public" + integrity: none +``` + +### 4. MCP Gateway Configuration Flow + +1. **Frontmatter Parsing** (`tools_parser.go`): + - Extracts `guard-policy` or `guard-policies` from GitHub tool config + - Parses into `GitHubGuardPolicy` struct + - Validates structure and types + +2. **Validation** (`tools_validation.go`): + - Validates repos format (all/public or valid patterns) + - Validates integrity level (none/reader/writer/merged) + - Validates repository pattern syntax (lowercase, valid characters, wildcard placement) + - Called during workflow compilation + +3. **Compilation**: + - Guard policies included in compiled GitHub tool configuration + - Passed through to MCP Gateway configuration + +4. **Runtime (MCP Gateway)**: + - Gateway receives guard policies in server configuration + - Enforces policies on all tool invocations + - Blocks unauthorized repository access + +### 5. Extensibility for Future Servers + +The design supports future MCP servers (Jira, WorkIQ) through: + +1. **Server-Specific Policy Types:** + ```go + type JiraGuardPolicy struct { + AllowOnly *JiraAllowOnlyPolicy `json:"allowonly,omitempty"` + } + + type JiraAllowOnlyPolicy struct { + Projects []string `json:"projects"` // Jira-specific field + IssueTypes []string `json:"issue_types,omitempty"` + } + ``` + +2. **General MCPServerConfig Field:** + ```go + type MCPServerConfig struct { + // ... + GuardPolicies map[string]any `yaml:"guard-policies,omitempty"` + } + ``` + +3. **Frontmatter Configuration:** + ```yaml + tools: + jira: + mode: remote + guard-policies: + allowonly: + projects: ["PROJ-*", "SHARED"] + issue_types: ["Bug", "Story"] + ``` + +## Implementation Details + +### Files Modified + +1. **pkg/workflow/tools_types.go** + - Added `GitHubIntegrityLevel` enum type + - Added `GitHubReposScope` type alias + - Added `GitHubAllowOnlyPolicy` struct + - Added `GitHubGuardPolicy` struct + - Extended `GitHubToolConfig` with `GuardPolicy` and `GuardPolicies` fields + - Extended `MCPServerConfig` with `GuardPolicies` field + +2. **pkg/workflow/schemas/mcp-gateway-config.schema.json** + - Added `guard-policies` field to `stdioServerConfig` + - Added `guard-policies` field to `httpServerConfig` + - Set `additionalProperties: true` for server-specific schemas + +3. **pkg/workflow/tools_parser.go** + - Added `parseGitHubGuardPolicy()` function + - Added `parseGitHubAllowOnlyPolicy()` function + - Extended `parseGitHubTool()` to extract guard policies + +4. **pkg/workflow/tools_validation.go** + - Added `validateGitHubGuardPolicy()` function + - Added `validateGitHubAllowOnlyPolicy()` function + - Added `validateReposScope()` function + - Added `validateRepoPattern()` function + - Added `isValidOwnerOrRepo()` helper function + +5. **pkg/workflow/compiler_orchestrator_workflow.go** + - Added call to `validateGitHubGuardPolicy()` + +6. **pkg/workflow/compiler_string_api.go** + - Added call to `validateGitHubGuardPolicy()` + +### Validation Rules + +**Repository Patterns:** +- Must be lowercase +- Format: `owner/repo`, `owner/*`, or `owner/prefix*` +- Owner and repo parts must contain only: lowercase letters, numbers, hyphens, underscores +- Wildcards only allowed at end of repo name +- Empty arrays not allowed + +**Integrity Levels:** +- Must be one of: `none`, `reader`, `writer`, `merged` +- Case-sensitive + +**Required Fields:** +- Both `repos` and `integrity` are required in `allowonly` policy + +## Error Messages + +The implementation provides clear, actionable error messages: + +``` +invalid guard policy: 'allowonly.repos' is required. +Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*']) + +invalid guard policy: repository pattern 'Owner/Repo' must be lowercase + +invalid guard policy: repository pattern 'owner/re*po' has wildcard in the middle. +Wildcards only allowed at the end (e.g., 'prefix*') + +invalid guard policy: 'allowonly.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. +Got: 'admin' +``` + +## Usage Examples + +### Example 1: Restrict to Organization + +```yaml +tools: + github: + mode: remote + toolsets: [default] + guard-policies: + allowonly: + repos: + - "myorg/*" + integrity: reader +``` + +### Example 2: Multiple Organizations + +```yaml +tools: + github: + mode: remote + toolsets: [default] + guard-policies: + allowonly: + repos: + - "frontend-org/*" + - "backend-org/*" + - "shared/infrastructure" + integrity: writer +``` + +### Example 3: Public Repositories Only + +```yaml +tools: + github: + mode: remote + toolsets: [repos, issues] + guard-policies: + allowonly: + repos: "public" + integrity: none +``` + +### Example 4: Prefix Matching + +```yaml +tools: + github: + mode: remote + toolsets: [default] + guard-policies: + allowonly: + repos: + - "myorg/api-*" # Matches api-gateway, api-service, etc. + - "myorg/web-*" # Matches web-frontend, web-backend, etc. + integrity: writer +``` + +## Testing Strategy + +1. **Unit Tests** (Pending): + - Test parsing of valid configurations + - Test validation of invalid configurations + - Test error messages for various failure modes + - Test edge cases (empty strings, special characters, etc.) + +2. **Integration Tests** (Pending): + - Test end-to-end workflow compilation with guard policies + - Test that guard policies appear in compiled workflow YAML + - Test that guard policies are passed to MCP gateway configuration + +## Next Steps + +1. **Write Comprehensive Tests**: + - Unit tests for parsing functions + - Unit tests for validation functions + - Integration tests for end-to-end workflow compilation + +2. **Update Documentation**: + - Add guard policies section to MCP gateway documentation + - Add examples to GitHub MCP server documentation + - Update frontmatter configuration reference + +3. **Runtime Implementation** (Separate from this PR): + - MCP Gateway enforcement of guard policies + - Repository pattern matching logic + - Integrity level verification + - Access control logging + +## Benefits + +1. **Security**: Restrict AI agent access to specific repositories +2. **Compliance**: Enforce minimum integrity requirements +3. **Flexibility**: Support diverse repository patterns and wildcards +4. **Extensibility**: Easy to add policies for Jira, WorkIQ, etc. +5. **Clarity**: Clear error messages and validation +6. **Documentation**: Self-documenting through type system + +## Open Questions + +1. Should we support negative patterns (e.g., exclude certain repos)? +2. Should we support combining multiple policies (AND/OR logic)? +3. How should conflicts between lockdown and guard policies be resolved? +4. Should we add a "dry-run" mode to test policies before enforcement? + +## Conclusion + +This implementation provides a solid foundation for guard policies in the MCP gateway. The design is: + +- **Type-safe**: Strongly-typed structs with validation +- **Extensible**: Easy to add new servers and policy types +- **User-friendly**: Intuitive frontmatter syntax +- **Well-validated**: Comprehensive validation with clear error messages +- **Forward-compatible**: Supports future enhancements + +The implementation follows established patterns in the codebase and integrates seamlessly with the existing compilation and validation infrastructure. From 4bb038ee764f242ac806f72a733969896284d20c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:11:38 +0000 Subject: [PATCH 06/11] Completing task Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- pkg/workflow/tools_types.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 8b39c692630..a63cb2f743b 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -304,16 +304,16 @@ type GitHubGuardPolicy struct { // GitHubToolConfig represents the configuration for the GitHub tool // Can be nil (enabled with defaults), string, or an object with specific settings type GitHubToolConfig struct { - Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` - Mode string `yaml:"mode,omitempty"` - Version string `yaml:"version,omitempty"` - Args []string `yaml:"args,omitempty"` - ReadOnly bool `yaml:"read-only,omitempty"` - GitHubToken string `yaml:"github-token,omitempty"` - Toolset GitHubToolsets `yaml:"toolsets,omitempty"` - Lockdown bool `yaml:"lockdown,omitempty"` - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting - GuardPolicy *GitHubGuardPolicy `yaml:"guard-policy,omitempty"` // Guard policy configuration for access control + Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` + Mode string `yaml:"mode,omitempty"` + Version string `yaml:"version,omitempty"` + Args []string `yaml:"args,omitempty"` + ReadOnly bool `yaml:"read-only,omitempty"` + GitHubToken string `yaml:"github-token,omitempty"` + Toolset GitHubToolsets `yaml:"toolsets,omitempty"` + Lockdown bool `yaml:"lockdown,omitempty"` + App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting + GuardPolicy *GitHubGuardPolicy `yaml:"guard-policy,omitempty"` // Guard policy configuration for access control GuardPolicies *GitHubGuardPolicy `yaml:"guard-policies,omitempty"` // Alias for guard-policy (supports both singular and plural) } From 6603a239943c257f09cdbce300826cee5cc0fdcd Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:19:16 +0000 Subject: [PATCH 07/11] Simplify guard policy structure - remove wrapper nesting Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- GUARD_POLICIES_PROPOSAL.md | 103 ++++++++++++++----------------- pkg/workflow/tools_parser.go | 24 +------ pkg/workflow/tools_types.go | 28 +++------ pkg/workflow/tools_validation.go | 16 +---- 4 files changed, 62 insertions(+), 109 deletions(-) diff --git a/GUARD_POLICIES_PROPOSAL.md b/GUARD_POLICIES_PROPOSAL.md index bfbf8c1b990..adaad0a38bb 100644 --- a/GUARD_POLICIES_PROPOSAL.md +++ b/GUARD_POLICIES_PROPOSAL.md @@ -21,8 +21,8 @@ The user requested support for guard policies in the MCP gateway configuration, ### 1. Type Hierarchy ``` -GitHubGuardPolicy (server-specific) - └── GitHubAllowOnlyPolicy +GitHubToolConfig (GitHub-specific) + └── AllowOnly: *GitHubAllowOnlyPolicy ├── Repos: GitHubReposScope (string or []string) └── Integrity: GitHubIntegrityLevel (enum) @@ -56,10 +56,9 @@ tools: github: mode: remote toolsets: [default] - guard-policies: - allowonly: - repos: "all" - integrity: reader + allowonly: + repos: "all" + integrity: reader ``` **With Repository Patterns:** @@ -68,30 +67,28 @@ tools: github: mode: remote toolsets: [default] - guard-policies: - allowonly: - repos: - - "myorg/*" - - "partner/shared-repo" - - "docs/api-*" - integrity: writer + allowonly: + repos: + - "myorg/*" + - "partner/shared-repo" + - "docs/api-*" + integrity: writer ``` -**Singular Form (also supported):** +**Public Repositories Only:** ```yaml tools: github: - guard-policy: - allowonly: - repos: "public" - integrity: none + allowonly: + repos: "public" + integrity: none ``` ### 4. MCP Gateway Configuration Flow 1. **Frontmatter Parsing** (`tools_parser.go`): - - Extracts `guard-policy` or `guard-policies` from GitHub tool config - - Parses into `GitHubGuardPolicy` struct + - Extracts `allowonly` directly from GitHub tool config + - Parses into `GitHubAllowOnlyPolicy` struct - Validates structure and types 2. **Validation** (`tools_validation.go`): @@ -101,7 +98,7 @@ tools: - Called during workflow compilation 3. **Compilation**: - - Guard policies included in compiled GitHub tool configuration + - AllowOnly policy included in compiled GitHub tool configuration - Passed through to MCP Gateway configuration 4. **Runtime (MCP Gateway)**: @@ -113,14 +110,15 @@ tools: The design supports future MCP servers (Jira, WorkIQ) through: -1. **Server-Specific Policy Types:** +1. **Server-Specific Policy Fields:** ```go - type JiraGuardPolicy struct { - AllowOnly *JiraAllowOnlyPolicy `json:"allowonly,omitempty"` + type JiraToolConfig struct { + // ... other fields ... + AllowOnly *JiraAllowOnlyPolicy `yaml:"allowonly,omitempty"` } type JiraAllowOnlyPolicy struct { - Projects []string `json:"projects"` // Jira-specific field + Projects []string `json:"projects"` // Jira-specific field IssueTypes []string `json:"issue_types,omitempty"` } ``` @@ -138,10 +136,9 @@ The design supports future MCP servers (Jira, WorkIQ) through: tools: jira: mode: remote - guard-policies: - allowonly: - projects: ["PROJ-*", "SHARED"] - issue_types: ["Bug", "Story"] + allowonly: + projects: ["PROJ-*", "SHARED"] + issue_types: ["Bug", "Story"] ``` ## Implementation Details @@ -152,8 +149,7 @@ The design supports future MCP servers (Jira, WorkIQ) through: - Added `GitHubIntegrityLevel` enum type - Added `GitHubReposScope` type alias - Added `GitHubAllowOnlyPolicy` struct - - Added `GitHubGuardPolicy` struct - - Extended `GitHubToolConfig` with `GuardPolicy` and `GuardPolicies` fields + - Extended `GitHubToolConfig` with `AllowOnly` field - Extended `MCPServerConfig` with `GuardPolicies` field 2. **pkg/workflow/schemas/mcp-gateway-config.schema.json** @@ -162,12 +158,11 @@ The design supports future MCP servers (Jira, WorkIQ) through: - Set `additionalProperties: true` for server-specific schemas 3. **pkg/workflow/tools_parser.go** - - Added `parseGitHubGuardPolicy()` function - Added `parseGitHubAllowOnlyPolicy()` function - - Extended `parseGitHubTool()` to extract guard policies + - Extended `parseGitHubTool()` to extract allowonly policy 4. **pkg/workflow/tools_validation.go** - - Added `validateGitHubGuardPolicy()` function + - Updated `validateGitHubGuardPolicy()` function - Added `validateGitHubAllowOnlyPolicy()` function - Added `validateReposScope()` function - Added `validateRepoPattern()` function @@ -221,11 +216,10 @@ tools: github: mode: remote toolsets: [default] - guard-policies: - allowonly: - repos: - - "myorg/*" - integrity: reader + allowonly: + repos: + - "myorg/*" + integrity: reader ``` ### Example 2: Multiple Organizations @@ -235,13 +229,12 @@ tools: github: mode: remote toolsets: [default] - guard-policies: - allowonly: - repos: - - "frontend-org/*" - - "backend-org/*" - - "shared/infrastructure" - integrity: writer + allowonly: + repos: + - "frontend-org/*" + - "backend-org/*" + - "shared/infrastructure" + integrity: writer ``` ### Example 3: Public Repositories Only @@ -251,10 +244,9 @@ tools: github: mode: remote toolsets: [repos, issues] - guard-policies: - allowonly: - repos: "public" - integrity: none + allowonly: + repos: "public" + integrity: none ``` ### Example 4: Prefix Matching @@ -264,12 +256,11 @@ tools: github: mode: remote toolsets: [default] - guard-policies: - allowonly: - repos: - - "myorg/api-*" # Matches api-gateway, api-service, etc. - - "myorg/web-*" # Matches web-frontend, web-backend, etc. - integrity: writer + allowonly: + repos: + - "myorg/api-*" # Matches api-gateway, api-service, etc. + - "myorg/web-*" # Matches web-frontend, web-backend, etc. + integrity: writer ``` ## Testing Strategy diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index ba2206c1e30..9f299358ab4 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -234,11 +234,9 @@ func parseGitHubTool(val any) *GitHubToolConfig { config.App = parseAppConfig(app) } - // Parse guard policy configuration (support both singular and plural forms) - if guardPolicy, ok := configMap["guard-policy"].(map[string]any); ok { - config.GuardPolicy = parseGitHubGuardPolicy(guardPolicy) - } else if guardPolicies, ok := configMap["guard-policies"].(map[string]any); ok { - config.GuardPolicies = parseGitHubGuardPolicy(guardPolicies) + // Parse allowonly guard policy configuration + if allowOnly, ok := configMap["allowonly"].(map[string]any); ok { + config.AllowOnly = parseGitHubAllowOnlyPolicy(allowOnly) } return config @@ -249,22 +247,6 @@ func parseGitHubTool(val any) *GitHubToolConfig { } } -// parseGitHubGuardPolicy converts raw guard policy configuration to GitHubGuardPolicy -func parseGitHubGuardPolicy(policyMap map[string]any) *GitHubGuardPolicy { - if policyMap == nil { - return nil - } - - policy := &GitHubGuardPolicy{} - - // Parse allowonly policy - if allowOnly, ok := policyMap["allowonly"].(map[string]any); ok { - policy.AllowOnly = parseGitHubAllowOnlyPolicy(allowOnly) - } - - return policy -} - // parseGitHubAllowOnlyPolicy parses the allowonly guard policy func parseGitHubAllowOnlyPolicy(allowOnlyMap map[string]any) *GitHubAllowOnlyPolicy { if allowOnlyMap == nil { diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index a63cb2f743b..6dcc5eb8c65 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -294,27 +294,19 @@ type GitHubAllowOnlyPolicy struct { Integrity GitHubIntegrityLevel `json:"integrity" yaml:"integrity"` } -// GitHubGuardPolicy represents guard policy configuration for GitHub MCP server -// Guard policies enforce access control at the MCP gateway level -type GitHubGuardPolicy struct { - // AllowOnly policy restricts access based on repository scope and integrity level - AllowOnly *GitHubAllowOnlyPolicy `json:"allowonly,omitempty" yaml:"allowonly,omitempty"` -} - // GitHubToolConfig represents the configuration for the GitHub tool // Can be nil (enabled with defaults), string, or an object with specific settings type GitHubToolConfig struct { - Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` - Mode string `yaml:"mode,omitempty"` - Version string `yaml:"version,omitempty"` - Args []string `yaml:"args,omitempty"` - ReadOnly bool `yaml:"read-only,omitempty"` - GitHubToken string `yaml:"github-token,omitempty"` - Toolset GitHubToolsets `yaml:"toolsets,omitempty"` - Lockdown bool `yaml:"lockdown,omitempty"` - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting - GuardPolicy *GitHubGuardPolicy `yaml:"guard-policy,omitempty"` // Guard policy configuration for access control - GuardPolicies *GitHubGuardPolicy `yaml:"guard-policies,omitempty"` // Alias for guard-policy (supports both singular and plural) + Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` + Mode string `yaml:"mode,omitempty"` + Version string `yaml:"version,omitempty"` + Args []string `yaml:"args,omitempty"` + ReadOnly bool `yaml:"read-only,omitempty"` + GitHubToken string `yaml:"github-token,omitempty"` + Toolset GitHubToolsets `yaml:"toolsets,omitempty"` + Lockdown bool `yaml:"lockdown,omitempty"` + App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting + AllowOnly *GitHubAllowOnlyPolicy `yaml:"allowonly,omitempty"` // Guard policy for repository access control } // PlaywrightToolConfig represents the configuration for the Playwright tool diff --git a/pkg/workflow/tools_validation.go b/pkg/workflow/tools_validation.go index c62a40a287d..0a6a2b7e642 100644 --- a/pkg/workflow/tools_validation.go +++ b/pkg/workflow/tools_validation.go @@ -93,21 +93,9 @@ func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { return nil } - // Get the guard policy (check both singular and plural forms) - var guardPolicy *GitHubGuardPolicy - if tools.GitHub.GuardPolicy != nil { - guardPolicy = tools.GitHub.GuardPolicy - } else if tools.GitHub.GuardPolicies != nil { - guardPolicy = tools.GitHub.GuardPolicies - } - - if guardPolicy == nil { - return nil // No guard policy configured - } - // Validate allowonly policy if present - if guardPolicy.AllowOnly != nil { - if err := validateGitHubAllowOnlyPolicy(guardPolicy.AllowOnly, workflowName); err != nil { + if tools.GitHub.AllowOnly != nil { + if err := validateGitHubAllowOnlyPolicy(tools.GitHub.AllowOnly, workflowName); err != nil { return err } } From 94511cf49a5af99015f82f440e282f4793952e24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:27:55 +0000 Subject: [PATCH 08/11] Address reviewer feedback: rename allowonly to allow-only, fix types/comments, add tests, propagate to renderer Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_github_config.go | 16 ++ pkg/workflow/mcp_renderer.go | 49 +++++- pkg/workflow/tools_parser.go | 6 +- pkg/workflow/tools_types.go | 8 +- pkg/workflow/tools_validation.go | 42 +++-- pkg/workflow/tools_validation_test.go | 225 ++++++++++++++++++++++++++ 6 files changed, 322 insertions(+), 24 deletions(-) diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 1e954ccaa43..327eaa1b245 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -241,6 +241,22 @@ func getGitHubAllowedTools(githubTool any) []string { return nil } +// getGitHubGuardPolicies extracts guard policies from GitHub tool configuration. +// It converts the allow-only policy into a map suitable 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 { + if allowOnly, exists := toolConfig["allow-only"]; exists { + if allowOnlyMap, ok := allowOnly.(map[string]any); ok { + return map[string]any{ + "allow-only": allowOnlyMap, + } + } + } + } + return nil +} + func getGitHubDockerImageVersion(githubTool any) string { githubDockerImageVersion := string(constants.DefaultGitHubMCPServerVersion) // Default Docker image version // Extract version setting from tool properties diff --git a/pkg/workflow/mcp_renderer.go b/pkg/workflow/mcp_renderer.go index 79395b84b5a..f1c2605145c 100644 --- a/pkg/workflow/mcp_renderer.go +++ b/pkg/workflow/mcp_renderer.go @@ -76,6 +76,7 @@ package workflow import ( + "encoding/json" "fmt" "os" "sort" @@ -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) @@ -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), }) } @@ -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). @@ -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 @@ -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. @@ -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") @@ -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") @@ -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. // diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 9f299358ab4..1d6b2699bfc 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -234,8 +234,8 @@ func parseGitHubTool(val any) *GitHubToolConfig { config.App = parseAppConfig(app) } - // Parse allowonly guard policy configuration - if allowOnly, ok := configMap["allowonly"].(map[string]any); ok { + // Parse allow-only guard policy configuration + if allowOnly, ok := configMap["allow-only"].(map[string]any); ok { config.AllowOnly = parseGitHubAllowOnlyPolicy(allowOnly) } @@ -247,7 +247,7 @@ func parseGitHubTool(val any) *GitHubToolConfig { } } -// parseGitHubAllowOnlyPolicy parses the allowonly guard policy +// parseGitHubAllowOnlyPolicy parses the allow-only guard policy func parseGitHubAllowOnlyPolicy(allowOnlyMap map[string]any) *GitHubAllowOnlyPolicy { if allowOnlyMap == nil { return nil diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 6dcc5eb8c65..9afcfe8226a 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -278,7 +278,7 @@ const ( // 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 []string +type GitHubReposScope any // string or []any (YAML-parsed arrays are []any) // GitHubAllowOnlyPolicy represents the allowonly guard policy configuration type GitHubAllowOnlyPolicy struct { @@ -305,8 +305,8 @@ type GitHubToolConfig struct { GitHubToken string `yaml:"github-token,omitempty"` Toolset GitHubToolsets `yaml:"toolsets,omitempty"` Lockdown bool `yaml:"lockdown,omitempty"` - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting - AllowOnly *GitHubAllowOnlyPolicy `yaml:"allowonly,omitempty"` // Guard policy for repository access control + App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting + AllowOnly *GitHubAllowOnlyPolicy `yaml:"allow-only,omitempty"` // Guard policy for repository access control } // PlaywrightToolConfig represents the configuration for the Playwright tool @@ -379,7 +379,7 @@ type MCPServerConfig struct { // Guard policies for access control at the MCP gateway level // This is a general field that can hold server-specific policy configurations - // For GitHub: use GitHubGuardPolicy + // 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"` diff --git a/pkg/workflow/tools_validation.go b/pkg/workflow/tools_validation.go index 0a6a2b7e642..9ac5ba9d40a 100644 --- a/pkg/workflow/tools_validation.go +++ b/pkg/workflow/tools_validation.go @@ -103,7 +103,7 @@ func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { return nil } -// validateGitHubAllowOnlyPolicy validates the allowonly guard policy configuration +// validateGitHubAllowOnlyPolicy validates the allow-only guard policy configuration func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName string) error { if policy == nil { return nil @@ -111,8 +111,8 @@ func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName s // Validate repos field (required) if policy.Repos == nil { - toolsValidationLog.Printf("Missing repos in allowonly policy for workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allowonly.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*'])") + toolsValidationLog.Printf("Missing repos in allow-only policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allow-only.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*'])") } // Validate repos format @@ -122,8 +122,8 @@ func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName s // Validate integrity field (required) if policy.Integrity == "" { - toolsValidationLog.Printf("Missing integrity in allowonly policy for workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allowonly.integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") + toolsValidationLog.Printf("Missing integrity in allow-only policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allow-only.integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") } // Validate integrity value @@ -136,35 +136,35 @@ func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName s if !validIntegrityLevels[policy.Integrity] { toolsValidationLog.Printf("Invalid integrity level '%s' in workflow: %s", policy.Integrity, workflowName) - return errors.New("invalid guard policy: 'allowonly.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(policy.Integrity) + "'") + return errors.New("invalid guard policy: 'allow-only.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(policy.Integrity) + "'") } return nil } -// validateReposScope validates the repos field in allowonly policy +// validateReposScope validates the repos field in allow-only policy func validateReposScope(repos any, workflowName string) error { // Case 1: String value ("all" or "public") if reposStr, ok := repos.(string); ok { if reposStr != "all" && reposStr != "public" { toolsValidationLog.Printf("Invalid repos string '%s' in workflow: %s", reposStr, workflowName) - return errors.New("invalid guard policy: 'allowonly.repos' string must be 'all' or 'public'. Got: '" + reposStr + "'") + return errors.New("invalid guard policy: 'allow-only.repos' string must be 'all' or 'public'. Got: '" + reposStr + "'") } return nil } - // Case 2: Array of patterns + // Case 2a: Array of patterns from YAML parsing ([]any) if reposArray, ok := repos.([]any); ok { if len(reposArray) == 0 { toolsValidationLog.Printf("Empty repos array in workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allowonly.repos' array cannot be empty. Provide at least one repository pattern") + return errors.New("invalid guard policy: 'allow-only.repos' array cannot be empty. Provide at least one repository pattern") } for i, item := range reposArray { pattern, ok := item.(string) if !ok { toolsValidationLog.Printf("Non-string item in repos array at index %d in workflow: %s", i, workflowName) - return errors.New("invalid guard policy: 'allowonly.repos' array must contain only strings") + return errors.New("invalid guard policy: 'allow-only.repos' array must contain only strings") } if err := validateRepoPattern(pattern, workflowName); err != nil { @@ -175,9 +175,25 @@ func validateReposScope(repos any, workflowName string) error { return nil } + // Case 2b: Array of patterns from programmatic construction ([]string) + if reposArray, ok := repos.([]string); ok { + if len(reposArray) == 0 { + toolsValidationLog.Printf("Empty repos array in workflow: %s", workflowName) + return errors.New("invalid guard policy: 'allow-only.repos' array cannot be empty. Provide at least one repository pattern") + } + + for _, pattern := range reposArray { + if err := validateRepoPattern(pattern, workflowName); err != nil { + return err + } + } + + return nil + } + // Invalid type toolsValidationLog.Printf("Invalid repos type in workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allowonly.repos' must be 'all', 'public', or an array of repository patterns") + return errors.New("invalid guard policy: 'allow-only.repos' must be 'all', 'public', or an array of repository patterns") } // validateRepoPattern validates a single repository pattern @@ -234,7 +250,7 @@ func isValidOwnerOrRepo(s string) bool { return false } for _, ch := range s { - if !((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_') { + if (ch < 'a' || ch > 'z') && (ch < '0' || ch > '9') && ch != '-' && ch != '_' { return false } } diff --git a/pkg/workflow/tools_validation_test.go b/pkg/workflow/tools_validation_test.go index 14097da97d1..8d63752792b 100644 --- a/pkg/workflow/tools_validation_test.go +++ b/pkg/workflow/tools_validation_test.go @@ -355,3 +355,228 @@ func TestValidateGitHubToolConfig(t *testing.T) { }) } } + +func TestValidateGitHubGuardPolicy(t *testing.T) { + tests := []struct { + name string + toolsMap map[string]any + shouldError bool + errorMsg string + }{ + { + name: "nil tools is valid", + toolsMap: nil, + shouldError: false, + }, + { + name: "no github tool is valid", + toolsMap: map[string]any{"bash": true}, + shouldError: false, + }, + { + name: "github tool without allow-only is valid", + toolsMap: map[string]any{"github": map[string]any{"mode": "remote"}}, + shouldError: false, + }, + { + name: "valid allow-only with repos=all", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": "all", + "integrity": "reader", + }, + }, + }, + shouldError: false, + }, + { + name: "valid allow-only with repos=public", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": "public", + "integrity": "writer", + }, + }, + }, + shouldError: false, + }, + { + name: "valid allow-only with repos array ([]any)", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": []any{"owner/repo", "owner/*"}, + "integrity": "merged", + }, + }, + }, + shouldError: false, + }, + { + name: "valid allow-only with integrity=none", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": "all", + "integrity": "none", + }, + }, + }, + shouldError: false, + }, + { + name: "missing repos field", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "integrity": "reader", + }, + }, + }, + shouldError: true, + errorMsg: "'allow-only.repos' is required", + }, + { + name: "missing integrity field", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": "all", + }, + }, + }, + shouldError: true, + errorMsg: "'allow-only.integrity' is required", + }, + { + name: "invalid integrity value", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": "all", + "integrity": "superuser", + }, + }, + }, + shouldError: true, + errorMsg: "'allow-only.integrity' must be one of", + }, + { + name: "invalid repos string value", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": "private", + "integrity": "reader", + }, + }, + }, + shouldError: true, + errorMsg: "'allow-only.repos' string must be 'all' or 'public'", + }, + { + name: "empty repos array", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": []any{}, + "integrity": "reader", + }, + }, + }, + shouldError: true, + errorMsg: "'allow-only.repos' array cannot be empty", + }, + { + name: "repos array with uppercase pattern", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": []any{"Owner/repo"}, + "integrity": "reader", + }, + }, + }, + shouldError: true, + errorMsg: "must be lowercase", + }, + { + name: "repos array with invalid pattern format", + toolsMap: map[string]any{ + "github": map[string]any{ + "allow-only": map[string]any{ + "repos": []any{"just-a-name"}, + "integrity": "reader", + }, + }, + }, + shouldError: true, + errorMsg: "must be in format", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tools := NewTools(tt.toolsMap) + err := validateGitHubGuardPolicy(tools, "test-workflow") + + if tt.shouldError { + require.Error(t, err, "Expected error for %s", tt.name) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected text") + } + } else { + assert.NoError(t, err, "Expected no error for %s", tt.name) + } + }) + } +} + +func TestValidateReposScopeWithStringSlice(t *testing.T) { + tests := []struct { + name string + repos any + shouldError bool + errorMsg string + }{ + { + name: "valid []string repos array", + repos: []string{"owner/repo", "owner/*"}, + shouldError: false, + }, + { + name: "valid []any repos array", + repos: []any{"owner/repo", "owner/*"}, + shouldError: false, + }, + { + name: "empty []string repos array", + repos: []string{}, + shouldError: true, + errorMsg: "array cannot be empty", + }, + { + name: "[]string with invalid pattern", + repos: []string{"Owner/Repo"}, + shouldError: true, + errorMsg: "must be lowercase", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateReposScope(tt.repos, "test-workflow") + + if tt.shouldError { + require.Error(t, err, "Expected error for %s", tt.name) + if tt.errorMsg != "" { + assert.Contains(t, err.Error(), tt.errorMsg, "Error message should contain expected text") + } + } else { + assert.NoError(t, err, "Expected no error for %s", tt.name) + } + }) + } +} From 436875b7cda90fa5b3753ca1c1ad967932c0890a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 04:27:25 +0000 Subject: [PATCH 09/11] Flatten github guard policy fields and move spec to scratchpad Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_github_config.go | 19 ++-- pkg/workflow/tools_parser.go | 30 ++---- pkg/workflow/tools_types.go | 40 ++++---- pkg/workflow/tools_validation.go | 57 +++++------ pkg/workflow/tools_validation_test.go | 82 ++++++---------- .../guard-policies-specification.md | 95 ++++++++----------- 6 files changed, 131 insertions(+), 192 deletions(-) rename GUARD_POLICIES_PROPOSAL.md => scratchpad/guard-policies-specification.md (78%) diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 327eaa1b245..057397fe562 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -242,15 +242,22 @@ func getGitHubAllowedTools(githubTool any) []string { } // getGitHubGuardPolicies extracts guard policies from GitHub tool configuration. -// It converts the allow-only policy into a map suitable for MCP gateway rendering. +// It reads the flat repos/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 { - if allowOnly, exists := toolConfig["allow-only"]; exists { - if allowOnlyMap, ok := allowOnly.(map[string]any); ok { - return map[string]any{ - "allow-only": allowOnlyMap, - } + repos, hasRepos := toolConfig["repos"] + integrity, hasIntegrity := toolConfig["integrity"] + if hasRepos || hasIntegrity { + policy := map[string]any{} + if hasRepos { + policy["repos"] = repos + } + if hasIntegrity { + policy["integrity"] = integrity + } + return map[string]any{ + "allow-only": policy, } } } diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 1d6b2699bfc..37775f18acd 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -234,9 +234,12 @@ func parseGitHubTool(val any) *GitHubToolConfig { config.App = parseAppConfig(app) } - // Parse allow-only guard policy configuration - if allowOnly, ok := configMap["allow-only"].(map[string]any); ok { - config.AllowOnly = parseGitHubAllowOnlyPolicy(allowOnly) + // Parse guard policy fields (flat syntax: repos and integrity directly under github:) + if repos, ok := configMap["repos"]; ok { + config.Repos = repos // Store as-is, validation will happen later + } + if integrity, ok := configMap["integrity"].(string); ok { + config.Integrity = GitHubIntegrityLevel(integrity) } return config @@ -247,27 +250,6 @@ func parseGitHubTool(val any) *GitHubToolConfig { } } -// parseGitHubAllowOnlyPolicy parses the allow-only guard policy -func parseGitHubAllowOnlyPolicy(allowOnlyMap map[string]any) *GitHubAllowOnlyPolicy { - if allowOnlyMap == nil { - return nil - } - - policy := &GitHubAllowOnlyPolicy{} - - // Parse repos field - can be string ("all", "public") or array of patterns - if repos, ok := allowOnlyMap["repos"]; ok { - policy.Repos = repos // Store as-is, validation will happen later - } - - // Parse integrity field - if integrity, ok := allowOnlyMap["integrity"].(string); ok { - policy.Integrity = GitHubIntegrityLevel(integrity) - } - - return policy -} - // parseBashTool converts raw bash tool configuration to BashToolConfig func parseBashTool(val any) *BashToolConfig { if val == nil { diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 9afcfe8226a..85d2d869f6e 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -280,33 +280,25 @@ const ( // Can be one of: "all", "public", or an array of repository patterns type GitHubReposScope any // string or []any (YAML-parsed arrays are []any) -// GitHubAllowOnlyPolicy represents the allowonly guard policy configuration -type GitHubAllowOnlyPolicy struct { - // Repos defines the access scope for policy enforcement - // Supports: - // - "all": all repositories - // - "public": public repositories only - // - array of patterns: ["owner/repo", "owner/*", "owner/re*"] (lowercase) - Repos GitHubReposScope `json:"repos" yaml:"repos"` - - // Integrity defines the minimum integrity level required - // Valid values: "none", "reader", "writer", "merged" - Integrity GitHubIntegrityLevel `json:"integrity" yaml:"integrity"` -} - // GitHubToolConfig represents the configuration for the GitHub tool // Can be nil (enabled with defaults), string, or an object with specific settings type GitHubToolConfig struct { - Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` - Mode string `yaml:"mode,omitempty"` - Version string `yaml:"version,omitempty"` - Args []string `yaml:"args,omitempty"` - ReadOnly bool `yaml:"read-only,omitempty"` - GitHubToken string `yaml:"github-token,omitempty"` - Toolset GitHubToolsets `yaml:"toolsets,omitempty"` - Lockdown bool `yaml:"lockdown,omitempty"` - App *GitHubAppConfig `yaml:"app,omitempty"` // GitHub App configuration for token minting - AllowOnly *GitHubAllowOnlyPolicy `yaml:"allow-only,omitempty"` // Guard policy for repository access control + Allowed GitHubAllowedTools `yaml:"allowed,omitempty"` + Mode string `yaml:"mode,omitempty"` + Version string `yaml:"version,omitempty"` + Args []string `yaml:"args,omitempty"` + ReadOnly bool `yaml:"read-only,omitempty"` + GitHubToken string `yaml:"github-token,omitempty"` + 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"` + // Integrity defines the minimum integrity level required: "none", "reader", "writer", "merged" + Integrity GitHubIntegrityLevel `yaml:"integrity,omitempty"` } // PlaywrightToolConfig represents the configuration for the Playwright tool diff --git a/pkg/workflow/tools_validation.go b/pkg/workflow/tools_validation.go index 9ac5ba9d40a..439dc4a035e 100644 --- a/pkg/workflow/tools_validation.go +++ b/pkg/workflow/tools_validation.go @@ -87,43 +87,38 @@ func validateGitHubToolConfig(tools *Tools, workflowName string) error { return nil } -// validateGitHubGuardPolicy validates the GitHub guard policy configuration +// validateGitHubGuardPolicy validates the GitHub guard policy configuration. +// Guard policy fields (repos, integrity) are specified flat under github:. +// Both fields must be present if either is specified. func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { if tools == nil || tools.GitHub == nil { return nil } - // Validate allowonly policy if present - if tools.GitHub.AllowOnly != nil { - if err := validateGitHubAllowOnlyPolicy(tools.GitHub.AllowOnly, workflowName); err != nil { - return err - } - } - - return nil -} + github := tools.GitHub + hasRepos := github.Repos != nil + hasIntegrity := github.Integrity != "" -// validateGitHubAllowOnlyPolicy validates the allow-only guard policy configuration -func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName string) error { - if policy == nil { + // No guard policy fields present - nothing to validate + if !hasRepos && !hasIntegrity { return nil } - // Validate repos field (required) - if policy.Repos == nil { - toolsValidationLog.Printf("Missing repos in allow-only policy for workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allow-only.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*'])") + // Validate repos field (required when integrity is set) + if !hasRepos { + toolsValidationLog.Printf("Missing repos in guard policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'github.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*'])") } // Validate repos format - if err := validateReposScope(policy.Repos, workflowName); err != nil { + if err := validateReposScope(github.Repos, workflowName); err != nil { return err } - // Validate integrity field (required) - if policy.Integrity == "" { - toolsValidationLog.Printf("Missing integrity in allow-only policy for workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allow-only.integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") + // Validate integrity field (required when repos is set) + if !hasIntegrity { + toolsValidationLog.Printf("Missing integrity in guard policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'github.integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") } // Validate integrity value @@ -134,21 +129,21 @@ func validateGitHubAllowOnlyPolicy(policy *GitHubAllowOnlyPolicy, workflowName s GitHubIntegrityMerged: true, } - if !validIntegrityLevels[policy.Integrity] { - toolsValidationLog.Printf("Invalid integrity level '%s' in workflow: %s", policy.Integrity, workflowName) - return errors.New("invalid guard policy: 'allow-only.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(policy.Integrity) + "'") + if !validIntegrityLevels[github.Integrity] { + toolsValidationLog.Printf("Invalid integrity level '%s' in workflow: %s", github.Integrity, workflowName) + return errors.New("invalid guard policy: 'github.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(github.Integrity) + "'") } return nil } -// validateReposScope validates the repos field in allow-only policy +// validateReposScope validates the repos field in the guard policy func validateReposScope(repos any, workflowName string) error { // Case 1: String value ("all" or "public") if reposStr, ok := repos.(string); ok { if reposStr != "all" && reposStr != "public" { toolsValidationLog.Printf("Invalid repos string '%s' in workflow: %s", reposStr, workflowName) - return errors.New("invalid guard policy: 'allow-only.repos' string must be 'all' or 'public'. Got: '" + reposStr + "'") + return errors.New("invalid guard policy: 'github.repos' string must be 'all' or 'public'. Got: '" + reposStr + "'") } return nil } @@ -157,14 +152,14 @@ func validateReposScope(repos any, workflowName string) error { if reposArray, ok := repos.([]any); ok { if len(reposArray) == 0 { toolsValidationLog.Printf("Empty repos array in workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allow-only.repos' array cannot be empty. Provide at least one repository pattern") + return errors.New("invalid guard policy: 'github.repos' array cannot be empty. Provide at least one repository pattern") } for i, item := range reposArray { pattern, ok := item.(string) if !ok { toolsValidationLog.Printf("Non-string item in repos array at index %d in workflow: %s", i, workflowName) - return errors.New("invalid guard policy: 'allow-only.repos' array must contain only strings") + return errors.New("invalid guard policy: 'github.repos' array must contain only strings") } if err := validateRepoPattern(pattern, workflowName); err != nil { @@ -179,7 +174,7 @@ func validateReposScope(repos any, workflowName string) error { if reposArray, ok := repos.([]string); ok { if len(reposArray) == 0 { toolsValidationLog.Printf("Empty repos array in workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allow-only.repos' array cannot be empty. Provide at least one repository pattern") + return errors.New("invalid guard policy: 'github.repos' array cannot be empty. Provide at least one repository pattern") } for _, pattern := range reposArray { @@ -193,7 +188,7 @@ func validateReposScope(repos any, workflowName string) error { // Invalid type toolsValidationLog.Printf("Invalid repos type in workflow: %s", workflowName) - return errors.New("invalid guard policy: 'allow-only.repos' must be 'all', 'public', or an array of repository patterns") + return errors.New("invalid guard policy: 'github.repos' must be 'all', 'public', or an array of repository patterns") } // validateRepoPattern validates a single repository pattern diff --git a/pkg/workflow/tools_validation_test.go b/pkg/workflow/tools_validation_test.go index 8d63752792b..4b35e70d197 100644 --- a/pkg/workflow/tools_validation_test.go +++ b/pkg/workflow/tools_validation_test.go @@ -374,54 +374,46 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { shouldError: false, }, { - name: "github tool without allow-only is valid", + name: "github tool without guard policy fields is valid", toolsMap: map[string]any{"github": map[string]any{"mode": "remote"}}, shouldError: false, }, { - name: "valid allow-only with repos=all", + name: "valid guard policy with repos=all", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": "all", - "integrity": "reader", - }, + "repos": "all", + "integrity": "reader", }, }, shouldError: false, }, { - name: "valid allow-only with repos=public", + name: "valid guard policy with repos=public", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": "public", - "integrity": "writer", - }, + "repos": "public", + "integrity": "writer", }, }, shouldError: false, }, { - name: "valid allow-only with repos array ([]any)", + name: "valid guard policy with repos array ([]any)", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": []any{"owner/repo", "owner/*"}, - "integrity": "merged", - }, + "repos": []any{"owner/repo", "owner/*"}, + "integrity": "merged", }, }, shouldError: false, }, { - name: "valid allow-only with integrity=none", + name: "valid guard policy with integrity=none", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": "all", - "integrity": "none", - }, + "repos": "all", + "integrity": "none", }, }, shouldError: false, @@ -430,73 +422,61 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "missing repos field", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "integrity": "reader", - }, + "integrity": "reader", }, }, shouldError: true, - errorMsg: "'allow-only.repos' is required", + errorMsg: "'github.repos' is required", }, { name: "missing integrity field", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": "all", - }, + "repos": "all", }, }, shouldError: true, - errorMsg: "'allow-only.integrity' is required", + errorMsg: "'github.integrity' is required", }, { name: "invalid integrity value", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": "all", - "integrity": "superuser", - }, + "repos": "all", + "integrity": "superuser", }, }, shouldError: true, - errorMsg: "'allow-only.integrity' must be one of", + errorMsg: "'github.integrity' must be one of", }, { name: "invalid repos string value", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": "private", - "integrity": "reader", - }, + "repos": "private", + "integrity": "reader", }, }, shouldError: true, - errorMsg: "'allow-only.repos' string must be 'all' or 'public'", + errorMsg: "'github.repos' string must be 'all' or 'public'", }, { name: "empty repos array", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": []any{}, - "integrity": "reader", - }, + "repos": []any{}, + "integrity": "reader", }, }, shouldError: true, - errorMsg: "'allow-only.repos' array cannot be empty", + errorMsg: "'github.repos' array cannot be empty", }, { name: "repos array with uppercase pattern", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": []any{"Owner/repo"}, - "integrity": "reader", - }, + "repos": []any{"Owner/repo"}, + "integrity": "reader", }, }, shouldError: true, @@ -506,10 +486,8 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "repos array with invalid pattern format", toolsMap: map[string]any{ "github": map[string]any{ - "allow-only": map[string]any{ - "repos": []any{"just-a-name"}, - "integrity": "reader", - }, + "repos": []any{"just-a-name"}, + "integrity": "reader", }, }, shouldError: true, diff --git a/GUARD_POLICIES_PROPOSAL.md b/scratchpad/guard-policies-specification.md similarity index 78% rename from GUARD_POLICIES_PROPOSAL.md rename to scratchpad/guard-policies-specification.md index adaad0a38bb..f5d6a739b3b 100644 --- a/GUARD_POLICIES_PROPOSAL.md +++ b/scratchpad/guard-policies-specification.md @@ -8,7 +8,7 @@ This document proposes an extensible guard policies framework for the MCP Gatewa The user requested support for guard policies in the MCP gateway configuration, with the following requirements: -1. Support GitHub-specific "allowonly" guard policies with: +1. Support GitHub-specific guard policies with flat frontmatter syntax: - `repos` (scope): Repository access patterns - `integrity` (minintegrity): Minimum integrity level required @@ -22,9 +22,8 @@ The user requested support for guard policies in the MCP gateway configuration, ``` GitHubToolConfig (GitHub-specific) - └── AllowOnly: *GitHubAllowOnlyPolicy - ├── Repos: GitHubReposScope (string or []string) - └── Integrity: GitHubIntegrityLevel (enum) + ├── Repos: GitHubReposScope (string or []any) + └── Integrity: GitHubIntegrityLevel (enum) MCPServerConfig (general) └── GuardPolicies: map[string]any (extensible for all servers) @@ -56,9 +55,8 @@ tools: github: mode: remote toolsets: [default] - allowonly: - repos: "all" - integrity: reader + repos: "all" + integrity: reader ``` **With Repository Patterns:** @@ -67,28 +65,26 @@ tools: github: mode: remote toolsets: [default] - allowonly: - repos: - - "myorg/*" - - "partner/shared-repo" - - "docs/api-*" - integrity: writer + repos: + - "myorg/*" + - "partner/shared-repo" + - "docs/api-*" + integrity: writer ``` **Public Repositories Only:** ```yaml tools: github: - allowonly: - repos: "public" - integrity: none + repos: "public" + integrity: none ``` ### 4. MCP Gateway Configuration Flow 1. **Frontmatter Parsing** (`tools_parser.go`): - - Extracts `allowonly` directly from GitHub tool config - - Parses into `GitHubAllowOnlyPolicy` struct + - Extracts `repos` and `integrity` directly from GitHub tool config + - Stores them as fields on `GitHubToolConfig` - Validates structure and types 2. **Validation** (`tools_validation.go`): @@ -98,7 +94,7 @@ tools: - Called during workflow compilation 3. **Compilation**: - - AllowOnly policy included in compiled GitHub tool configuration + - Guard policy fields (repos, integrity) included in compiled GitHub tool configuration - Passed through to MCP Gateway configuration 4. **Runtime (MCP Gateway)**: @@ -114,12 +110,9 @@ The design supports future MCP servers (Jira, WorkIQ) through: ```go type JiraToolConfig struct { // ... other fields ... - AllowOnly *JiraAllowOnlyPolicy `yaml:"allowonly,omitempty"` - } - - type JiraAllowOnlyPolicy struct { - Projects []string `json:"projects"` // Jira-specific field - IssueTypes []string `json:"issue_types,omitempty"` + // Guard policy fields (flat syntax under jira:) + Projects []string `yaml:"projects,omitempty"` + IssueTypes []string `yaml:"issue-types,omitempty"` } ``` @@ -136,9 +129,8 @@ The design supports future MCP servers (Jira, WorkIQ) through: tools: jira: mode: remote - allowonly: - projects: ["PROJ-*", "SHARED"] - issue_types: ["Bug", "Story"] + projects: ["PROJ-*", "SHARED"] + issue-types: ["Bug", "Story"] ``` ## Implementation Details @@ -148,8 +140,7 @@ The design supports future MCP servers (Jira, WorkIQ) through: 1. **pkg/workflow/tools_types.go** - Added `GitHubIntegrityLevel` enum type - Added `GitHubReposScope` type alias - - Added `GitHubAllowOnlyPolicy` struct - - Extended `GitHubToolConfig` with `AllowOnly` field + - Extended `GitHubToolConfig` with flat `Repos` and `Integrity` fields - Extended `MCPServerConfig` with `GuardPolicies` field 2. **pkg/workflow/schemas/mcp-gateway-config.schema.json** @@ -158,12 +149,10 @@ The design supports future MCP servers (Jira, WorkIQ) through: - Set `additionalProperties: true` for server-specific schemas 3. **pkg/workflow/tools_parser.go** - - Added `parseGitHubAllowOnlyPolicy()` function - - Extended `parseGitHubTool()` to extract allowonly policy + - Extended `parseGitHubTool()` to extract `repos` and `integrity` directly 4. **pkg/workflow/tools_validation.go** - - Updated `validateGitHubGuardPolicy()` function - - Added `validateGitHubAllowOnlyPolicy()` function + - Updated `validateGitHubGuardPolicy()` function (validates flat fields) - Added `validateReposScope()` function - Added `validateRepoPattern()` function - Added `isValidOwnerOrRepo()` helper function @@ -188,14 +177,14 @@ The design supports future MCP servers (Jira, WorkIQ) through: - Case-sensitive **Required Fields:** -- Both `repos` and `integrity` are required in `allowonly` policy +- Both `repos` and `integrity` are required when either is specified under `github:` ## Error Messages The implementation provides clear, actionable error messages: ``` -invalid guard policy: 'allowonly.repos' is required. +invalid guard policy: 'github.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*']) invalid guard policy: repository pattern 'Owner/Repo' must be lowercase @@ -203,7 +192,7 @@ invalid guard policy: repository pattern 'Owner/Repo' must be lowercase invalid guard policy: repository pattern 'owner/re*po' has wildcard in the middle. Wildcards only allowed at the end (e.g., 'prefix*') -invalid guard policy: 'allowonly.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. +invalid guard policy: 'github.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: 'admin' ``` @@ -216,10 +205,9 @@ tools: github: mode: remote toolsets: [default] - allowonly: - repos: - - "myorg/*" - integrity: reader + repos: + - "myorg/*" + integrity: reader ``` ### Example 2: Multiple Organizations @@ -229,12 +217,11 @@ tools: github: mode: remote toolsets: [default] - allowonly: - repos: - - "frontend-org/*" - - "backend-org/*" - - "shared/infrastructure" - integrity: writer + repos: + - "frontend-org/*" + - "backend-org/*" + - "shared/infrastructure" + integrity: writer ``` ### Example 3: Public Repositories Only @@ -244,9 +231,8 @@ tools: github: mode: remote toolsets: [repos, issues] - allowonly: - repos: "public" - integrity: none + repos: "public" + integrity: none ``` ### Example 4: Prefix Matching @@ -256,11 +242,10 @@ tools: github: mode: remote toolsets: [default] - allowonly: - repos: - - "myorg/api-*" # Matches api-gateway, api-service, etc. - - "myorg/web-*" # Matches web-frontend, web-backend, etc. - integrity: writer + repos: + - "myorg/api-*" # Matches api-gateway, api-service, etc. + - "myorg/web-*" # Matches web-frontend, web-backend, etc. + integrity: writer ``` ## Testing Strategy From 1cba6cd3c78034dbe8774ace1339d8a19dfc37de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 05:00:46 +0000 Subject: [PATCH 10/11] Rename integrity to min-integrity in guard policy frontmatter Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_github_config.go | 6 +-- pkg/workflow/tools_parser.go | 6 +-- pkg/workflow/tools_types.go | 4 +- pkg/workflow/tools_validation.go | 24 +++++------ pkg/workflow/tools_validation_test.go | 48 +++++++++++----------- scratchpad/guard-policies-specification.md | 36 ++++++++-------- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/pkg/workflow/mcp_github_config.go b/pkg/workflow/mcp_github_config.go index 057397fe562..9da6c1949d7 100644 --- a/pkg/workflow/mcp_github_config.go +++ b/pkg/workflow/mcp_github_config.go @@ -242,19 +242,19 @@ func getGitHubAllowedTools(githubTool any) []string { } // getGitHubGuardPolicies extracts guard policies from GitHub tool configuration. -// It reads the flat repos/integrity fields and wraps them for MCP gateway rendering. +// 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["integrity"] + integrity, hasIntegrity := toolConfig["min-integrity"] if hasRepos || hasIntegrity { policy := map[string]any{} if hasRepos { policy["repos"] = repos } if hasIntegrity { - policy["integrity"] = integrity + policy["min-integrity"] = integrity } return map[string]any{ "allow-only": policy, diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 37775f18acd..810b458e967 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -234,12 +234,12 @@ func parseGitHubTool(val any) *GitHubToolConfig { config.App = parseAppConfig(app) } - // Parse guard policy fields (flat syntax: repos and integrity directly under github:) + // 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["integrity"].(string); ok { - config.Integrity = GitHubIntegrityLevel(integrity) + if integrity, ok := configMap["min-integrity"].(string); ok { + config.MinIntegrity = GitHubIntegrityLevel(integrity) } return config diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 85d2d869f6e..65624a2181b 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -297,8 +297,8 @@ type GitHubToolConfig struct { // 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"` - // Integrity defines the minimum integrity level required: "none", "reader", "writer", "merged" - Integrity GitHubIntegrityLevel `yaml:"integrity,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 diff --git a/pkg/workflow/tools_validation.go b/pkg/workflow/tools_validation.go index 439dc4a035e..49ea7ad3d23 100644 --- a/pkg/workflow/tools_validation.go +++ b/pkg/workflow/tools_validation.go @@ -88,7 +88,7 @@ func validateGitHubToolConfig(tools *Tools, workflowName string) error { } // validateGitHubGuardPolicy validates the GitHub guard policy configuration. -// Guard policy fields (repos, integrity) are specified flat under github:. +// Guard policy fields (repos, min-integrity) are specified flat under github:. // Both fields must be present if either is specified. func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { if tools == nil || tools.GitHub == nil { @@ -97,14 +97,14 @@ func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { github := tools.GitHub hasRepos := github.Repos != nil - hasIntegrity := github.Integrity != "" + hasMinIntegrity := github.MinIntegrity != "" // No guard policy fields present - nothing to validate - if !hasRepos && !hasIntegrity { + if !hasRepos && !hasMinIntegrity { return nil } - // Validate repos field (required when integrity is set) + // Validate repos field (required when min-integrity is set) if !hasRepos { toolsValidationLog.Printf("Missing repos in guard policy for workflow: %s", workflowName) return errors.New("invalid guard policy: 'github.repos' is required. Use 'all', 'public', or an array of repository patterns (e.g., ['owner/repo', 'owner/*'])") @@ -115,13 +115,13 @@ func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { return err } - // Validate integrity field (required when repos is set) - if !hasIntegrity { - toolsValidationLog.Printf("Missing integrity in guard policy for workflow: %s", workflowName) - return errors.New("invalid guard policy: 'github.integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") + // Validate min-integrity field (required when repos is set) + if !hasMinIntegrity { + toolsValidationLog.Printf("Missing min-integrity in guard policy for workflow: %s", workflowName) + return errors.New("invalid guard policy: 'github.min-integrity' is required. Valid values: 'none', 'reader', 'writer', 'merged'") } - // Validate integrity value + // Validate min-integrity value validIntegrityLevels := map[GitHubIntegrityLevel]bool{ GitHubIntegrityNone: true, GitHubIntegrityReader: true, @@ -129,9 +129,9 @@ func validateGitHubGuardPolicy(tools *Tools, workflowName string) error { GitHubIntegrityMerged: true, } - if !validIntegrityLevels[github.Integrity] { - toolsValidationLog.Printf("Invalid integrity level '%s' in workflow: %s", github.Integrity, workflowName) - return errors.New("invalid guard policy: 'github.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(github.Integrity) + "'") + if !validIntegrityLevels[github.MinIntegrity] { + toolsValidationLog.Printf("Invalid min-integrity level '%s' in workflow: %s", github.MinIntegrity, workflowName) + return errors.New("invalid guard policy: 'github.min-integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: '" + string(github.MinIntegrity) + "'") } return nil diff --git a/pkg/workflow/tools_validation_test.go b/pkg/workflow/tools_validation_test.go index 4b35e70d197..ee34344ed1e 100644 --- a/pkg/workflow/tools_validation_test.go +++ b/pkg/workflow/tools_validation_test.go @@ -382,8 +382,8 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "valid guard policy with repos=all", toolsMap: map[string]any{ "github": map[string]any{ - "repos": "all", - "integrity": "reader", + "repos": "all", + "min-integrity": "reader", }, }, shouldError: false, @@ -392,8 +392,8 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "valid guard policy with repos=public", toolsMap: map[string]any{ "github": map[string]any{ - "repos": "public", - "integrity": "writer", + "repos": "public", + "min-integrity": "writer", }, }, shouldError: false, @@ -402,18 +402,18 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "valid guard policy with repos array ([]any)", toolsMap: map[string]any{ "github": map[string]any{ - "repos": []any{"owner/repo", "owner/*"}, - "integrity": "merged", + "repos": []any{"owner/repo", "owner/*"}, + "min-integrity": "merged", }, }, shouldError: false, }, { - name: "valid guard policy with integrity=none", + name: "valid guard policy with min-integrity=none", toolsMap: map[string]any{ "github": map[string]any{ - "repos": "all", - "integrity": "none", + "repos": "all", + "min-integrity": "none", }, }, shouldError: false, @@ -422,39 +422,39 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "missing repos field", toolsMap: map[string]any{ "github": map[string]any{ - "integrity": "reader", + "min-integrity": "reader", }, }, shouldError: true, errorMsg: "'github.repos' is required", }, { - name: "missing integrity field", + name: "missing min-integrity field", toolsMap: map[string]any{ "github": map[string]any{ "repos": "all", }, }, shouldError: true, - errorMsg: "'github.integrity' is required", + errorMsg: "'github.min-integrity' is required", }, { - name: "invalid integrity value", + name: "invalid min-integrity value", toolsMap: map[string]any{ "github": map[string]any{ - "repos": "all", - "integrity": "superuser", + "repos": "all", + "min-integrity": "superuser", }, }, shouldError: true, - errorMsg: "'github.integrity' must be one of", + errorMsg: "'github.min-integrity' must be one of", }, { name: "invalid repos string value", toolsMap: map[string]any{ "github": map[string]any{ - "repos": "private", - "integrity": "reader", + "repos": "private", + "min-integrity": "reader", }, }, shouldError: true, @@ -464,8 +464,8 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "empty repos array", toolsMap: map[string]any{ "github": map[string]any{ - "repos": []any{}, - "integrity": "reader", + "repos": []any{}, + "min-integrity": "reader", }, }, shouldError: true, @@ -475,8 +475,8 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "repos array with uppercase pattern", toolsMap: map[string]any{ "github": map[string]any{ - "repos": []any{"Owner/repo"}, - "integrity": "reader", + "repos": []any{"Owner/repo"}, + "min-integrity": "reader", }, }, shouldError: true, @@ -486,8 +486,8 @@ func TestValidateGitHubGuardPolicy(t *testing.T) { name: "repos array with invalid pattern format", toolsMap: map[string]any{ "github": map[string]any{ - "repos": []any{"just-a-name"}, - "integrity": "reader", + "repos": []any{"just-a-name"}, + "min-integrity": "reader", }, }, shouldError: true, diff --git a/scratchpad/guard-policies-specification.md b/scratchpad/guard-policies-specification.md index f5d6a739b3b..f8b5f5f3827 100644 --- a/scratchpad/guard-policies-specification.md +++ b/scratchpad/guard-policies-specification.md @@ -10,7 +10,7 @@ The user requested support for guard policies in the MCP gateway configuration, 1. Support GitHub-specific guard policies with flat frontmatter syntax: - `repos` (scope): Repository access patterns - - `integrity` (minintegrity): Minimum integrity level required + - `min-integrity` (minintegrity): Minimum min-min-integrity level required 2. Design an extensible system that can support future MCP servers (Jira, WorkIQ) with different policy schemas @@ -23,7 +23,7 @@ The user requested support for guard policies in the MCP gateway configuration, ``` GitHubToolConfig (GitHub-specific) ├── Repos: GitHubReposScope (string or []any) - └── Integrity: GitHubIntegrityLevel (enum) + └── MinIntegrity: GitHubIntegrityLevel (enum) MCPServerConfig (general) └── GuardPolicies: map[string]any (extensible for all servers) @@ -42,7 +42,7 @@ Based on the provided JSON schema, the implementation supports: - `"owner/prefix*"` - Repositories with name prefix under owner **Integrity Levels:** -- `"none"` - No integrity requirements +- `"none"` - No min-integrity requirements - `"reader"` - Read-level integrity - `"writer"` - Write-level integrity - `"merged"` - Merged-level integrity @@ -56,7 +56,7 @@ tools: mode: remote toolsets: [default] repos: "all" - integrity: reader + min-integrity: reader ``` **With Repository Patterns:** @@ -69,7 +69,7 @@ tools: - "myorg/*" - "partner/shared-repo" - "docs/api-*" - integrity: writer + min-integrity: writer ``` **Public Repositories Only:** @@ -77,24 +77,24 @@ tools: tools: github: repos: "public" - integrity: none + min-integrity: none ``` ### 4. MCP Gateway Configuration Flow 1. **Frontmatter Parsing** (`tools_parser.go`): - - Extracts `repos` and `integrity` directly from GitHub tool config + - Extracts `repos` and `min-integrity` directly from GitHub tool config - Stores them as fields on `GitHubToolConfig` - Validates structure and types 2. **Validation** (`tools_validation.go`): - Validates repos format (all/public or valid patterns) - - Validates integrity level (none/reader/writer/merged) + - Validates min-integrity level (none/reader/writer/merged) - Validates repository pattern syntax (lowercase, valid characters, wildcard placement) - Called during workflow compilation 3. **Compilation**: - - Guard policy fields (repos, integrity) included in compiled GitHub tool configuration + - Guard policy fields (repos, min-integrity) included in compiled GitHub tool configuration - Passed through to MCP Gateway configuration 4. **Runtime (MCP Gateway)**: @@ -140,7 +140,7 @@ The design supports future MCP servers (Jira, WorkIQ) through: 1. **pkg/workflow/tools_types.go** - Added `GitHubIntegrityLevel` enum type - Added `GitHubReposScope` type alias - - Extended `GitHubToolConfig` with flat `Repos` and `Integrity` fields + - Extended `GitHubToolConfig` with flat `Repos` and `MinIntegrity` fields - Extended `MCPServerConfig` with `GuardPolicies` field 2. **pkg/workflow/schemas/mcp-gateway-config.schema.json** @@ -149,7 +149,7 @@ The design supports future MCP servers (Jira, WorkIQ) through: - Set `additionalProperties: true` for server-specific schemas 3. **pkg/workflow/tools_parser.go** - - Extended `parseGitHubTool()` to extract `repos` and `integrity` directly + - Extended `parseGitHubTool()` to extract `repos` and `min-integrity` directly 4. **pkg/workflow/tools_validation.go** - Updated `validateGitHubGuardPolicy()` function (validates flat fields) @@ -177,7 +177,7 @@ The design supports future MCP servers (Jira, WorkIQ) through: - Case-sensitive **Required Fields:** -- Both `repos` and `integrity` are required when either is specified under `github:` +- Both `repos` and `min-integrity` are required when either is specified under `github:` ## Error Messages @@ -192,7 +192,7 @@ invalid guard policy: repository pattern 'Owner/Repo' must be lowercase invalid guard policy: repository pattern 'owner/re*po' has wildcard in the middle. Wildcards only allowed at the end (e.g., 'prefix*') -invalid guard policy: 'github.integrity' must be one of: 'none', 'reader', 'writer', 'merged'. +invalid guard policy: 'github.min-integrity' must be one of: 'none', 'reader', 'writer', 'merged'. Got: 'admin' ``` @@ -207,7 +207,7 @@ tools: toolsets: [default] repos: - "myorg/*" - integrity: reader + min-integrity: reader ``` ### Example 2: Multiple Organizations @@ -221,7 +221,7 @@ tools: - "frontend-org/*" - "backend-org/*" - "shared/infrastructure" - integrity: writer + min-integrity: writer ``` ### Example 3: Public Repositories Only @@ -232,7 +232,7 @@ tools: mode: remote toolsets: [repos, issues] repos: "public" - integrity: none + min-integrity: none ``` ### Example 4: Prefix Matching @@ -245,7 +245,7 @@ tools: repos: - "myorg/api-*" # Matches api-gateway, api-service, etc. - "myorg/web-*" # Matches web-frontend, web-backend, etc. - integrity: writer + min-integrity: writer ``` ## Testing Strategy @@ -282,7 +282,7 @@ tools: ## Benefits 1. **Security**: Restrict AI agent access to specific repositories -2. **Compliance**: Enforce minimum integrity requirements +2. **Compliance**: Enforce minimum min-integrity requirements 3. **Flexibility**: Support diverse repository patterns and wildcards 4. **Extensibility**: Easy to add policies for Jira, WorkIQ, etc. 5. **Clarity**: Clear error messages and validation From a078227b3f39b20f985e8087b4440a56e42ddc49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:33:42 +0000 Subject: [PATCH 11/11] Fix spec doc: update Testing Strategy and fix min-min-integrity typo Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- scratchpad/guard-policies-specification.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scratchpad/guard-policies-specification.md b/scratchpad/guard-policies-specification.md index f8b5f5f3827..f44e18e341c 100644 --- a/scratchpad/guard-policies-specification.md +++ b/scratchpad/guard-policies-specification.md @@ -10,7 +10,7 @@ The user requested support for guard policies in the MCP gateway configuration, 1. Support GitHub-specific guard policies with flat frontmatter syntax: - `repos` (scope): Repository access patterns - - `min-integrity` (minintegrity): Minimum min-min-integrity level required + - `min-integrity` (minintegrity): Minimum min-integrity level required 2. Design an extensible system that can support future MCP servers (Jira, WorkIQ) with different policy schemas @@ -250,11 +250,10 @@ tools: ## Testing Strategy -1. **Unit Tests** (Pending): - - Test parsing of valid configurations - - Test validation of invalid configurations - - Test error messages for various failure modes - - Test edge cases (empty strings, special characters, etc.) +1. **Unit Tests** (Complete): + - `TestValidateGitHubGuardPolicy`: 14 cases covering valid/invalid repos values, invalid min-integrity, missing fields + - `TestValidateReposScopeWithStringSlice`: 4 cases covering `[]string` and `[]any` input types + - Tests live in `pkg/workflow/tools_validation_test.go` 2. **Integration Tests** (Pending): - Test end-to-end workflow compilation with guard policies