diff --git a/internal/config/config_core.go b/internal/config/config_core.go index 1630c4a9..f559a9a9 100644 --- a/internal/config/config_core.go +++ b/internal/config/config_core.go @@ -263,6 +263,11 @@ func LoadFromFile(path string) (*Config, error) { cfg.Gateway = &GatewayConfig{} } + // Validate trusted_bots per spec §4.1.3.4: must be non-empty array when present + if err := validateTrustedBots(cfg.Gateway.TrustedBots); err != nil { + return nil, err + } + // Apply core gateway defaults applyGatewayDefaults(cfg.Gateway) diff --git a/internal/config/config_stdin.go b/internal/config/config_stdin.go index 70c8c866..03561388 100644 --- a/internal/config/config_stdin.go +++ b/internal/config/config_stdin.go @@ -281,7 +281,10 @@ func convertStdinConfig(stdinCfg *StdinConfig) (*Config, error) { if stdinCfg.Gateway.PayloadDir != "" { cfg.Gateway.PayloadDir = stdinCfg.Gateway.PayloadDir } - if len(stdinCfg.Gateway.TrustedBots) > 0 { + if stdinCfg.Gateway.TrustedBots != nil { + if err := validateTrustedBots(stdinCfg.Gateway.TrustedBots); err != nil { + return nil, err + } cfg.Gateway.TrustedBots = stdinCfg.Gateway.TrustedBots } } else { diff --git a/internal/config/config_stdin_test.go b/internal/config/config_stdin_test.go index b79b1ebe..4db148f8 100644 --- a/internal/config/config_stdin_test.go +++ b/internal/config/config_stdin_test.go @@ -988,7 +988,7 @@ func TestConvertStdinConfig_TrustedBots(t *testing.T) { assert.Equal(t, []string{"copilot-swe-agent[bot]", "my-org-bot"}, cfg.Gateway.TrustedBots) }) - t.Run("empty trustedBots not propagated", func(t *testing.T) { + t.Run("empty trustedBots rejected per spec §4.1.3.4", func(t *testing.T) { stdinCfg := &StdinConfig{ MCPServers: map[string]*StdinServerConfig{}, Gateway: &StdinGatewayConfig{ @@ -996,10 +996,9 @@ func TestConvertStdinConfig_TrustedBots(t *testing.T) { }, } - cfg, err := convertStdinConfig(stdinCfg) - require.NoError(t, err) - require.NotNil(t, cfg.Gateway) - assert.Nil(t, cfg.Gateway.TrustedBots) + _, err := convertStdinConfig(stdinCfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "trusted_bots must be a non-empty array when present") }) t.Run("nil trustedBots not propagated", func(t *testing.T) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c39d94e4..46747326 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1701,6 +1701,7 @@ args = ["run", "--rm", "-i", "test/container:latest"] _, err = LoadFromFile(tmpFile) require.Error(t, err) } + // TestLoadFromStdin_WithTrustedBots verifies JSON stdin parsing of trustedBots. // Covers spec §4.1.3.4 (Trusted Bot Identity Configuration). func TestLoadFromStdin_WithTrustedBots(t *testing.T) { @@ -1737,3 +1738,38 @@ func TestLoadFromStdin_WithTrustedBots(t *testing.T) { assert.Equal(t, []string{"github-actions[bot]", "copilot-swe-agent[bot]"}, cfg.Gateway.TrustedBots) } + +// TestLoadFromStdin_WithEmptyTrustedBots verifies JSON stdin parsing rejects trustedBots: []. +// Covers spec §4.1.3.4 (trustedBots MUST be a non-empty array when present). +func TestLoadFromStdin_WithEmptyTrustedBots(t *testing.T) { + stdinJSON := `{ + "mcpServers": { + "test": { + "container": "test/container:latest", + "type": "stdio" + } + }, + "gateway": { + "port": 8080, + "domain": "localhost", + "apiKey": "test-key", + "trustedBots": [] + } + }` + + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() + + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdin = r + + go func() { + defer w.Close() + _, _ = w.Write([]byte(stdinJSON)) + }() + + _, err = LoadFromStdin() + require.Error(t, err) + assert.Contains(t, err.Error(), "trustedBots") +} diff --git a/internal/config/validation.go b/internal/config/validation.go index f91eced4..57c156ff 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -365,10 +365,32 @@ func validateGatewayConfig(gateway *StdinGatewayConfig) error { } } + // Validate trustedBots per spec §4.1.3.4: must be non-empty array when present + if err := validateTrustedBots(gateway.TrustedBots); err != nil { + return err + } + logValidation.Print("Gateway config validation passed") return nil } +// validateTrustedBots checks that the trusted_bots/trustedBots list conforms to spec §4.1.3.4: +// when present, it must be a non-empty array of non-empty strings. +func validateTrustedBots(bots []string) error { + if bots == nil { + return nil + } + if len(bots) == 0 { + return fmt.Errorf("trusted_bots must be a non-empty array when present (spec §4.1.3.4)") + } + for i, bot := range bots { + if strings.TrimSpace(bot) == "" { + return fmt.Errorf("trusted_bots[%d] must be a non-empty string", i) + } + } + return nil +} + // validateTOMLStdioContainerization validates that TOML stdio servers use Docker for containerization. // This enforces MCP Gateway Specification Section 3.2.1: "Stdio-based MCP servers MUST be containerized." func validateTOMLStdioContainerization(servers map[string]*ServerConfig) error {