Skip to content

Commit 80d9f1e

Browse files
Copilotlpcox
andauthored
Remove X-GitHub-Actor header logic; trustedBots is config-only; wire frontmatter to renderer
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/a5944233-9f4e-4300-907c-57f2cb82dafe
1 parent cc60fe7 commit 80d9f1e

9 files changed

Lines changed: 110 additions & 61 deletions

docs/public/schemas/mcp-gateway-config.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@
249249
},
250250
"trustedBots": {
251251
"type": "array",
252-
"description": "Additional trusted bot identities that are permitted to call the gateway, merged with the gateway's built-in internal trusted identity list. When bot identity enforcement is active, only requests whose 'X-GitHub-Actor' header matches an entry in the combined list (built-in + this field) are accepted. Typically GitHub bot usernames such as 'github-actions[bot]' or 'copilot-swe-agent[bot]'. This field is additive and cannot remove entries from the gateway's internal built-in trusted identity list.",
252+
"description": "Additional trusted bot identity strings passed to the gateway and merged with its built-in internal trusted identity list. This field is additive and cannot remove entries from the gateway's built-in list. Typically GitHub bot usernames such as 'github-actions[bot]' or 'copilot-swe-agent[bot]'.",
253253
"items": {
254254
"type": "string",
255255
"minLength": 1

docs/src/content/docs/reference/mcp-gateway.md

Lines changed: 27 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ The `gateway` section is required and configures gateway-specific behavior:
248248
| `payloadDir` | string | No | Directory path for storing large payload JSON files for authenticated clients |
249249
| `payloadPathPrefix` | string | No | Path prefix to remap payload paths for agent containers (e.g., /workspace/payloads) |
250250
| `payloadSizeThreshold` | integer | No | Size threshold in bytes for storing payloads to disk (default: 524288 = 512KB) |
251-
| `trustedBots` | array[string] | No | Additional GitHub bot identity strings (e.g., `github-actions[bot]`) added to the gateway's built-in trusted identity list. This field is additive — it extends the internal list but cannot remove built-in entries. When bot identity enforcement is active, only callers whose `X-GitHub-Actor` header matches an entry in the combined list are accepted. |
251+
| `trustedBots` | array[string] | No | Additional GitHub bot identity strings (e.g., `github-actions[bot]`) passed to the gateway and merged with its built-in trusted identity list. This field is additive — it extends the internal list but cannot remove built-in entries. |
252252

253253
#### 4.1.3.1 Payload Directory Path Validation
254254

@@ -370,16 +370,9 @@ payload_size_threshold = 262144 # 256KB - more aggressive disk storage
370370

371371
#### 4.1.3.4 Trusted Bot Identity Configuration
372372

373-
The optional `trustedBots` field in the gateway configuration provides an identity-based allowlist of additional GitHub bot accounts that are permitted to call the gateway. This is independent of the `apiKey` mechanism and operates at the HTTP request level by inspecting the `X-GitHub-Actor` header.
373+
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 recognises.
374374

375-
> **Important**: `trustedBots` is **additive**. The gateway maintains its own internal list of trusted bot identities that are always permitted regardless of this field. The `trustedBots` field extends that internal list with additional identities; it cannot remove or override the gateway's built-in trusted identities.
376-
377-
**How it works**:
378-
379-
1. The caller includes an `X-GitHub-Actor` header in each request identifying the GitHub actor (bot or user) making the request
380-
2. The gateway checks the header value against its **combined** list of trusted identities: the gateway's internal built-in list **plus** any entries in `trustedBots`
381-
3. If bot identity enforcement is active and the actor does not match any entry in the combined list, the gateway rejects the request with HTTP 403
382-
4. If `trustedBots` is not configured, the gateway still permits callers that match its internal built-in identities; no additional identities are added
375+
> **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.
383376
384377
**Configuration Example**:
385378

@@ -397,23 +390,23 @@ The optional `trustedBots` field in the gateway configuration provides an identi
397390
}
398391
```
399392

393+
**Frontmatter Example** (workflow author):
394+
395+
```yaml
396+
sandbox:
397+
mcp:
398+
trusted-bots:
399+
- github-actions[bot]
400+
- copilot-swe-agent[bot]
401+
```
402+
400403
**Requirements**:
401404
- `trustedBots` MUST be a non-empty array of strings when present
402405
- Each entry MUST be a non-empty string (typically a GitHub username such as `github-actions[bot]`)
403-
- `trustedBots` entries are **merged** with the gateway's internal built-in trusted identities to form the effective allowlist
406+
- `trustedBots` entries are **merged** with the gateway's internal built-in trusted identities to form the effective list passed to the gateway
404407
- `trustedBots` MUST NOT be used to remove or override entries in the gateway's internal built-in trusted identity list
405-
- When bot identity enforcement is active, the gateway MUST reject requests where `X-GitHub-Actor` is absent or does not match any entry in the combined list (HTTP 403)
406-
- When `trustedBots` is omitted, only the gateway's built-in trusted identities are consulted
407-
- Bot identity checks are applied **after** API key authentication; a request must pass both checks when both are configured
408-
- Entries are compared case-sensitively
409-
410-
**Security Considerations**:
411-
- `trustedBots` provides defense-in-depth: even if an API key is leaked, callers that cannot supply a matching `X-GitHub-Actor` header are denied
412-
- Because `trustedBots` is additive, the internal built-in identities cannot be narrowed through configuration — only expanded
413-
- The `X-GitHub-Actor` header MUST be treated as an untrusted claim unless the deployment ensures that only the gateway's own infrastructure can set it (e.g., the gateway runs inside a trusted network boundary)
414-
- Implementations SHOULD log rejected bot identity mismatches at the warning level without logging the header value in plaintext
415408

416-
**Compliance Test**: T-AUTH-006, T-AUTH-007 - Trusted Bot Identity Enforcement
409+
**Compliance Test**: T-AUTH-006 - Trusted Bot Identity Configuration
417410

418411
#### 4.1.3a Top-Level Configuration Fields
419412

@@ -991,32 +984,13 @@ The following endpoints MUST NOT require authentication:
991984

992985
- `/health`
993986

994-
### 7.5 Trusted Bot Identity Authentication
995-
996-
When bot identity enforcement is active (either via `gateway.trustedBots` or the gateway's internal built-in trusted identity list), the gateway enforces an additional identity check **after** API key authentication:
997-
998-
1. The gateway inspects the `X-GitHub-Actor` request header on all RPC requests to `/mcp/{server-name}` and `/close` endpoints
999-
2. The header value is checked against the **combined** list of trusted identities: the gateway's built-in internal list **plus** any entries supplied in `gateway.trustedBots`
1000-
3. `gateway.trustedBots` is **additive** — it extends the built-in list but cannot remove entries from it
1001-
4. If the header is absent or does not match any entry in the combined list, the gateway MUST reject the request with HTTP 403
1002-
1003-
**Example request** (both API key and bot identity supplied):
1004-
1005-
```http
1006-
POST /mcp/github HTTP/1.1
1007-
Authorization: my-secret-api-key-12345
1008-
X-GitHub-Actor: github-actions[bot]
1009-
Content-Type: application/json
1010-
```
987+
### 7.5 Trusted Bot Identity Configuration
1011988

1012-
**Error responses**:
989+
The `gateway.trustedBots` field allows workflow authors to pass additional trusted bot identity strings to the gateway via the generated gateway configuration file. The gateway merges these entries with its own built-in trusted identity list.
1013990

1014-
| Condition | HTTP Status | Description |
1015-
|-----------|-------------|-------------|
1016-
| `X-GitHub-Actor` header missing | 403 | Bot identity required but not supplied |
1017-
| `X-GitHub-Actor` does not match any trusted bot | 403 | Caller is not in the trusted bot list |
991+
`gateway.trustedBots` is **additive** — it extends the gateway's built-in list but cannot remove entries from it.
1018992

1019-
**Security Note**: The `/health` endpoint MUST remain exempt from trusted bot identity checks (same as API key exemption).
993+
Workflow authors set this via the `sandbox.mcp.trusted-bots` frontmatter field; the compiler translates it into the `trustedBots` array in the generated `gateway` section of the MCP config file.
1020994

1021995
---
1022996

@@ -1205,8 +1179,7 @@ A conforming implementation MUST pass the following test categories:
12051179
- **T-AUTH-003**: Missing token rejection
12061180
- **T-AUTH-004**: Health endpoint exemption
12071181
- **T-AUTH-005**: Token rotation support
1208-
- **T-AUTH-006**: Trusted bot identity acceptance — request with matching `X-GitHub-Actor` header is accepted when `trustedBots` is configured
1209-
- **T-AUTH-007**: Trusted bot identity rejection — request with absent or non-matching `X-GitHub-Actor` header is rejected with HTTP 403 when `trustedBots` is configured
1182+
- **T-AUTH-006**: Trusted bot identity configuration — `trustedBots` entries are present in the generated gateway config and merged with the gateway's built-in list
12101183

12111184
#### 10.1.5 Timeout Tests
12121185

@@ -1549,18 +1522,13 @@ Content-Type: application/json
15491522
### Version 1.9.0 (Draft)
15501523

15511524
- **Added**: `trustedBots` field to gateway configuration (Section 4.1.3, 4.1.3.4)
1552-
- Optional array of GitHub bot identity strings permitted to call the gateway
1553-
- When configured, the gateway enforces an `X-GitHub-Actor` header check on all RPC requests
1554-
- Requests whose `X-GitHub-Actor` does not match any entry are rejected with HTTP 403
1555-
- When omitted, bot identity is not checked (any authenticated caller is accepted)
1556-
- Entries are compared case-sensitively
1557-
- The `/health` endpoint is exempt from bot identity checks
1558-
- **Added**: Section 7.5 — Trusted Bot Identity Authentication
1559-
- Specifies HTTP 403 error behavior for absent or non-matching `X-GitHub-Actor` headers
1560-
- Bot identity check runs after API key authentication
1561-
- **Added**: Compliance tests for trusted bot identities (Section 10.1.4)
1562-
- T-AUTH-006: Trusted bot identity acceptance
1563-
- T-AUTH-007: Trusted bot identity rejection
1525+
- Optional array of GitHub bot identity strings passed to the gateway via the generated config
1526+
- Merged with the gateway's built-in trusted identity list (additive — cannot remove built-in entries)
1527+
- Workflow authors configure via `sandbox.mcp.trusted-bots` in frontmatter; the compiler translates it into the gateway config
1528+
- **Added**: Section 7.5 — Trusted Bot Identity Configuration
1529+
- Describes `trustedBots` as a config-passing mechanism from frontmatter to gateway config
1530+
- **Added**: Compliance test for trusted bot identity configuration (Section 10.1.4)
1531+
- T-AUTH-006: Trusted bot identity configuration
15641532
- **Updated**: JSON Schema with `trustedBots` property in `gatewayConfig` definition
15651533

15661534
### Version 1.8.0 (Draft)

pkg/workflow/frontmatter_extraction_security.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,22 @@ func (c *Compiler) extractMCPGatewayConfig(mcpVal any) *MCPGatewayRuntimeConfig
479479
}
480480
}
481481

482+
// Extract trustedBots / trusted-bots (additional bot identities to pass to the gateway)
483+
for _, key := range []string{"trustedBots", "trusted-bots"} {
484+
if trustedBotsVal, hasTrustedBots := mcpObj[key]; hasTrustedBots {
485+
if trustedBotsSlice, ok := trustedBotsVal.([]any); ok {
486+
for _, bot := range trustedBotsSlice {
487+
if botStr, ok := bot.(string); ok {
488+
mcpConfig.TrustedBots = append(mcpConfig.TrustedBots, botStr)
489+
}
490+
}
491+
}
492+
if len(mcpConfig.TrustedBots) > 0 {
493+
break
494+
}
495+
}
496+
}
497+
482498
return mcpConfig
483499
}
484500

pkg/workflow/frontmatter_extraction_security_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,37 @@ func TestExtractMCPGatewayConfigPayloadFields(t *testing.T) {
218218
assert.Equal(t, 0, config.PayloadSizeThreshold, "PayloadSizeThreshold should be 0 when not specified")
219219
})
220220
}
221+
222+
// TestExtractMCPGatewayConfigTrustedBots tests extraction of trustedBots from MCP gateway frontmatter
223+
func TestExtractMCPGatewayConfigTrustedBots(t *testing.T) {
224+
compiler := &Compiler{}
225+
226+
t.Run("extracts trustedBots using camelCase key", func(t *testing.T) {
227+
mcpObj := map[string]any{
228+
"container": "ghcr.io/github/gh-aw-mcpg",
229+
"trustedBots": []any{"github-actions[bot]", "copilot-swe-agent[bot]"},
230+
}
231+
config := compiler.extractMCPGatewayConfig(mcpObj)
232+
require.NotNil(t, config, "Should extract MCP gateway config")
233+
assert.Equal(t, []string{"github-actions[bot]", "copilot-swe-agent[bot]"}, config.TrustedBots, "Should extract trustedBots")
234+
})
235+
236+
t.Run("extracts trustedBots using kebab-case key", func(t *testing.T) {
237+
mcpObj := map[string]any{
238+
"container": "ghcr.io/github/gh-aw-mcpg",
239+
"trusted-bots": []any{"github-actions[bot]"},
240+
}
241+
config := compiler.extractMCPGatewayConfig(mcpObj)
242+
require.NotNil(t, config, "Should extract MCP gateway config")
243+
assert.Equal(t, []string{"github-actions[bot]"}, config.TrustedBots, "Should extract trusted-bots")
244+
})
245+
246+
t.Run("leaves trustedBots nil when not specified", func(t *testing.T) {
247+
mcpObj := map[string]any{
248+
"container": "ghcr.io/github/gh-aw-mcpg",
249+
}
250+
config := compiler.extractMCPGatewayConfig(mcpObj)
251+
require.NotNil(t, config, "Should extract MCP gateway config")
252+
assert.Nil(t, config.TrustedBots, "TrustedBots should be nil when not specified")
253+
})
254+
}

pkg/workflow/mcp_gateway_config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func buildMCPGatewayConfig(workflowData *WorkflowData) *MCPGatewayRuntimeConfig
137137
PayloadDir: "${MCP_GATEWAY_PAYLOAD_DIR}", // Gateway variable expression for payload directory
138138
PayloadPathPrefix: workflowData.SandboxConfig.MCP.PayloadPathPrefix, // Optional path prefix for agent containers
139139
PayloadSizeThreshold: payloadSizeThreshold, // Size threshold in bytes
140+
TrustedBots: workflowData.SandboxConfig.MCP.TrustedBots, // Additional trusted bot identities from frontmatter
140141
}
141142
}
142143

pkg/workflow/mcp_gateway_config_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,24 @@ func TestBuildMCPGatewayConfig(t *testing.T) {
315315
PayloadSizeThreshold: constants.DefaultMCPGatewayPayloadSizeThreshold,
316316
},
317317
},
318+
{
319+
name: "propagates trustedBots from frontmatter config",
320+
workflowData: &WorkflowData{
321+
SandboxConfig: &SandboxConfig{
322+
MCP: &MCPGatewayRuntimeConfig{
323+
TrustedBots: []string{"github-actions[bot]", "copilot-swe-agent[bot]"},
324+
},
325+
},
326+
},
327+
expected: &MCPGatewayRuntimeConfig{
328+
Port: int(DefaultMCPGatewayPort),
329+
Domain: "${MCP_GATEWAY_DOMAIN}",
330+
APIKey: "${MCP_GATEWAY_API_KEY}",
331+
PayloadDir: "${MCP_GATEWAY_PAYLOAD_DIR}",
332+
PayloadSizeThreshold: constants.DefaultMCPGatewayPayloadSizeThreshold,
333+
TrustedBots: []string{"github-actions[bot]", "copilot-swe-agent[bot]"},
334+
},
335+
},
318336
}
319337

320338
for _, tt := range tests {
@@ -330,6 +348,7 @@ func TestBuildMCPGatewayConfig(t *testing.T) {
330348
assert.Equal(t, tt.expected.PayloadDir, result.PayloadDir, "PayloadDir should match")
331349
assert.Equal(t, tt.expected.PayloadPathPrefix, result.PayloadPathPrefix, "PayloadPathPrefix should match")
332350
assert.Equal(t, tt.expected.PayloadSizeThreshold, result.PayloadSizeThreshold, "PayloadSizeThreshold should match")
351+
assert.Equal(t, tt.expected.TrustedBots, result.TrustedBots, "TrustedBots should match")
333352
}
334353
})
335354
}

pkg/workflow/mcp_renderer.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ func RenderJSONMCPConfig(
193193
if options.GatewayConfig.PayloadDir != "" {
194194
fmt.Fprintf(&configBuilder, ",\n \"payloadDir\": \"%s\"", options.GatewayConfig.PayloadDir)
195195
}
196+
if len(options.GatewayConfig.TrustedBots) > 0 {
197+
configBuilder.WriteString(",\n \"trustedBots\": [")
198+
for i, bot := range options.GatewayConfig.TrustedBots {
199+
if i > 0 {
200+
configBuilder.WriteString(", ")
201+
}
202+
fmt.Fprintf(&configBuilder, "%q", bot)
203+
}
204+
configBuilder.WriteString("]")
205+
}
196206
configBuilder.WriteString("\n")
197207
configBuilder.WriteString(" }\n")
198208
} else {

pkg/workflow/schemas/mcp-gateway-config.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
},
219219
"trustedBots": {
220220
"type": "array",
221-
"description": "Additional trusted bot identities that are permitted to call the gateway, merged with the gateway's built-in internal trusted identity list. When bot identity enforcement is active, only requests whose 'X-GitHub-Actor' header matches an entry in the combined list (built-in + this field) are accepted. Typically GitHub bot usernames such as 'github-actions[bot]' or 'copilot-swe-agent[bot]'. This field is additive and cannot remove entries from the gateway's internal built-in trusted identity list.",
221+
"description": "Additional trusted bot identity strings passed to the gateway and merged with its built-in internal trusted identity list. This field is additive and cannot remove entries from the gateway's built-in list. Typically GitHub bot usernames such as 'github-actions[bot]' or 'copilot-swe-agent[bot]'.",
222222
"items": {
223223
"type": "string",
224224
"minLength": 1

pkg/workflow/tools_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ type MCPGatewayRuntimeConfig struct {
396396
PayloadDir string `yaml:"payload-dir,omitempty"` // Directory path for storing large payload JSON files (must be absolute path)
397397
PayloadPathPrefix string `yaml:"payload-path-prefix,omitempty"` // Path prefix to remap payload paths for agent containers (e.g., /workspace/payloads)
398398
PayloadSizeThreshold int `yaml:"payload-size-threshold,omitempty"` // Size threshold in bytes for storing payloads to disk (default: 524288 = 512KB)
399+
TrustedBots []string `yaml:"trusted-bots,omitempty"` // Additional bot identity strings to pass to the gateway, merged with its built-in list
399400
}
400401

401402
// HasTool checks if a tool is present in the configuration

0 commit comments

Comments
 (0)