fix: align MCP gateway schema and reference examples by pelikhan · Pull Request #70865 · github/gh-aw · GitHub
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
47 changes: 37 additions & 10 deletions docs/src/content/docs/reference/mcp-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ payload_size_threshold = 262144 # 256KB - more aggressive disk storage

The optional `trustedBots` field in the gateway configuration passes an additional list of GitHub bot identity strings to the gateway. The gateway merges this list with its own built-in trusted identity list to form the effective set of identities it recognizes.

Workflow frontmatter accepts only the kebab-case `sandbox.mcp.trusted-bots` key. The compiler translates it into camelCase `gateway.trustedBots` in the generated gateway JSON; `trustedBots` is not a frontmatter alias.

> **Important**: `trustedBots` is **additive**. The gateway maintains its own internal list of trusted bot identities. The `trustedBots` field extends that internal list with additional identities; it cannot remove or override the gateway's built-in trusted identities.

**Configuration Example**:
Expand All @@ -407,17 +409,30 @@ The optional `trustedBots` field in the gateway configuration passes an addition

**Frontmatter Example** (workflow author):

```yaml
```aw wrap title=".github/workflows/trusted-bots.md"
---
on: workflow_dispatch
permissions:
contents: read
issues: read
engine: copilot
tools:
github:
toolsets: [issues]
sandbox:
mcp:
trusted-bots:
- github-actions[bot]
- copilot-swe-agent[bot]
---

Summarize the open issues in this repository.
```

**Requirements**:
- `trustedBots` MUST be a non-empty array of strings when present
- Each entry MUST be a non-empty string (typically a GitHub username such as `github-actions[bot]`)
- When configured through workflow frontmatter, entries MUST contain only ASCII letters, digits, hyphens, or underscores, optionally followed by `[bot]`. The compiler also validates these characters before rendering the gateway configuration to prevent shell expansion.
- `trustedBots` entries are **merged** with the gateway's internal built-in trusted identities to form the effective list passed to the gateway
- `trustedBots` MUST NOT be used to remove or override entries in the gateway's internal built-in trusted identity list

Expand Down Expand Up @@ -1309,15 +1324,27 @@ In this case, the audience defaults to `"https://my-server.example.com/mcp"`.

**Frontmatter Example** (workflow author):

```yaml
tools:
mcp-servers:
my-mcp-server:
type: http
url: "https://my-server.example.com/mcp"
auth:
type: github-oidc
audience: "https://my-server.example.com"
```aw wrap title=".github/workflows/oidc-mcp.md"
---
on: workflow_dispatch
permissions:
contents: read
id-token: write
engine: copilot
network:
allowed:
- defaults
- my-server.example.com
mcp-servers:
my-mcp-server:
type: http
url: "https://my-server.example.com/mcp"
auth:
type: github-oidc
audience: "https://my-server.example.com"
---

Use my-mcp-server to summarize the available project information.
```

#### 7.6.2 Interaction with Static Headers
Expand Down
61 changes: 61 additions & 0 deletions pkg/parser/schema_trusted_bots_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build !integration

package parser

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestSandboxMCPTrustedBotsSchema(t *testing.T) {
for _, test := range []struct {
name string
value any
wantErr bool
}{
{"one bot", []any{"github-actions[bot]"}, false},
{"multiple bots", []any{"github-actions[bot]", "copilot-swe-agent[bot]"}, false},
{"login without suffix", []any{"My-Bot_123"}, false},
{"empty list", []any{}, true},
{"empty identity", []any{""}, true},
{"command substitution", []any{"$(id)"}, true},
{"backtick substitution", []any{"`id`"}, true},
{"variable expansion", []any{"${HOME}"}, true},
{"workflow expression", []any{"${{ github.actor }}"}, true},
{"whitespace", []any{"my bot"}, true},
{"trailing newline", []any{"github-actions[bot]\n"}, true},
{"quote", []any{`my"bot`}, true},
{"backslash", []any{`my\bot`}, true},
{"non-ASCII identity", []any{"böt"}, true},
{"invalid suffix", []any{"my-bot[other]"}, true},
{"mixed types", []any{"github-actions[bot]", 42}, true},
{"scalar", "github-actions[bot]", true},
{"null", nil, true},
} {
t.Run(test.name, func(t *testing.T) {
err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(map[string]any{
"on": "workflow_dispatch",
"sandbox": map[string]any{
"mcp": map[string]any{"trusted-bots": test.value},
},
}, "trusted-bots.md")
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}

t.Run("gateway JSON key is not a frontmatter alias", func(t *testing.T) {
err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(map[string]any{
"on": "workflow_dispatch",
"sandbox": map[string]any{
"mcp": map[string]any{"trustedBots": []any{"github-actions[bot]"}},
},
}, "trusted-bots.md")
require.Error(t, err)
require.Contains(t, err.Error(), "trustedBots")
})
}
11 changes: 11 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4650,6 +4650,17 @@
"description": "Keepalive ping interval in seconds for HTTP MCP backends. Sends periodic pings to prevent session expiry during long-running agent tasks. Set to -1 to disable keepalive pings. Unset or 0 uses the gateway default (1500 seconds = 25 minutes).",
"minimum": -1,
"examples": [-1, 300, 600, 1500]
Comment thread
pelikhan marked this conversation as resolved.
},
"trusted-bots": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1,
"pattern": "^[A-Za-z0-9_-]+(\\[bot\\])?$"
},
"description": "Additional GitHub bot identities merged with the MCP gateway's built-in trusted identities. Use only ASCII letters, digits, hyphens, or underscores, optionally followed by [bot]. Cannot remove or override built-in identities. Compiled to gateway.trustedBots in the gateway JSON configuration.",
"examples": [["github-actions[bot]", "copilot-swe-agent[bot]"]]
}
},
"additionalProperties": false
Expand Down
17 changes: 6 additions & 11 deletions pkg/workflow/frontmatter_extraction_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,19 +622,14 @@ func (c *Compiler) extractMCPGatewayConfig(mcpVal any) *MCPGatewayRuntimeConfig
}
}

// Extract trustedBots / trusted-bots (additional bot identities to pass to the gateway)
for _, key := range []string{"trustedBots", "trusted-bots"} {
if trustedBotsVal, hasTrustedBots := mcpObj[key]; hasTrustedBots {
if trustedBotsSlice, ok := trustedBotsVal.([]any); ok {
for _, bot := range trustedBotsSlice {
if botStr, ok := bot.(string); ok {
mcpConfig.TrustedBots = append(mcpConfig.TrustedBots, botStr)
}
// Frontmatter uses trusted-bots; trustedBots is the generated gateway JSON field.
if trustedBotsVal, hasTrustedBots := mcpObj["trusted-bots"]; hasTrustedBots {
if trustedBotsSlice, ok := trustedBotsVal.([]any); ok {
for _, bot := range trustedBotsSlice {
if botStr, ok := bot.(string); ok {
mcpConfig.TrustedBots = append(mcpConfig.TrustedBots, botStr)
}
}
if len(mcpConfig.TrustedBots) > 0 {
break
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions pkg/workflow/frontmatter_extraction_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,24 +524,24 @@ func TestExtractMCPGatewayConfigPayloadFields(t *testing.T) {
func TestExtractMCPGatewayConfigTrustedBots(t *testing.T) {
compiler := &Compiler{}

t.Run("extracts trustedBots using camelCase key", func(t *testing.T) {
t.Run("does not extract gateway JSON camelCase key", func(t *testing.T) {
mcpObj := map[string]any{
"container": "ghcr.io/github/gh-aw-mcpg",
"trustedBots": []any{"github-actions[bot]", "copilot-swe-agent[bot]"},
}
config := compiler.extractMCPGatewayConfig(mcpObj)
require.NotNil(t, config, "Should extract MCP gateway config")
assert.Equal(t, []string{"github-actions[bot]", "copilot-swe-agent[bot]"}, config.TrustedBots, "Should extract trustedBots")
assert.Nil(t, config.TrustedBots, "trustedBots is not a frontmatter field")
})

t.Run("extracts trustedBots using kebab-case key", func(t *testing.T) {
mcpObj := map[string]any{
"container": "ghcr.io/github/gh-aw-mcpg",
"trusted-bots": []any{"github-actions[bot]"},
"trusted-bots": []any{"github-actions[bot]", "copilot-swe-agent[bot]", "my-bot"},
}
config := compiler.extractMCPGatewayConfig(mcpObj)
require.NotNil(t, config, "Should extract MCP gateway config")
assert.Equal(t, []string{"github-actions[bot]"}, config.TrustedBots, "Should extract trusted-bots")
assert.Equal(t, []string{"github-actions[bot]", "copilot-swe-agent[bot]", "my-bot"}, config.TrustedBots, "Should extract trusted-bots")
})

t.Run("leaves trustedBots nil when not specified", func(t *testing.T) {
Expand Down
60 changes: 60 additions & 0 deletions pkg/workflow/mcp_gateway_reference_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//go:build !integration

package workflow

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestMCPGatewayReferenceExamplesCompile(t *testing.T) {
pagePath := filepath.Join("..", "..", "docs", "src", "content", "docs", "reference", "mcp-gateway.md")
page, err := os.ReadFile(pagePath)
require.NoError(t, err)

for _, test := range []struct {
name string
want []string
}{
{
name: "trusted-bots",
want: []string{`"trustedBots": ["github-actions[bot]", "copilot-swe-agent[bot]"]`},
},
{
name: "oidc-mcp",
want: []string{
`"url": "https://my-server.example.com/mcp"`,
`"type": "github-oidc"`,
`"audience": "https://my-server.example.com"`,
"id-token: write",
"-e ACTIONS_ID_TOKEN_REQUEST_URL",
"-e ACTIONS_ID_TOKEN_REQUEST_TOKEN",
},
},
} {
t.Run(test.name, func(t *testing.T) {
fence := "```aw wrap title=\".github/workflows/" + test.name + ".md\"\n"
require.Equal(t, 1, strings.Count(string(page), fence))
_, rest, found := strings.Cut(string(page), fence)
require.True(t, found)
example, _, found := strings.Cut(rest, "\n```")
require.True(t, found, "reference example must have a closing fence")

dir := t.TempDir()
path := filepath.Join(dir, test.name+".md")
require.NoError(t, os.WriteFile(path, []byte(example+"\n"), 0o600))
compiler := NewCompiler()
require.NoError(t, compiler.CompileWorkflow(path))

lock, err := os.ReadFile(filepath.Join(dir, test.name+".lock.yml"))
require.NoError(t, err)
for _, want := range test.want {
require.Contains(t, string(lock), want)
}
})
}
}
6 changes: 6 additions & 0 deletions pkg/workflow/mcp_renderer.go
Loading
Loading