Skip to content

Commit 2612cc6

Browse files
authored
[Repo Assist] fix(config): add promotion-label and demotion-label fields to AllowOnlyPolicy (#4928)
🤖 *This PR was created by Repo Assist, an automated AI assistant.* ## Summary Fixes a compliance regression introduced in commit `662a784` where `promotion_label` and `demotion_label` were added to the Rust WASM guard's `AllowOnlyPolicy` but the Go config layer was never updated. **Without this fix**, any operator using: ```json {"allow-only": {"repos": "public", "min-integrity": "unapproved", "promotion-label": "agent-approved", "demotion-label": "agent-blocked"}} ``` ...via `--guard-policy-json`, `MCP_GATEWAY_GUARD_POLICY_JSON`, or a config file would receive: ``` allow-only contains unsupported field "promotion-label" ``` and the gateway would refuse to start. The feature added in `662a784` was entirely inaccessible. ## Changes - **`internal/config/guard_policy.go`**: Add `PromotionLabel`/`DemotionLabel` to `AllowOnlyPolicy` struct, `UnmarshalJSON` switch cases, `MarshalJSON` serialized struct, and `NormalizedGuardPolicy` - **`internal/config/guard_policy_validation.go`**: Pass through `PromotionLabel`/`DemotionLabel` in `NormalizeGuardPolicy` - **`internal/config/guard_policy_test.go`**: Add 6 test cases covering round-trip parsing, both fields combined, empty-field behaviour, and `ParseGuardPolicyJSON` acceptance ## Test Status The Go module proxy (`proxy.golang.org`) is blocked by the network firewall in this environment, preventing `go test` execution. The changes are syntactically verified with `gofmt -l` (clean) and follow the established pattern used by all other `AllowOnlyPolicy` fields (`DisapprovalIntegrity`, `EndorserMinIntegrity`, etc.). Closes #4920 > [!WARNING] > **⚠️ Firewall blocked 1 domain** > > The following domain was blocked by the firewall during workflow execution: > > - `proxy.golang.org` > > To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "proxy.golang.org" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > Generated by [Repo Assist](https://github.com/github/gh-aw-mcpg/actions/runs/25214503729/agentic_workflow) · ● 4.6M · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw-mcpg+%22gh-aw-workflow-id%3A+repo-assist%22&type=pullrequests) > > To install this [agentic workflow](https://github.com/githubnext/agentics/blob/851905c06e905bf362a9f6cc54f912e3df747d55/workflows/repo-assist.md), run > ``` > gh aw add githubnext/agentics@851905c > ``` <!-- gh-aw-agentic-workflow: Repo Assist, engine: copilot, version: 1.0.21, model: auto, id: 25214503729, workflow_id: repo-assist, run: https://github.com/github/gh-aw-mcpg/actions/runs/25214503729 --> <!-- gh-aw-workflow-id: repo-assist -->
2 parents 662a784 + 553b923 commit 2612cc6

3 files changed

Lines changed: 98 additions & 0 deletions

File tree

internal/config/guard_policy.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ type AllowOnlyPolicy struct {
4747
DisapprovalReactions []string `toml:"DisapprovalReactions" json:"disapproval-reactions,omitempty"`
4848
DisapprovalIntegrity string `toml:"DisapprovalIntegrity" json:"disapproval-integrity,omitempty"`
4949
EndorserMinIntegrity string `toml:"EndorserMinIntegrity" json:"endorser-min-integrity,omitempty"`
50+
PromotionLabel string `toml:"PromotionLabel" json:"promotion-label,omitempty"`
51+
DemotionLabel string `toml:"DemotionLabel" json:"demotion-label,omitempty"`
5052
}
5153

5254
// NormalizedGuardPolicy is a canonical policy representation for caching and observability.
@@ -61,6 +63,8 @@ type NormalizedGuardPolicy struct {
6163
DisapprovalReactions []string `json:"disapproval-reactions,omitempty"`
6264
DisapprovalIntegrity string `json:"disapproval-integrity,omitempty"`
6365
EndorserMinIntegrity string `json:"endorser-min-integrity,omitempty"`
66+
PromotionLabel string `json:"promotion-label,omitempty"`
67+
DemotionLabel string `json:"demotion-label,omitempty"`
6468
}
6569

6670
func (p *GuardPolicy) UnmarshalJSON(data []byte) error {
@@ -168,6 +172,14 @@ func (p *AllowOnlyPolicy) UnmarshalJSON(data []byte) error {
168172
if err := json.Unmarshal(value, &p.EndorserMinIntegrity); err != nil {
169173
return fmt.Errorf("invalid allow-only.endorser-min-integrity: %w", err)
170174
}
175+
case "promotion-label":
176+
if err := json.Unmarshal(value, &p.PromotionLabel); err != nil {
177+
return fmt.Errorf("invalid allow-only.promotion-label: %w", err)
178+
}
179+
case "demotion-label":
180+
if err := json.Unmarshal(value, &p.DemotionLabel); err != nil {
181+
return fmt.Errorf("invalid allow-only.demotion-label: %w", err)
182+
}
171183
default:
172184
return fmt.Errorf("allow-only contains unsupported field %q", key)
173185
}
@@ -195,6 +207,8 @@ func (p AllowOnlyPolicy) MarshalJSON() ([]byte, error) {
195207
DisapprovalReactions []string `json:"disapproval-reactions,omitempty"`
196208
DisapprovalIntegrity string `json:"disapproval-integrity,omitempty"`
197209
EndorserMinIntegrity string `json:"endorser-min-integrity,omitempty"`
210+
PromotionLabel string `json:"promotion-label,omitempty"`
211+
DemotionLabel string `json:"demotion-label,omitempty"`
198212
}
199213

200214
return json.Marshal(serializedAllowOnly(p))

internal/config/guard_policy_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,3 +1140,79 @@ func TestNormalizeGuardPolicyReactionEndorsement(t *testing.T) {
11401140
assert.Equal(t, original.EndorserMinIntegrity, got.EndorserMinIntegrity)
11411141
})
11421142
}
1143+
1144+
// TestNormalizeGuardPolicyPromotionDemotionLabels tests NormalizeGuardPolicy with promotion-label and demotion-label.
1145+
func TestNormalizeGuardPolicyPromotionDemotionLabels(t *testing.T) {
1146+
t.Run("promotion-label round-trips through NormalizeGuardPolicy", func(t *testing.T) {
1147+
policy := &GuardPolicy{AllowOnly: &AllowOnlyPolicy{
1148+
Repos: "public",
1149+
MinIntegrity: "unapproved",
1150+
PromotionLabel: "agent-approved",
1151+
}}
1152+
got, err := NormalizeGuardPolicy(policy)
1153+
require.NoError(t, err)
1154+
assert.Equal(t, "agent-approved", got.PromotionLabel)
1155+
assert.Empty(t, got.DemotionLabel)
1156+
})
1157+
1158+
t.Run("demotion-label round-trips through NormalizeGuardPolicy", func(t *testing.T) {
1159+
policy := &GuardPolicy{AllowOnly: &AllowOnlyPolicy{
1160+
Repos: "public",
1161+
MinIntegrity: "unapproved",
1162+
DemotionLabel: "agent-blocked",
1163+
}}
1164+
got, err := NormalizeGuardPolicy(policy)
1165+
require.NoError(t, err)
1166+
assert.Equal(t, "agent-blocked", got.DemotionLabel)
1167+
assert.Empty(t, got.PromotionLabel)
1168+
})
1169+
1170+
t.Run("both labels set together", func(t *testing.T) {
1171+
policy := &GuardPolicy{AllowOnly: &AllowOnlyPolicy{
1172+
Repos: "public",
1173+
MinIntegrity: "unapproved",
1174+
PromotionLabel: "agent-approved",
1175+
DemotionLabel: "agent-blocked",
1176+
}}
1177+
got, err := NormalizeGuardPolicy(policy)
1178+
require.NoError(t, err)
1179+
assert.Equal(t, "agent-approved", got.PromotionLabel)
1180+
assert.Equal(t, "agent-blocked", got.DemotionLabel)
1181+
})
1182+
1183+
t.Run("labels absent → normalized fields empty", func(t *testing.T) {
1184+
policy := &GuardPolicy{AllowOnly: &AllowOnlyPolicy{
1185+
Repos: "public",
1186+
MinIntegrity: "none",
1187+
}}
1188+
got, err := NormalizeGuardPolicy(policy)
1189+
require.NoError(t, err)
1190+
assert.Empty(t, got.PromotionLabel)
1191+
assert.Empty(t, got.DemotionLabel)
1192+
})
1193+
1194+
t.Run("JSON round-trip with promotion-label and demotion-label", func(t *testing.T) {
1195+
original := AllowOnlyPolicy{
1196+
Repos: "public",
1197+
MinIntegrity: "unapproved",
1198+
PromotionLabel: "agent-approved",
1199+
DemotionLabel: "agent-blocked",
1200+
}
1201+
data, err := json.Marshal(original)
1202+
require.NoError(t, err)
1203+
1204+
var got AllowOnlyPolicy
1205+
require.NoError(t, json.Unmarshal(data, &got))
1206+
assert.Equal(t, original.PromotionLabel, got.PromotionLabel)
1207+
assert.Equal(t, original.DemotionLabel, got.DemotionLabel)
1208+
})
1209+
1210+
t.Run("ParseGuardPolicyJSON accepts promotion-label and demotion-label", func(t *testing.T) {
1211+
jsonStr := `{"allow-only":{"repos":"public","min-integrity":"unapproved","promotion-label":"agent-approved","demotion-label":"agent-blocked"}}`
1212+
got, err := ParseGuardPolicyJSON(jsonStr)
1213+
require.NoError(t, err)
1214+
require.NotNil(t, got.AllowOnly)
1215+
assert.Equal(t, "agent-approved", got.AllowOnly.PromotionLabel)
1216+
assert.Equal(t, "agent-blocked", got.AllowOnly.DemotionLabel)
1217+
})
1218+
}

internal/config/guard_policy_validation.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ func NormalizeGuardPolicy(policy *GuardPolicy) (*NormalizedGuardPolicy, error) {
206206
normalized.EndorserMinIntegrity = v
207207
}
208208

209+
// Pass through promotion-label and demotion-label as-is (validated by Rust guard).
210+
if v := strings.TrimSpace(policy.AllowOnly.PromotionLabel); v != "" {
211+
normalized.PromotionLabel = v
212+
}
213+
if v := strings.TrimSpace(policy.AllowOnly.DemotionLabel); v != "" {
214+
normalized.DemotionLabel = v
215+
}
216+
209217
switch scope := policy.AllowOnly.Repos.(type) {
210218
case string:
211219
scopeValue := strings.ToLower(strings.TrimSpace(scope))

0 commit comments

Comments
 (0)