Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 15 additions & 1 deletion .github/workflows/ponytail-reviewer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion .github/workflows/pr-code-quality-reviewer.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions pkg/workflow/safe_outputs_tools_generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"path/filepath"
"slices"
"strings"

"github.com/github/gh-aw/pkg/sliceutil"
"github.com/github/gh-aw/pkg/stringutil"
Expand Down Expand Up @@ -277,6 +279,11 @@ func computeRequiredFieldAdditions(safeOutputs *SafeOutputsConfig) map[string][]
if safeOutputs.AssignToAgent != nil && issueIntentRequired(safeOutputs.AssignToAgent.IssueIntent) {
additions["assign_to_agent"] = issueIntentRequiredFields
}
if safeOutputs.SubmitPullRequestReview != nil && len(safeOutputs.SubmitPullRequestReview.AllowedEvents) > 0 {
if !slices.Contains(safeOutputs.SubmitPullRequestReview.AllowedEvents, "COMMENT") {
additions["submit_pull_request_review"] = []string{"event"}
}
}
return additions
}

Expand Down Expand Up @@ -341,6 +348,22 @@ func computePropertyInjections(safeOutputs *SafeOutputsConfig) map[string]map[st
}
}

// submit_pull_request_review event: when allowed-events restricts the set of review
// decisions, narrow the tool schema's event enum to match so the agent cannot select
// an event that runtime policy will reject. This retains runtime enforcement as
// defense in depth while preventing the doomed call in the first place.
if safeOutputs.SubmitPullRequestReview != nil && len(safeOutputs.SubmitPullRequestReview.AllowedEvents) > 0 {
allowedEvents := safeOutputs.SubmitPullRequestReview.AllowedEvents
injections["submit_pull_request_review"] = map[string]any{
"event": map[string]any{
"type": "string",
"enum": allowedEvents,
Comment on lines +355 to +360
"description": "Review decision. Restricted by allowed-events configuration to: " + strings.Join(allowedEvents, ", ") + ".",
"x-synonyms": []string{"action"},
},
}
}

if safeOutputs.DataEnabled {
dataProperty := map[string]any{"$ref": "#/0/inputSchema/$defs/structured_data"}
if safeOutputs.NormalizedDataSchema != nil {
Expand Down
68 changes: 68 additions & 0 deletions pkg/workflow/safe_outputs_tools_generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,26 @@ func TestComputeRequiredFieldAdditionsDisabledByDefault(t *testing.T) {
assert.Empty(t, additions)
}

func TestComputeRequiredFieldAdditionsSubmitPRReviewEventRequiredWhenCommentDisallowed(t *testing.T) {
additions := computeRequiredFieldAdditions(&SafeOutputsConfig{
SubmitPullRequestReview: &SubmitPullRequestReviewConfig{
AllowedEvents: []string{"APPROVE"},
},
})

assert.Equal(t, []string{"event"}, additions["submit_pull_request_review"])
}

func TestComputeRequiredFieldAdditionsSubmitPRReviewEventOptionalWhenCommentAllowed(t *testing.T) {
additions := computeRequiredFieldAdditions(&SafeOutputsConfig{
SubmitPullRequestReview: &SubmitPullRequestReviewConfig{
AllowedEvents: []string{"COMMENT", "REQUEST_CHANGES"},
},
})

assert.NotContains(t, additions, "submit_pull_request_review")
}

func TestComputeRequiredFieldAdditionsIssueIntentDefaultDisabled(t *testing.T) {
additions := computeRequiredFieldAdditions(&SafeOutputsConfig{
CloseIssues: &CloseIssuesConfig{},
Expand Down Expand Up @@ -588,6 +608,54 @@ func TestComputePropertyInjectionsNilCloseIssues(t *testing.T) {
assert.Empty(t, injections)
}

// TestComputePropertyInjectionsNilSubmitPRReview verifies that nil submit-pull-request-review
// does not add submit_pull_request_review property injections.
func TestComputePropertyInjectionsNilSubmitPRReview(t *testing.T) {
injections := computePropertyInjections(&SafeOutputsConfig{
SubmitPullRequestReview: nil,
})
assert.NotContains(t, injections, "submit_pull_request_review")
}

// TestComputePropertyInjectionsAllowedEventsSubmitPRReview verifies that a configured
// allowed-events list narrows the submit_pull_request_review event enum in the tool schema.
func TestComputePropertyInjectionsAllowedEventsSubmitPRReview(t *testing.T) {
injections := computePropertyInjections(&SafeOutputsConfig{
SubmitPullRequestReview: &SubmitPullRequestReviewConfig{
AllowedEvents: []string{"COMMENT"},
},
})

require.Contains(t, injections, "submit_pull_request_review")
prop, ok := injections["submit_pull_request_review"]["event"].(map[string]any)
require.True(t, ok, "event should be a property map")
assert.Equal(t, []string{"COMMENT"}, prop["enum"])
}

// TestComputePropertyInjectionsAllowedEventsMultipleSubmitPRReview verifies multiple allowed
// events are all present in the narrowed enum.
func TestComputePropertyInjectionsAllowedEventsMultipleSubmitPRReview(t *testing.T) {
injections := computePropertyInjections(&SafeOutputsConfig{
SubmitPullRequestReview: &SubmitPullRequestReviewConfig{
AllowedEvents: []string{"COMMENT", "REQUEST_CHANGES"},
},
})

prop, ok := injections["submit_pull_request_review"]["event"].(map[string]any)
require.True(t, ok, "event should be a property map")
assert.Equal(t, []string{"COMMENT", "REQUEST_CHANGES"}, prop["enum"])
}

// TestComputePropertyInjectionsNoAllowedEventsSubmitPRReview verifies that no injection
// happens when allowed-events is not configured, so the static schema's full enum applies.
func TestComputePropertyInjectionsNoAllowedEventsSubmitPRReview(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The close_issue nil-config test (TestComputePropertyInjectionsNilCloseIssues) covers nil config explicitly — adding the same for submit_pull_request_review would mirror the pattern and document the nil contract.

💡 Suggested addition
func TestComputePropertyInjectionsNilSubmitPRReview(t *testing.T) {
    injections := computePropertyInjections(&SafeOutputsConfig{
        SubmitPullRequestReview: nil,
    })
    assert.NotContains(t, injections, "submit_pull_request_review")
}

Low-risk omission — the != nil guard already handles this — but the symmetry with close_issue tests makes the contract explicit.

@copilot please address this.

injections := computePropertyInjections(&SafeOutputsConfig{
SubmitPullRequestReview: &SubmitPullRequestReviewConfig{},
})

assert.NotContains(t, injections, "submit_pull_request_review", "no allowed-events should not inject an event enum")
}

// TestPreprocessStateReasonListSlice verifies that a []any slice is converted to allowed-state-reason.
func TestPreprocessStateReasonListSlice(t *testing.T) {
configData := map[string]any{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,10 +511,10 @@ jobs:
gh aw version
env:
GH_TOKEN: ${{ github.token }}
- name: Copy gh-aw binary for MCP server
- name: Copy gh-aw binary for MCP Server
run: |
gh aw --version
# Copy the gh-aw binary to ${RUNNER_TEMP}/gh-aw for MCP server containerization
# Copy the gh-aw binary to ${RUNNER_TEMP}/gh-aw for MCP Server containerization
mkdir -p "${RUNNER_TEMP}/gh-aw"
GH_AW_BIN=""
GH_AW_BIN=$(command -v gh-aw 2>/dev/null) || true
Expand All @@ -532,7 +532,7 @@ jobs:
chmod +x "${RUNNER_TEMP}/gh-aw/gh-aw"
echo "Copied gh-aw binary to ${RUNNER_TEMP}/gh-aw/gh-aw"
else
echo "::error::Failed to find gh-aw binary for MCP server"
echo "::error::Failed to find gh-aw binary for MCP Server"
exit 1
fi
- name: Start MCP Gateway
Expand Down