Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkg/constants/feature_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@ const (
// features:
// copilot-integration-id: true
CopilotIntegrationIDFeatureFlag FeatureFlag = "copilot-integration-id"
// IntegrityReactionsFeatureFlag enables reaction-based integrity promotion/demotion
// in the MCPG allow-only policy. When enabled, the compiler injects
// endorsement-reactions and disapproval-reactions fields into the allow-only policy.
// Requires MCPG >= v0.2.18.
//
// Workflow frontmatter usage:
//
// features:
// integrity-reactions: true
IntegrityReactionsFeatureFlag FeatureFlag = "integrity-reactions"
)
4 changes: 4 additions & 0 deletions pkg/constants/version_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ const CopilotNoAskUserMinVersion Version = "1.0.19"
// DefaultMCPGatewayVersion is the default version of the MCP Gateway (gh-aw-mcpg) Docker image
const DefaultMCPGatewayVersion Version = "v0.2.17"

// MCPGIntegrityReactionsMinVersion is the minimum MCPG version that supports
// endorsement-reactions and disapproval-reactions in the allow-only policy.
const MCPGIntegrityReactionsMinVersion Version = "v0.2.18"

// DefaultPlaywrightMCPVersion is the default version of the @playwright/mcp package
const DefaultPlaywrightMCPVersion Version = "0.0.70"

Expand Down
34 changes: 34 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3674,6 +3674,40 @@
}
]
},
"endorsement-reactions": {
"type": "array",
"description": "Guard policy: GitHub reaction types that promote a content item's integrity to 'approved' when added by maintainers. Requires the 'integrity-reactions' feature flag, 'min-integrity' to be set, and MCPG >= v0.2.18.",
"items": {
"type": "string",
"description": "GitHub ReactionContent enum value",
"enum": ["THUMBS_UP", "THUMBS_DOWN", "HEART", "HOORAY", "CONFUSED", "ROCKET", "EYES", "LAUGH"]
},
"minItems": 1,
"examples": [["THUMBS_UP", "HEART"]]
},
"disapproval-reactions": {
"type": "array",
"description": "Guard policy: GitHub reaction types that demote content integrity when added by maintainers. Disapproval overrides endorsement (safe default). Requires the 'integrity-reactions' feature flag, 'min-integrity' to be set, and MCPG >= v0.2.18.",
"items": {
"type": "string",
"description": "GitHub ReactionContent enum value",
"enum": ["THUMBS_UP", "THUMBS_DOWN", "HEART", "HOORAY", "CONFUSED", "ROCKET", "EYES", "LAUGH"]
},
"minItems": 1,
"examples": [["THUMBS_DOWN", "CONFUSED"]]
},
"disapproval-integrity": {
"type": "string",
"description": "Guard policy: integrity level assigned when a disapproval reaction is present. Optional, defaults to 'none'. Requires the 'integrity-reactions' feature flag and MCPG >= v0.2.18.",
"enum": ["none", "unapproved", "approved", "merged"],
"default": "none"
},
"endorser-min-integrity": {
"type": "string",
"description": "Guard policy: minimum integrity level required for an endorser (reactor) to promote content. Optional, defaults to 'approved'. Requires the 'integrity-reactions' feature flag and MCPG >= v0.2.18.",
"enum": ["unapproved", "approved", "merged"],
"default": "approved"
},
"github-app": {
"$ref": "#/$defs/github_app",
"description": "GitHub App configuration for token minting. When configured, a GitHub App installation access token is minted at workflow start and used instead of the default token. This token overrides any custom github-token setting and provides fine-grained permissions matching the agent job requirements."
Expand Down
20 changes: 14 additions & 6 deletions pkg/workflow/compiler_difc_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,12 @@ func hasPreAgentStepsWithGHToken(data *WorkflowData) bool {
// compile time: min-integrity and repos. This is because the proxy starts before the
// parse-guard-vars step that produces those dynamic outputs.
//
// When the integrity-reactions feature flag is enabled and the MCPG version supports it,
// reaction fields (endorsement-reactions, disapproval-reactions, disapproval-integrity,
// endorser-min-integrity) are also included in the proxy policy.
//
// Returns an empty string if no guard policy fields are found.
func getDIFCProxyPolicyJSON(githubTool any) string {
func getDIFCProxyPolicyJSON(githubTool any, data *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig) string {
toolConfig, ok := githubTool.(map[string]any)
if !ok {
return ""
Expand Down Expand Up @@ -188,6 +192,9 @@ func getDIFCProxyPolicyJSON(githubTool any) string {
policy["min-integrity"] = integrity
}

// Inject reaction fields when the feature flag is enabled and MCPG supports it.
injectIntegrityReactionFields(policy, toolConfig, data, gatewayConfig)

guardPolicy := map[string]any{
"allow-only": policy,
}
Expand Down Expand Up @@ -224,15 +231,16 @@ func (c *Compiler) buildStartDIFCProxyStepYAML(data *WorkflowData) string {
effectiveToken := getEffectiveGitHubToken(customGitHubToken)

// Build the simplified guard policy JSON (static fields only)
policyJSON := getDIFCProxyPolicyJSON(githubTool)
// (plus reaction fields when integrity-reactions feature flag is enabled)
ensureDefaultMCPGatewayConfig(data)
policyJSON := getDIFCProxyPolicyJSON(githubTool, data, data.SandboxConfig.MCP)
if policyJSON == "" {
difcProxyLog.Print("Could not build DIFC proxy policy JSON, skipping proxy start")
return ""
}

// Resolve the container image from the MCP gateway configuration
// (proxy uses the same image as the gateway, just in "proxy" mode)
ensureDefaultMCPGatewayConfig(data)
containerImage := resolveProxyContainerImage(data.SandboxConfig.MCP)

var sb strings.Builder
Expand Down Expand Up @@ -379,18 +387,18 @@ func (c *Compiler) buildStartCliProxyStepYAML(data *WorkflowData) string {
customGitHubToken := getGitHubToken(githubTool)
effectiveToken := getEffectiveGitHubToken(customGitHubToken)

// Build the guard policy JSON (static fields only).
// Build the guard policy JSON (static fields only, plus reaction fields when enabled).
// The CLI proxy requires a policy to forward requests — without one, all API
// calls return HTTP 503 ("proxy enforcement not configured"). Use the default
// permissive policy when no guard policy is configured in the frontmatter.
policyJSON := getDIFCProxyPolicyJSON(githubTool)
ensureDefaultMCPGatewayConfig(data)
policyJSON := getDIFCProxyPolicyJSON(githubTool, data, data.SandboxConfig.MCP)
if policyJSON == "" {
policyJSON = defaultCliProxyPolicyJSON
difcProxyLog.Print("No guard policy configured, using default CLI proxy policy")
}

// Resolve the container image from the MCP gateway configuration
ensureDefaultMCPGatewayConfig(data)
containerImage := resolveProxyContainerImage(data.SandboxConfig.MCP)

var sb strings.Builder
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_difc_proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func TestGetDIFCProxyPolicyJSON(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getDIFCProxyPolicyJSON(tt.githubTool)
got := getDIFCProxyPolicyJSON(tt.githubTool, nil, nil)

if tt.expectEmpty {
assert.Empty(t, got, "policy JSON should be empty for: %s", tt.name)
Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error)
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}

// Validate integrity-reactions feature configuration
var gatewayConfig *MCPGatewayRuntimeConfig
if workflowData.SandboxConfig != nil {
gatewayConfig = workflowData.SandboxConfig.MCP
}
if err := validateIntegrityReactions(workflowData.ParsedTools, workflowData.Name, workflowData, gatewayConfig); err != nil {
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}
Comment on lines +115 to +122
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

validateIntegrityReactions runs before features from imports are merged (MergeFeatures is later). This can reject workflows that enable integrity-reactions through an imported workflow because isFeatureEnabled() won’t observe the merged feature flags yet. Run this validation after feature merging (or validate against the merged feature map).

Copilot uses AI. Check for mistakes.

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

// Validate integrity-reactions feature configuration
var gatewayConfig *MCPGatewayRuntimeConfig
if workflowData.SandboxConfig != nil {
gatewayConfig = workflowData.SandboxConfig.MCP
}
if err := validateIntegrityReactions(workflowData.ParsedTools, workflowData.Name, workflowData, gatewayConfig); err != nil {
return nil, fmt.Errorf("%s: %w", cleanPath, err)
}
Comment on lines +153 to +160
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

validateIntegrityReactions is executed before imported features are merged (MergeFeatures happens later in this function). If a workflow enables integrity-reactions via an import, this validation will incorrectly fail because isFeatureEnabled() won’t see the merged flag yet. Move this validation to after the features merge (or validate against the merged feature set).

Copilot uses AI. Check for mistakes.

// Setup action cache and resolver
actionCache, actionResolver := c.getSharedActionResolver()
workflowData.ActionCache = actionCache
Expand Down
58 changes: 58 additions & 0 deletions pkg/workflow/mcp_github_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import (

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/semverutil"
)

var githubConfigLog = logger.New("workflow:mcp_github_config")
Expand Down Expand Up @@ -287,6 +288,63 @@ func getGitHubGuardPolicies(githubTool any) map[string]any {
return nil
}

// injectIntegrityReactionFields adds endorsement-reactions, disapproval-reactions,
// disapproval-integrity, and endorser-min-integrity into an existing allow-only policy
// map when the integrity-reactions feature flag is enabled and the MCPG version supports it.
// - policy is the inner allow-only map (not the outer allow-only wrapper).
// - toolConfig is the raw github tool configuration map.
// - data contains workflow data including feature flags used to check if integrity-reactions is enabled.
// - gatewayConfig contains MCP gateway version configuration used to version-gate the injection.
//
// No-op when the feature flag is disabled or the MCPG version is too old.
func injectIntegrityReactionFields(policy map[string]any, toolConfig map[string]any, data *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig) {
if !isFeatureEnabled(constants.IntegrityReactionsFeatureFlag, data) {
return
}
if !mcpgSupportsIntegrityReactions(gatewayConfig) {
return
}
if endorsement, ok := toolConfig["endorsement-reactions"]; ok {
policy["endorsement-reactions"] = endorsement
}
if disapproval, ok := toolConfig["disapproval-reactions"]; ok {
policy["disapproval-reactions"] = disapproval
}
if disapprovalIntegrity, ok := toolConfig["disapproval-integrity"]; ok {
policy["disapproval-integrity"] = disapprovalIntegrity
}
if endorserMinIntegrity, ok := toolConfig["endorser-min-integrity"]; ok {
policy["endorser-min-integrity"] = endorserMinIntegrity
}
}

// mcpgSupportsIntegrityReactions returns true when the effective MCPG version supports
// endorsement-reactions and disapproval-reactions in the allow-only policy (>= v0.2.18).
//
// Special cases:
// - gatewayConfig is nil or has no Version: use DefaultMCPGatewayVersion for comparison.
// - "latest": always returns true (latest is always a new release).
// - Any semver string >= MCPGIntegrityReactionsMinVersion: returns true.
// - Any semver string < MCPGIntegrityReactionsMinVersion: returns false.
// - Non-semver string (e.g. a branch name): returns false (conservative).
func mcpgSupportsIntegrityReactions(gatewayConfig *MCPGatewayRuntimeConfig) bool {
var version string
if gatewayConfig != nil && gatewayConfig.Version != "" {
version = gatewayConfig.Version
} else {
// No override → use the default version for comparison.
version = string(constants.DefaultMCPGatewayVersion)
}

// "latest" means the newest release — always supports the field.
if strings.EqualFold(version, "latest") {
return true
}

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

mcpgSupportsIntegrityReactions() claims non-semver versions should return false, but it unconditionally calls semverutil.Compare(version, minVersion). semverutil.Compare delegates to x/mod/semver.Compare without validating inputs, so non-semver strings (e.g. branch names) may compare unpredictably and could incorrectly pass the gate. Add an explicit semverutil.IsValid(version) check (after handling "latest") and return false when invalid.

Suggested change
if !semverutil.IsValid(version) {
return false
}

Copilot uses AI. Check for mistakes.
minVersion := string(constants.MCPGIntegrityReactionsMinVersion)
return semverutil.Compare(version, minVersion) >= 0
}

// deriveSafeOutputsGuardPolicyFromGitHub generates a safeoutputs guard-policy from GitHub guard-policy.
// When the GitHub MCP server has a guard-policy with repos, the safeoutputs MCP must also have
// a linked guard-policy with accept field derived from repos according to these rules:
Expand Down
13 changes: 13 additions & 0 deletions pkg/workflow/mcp_renderer_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ func (r *MCPConfigRendererUnified) RenderGitHubMCP(yaml *strings.Builder, github
// guard policy is configured and no GitHub App token is in use.
// The determine-automatic-lockdown step outputs min_integrity and repos for public repos.
explicitGuardPolicies := getGitHubGuardPolicies(githubTool)
// Inject integrity reaction fields into the allow-only policy when the feature flag is
// enabled and the MCPG version supports it.
if len(explicitGuardPolicies) > 0 {
if toolConfig, ok := githubTool.(map[string]any); ok {
if allowOnly, ok := explicitGuardPolicies["allow-only"].(map[string]any); ok {
var gatewayConfig *MCPGatewayRuntimeConfig
if workflowData != nil && workflowData.SandboxConfig != nil {
gatewayConfig = workflowData.SandboxConfig.MCP
}
injectIntegrityReactionFields(allowOnly, toolConfig, workflowData, gatewayConfig)
}
}
}
shouldUseStepOutputForGuardPolicy := len(explicitGuardPolicies) == 0 && !hasGitHubApp(githubTool)

toolsets := getGitHubToolsets(githubTool)
Expand Down
28 changes: 28 additions & 0 deletions pkg/workflow/tools_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,34 @@ func parseGitHubTool(val any) *GitHubToolConfig {
}
}

// Parse reaction-based integrity fields (requires integrity-reactions feature flag + MCPG >= v0.2.18)
if endorsementReactions, ok := configMap["endorsement-reactions"].([]any); ok {
config.EndorsementReactions = make([]string, 0, len(endorsementReactions))
for _, item := range endorsementReactions {
if str, ok := item.(string); ok {
config.EndorsementReactions = append(config.EndorsementReactions, str)
}
}
} else if endorsementReactions, ok := configMap["endorsement-reactions"].([]string); ok {
config.EndorsementReactions = endorsementReactions
}
if disapprovalReactions, ok := configMap["disapproval-reactions"].([]any); ok {
config.DisapprovalReactions = make([]string, 0, len(disapprovalReactions))
for _, item := range disapprovalReactions {
if str, ok := item.(string); ok {
config.DisapprovalReactions = append(config.DisapprovalReactions, str)
}
}
} else if disapprovalReactions, ok := configMap["disapproval-reactions"].([]string); ok {
config.DisapprovalReactions = disapprovalReactions
}
if disapprovalIntegrity, ok := configMap["disapproval-integrity"].(string); ok {
config.DisapprovalIntegrity = disapprovalIntegrity
}
if endorserMinIntegrity, ok := configMap["endorser-min-integrity"].(string); ok {
config.EndorserMinIntegrity = endorserMinIntegrity
}

return config
}

Expand Down
19 changes: 19 additions & 0 deletions pkg/workflow/tools_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,25 @@ type GitHubToolConfig struct {
// resolves at runtime to a comma- or newline-separated list of approval label names.
// Set when the approval-labels field is a string expression rather than a literal array.
ApprovalLabelsExpr string `yaml:"-"`
// EndorsementReactions is an optional list of GitHub reaction types that promote content
// integrity to "approved" when added by maintainers. Requires integrity-reactions feature flag
// and MCPG >= v0.2.18.
// Valid values: THUMBS_UP, THUMBS_DOWN, HEART, HOORAY, CONFUSED, ROCKET, EYES, LAUGH
EndorsementReactions []string `yaml:"endorsement-reactions,omitempty"`
// DisapprovalReactions is an optional list of GitHub reaction types that demote content
// integrity when added by maintainers. Requires integrity-reactions feature flag
// and MCPG >= v0.2.18.
// Valid values: THUMBS_UP, THUMBS_DOWN, HEART, HOORAY, CONFUSED, ROCKET, EYES, LAUGH
DisapprovalReactions []string `yaml:"disapproval-reactions,omitempty"`
// DisapprovalIntegrity is the integrity level assigned when a disapproval reaction is present.
// Optional, defaults to "none". Requires integrity-reactions feature flag and MCPG >= v0.2.18.
// Valid values: "none", "unapproved", "approved", "merged"
DisapprovalIntegrity string `yaml:"disapproval-integrity,omitempty"`
// EndorserMinIntegrity is the minimum integrity level required for an endorser (reactor) to
// promote content. Optional, defaults to "approved". Requires integrity-reactions feature flag
// and MCPG >= v0.2.18.
// Valid values: "approved", "unapproved", "merged"
EndorserMinIntegrity string `yaml:"endorser-min-integrity,omitempty"`
}

// PlaywrightToolConfig represents the configuration for the Playwright tool
Expand Down
Loading
Loading