fix(agent): fail closed on invalid AGENT frontmatter

This commit is contained in:
afjcjsbx 2026-03-29 23:41:32 +02:00
parent 847218ef29
commit 409251e69d
3 changed files with 68 additions and 6 deletions

View file

@ -45,6 +45,7 @@ type AgentPromptDefinition struct {
Body string `json:"body"` Body string `json:"body"`
RawFrontmatter string `json:"raw_frontmatter,omitempty"` RawFrontmatter string `json:"raw_frontmatter,omitempty"`
Frontmatter AgentFrontmatter `json:"frontmatter"` Frontmatter AgentFrontmatter `json:"frontmatter"`
FrontmatterErr string `json:"frontmatter_error,omitempty"`
} }
// SoulDefinition represents the resolved SOUL.md file linked to the agent. // 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 { func parseAgentPromptDefinition(path, content string) AgentPromptDefinition {
frontmatter, body := splitAgentFrontmatter(content) frontmatter, body := splitAgentFrontmatter(content)
parsedFrontmatter, err := parseAgentFrontmatter(path, frontmatter)
return AgentPromptDefinition{ return AgentPromptDefinition{
Path: path, Path: path,
Raw: content, Raw: content,
Body: body, Body: body,
RawFrontmatter: frontmatter, 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) frontmatter = strings.TrimSpace(frontmatter)
if frontmatter == "" { if frontmatter == "" {
return AgentFrontmatter{} return AgentFrontmatter{}, nil
} }
rawFields := make(map[string]any) rawFields := make(map[string]any)
@ -167,7 +170,7 @@ func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter {
"path": path, "path": path,
"error": err.Error(), "error": err.Error(),
}) })
return AgentFrontmatter{} return AgentFrontmatter{}, err
} }
var typed struct { var typed struct {
@ -184,7 +187,7 @@ func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter {
"path": path, "path": path,
"error": err.Error(), "error": err.Error(),
}) })
return AgentFrontmatter{} return AgentFrontmatter{}, err
} }
return AgentFrontmatter{ return AgentFrontmatter{
@ -196,7 +199,7 @@ func parseAgentFrontmatter(path, frontmatter string) AgentFrontmatter {
Skills: append([]string(nil), typed.Skills...), Skills: append([]string(nil), typed.Skills...),
MCPServers: append([]string(nil), typed.MCPServers...), MCPServers: append([]string(nil), typed.MCPServers...),
Fields: rawFields, Fields: rawFields,
} }, nil
} }
func splitAgentFrontmatter(content string) (frontmatter, body string) { func splitAgentFrontmatter(content string) (frontmatter, body string) {
@ -253,3 +256,10 @@ func fileExists(path string) bool {
_, err := os.Stat(path) _, err := os.Stat(path)
return err == nil return err == nil
} }
func errorString(err error) string {
if err == nil {
return ""
}
return err.Error()
}

View file

@ -330,3 +330,39 @@ Use frontmatter identity.
t.Fatal("expected slack MCP server to be blocked by frontmatter allowlist") 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")
}
}

View file

@ -6,6 +6,9 @@ import (
) )
func resolveAgentToolAllowlist(definition AgentContextDefinition) []string { func resolveAgentToolAllowlist(definition AgentContextDefinition) []string {
if frontmatterParseFailed(definition) {
return []string{}
}
if definition.Agent == nil || definition.Agent.Frontmatter.Tools == nil { if definition.Agent == nil || definition.Agent.Frontmatter.Tools == nil {
return nil return nil
} }
@ -28,6 +31,9 @@ func resolveAgentToolAllowlist(definition AgentContextDefinition) []string {
} }
func resolveAgentMCPServerAllowlist(definition AgentContextDefinition) map[string]struct{} { func resolveAgentMCPServerAllowlist(definition AgentContextDefinition) map[string]struct{} {
if frontmatterParseFailed(definition) {
return map[string]struct{}{}
}
if definition.Agent == nil || definition.Agent.Frontmatter.MCPServers == nil { if definition.Agent == nil || definition.Agent.Frontmatter.MCPServers == nil {
return nil return nil
} }
@ -43,3 +49,13 @@ func resolveAgentMCPServerAllowlist(definition AgentContextDefinition) map[strin
return allowlist 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) != ""
}