223 lines
5.5 KiB
Go
223 lines
5.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
"github.com/sipeed/picoclaw/pkg/tools"
|
|
)
|
|
|
|
const dynamicMCPToolPrefix = "mcp_"
|
|
|
|
func normalizeMCPServerName(name string) string {
|
|
return strings.ToLower(strings.TrimSpace(name))
|
|
}
|
|
|
|
func normalizedMCPServerNameSet(
|
|
servers map[string]config.MCPServerConfig,
|
|
) map[string]struct{} {
|
|
normalized := make(map[string]struct{}, len(servers))
|
|
for serverName := range servers {
|
|
name := normalizeMCPServerName(serverName)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
normalized[name] = struct{}{}
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func warnOnUnknownAgentToolDeclarations(
|
|
agentID, workspace string,
|
|
definition AgentContextDefinition,
|
|
registry *tools.ToolRegistry,
|
|
) {
|
|
if registry == nil || frontmatterParseFailed(definition) {
|
|
return
|
|
}
|
|
|
|
if unknownTools := unknownAgentToolNames(registry, definition); len(unknownTools) > 0 {
|
|
logger.WarnCF("agent", "AGENT.md declares unregistered tool names",
|
|
map[string]any{
|
|
"agent_id": agentID,
|
|
"workspace": workspace,
|
|
"tools": unknownTools,
|
|
})
|
|
}
|
|
}
|
|
|
|
func warnOnUnknownAgentMCPServerDeclarations(
|
|
agentID, workspace string,
|
|
cfg *config.Config,
|
|
definition AgentContextDefinition,
|
|
) {
|
|
if cfg == nil || frontmatterParseFailed(definition) {
|
|
return
|
|
}
|
|
|
|
if unknownServers := unknownAgentMCPServerNames(cfg, definition); len(unknownServers) > 0 {
|
|
logger.WarnCF("agent", "AGENT.md declares unknown MCP server names",
|
|
map[string]any{
|
|
"agent_id": agentID,
|
|
"workspace": workspace,
|
|
"mcp_servers": unknownServers,
|
|
})
|
|
}
|
|
}
|
|
|
|
func unknownAgentToolNames(
|
|
registry *tools.ToolRegistry,
|
|
definition AgentContextDefinition,
|
|
) []string {
|
|
if definition.Agent == nil || definition.Agent.Frontmatter.ToolPolicy == nil {
|
|
return nil
|
|
}
|
|
|
|
known := registeredRuntimeToolNames(registry)
|
|
unknown := make(map[string]struct{})
|
|
for _, raw := range definition.Agent.Frontmatter.ToolPolicy.Allow {
|
|
name := strings.ToLower(strings.TrimSpace(raw))
|
|
if name == "" || strings.HasPrefix(name, dynamicMCPToolPrefix) || containsGlobMeta(name) {
|
|
continue
|
|
}
|
|
if _, ok := known[name]; ok {
|
|
continue
|
|
}
|
|
unknown[name] = struct{}{}
|
|
}
|
|
|
|
return sortedKeys(unknown)
|
|
}
|
|
|
|
func registeredRuntimeToolNames(registry *tools.ToolRegistry) map[string]struct{} {
|
|
known := make(map[string]struct{})
|
|
if registry == nil {
|
|
return known
|
|
}
|
|
for _, raw := range registry.List() {
|
|
name := strings.ToLower(strings.TrimSpace(raw))
|
|
if name == "" {
|
|
continue
|
|
}
|
|
known[name] = struct{}{}
|
|
}
|
|
return known
|
|
}
|
|
|
|
func unknownAgentMCPServerNames(cfg *config.Config, definition AgentContextDefinition) []string {
|
|
if cfg == nil || definition.Agent == nil || definition.Agent.Frontmatter.MCPPolicy == nil {
|
|
return nil
|
|
}
|
|
|
|
knownServers := normalizedMCPServerNameSet(cfg.Tools.MCP.Servers)
|
|
unknown := make(map[string]struct{})
|
|
for _, raw := range definition.Agent.Frontmatter.MCPPolicy.Allow {
|
|
name := normalizeMCPServerName(raw)
|
|
if name == "" || containsGlobMeta(name) {
|
|
continue
|
|
}
|
|
if _, ok := knownServers[name]; ok {
|
|
continue
|
|
}
|
|
unknown[name] = struct{}{}
|
|
}
|
|
|
|
return sortedKeys(unknown)
|
|
}
|
|
|
|
func sortedKeys(values map[string]struct{}) []string {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make([]string, 0, len(values))
|
|
for value := range values {
|
|
result = append(result, value)
|
|
}
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
func resolveAgentToolPolicy(definition AgentContextDefinition) *PatternPolicy {
|
|
if frontmatterParseFailed(definition) {
|
|
return &PatternPolicy{Allow: []string{}, form: patternPolicyFormList}
|
|
}
|
|
if definition.Agent == nil || !frontmatterDeclaresField(definition, "tools") {
|
|
return nil
|
|
}
|
|
return normalizePatternPolicy(definition.Agent.Frontmatter.ToolPolicy)
|
|
}
|
|
|
|
func resolveAgentMCPServerPolicy(definition AgentContextDefinition) *PatternPolicy {
|
|
if frontmatterParseFailed(definition) {
|
|
return &PatternPolicy{Allow: []string{}, form: patternPolicyFormList}
|
|
}
|
|
if definition.Agent == nil || !frontmatterDeclaresField(definition, "mcpServers") {
|
|
return nil
|
|
}
|
|
return normalizePatternPolicy(definition.Agent.Frontmatter.MCPPolicy)
|
|
}
|
|
|
|
func normalizePatternPolicy(policy *PatternPolicy) *PatternPolicy {
|
|
if policy == nil {
|
|
return nil
|
|
}
|
|
|
|
normalized := &PatternPolicy{form: policy.form}
|
|
normalized.Allow = normalizePatterns(policy.Allow)
|
|
normalized.Deny = normalizePatterns(policy.Deny)
|
|
|
|
if policy.form == patternPolicyFormList && len(normalized.Allow) == 0 {
|
|
normalized.Allow = []string{}
|
|
}
|
|
|
|
return normalized
|
|
}
|
|
|
|
func normalizePatterns(patterns []string) []string {
|
|
if len(patterns) == 0 {
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(patterns))
|
|
result := make([]string, 0, len(patterns))
|
|
for _, raw := range patterns {
|
|
trimmed := strings.ToLower(strings.TrimSpace(raw))
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[trimmed]; ok {
|
|
continue
|
|
}
|
|
seen[trimmed] = struct{}{}
|
|
result = append(result, trimmed)
|
|
}
|
|
if len(result) == 0 {
|
|
return nil
|
|
}
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
func containsGlobMeta(value string) bool {
|
|
return strings.ContainsAny(value, "*?[")
|
|
}
|
|
|
|
func frontmatterDeclaresField(definition AgentContextDefinition, field string) bool {
|
|
if definition.Agent == nil || definition.Agent.Frontmatter.Fields == nil {
|
|
return false
|
|
}
|
|
_, ok := definition.Agent.Frontmatter.Fields[field]
|
|
return ok
|
|
}
|
|
|
|
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) != ""
|
|
}
|