Skip to content

Commit 12dca7d

Browse files
Copilotlpcoxgithub-actions[bot]claudeCopilot
authored
feat: add integrity-reactions feature flag for MCPG reaction-based integrity promotion/demotion (#25948)
* Initial plan * feat: add integrity-reactions feature flag for MCPG allow-only policy Add new feature flag `integrity-reactions` that, when enabled, injects `endorsement-reactions` and `disapproval-reactions` fields into the MCPG allow-only integrity policy. Requires MCPG >= v0.2.18. Changes: - Add IntegrityReactionsFeatureFlag constant - Add MCPGIntegrityReactionsMinVersion = "v0.2.18" constant - Add new fields to GitHubToolConfig struct - Parse new reaction fields in tools_parser.go - Add mcpgSupportsIntegrityReactions() version gate helper - Add injectIntegrityReactionFields() helper for both code paths - Inject reactions into gateway allow-only policy (mcp_renderer_github.go) - Inject reactions into DIFC proxy policy (compiler_difc_proxy.go) - Add validateIntegrityReactions() validation - Wire up validation in both compiler paths - Update JSON schema with new fields - Add comprehensive unit tests Agent-Logs-Url: https://github.com/github/gh-aw/sessions/24601d6f-99dd-4b19-ac56-7c90f187f8e9 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: address code review comments - clean up incomplete comment and improve function doc Agent-Logs-Url: https://github.com/github/gh-aw/sessions/24601d6f-99dd-4b19-ac56-7c90f187f8e9 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * docs(adr): add draft ADR-25948 for version-gated integrity reactions Generated by Design Decision Gate workflow. Records the decision to introduce the integrity-reactions feature flag with a semver version gate (MCPG >= v0.2.18) and a shared injection helper across all policy code paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: make reaction lists optional with defaults, proxy-only enforcement - endorsement-reactions defaults to [THUMBS_UP, HEART] when integrity-reactions feature flag is enabled - disapproval-reactions defaults to [THUMBS_DOWN, CONFUSED] - Reaction-based integrity is only enforced in proxy mode (DIFC/CLI proxy), not MCP gateway mode, because the GitHub MCP server protocol does not expose reaction author information - Compiler emits a warning when reactions are configured with the gateway path - Validation now requires min-integrity when feature flag is enabled (even without explicit reaction lists, since defaults will be injected) - Schema updated: removed minItems constraint, added default values - Updated all type docs and schema descriptions to clarify proxy-only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6c55eeb commit 12dca7d

14 files changed

+839
-7
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# ADR-25948: Version-Gated Integrity Reactions for MCPG Allow-Only Policy
2+
3+
**Date**: 2026-04-13
4+
**Status**: Draft
5+
**Deciders**: lpcox, Copilot (inferred from PR #25948)
6+
7+
---
8+
9+
## Part 1 — Narrative (Human-Friendly)
10+
11+
### Context
12+
13+
The gh-aw workflow compiler generates MCP gateway (MCPG) guard policies that control which tool calls agents are allowed to make. Until now, integrity promotion and demotion was determined solely by static fields (`min-integrity`, `repos`) in the `allow-only` policy block. A new capability in MCPG v0.2.18 allows reaction-based integrity signals: GitHub reactions (e.g., 👍, ❤️) from maintainers can dynamically promote or demote the content integrity level, enabling lightweight, in-band approval workflows without requiring separate label-based gating. Introducing this capability requires extending the compiler in a way that is both backwards-compatible with existing workflows and gated to MCPG versions that support it.
14+
15+
### Decision
16+
17+
We will introduce a `integrity-reactions` feature flag that workflow authors must explicitly opt into, combined with a semver version gate that ensures the feature is only compiled into guard policies when the configured MCPG version is `>= v0.2.18`. A shared `injectIntegrityReactionFields()` helper centralizes the injection logic and is called from both the MCP renderer (`mcp_renderer_github.go`) and the DIFC proxy policy builder (`compiler_difc_proxy.go`), ensuring consistent behavior across all policy code paths. The default MCPG version (`v0.2.17`) is deliberately below the minimum, so no existing workflow is affected without an explicit opt-in.
18+
19+
### Alternatives Considered
20+
21+
#### Alternative 1: Unconditional Rollout (No Feature Flag)
22+
23+
Add `endorsement-reactions` and `disapproval-reactions` to the allow-only policy for all workflows that already set `min-integrity`. This would require no feature flag infrastructure but would silently change the behaviour of every existing workflow using integrity gating as soon as MCPG >= v0.2.18 is deployed. Reaction fields default to empty arrays in MCPG so the net change would likely be benign, but the compiler would generate different output for unchanged workflow files, violating the principle that `make recompile` is idempotent without frontmatter changes. This alternative was rejected because it breaks the stable, reproducible lock-file guarantee.
24+
25+
#### Alternative 2: Separate Policy Type for Reaction-Based Integrity
26+
27+
Introduce a new top-level policy key (e.g., `reaction-integrity`) separate from the existing `allow-only` block, requiring workflow authors to restructure their guard policy when adding reactions. This would be a cleaner schema evolution in isolation but would break the conceptual unity of the guard policy (integrity level and reactions belong to the same policy object in MCPG) and would force unnecessary churn for adopters already using `min-integrity`. It was rejected because the MCPG data model treats reactions as additional fields within the existing `allow-only` block, so mirroring that structure in the frontmatter is more natural and less disruptive.
28+
29+
#### Alternative 3: Compiler-Inlined Version Check Instead of Helper
30+
31+
Duplicate the semver version-gate logic inline at each call site (MCP renderer and DIFC proxy builder) rather than centralizing it in `mcpgSupportsIntegrityReactions()` and `injectIntegrityReactionFields()`. This would eliminate the shared helper but scatter the version-comparison logic and the reaction-injection logic across multiple files, making it harder to update the minimum version or add new reaction fields in the future. It was rejected because the injection logic is non-trivial (four optional fields, two code paths) and centralization reduces the surface area for bugs when either code path is later changed.
32+
33+
### Consequences
34+
35+
#### Positive
36+
- Existing workflows are completely unaffected — `make recompile` produces no diff unless the `integrity-reactions` feature flag is explicitly enabled in frontmatter.
37+
- A single `injectIntegrityReactionFields()` helper ensures both the MCP renderer and DIFC proxy policy builder stay in sync when reaction fields are added or modified.
38+
- Compile-time validation (`validateIntegrityReactions()`) catches invalid reaction content enum values and missing `min-integrity` prerequisites before any workflow runs.
39+
- The semver gate pattern is consistent with the `version-gated-no-ask-user-flag` decision (ADR-25822), reinforcing a repository-wide convention for introducing MCPG-version-specific features.
40+
41+
#### Negative
42+
- Workflow authors who want reaction-based integrity must add both `features: integrity-reactions: true` and update their MCPG version to `>= v0.2.18` — a two-part opt-in that could cause confusion if only one is set (though validation errors guide the author).
43+
- The `getDIFCProxyPolicyJSON` function signature changed from `(githubTool any)` to `(githubTool any, data *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig)`, making it a slightly more complex internal API.
44+
- The `ensureDefaultMCPGatewayConfig(data)` call was moved earlier in `buildStartDIFCProxyStepYAML` to ensure the gateway config is populated before policy injection — a subtle ordering dependency that future maintainers must preserve.
45+
46+
#### Neutral
47+
- The `validReactionContents` enum set matches the GitHub GraphQL `ReactionContent` enum at the time of writing; if GitHub adds new reaction types, the validation set must be updated manually.
48+
- The "latest" version string is treated as always supporting the feature — a pragmatic choice that simplifies CI pipelines that pin to `latest`, at the cost of slightly weaker version semantics.
49+
- JSON schema (`main_workflow_schema.json`) was extended with enum constraints for the new fields, providing IDE autocompletion and static validation independent of the Go validation layer.
50+
51+
---
52+
53+
## Part 2 — Normative Specification (RFC 2119)
54+
55+
> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
56+
57+
### Feature Flag and Version Gate
58+
59+
1. Implementations **MUST NOT** inject `endorsement-reactions`, `disapproval-reactions`, `disapproval-integrity`, or `endorser-min-integrity` into any MCPG guard policy unless the `integrity-reactions` feature flag is explicitly enabled in the workflow frontmatter.
60+
2. Implementations **MUST NOT** inject reaction fields if the effective MCPG version is below `v0.2.18`, even when the feature flag is enabled.
61+
3. Implementations **MUST** treat the string `"latest"` (case-insensitive) as satisfying the minimum MCPG version requirement.
62+
4. Implementations **MUST** treat any non-semver MCPG version string (other than `"latest"`) as failing the version gate, defaulting to conservative rejection.
63+
5. Implementations **MUST** use `DefaultMCPGatewayVersion` when no MCPG version is explicitly configured, which **MUST** be a version below `MCPGIntegrityReactionsMinVersion` to preserve backwards compatibility.
64+
65+
### Reaction Field Injection
66+
67+
1. Implementations **MUST** inject reaction fields via the shared `injectIntegrityReactionFields()` helper — direct inline injection at individual call sites is **NOT RECOMMENDED**.
68+
2. `injectIntegrityReactionFields()` **MUST** be called in all policy-generation code paths, including the MCP renderer (`mcp_renderer_github.go`) and the DIFC proxy policy builder (`compiler_difc_proxy.go`).
69+
3. Implementations **MUST** inject reaction fields into the inner `allow-only` policy map, not into the outer policy wrapper object.
70+
4. Implementations **SHOULD** call `ensureDefaultMCPGatewayConfig(data)` before invoking `injectIntegrityReactionFields()` to guarantee the gateway config is non-nil.
71+
72+
### Validation
73+
74+
1. Implementations **MUST** validate that `endorsement-reactions` and `disapproval-reactions` contain only values from the GitHub `ReactionContent` enum: `THUMBS_UP`, `THUMBS_DOWN`, `HEART`, `HOORAY`, `CONFUSED`, `ROCKET`, `EYES`, `LAUGH`.
75+
2. Implementations **MUST** return a compile-time error if any reaction array field is set without the `integrity-reactions` feature flag.
76+
3. Implementations **MUST** return a compile-time error if the `integrity-reactions` feature flag is enabled but the MCPG version is below `v0.2.18`.
77+
4. Implementations **MUST** return a compile-time error if `endorsement-reactions` or `disapproval-reactions` are set without `min-integrity` being configured.
78+
5. Implementations **MUST** validate that `disapproval-integrity`, when set, is one of: `"none"`, `"unapproved"`, `"approved"`, `"merged"`.
79+
6. Implementations **MUST** validate that `endorser-min-integrity`, when set, is one of: `"unapproved"`, `"approved"`, `"merged"`.
80+
81+
### Schema
82+
83+
1. The JSON schema for workflow frontmatter **MUST** define `endorsement-reactions` and `disapproval-reactions` as arrays of strings constrained to the `ReactionContent` enum values.
84+
2. The JSON schema **MUST** define `disapproval-integrity` and `endorser-min-integrity` as strings constrained to their respective valid integrity level sets.
85+
86+
### Conformance
87+
88+
An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. In particular: injecting reaction fields without the feature flag, injecting reaction fields when the MCPG version is below `v0.2.18`, or omitting validation of reaction enum values are all non-conformant behaviors.
89+
90+
---
91+
92+
*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.*

pkg/constants/feature_constants.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,14 @@ const (
4949
// features:
5050
// copilot-integration-id: true
5151
CopilotIntegrationIDFeatureFlag FeatureFlag = "copilot-integration-id"
52+
// IntegrityReactionsFeatureFlag enables reaction-based integrity promotion/demotion
53+
// in the MCPG allow-only policy. When enabled, the compiler injects
54+
// endorsement-reactions and disapproval-reactions fields into the allow-only policy.
55+
// Requires MCPG >= v0.2.18.
56+
//
57+
// Workflow frontmatter usage:
58+
//
59+
// features:
60+
// integrity-reactions: true
61+
IntegrityReactionsFeatureFlag FeatureFlag = "integrity-reactions"
5262
)

pkg/constants/version_constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ const CopilotNoAskUserMinVersion Version = "1.0.19"
7070
// DefaultMCPGatewayVersion is the default version of the MCP Gateway (gh-aw-mcpg) Docker image
7171
const DefaultMCPGatewayVersion Version = "v0.2.17"
7272

73+
// MCPGIntegrityReactionsMinVersion is the minimum MCPG version that supports
74+
// endorsement-reactions and disapproval-reactions in the allow-only policy.
75+
const MCPGIntegrityReactionsMinVersion Version = "v0.2.18"
76+
7377
// DefaultPlaywrightMCPVersion is the default version of the @playwright/mcp package
7478
const DefaultPlaywrightMCPVersion Version = "0.0.70"
7579

pkg/parser/schemas/main_workflow_schema.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3674,6 +3674,40 @@
36743674
}
36753675
]
36763676
},
3677+
"endorsement-reactions": {
3678+
"type": "array",
3679+
"description": "Guard policy: GitHub reaction types that promote a content item's integrity to 'approved' when added by maintainers. Only enforced in proxy mode (DIFC/CLI proxy); ignored in MCP gateway mode because reaction authors cannot be identified. Optional; defaults to [\"THUMBS_UP\", \"HEART\"] when the integrity-reactions feature flag is enabled. Requires 'min-integrity' to be set and MCPG >= v0.2.18.",
3680+
"items": {
3681+
"type": "string",
3682+
"description": "GitHub ReactionContent enum value",
3683+
"enum": ["THUMBS_UP", "THUMBS_DOWN", "HEART", "HOORAY", "CONFUSED", "ROCKET", "EYES", "LAUGH"]
3684+
},
3685+
"default": ["THUMBS_UP", "HEART"],
3686+
"examples": [["THUMBS_UP", "HEART"]]
3687+
},
3688+
"disapproval-reactions": {
3689+
"type": "array",
3690+
"description": "Guard policy: GitHub reaction types that demote content integrity when added by maintainers. Only enforced in proxy mode (DIFC/CLI proxy); ignored in MCP gateway mode because reaction authors cannot be identified. Optional; defaults to [\"THUMBS_DOWN\", \"CONFUSED\"] when the integrity-reactions feature flag is enabled. Disapproval overrides endorsement (safe default). Requires 'min-integrity' to be set and MCPG >= v0.2.18.",
3691+
"items": {
3692+
"type": "string",
3693+
"description": "GitHub ReactionContent enum value",
3694+
"enum": ["THUMBS_UP", "THUMBS_DOWN", "HEART", "HOORAY", "CONFUSED", "ROCKET", "EYES", "LAUGH"]
3695+
},
3696+
"default": ["THUMBS_DOWN", "CONFUSED"],
3697+
"examples": [["THUMBS_DOWN", "CONFUSED"]]
3698+
},
3699+
"disapproval-integrity": {
3700+
"type": "string",
3701+
"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.",
3702+
"enum": ["none", "unapproved", "approved", "merged"],
3703+
"default": "none"
3704+
},
3705+
"endorser-min-integrity": {
3706+
"type": "string",
3707+
"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.",
3708+
"enum": ["unapproved", "approved", "merged"],
3709+
"default": "approved"
3710+
},
36773711
"github-app": {
36783712
"$ref": "#/$defs/github_app",
36793713
"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."

pkg/workflow/compiler_difc_proxy.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,12 @@ func hasPreAgentStepsWithGHToken(data *WorkflowData) bool {
157157
// compile time: min-integrity and repos. This is because the proxy starts before the
158158
// parse-guard-vars step that produces those dynamic outputs.
159159
//
160+
// When the integrity-reactions feature flag is enabled and the MCPG version supports it,
161+
// reaction fields (endorsement-reactions, disapproval-reactions, disapproval-integrity,
162+
// endorser-min-integrity) are also included in the proxy policy.
163+
//
160164
// Returns an empty string if no guard policy fields are found.
161-
func getDIFCProxyPolicyJSON(githubTool any) string {
165+
func getDIFCProxyPolicyJSON(githubTool any, data *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig) string {
162166
toolConfig, ok := githubTool.(map[string]any)
163167
if !ok {
164168
return ""
@@ -188,6 +192,9 @@ func getDIFCProxyPolicyJSON(githubTool any) string {
188192
policy["min-integrity"] = integrity
189193
}
190194

195+
// Inject reaction fields when the feature flag is enabled and MCPG supports it.
196+
injectIntegrityReactionFields(policy, toolConfig, data, gatewayConfig)
197+
191198
guardPolicy := map[string]any{
192199
"allow-only": policy,
193200
}
@@ -224,15 +231,16 @@ func (c *Compiler) buildStartDIFCProxyStepYAML(data *WorkflowData) string {
224231
effectiveToken := getEffectiveGitHubToken(customGitHubToken)
225232

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

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

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

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

392401
// Resolve the container image from the MCP gateway configuration
393-
ensureDefaultMCPGatewayConfig(data)
394402
containerImage := resolveProxyContainerImage(data.SandboxConfig.MCP)
395403

396404
var sb strings.Builder

pkg/workflow/compiler_difc_proxy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func TestGetDIFCProxyPolicyJSON(t *testing.T) {
262262

263263
for _, tt := range tests {
264264
t.Run(tt.name, func(t *testing.T) {
265-
got := getDIFCProxyPolicyJSON(tt.githubTool)
265+
got := getDIFCProxyPolicyJSON(tt.githubTool, nil, nil)
266266

267267
if tt.expectEmpty {
268268
assert.Empty(t, got, "policy JSON should be empty for: %s", tt.name)

pkg/workflow/compiler_orchestrator_workflow.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error)
112112
return nil, fmt.Errorf("%s: %w", cleanPath, err)
113113
}
114114

115+
// Validate integrity-reactions feature configuration
116+
var gatewayConfig *MCPGatewayRuntimeConfig
117+
if workflowData.SandboxConfig != nil {
118+
gatewayConfig = workflowData.SandboxConfig.MCP
119+
}
120+
if err := validateIntegrityReactions(workflowData.ParsedTools, workflowData.Name, workflowData, gatewayConfig); err != nil {
121+
return nil, fmt.Errorf("%s: %w", cleanPath, err)
122+
}
123+
115124
// Use shared action cache and resolver from the compiler
116125
actionCache, actionResolver := c.getSharedActionResolver()
117126
workflowData.ActionCache = actionCache

pkg/workflow/compiler_string_api.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,15 @@ func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*Wor
150150
return nil, fmt.Errorf("%s: %w", cleanPath, err)
151151
}
152152

153+
// Validate integrity-reactions feature configuration
154+
var gatewayConfig *MCPGatewayRuntimeConfig
155+
if workflowData.SandboxConfig != nil {
156+
gatewayConfig = workflowData.SandboxConfig.MCP
157+
}
158+
if err := validateIntegrityReactions(workflowData.ParsedTools, workflowData.Name, workflowData, gatewayConfig); err != nil {
159+
return nil, fmt.Errorf("%s: %w", cleanPath, err)
160+
}
161+
153162
// Setup action cache and resolver
154163
actionCache, actionResolver := c.getSharedActionResolver()
155164
workflowData.ActionCache = actionCache

0 commit comments

Comments
 (0)