feat: add configurable trusted bots list with approved integrity elevation#2204
feat: add configurable trusted bots list with approved integrity elevation#2204
Conversation
Implements the `trustedBots` field from the new mcp-gateway config schema. Bots listed here receive approved integrity for objects they author, additive to the built-in trusted bot list (dependabot[bot], github-actions[bot], etc.). - GatewayConfig.TrustedBots []string (TOML: trusted_bots) - StdinGatewayConfig.TrustedBots []string (JSON stdin: trustedBots) - guard.BuildLabelAgentPayload() merges trusted bots into label_agent payload - buildStrictLabelAgentPayload() validates optional trusted-bots key - UnifiedServer.getTrustedBots() + updated ensureGuardInitialized() - Rust: LabelAgentInput.trusted_bots, PolicyContext.trusted_bots, is_configured_trusted_bot(), author_association_floor() checks both lists - Tests for all new Go and Rust functionality Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/2adba402-27f1-4075-b747-df6840536210
There was a problem hiding this comment.
Pull request overview
Adds a gateway-level configuration hook for operator-specified “trusted bots” and wires it end-to-end into the WASM GitHub guard so those actors receive approved (writer) integrity (matching existing built-in trusted bots).
Changes:
- Introduces
GatewayConfig.TrustedBots(trusted_botsin TOML) andStdinGatewayConfig.TrustedBots(trustedBotsin stdin JSON). - Extends the label_agent transport payload to optionally include
trusted-bots, and updates strict payload validation + Go unit tests. - Extends the Rust guard’s
PolicyContextand integrity-floor logic to honor configured trusted bots (case-insensitive), with new tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/server/unified.go | Builds a combined label_agent payload (policy + trusted bots), hashes it for session cache invalidation, and passes it to LabelAgent. |
| internal/guard/wasm.go | Allows trusted-bots as an optional top-level key in strict payload validation; adds helper to build the merged payload. |
| internal/guard/wasm_test.go | Adds coverage for strict payload acceptance/rejection of trusted-bots and for BuildLabelAgentPayload. |
| internal/config/config_stdin.go | Adds stdin JSON support for gateway.trustedBots and converts it into internal config. |
| internal/config/config_core.go | Adds TOML/JSON struct field GatewayConfig.TrustedBots with documentation. |
| guards/github-guard/rust-guard/src/lib.rs | Deserializes trusted-bots from label_agent input and stores it in PolicyContext. |
| guards/github-guard/rust-guard/src/labels/mod.rs | Updates/extends tests and adds new tests validating configured-trusted-bot behavior. |
| guards/github-guard/rust-guard/src/labels/helpers.rs | Adds PolicyContext.trusted_bots, implements membership check, and integrates it into author_association_floor. |
Comments suppressed due to low confidence (1)
internal/server/unified.go:1173
trustedBotsis taken directly from config and injected into the label_agent payload/hash without normalization. Duplicate entries, leading/trailing whitespace, casing differences, or reordered lists will unnecessarily invalidate the cached session state (policyHash) and can also causelabel_agentto fail later if any entry is blank/whitespace-only. Consider canonicalizing once (trim + drop empty + dedupe, and optionally sort / lowercase) before building the payload and computingpolicyHash(e.g., via internal/strutil.DeduplicateStrings).
// Build the label_agent payload, merging in any configured trusted bots.
// The policyHash covers both the policy and trusted bots so that any change
// to either field invalidates the cached guard session state.
trustedBots := us.getTrustedBots()
labelAgentPayload := guard.BuildLabelAgentPayload(policy, trustedBots)
payloadJSON, err := json.Marshal(labelAgentPayload)
if err != nil {
return defaultMode, fmt.Errorf("failed to serialize label_agent payload: %w", err)
}
policyHash := string(payloadJSON)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1175,7 +1185,7 @@ func (us *UnifiedServer) ensureGuardInitialized( | |||
|
|
|||
| log.Printf("[DIFC] Initializing guard session state: server=%s, session=%s, policy_source=%s", serverID, sessionID, source) | |||
| log.Printf("[DIFC] Calling label_agent: server=%s, session=%s, guard=%s, policy=%s", serverID, sessionID, g.Name(), string(policyJSON)) | |||
There was a problem hiding this comment.
The log line for "Calling label_agent" prints policyJSON, but the actual payload passed to LabelAgent is labelAgentPayload, which may include the trusted-bots key. This makes debugging policy/session-cache issues harder because logs won’t reflect what was sent to the guard. Consider logging payloadJSON (or renaming the field in the log to "payload") when trusted bots are configured.
This issue also appears on line 1164 of the same file.
| log.Printf("[DIFC] Calling label_agent: server=%s, session=%s, guard=%s, policy=%s", serverID, sessionID, g.Name(), string(policyJSON)) | |
| log.Printf("[DIFC] Calling label_agent: server=%s, session=%s, guard=%s, payload=%s", serverID, sessionID, g.Name(), string(payloadJSON)) |
| cfg.Gateway.PayloadDir = stdinCfg.Gateway.PayloadDir | ||
| } | ||
| if len(stdinCfg.Gateway.TrustedBots) > 0 { | ||
| cfg.Gateway.TrustedBots = stdinCfg.Gateway.TrustedBots | ||
| } |
There was a problem hiding this comment.
convertStdinConfig copies trustedBots into cfg.Gateway.TrustedBots without any validation/normalization. If stdin contains entries like "" or " ", guard initialization will fail later in buildStrictLabelAgentPayload with a less actionable error at runtime. Consider validating each entry here (trim + require non-empty) and normalizing (dedupe/sort) so config errors fail fast during load.
| let lower = username.to_lowercase(); | ||
| ctx.trusted_bots.iter().any(|b| b.to_lowercase() == lower) |
There was a problem hiding this comment.
is_configured_trusted_bot allocates on every call (username.to_lowercase() and b.to_lowercase() for each entry). This function is used in the integrity floor path and can run frequently, so this may become a hot spot. Consider avoiding allocations by using eq_ignore_ascii_case for comparisons and/or storing a pre-normalized (lowercased) set/vector in PolicyContext at label_agent parse time.
| let lower = username.to_lowercase(); | |
| ctx.trusted_bots.iter().any(|b| b.to_lowercase() == lower) | |
| ctx.trusted_bots | |
| .iter() | |
| .any(|b| b.eq_ignore_ascii_case(username)) |
The mcp-gateway schema defines a
trustedBotsgateway config field for operator-specified bots that should receiveapprovedintegrity (same as built-in bots likedependabot[bot],github-actions[bot]). This wires that field end-to-end from config parsing through the WASM guard.Config
GatewayConfig.TrustedBots []string— TOML:trusted_botsStdinGatewayConfig.TrustedBots []string— JSON stdin:trustedBotsGuard payload (
internal/guard/wasm.go)BuildLabelAgentPayload(policy, trustedBots)— merges bots into thelabel_agentJSON payload astrusted-botsbuildStrictLabelAgentPayload— relaxed to accepttrusted-botsas an optional top-level key alongsideallow-only; all other unexpected keys still rejectedServer (
internal/server/unified.go)ensureGuardInitializedbuilds the combined payload before callingLabelAgent;policyHashcovers both policy and trusted bots, so session cache invalidates on any changeRust guard
LabelAgentInput.trusted_bots— deserialized fromtrusted-bots(default[])PolicyContext.trusted_bots: Vec<String>— populated atlabel_agentparse time, available to all label functions via the existing context patternis_configured_trusted_bot(username, ctx)— case-insensitive membership checkauthor_association_floor()— now checks bothis_trusted_first_party_botandis_configured_trusted_bot; either match elevates towriter_integrity(approved)The list is purely additive — it cannot remove built-in trusted bots.