diff --git a/pkg/agent/definition.go b/pkg/agent/definition.go index 90a69eaa4..5b0e29137 100644 --- a/pkg/agent/definition.go +++ b/pkg/agent/definition.go @@ -45,6 +45,7 @@ type AgentPromptDefinition struct { Body string `json:"body"` RawFrontmatter string `json:"raw_frontmatter,omitempty"` Frontmatter AgentFrontmatter `json:"frontmatter"` + FrontmatterErr string `json:"frontmatter_error,omitempty"` } // SoulDefinition represents the resolved SOUL.md file linked to the agent. @@ -146,19 +147,21 @@ func loadUserDefinition(workspace string) *UserDefinition { func parseAgentPromptDefinition(path, content string) AgentPromptDefinition { frontmatter, body := splitAgentFrontmatter(content) + parsedFrontmatter, err := parseAgentFrontmatter(path, frontmatter) return AgentPromptDefinition{ Path: path, Raw: content, Body: body, RawFrontmatter: frontmatter, - Frontmatter: parseAgentFrontmatter(path, frontmatter), + Frontmatter: parsedFrontmatter, + FrontmatterErr: errorString(err), } } -func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter { +func parseAgentFrontmatter(path, frontmatter string) (AgentFrontmatter, error) { frontmatter = strings.TrimSpace(frontmatter) if frontmatter == "" { - return AgentFrontmatter{} + return AgentFrontmatter{}, nil } rawFields := make(map[string]any) @@ -167,7 +170,7 @@ func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter { "path": path, "error": err.Error(), }) - return AgentFrontmatter{} + return AgentFrontmatter{}, err } var typed struct { @@ -184,7 +187,7 @@ func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter { "path": path, "error": err.Error(), }) - return AgentFrontmatter{} + return AgentFrontmatter{}, err } return AgentFrontmatter{ @@ -196,7 +199,7 @@ func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter { Skills: append([]string(nil), typed.Skills...), MCPServers: append([]string(nil), typed.MCPServers...), Fields: rawFields, - } + }, nil } func splitAgentFrontmatter(content string) (frontmatter, body string) { @@ -253,3 +256,10 @@ func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } + +func errorString(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/pkg/agent/instance_test.go b/pkg/agent/instance_test.go index 869e5fbc7..3edac0724 100644 --- a/pkg/agent/instance_test.go +++ b/pkg/agent/instance_test.go @@ -330,3 +330,39 @@ Use frontmatter identity. t.Fatal("expected slack MCP server to be blocked by frontmatter allowlist") } } + +func TestNewAgentInstance_InvalidFrontmatterFailsClosedForToolsAndMCPServers(t *testing.T) { + workspace := setupWorkspace(t, map[string]string{ + "AGENT.md": `--- +tools: [read_file +mcpServers: [github] +--- +# Agent +`, + }) + defer cleanupWorkspace(t, workspace) + + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: workspace, + ModelName: "default-model", + }, + }, + Tools: config.ToolsConfig{ + ReadFile: config.ReadFileToolConfig{Enabled: true}, + }, + } + + agent := NewAgentInstance(&config.AgentConfig{ + ID: "research", + Workspace: workspace, + }, &cfg.Agents.Defaults, cfg, &mockProvider{}) + + if _, ok := agent.Tools.Get("read_file"); ok { + t.Fatal("expected malformed frontmatter to fail closed and block read_file") + } + if agent.AllowsMCPServer("github") { + t.Fatal("expected malformed frontmatter to fail closed for MCP servers") + } +} diff --git a/pkg/agent/tool_allowlist.go b/pkg/agent/tool_allowlist.go index de68352ad..f7434c188 100644 --- a/pkg/agent/tool_allowlist.go +++ b/pkg/agent/tool_allowlist.go @@ -6,6 +6,9 @@ import ( ) func resolveAgentToolAllowlist(definition AgentContextDefinition) []string { + if frontmatterParseFailed(definition) { + return []string{} + } if definition.Agent == nil || definition.Agent.Frontmatter.Tools == nil { return nil } @@ -28,6 +31,9 @@ func resolveAgentToolAllowlist(definition AgentContextDefinition) []string { } func resolveAgentMCPServerAllowlist(definition AgentContextDefinition) map[string]struct{} { + if frontmatterParseFailed(definition) { + return map[string]struct{}{} + } if definition.Agent == nil || definition.Agent.Frontmatter.MCPServers == nil { return nil } @@ -43,3 +49,13 @@ func resolveAgentMCPServerAllowlist(definition AgentContextDefinition) map[strin return allowlist } + +func frontmatterParseFailed(definition AgentContextDefinition) bool { + if definition.Agent == nil { + return false + } + if strings.TrimSpace(definition.Agent.RawFrontmatter) == "" { + return false + } + return strings.TrimSpace(definition.Agent.FrontmatterErr) != "" +}