mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-04 05:37:28 +08:00
feat: expand mcp transports and skill execution
This commit is contained in:
@@ -105,6 +105,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
|
||||
toolsRegistry.Register(tools.NewAliasTool("edit", "Edit file content (OpenClaw-compatible alias of edit_file)", tools.NewEditFileTool(workspace), map[string]string{"file_path": "path", "old_string": "oldText", "new_string": "newText"}))
|
||||
toolsRegistry.Register(tools.NewExecTool(cfg.Tools.Shell, workspace, processManager))
|
||||
toolsRegistry.Register(tools.NewProcessTool(processManager))
|
||||
toolsRegistry.Register(tools.NewSkillExecTool(workspace))
|
||||
nodesManager := nodes.DefaultManager()
|
||||
nodesManager.SetAuditPath(filepath.Join(workspace, "memory", "nodes-audit.jsonl"))
|
||||
nodesManager.SetStatePath(filepath.Join(workspace, "memory", "nodes-state.json"))
|
||||
@@ -935,7 +936,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
"max": al.maxIterations,
|
||||
})
|
||||
|
||||
toolDefs := al.tools.GetDefinitions()
|
||||
toolDefs := al.filteredToolDefinitionsForContext(ctx)
|
||||
providerToolDefs := al.buildProviderToolDefs(toolDefs)
|
||||
|
||||
// Log LLM request details
|
||||
@@ -1301,7 +1302,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
|
||||
for iteration < al.maxIterations {
|
||||
iteration++
|
||||
|
||||
toolDefs := al.tools.GetDefinitions()
|
||||
toolDefs := al.filteredToolDefinitionsForContext(ctx)
|
||||
providerToolDefs := al.buildProviderToolDefs(toolDefs)
|
||||
|
||||
// Log LLM request details
|
||||
@@ -1459,6 +1460,37 @@ func (al *AgentLoop) buildProviderToolDefs(toolDefs []map[string]interface{}) []
|
||||
return providerToolDefs
|
||||
}
|
||||
|
||||
func (al *AgentLoop) filteredToolDefinitionsForContext(ctx context.Context) []map[string]interface{} {
|
||||
if al == nil || al.tools == nil {
|
||||
return nil
|
||||
}
|
||||
return filterToolDefinitionsByContext(ctx, al.tools.GetDefinitions())
|
||||
}
|
||||
|
||||
func filterToolDefinitionsByContext(ctx context.Context, toolDefs []map[string]interface{}) []map[string]interface{} {
|
||||
allow := toolAllowlistFromContext(ctx)
|
||||
if len(allow) == 0 {
|
||||
return toolDefs
|
||||
}
|
||||
|
||||
filtered := make([]map[string]interface{}, 0, len(toolDefs))
|
||||
for _, td := range toolDefs {
|
||||
fnRaw, ok := td["function"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := fnRaw["name"].(string)
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if isToolNameAllowed(allow, name) || isImplicitlyAllowedSubagentTool(name) {
|
||||
filtered = append(filtered, td)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (al *AgentLoop) buildResponsesOptions(sessionKey string, maxTokens int64, temperature float64) map[string]interface{} {
|
||||
options := map[string]interface{}{
|
||||
"max_tokens": maxTokens,
|
||||
@@ -1702,10 +1734,22 @@ func (al *AgentLoop) GetToolCatalog() []map[string]interface{} {
|
||||
if fmt.Sprint(item["source"]) != "mcp" {
|
||||
item["source"] = "local"
|
||||
}
|
||||
name := strings.TrimSpace(fmt.Sprint(item["name"]))
|
||||
item["visibility"] = map[string]interface{}{
|
||||
"main_agent": true,
|
||||
"subagent": subagentToolVisibilityMode(name),
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func subagentToolVisibilityMode(name string) string {
|
||||
if isImplicitlyAllowedSubagentTool(name) {
|
||||
return "inherited"
|
||||
}
|
||||
return "allowlist"
|
||||
}
|
||||
|
||||
// formatMessagesForLog formats messages for logging
|
||||
func formatMessagesForLog(messages []providers.Message) string {
|
||||
if len(messages) == 0 {
|
||||
@@ -1804,11 +1848,32 @@ func withToolContextArgs(toolName string, args map[string]interface{}, channel,
|
||||
return next
|
||||
}
|
||||
|
||||
func withToolRuntimeArgs(ctx context.Context, toolName string, args map[string]interface{}) map[string]interface{} {
|
||||
if strings.TrimSpace(toolName) != "skill_exec" {
|
||||
return args
|
||||
}
|
||||
ns := normalizeMemoryNamespace(memoryNamespaceFromContext(ctx))
|
||||
callerAgent := ns
|
||||
callerScope := "subagent"
|
||||
if callerAgent == "" || callerAgent == "main" {
|
||||
callerAgent = "main"
|
||||
callerScope = "main_agent"
|
||||
}
|
||||
next := make(map[string]interface{}, len(args)+2)
|
||||
for k, v := range args {
|
||||
next[k] = v
|
||||
}
|
||||
next["caller_agent"] = callerAgent
|
||||
next["caller_scope"] = callerScope
|
||||
return next
|
||||
}
|
||||
|
||||
func (al *AgentLoop) executeToolCall(ctx context.Context, toolName string, args map[string]interface{}, currentChannel, currentChatID string) (string, error) {
|
||||
if err := ensureToolAllowedByContext(ctx, toolName, args); err != nil {
|
||||
return "", err
|
||||
}
|
||||
args = withToolMemoryNamespaceArgs(toolName, args, memoryNamespaceFromContext(ctx))
|
||||
args = withToolRuntimeArgs(ctx, toolName, args)
|
||||
if shouldSuppressSelfMessageSend(toolName, args, currentChannel, currentChatID) {
|
||||
return "Suppressed message tool self-send in current chat; assistant will reply via normal outbound.", nil
|
||||
}
|
||||
@@ -1887,7 +1952,7 @@ func ensureToolAllowedByContext(ctx context.Context, toolName string, args map[s
|
||||
if name == "" {
|
||||
return fmt.Errorf("tool name is empty")
|
||||
}
|
||||
if !isToolNameAllowed(allow, name) {
|
||||
if !isToolNameAllowed(allow, name) && !isImplicitlyAllowedSubagentTool(name) {
|
||||
return fmt.Errorf("tool '%s' is not allowed by subagent profile", toolName)
|
||||
}
|
||||
|
||||
@@ -1914,13 +1979,22 @@ func validateParallelAllowlistArgs(allow map[string]struct{}, args map[string]in
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if !isToolNameAllowed(allow, name) {
|
||||
if !isToolNameAllowed(allow, name) && !isImplicitlyAllowedSubagentTool(name) {
|
||||
return fmt.Errorf("tool 'parallel' contains disallowed call[%d]: %s", i, tool)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isImplicitlyAllowedSubagentTool(name string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||
case "skill_exec":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeToolAllowlist(in []string) map[string]struct{} {
|
||||
expanded := tools.ExpandToolAllowlistEntries(in)
|
||||
if len(expanded) == 0 {
|
||||
|
||||
@@ -19,6 +19,9 @@ func TestEnsureToolAllowedByContext(t *testing.T) {
|
||||
if err := ensureToolAllowedByContext(restricted, "write_file", map[string]interface{}{}); err == nil {
|
||||
t.Fatalf("expected disallowed tool to fail")
|
||||
}
|
||||
if err := ensureToolAllowedByContext(restricted, "skill_exec", map[string]interface{}{}); err != nil {
|
||||
t.Fatalf("expected skill_exec to bypass subagent allowlist, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureToolAllowedByContextParallelNested(t *testing.T) {
|
||||
@@ -41,6 +44,15 @@ func TestEnsureToolAllowedByContextParallelNested(t *testing.T) {
|
||||
if err := ensureToolAllowedByContext(restricted, "parallel", badArgs); err == nil {
|
||||
t.Fatalf("expected parallel with disallowed nested tool to fail")
|
||||
}
|
||||
|
||||
skillArgs := map[string]interface{}{
|
||||
"calls": []interface{}{
|
||||
map[string]interface{}{"tool": "skill_exec", "arguments": map[string]interface{}{"skill": "demo", "script": "scripts/run.sh"}},
|
||||
},
|
||||
}
|
||||
if err := ensureToolAllowedByContext(restricted, "parallel", skillArgs); err != nil {
|
||||
t.Fatalf("expected parallel with nested skill_exec to pass, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureToolAllowedByContext_GroupAllowlist(t *testing.T) {
|
||||
@@ -52,3 +64,48 @@ func TestEnsureToolAllowedByContext_GroupAllowlist(t *testing.T) {
|
||||
t.Fatalf("expected files_read group to block write_file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilteredToolDefinitionsForContext(t *testing.T) {
|
||||
ctx := withToolAllowlistContext(context.Background(), []string{"read_file", "parallel"})
|
||||
toolDefs := []map[string]interface{}{
|
||||
{"function": map[string]interface{}{"name": "read_file", "description": "read", "parameters": map[string]interface{}{}}},
|
||||
{"function": map[string]interface{}{"name": "write_file", "description": "write", "parameters": map[string]interface{}{}}},
|
||||
{"function": map[string]interface{}{"name": "parallel", "description": "parallel", "parameters": map[string]interface{}{}}},
|
||||
{"function": map[string]interface{}{"name": "skill_exec", "description": "skill", "parameters": map[string]interface{}{}}},
|
||||
}
|
||||
filtered := filterToolDefinitionsByContext(ctx, toolDefs)
|
||||
got := map[string]bool{}
|
||||
for _, td := range filtered {
|
||||
fnRaw, _ := td["function"].(map[string]interface{})
|
||||
name, _ := fnRaw["name"].(string)
|
||||
got[name] = true
|
||||
}
|
||||
if !got["read_file"] || !got["parallel"] || !got["skill_exec"] {
|
||||
t.Fatalf("expected filtered tools to include read_file, parallel, and inherited skill_exec, got: %v", got)
|
||||
}
|
||||
if got["write_file"] {
|
||||
t.Fatalf("expected filtered tools to exclude write_file, got: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithToolRuntimeArgsForSkillExec(t *testing.T) {
|
||||
mainArgs := withToolRuntimeArgs(context.Background(), "skill_exec", map[string]interface{}{"skill": "demo"})
|
||||
if mainArgs["caller_agent"] != "main" || mainArgs["caller_scope"] != "main_agent" {
|
||||
t.Fatalf("expected main agent runtime args, got: %#v", mainArgs)
|
||||
}
|
||||
|
||||
subagentCtx := withMemoryNamespaceContext(context.Background(), "coder")
|
||||
subArgs := withToolRuntimeArgs(subagentCtx, "skill_exec", map[string]interface{}{"skill": "demo"})
|
||||
if subArgs["caller_agent"] != "coder" || subArgs["caller_scope"] != "subagent" {
|
||||
t.Fatalf("expected subagent runtime args, got: %#v", subArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubagentToolVisibilityMode(t *testing.T) {
|
||||
if got := subagentToolVisibilityMode("skill_exec"); got != "inherited" {
|
||||
t.Fatalf("expected skill_exec inherited, got %q", got)
|
||||
}
|
||||
if got := subagentToolVisibilityMode("write_file"); got != "allowlist" {
|
||||
t.Fatalf("expected write_file allowlist, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
37
pkg/agent/loop_skill_exec_test.go
Normal file
37
pkg/agent/loop_skill_exec_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"clawgo/pkg/bus"
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/providers"
|
||||
)
|
||||
|
||||
type stubLLMProvider struct{}
|
||||
|
||||
func (stubLLMProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
return &providers.LLMResponse{Content: "ok", FinishReason: "stop"}, nil
|
||||
}
|
||||
|
||||
func (stubLLMProvider) GetDefaultModel() string { return "stub-model" }
|
||||
|
||||
func TestNewAgentLoopRegistersSkillExec(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.Agents.Defaults.Workspace = t.TempDir()
|
||||
|
||||
loop := NewAgentLoop(cfg, bus.NewMessageBus(), stubLLMProvider{}, nil)
|
||||
catalog := loop.GetToolCatalog()
|
||||
|
||||
found := false
|
||||
for _, item := range catalog {
|
||||
if item["name"] == "skill_exec" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected skill_exec in tool catalog, got %v", catalog)
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,7 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
}
|
||||
}
|
||||
}
|
||||
toolInfo := al.describeSubagentTools(subcfg.Tools.Allowlist)
|
||||
items = append(items, map[string]interface{}{
|
||||
"agent_id": agentID,
|
||||
"enabled": subcfg.Enabled,
|
||||
@@ -140,6 +141,9 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
"prompt_file_found": promptFileFound,
|
||||
"memory_namespace": subcfg.MemoryNamespace,
|
||||
"tool_allowlist": append([]string(nil), subcfg.Tools.Allowlist...),
|
||||
"tool_visibility": toolInfo,
|
||||
"effective_tools": toolInfo["effective_tools"],
|
||||
"inherited_tools": toolInfo["inherited_tools"],
|
||||
"routing_keywords": routeKeywordsForRegistry(cfg.Agents.Router.Rules, agentID),
|
||||
"managed_by": "config.json",
|
||||
})
|
||||
@@ -151,6 +155,7 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
if strings.TrimSpace(profile.ManagedBy) != "node_registry" {
|
||||
continue
|
||||
}
|
||||
toolInfo := al.describeSubagentTools(profile.ToolAllowlist)
|
||||
items = append(items, map[string]interface{}{
|
||||
"agent_id": profile.AgentID,
|
||||
"enabled": strings.EqualFold(strings.TrimSpace(profile.Status), "active"),
|
||||
@@ -167,6 +172,9 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
"prompt_file_found": false,
|
||||
"memory_namespace": profile.MemoryNamespace,
|
||||
"tool_allowlist": append([]string(nil), profile.ToolAllowlist...),
|
||||
"tool_visibility": toolInfo,
|
||||
"effective_tools": toolInfo["effective_tools"],
|
||||
"inherited_tools": toolInfo["inherited_tools"],
|
||||
"routing_keywords": []string{},
|
||||
"managed_by": profile.ManagedBy,
|
||||
})
|
||||
@@ -435,6 +443,51 @@ func (al *AgentLoop) HandleSubagentRuntime(ctx context.Context, action string, a
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) describeSubagentTools(allowlist []string) map[string]interface{} {
|
||||
inherited := implicitSubagentTools()
|
||||
allTools := make([]string, 0)
|
||||
if al != nil && al.tools != nil {
|
||||
allTools = al.tools.List()
|
||||
sort.Strings(allTools)
|
||||
}
|
||||
|
||||
normalizedAllow := normalizeToolAllowlist(allowlist)
|
||||
mode := "allowlist"
|
||||
effective := make([]string, 0)
|
||||
if len(normalizedAllow) == 0 {
|
||||
mode = "unrestricted"
|
||||
effective = append(effective, allTools...)
|
||||
} else if _, ok := normalizedAllow["*"]; ok {
|
||||
mode = "all"
|
||||
effective = append(effective, allTools...)
|
||||
} else if _, ok := normalizedAllow["all"]; ok {
|
||||
mode = "all"
|
||||
effective = append(effective, allTools...)
|
||||
} else {
|
||||
for _, name := range allTools {
|
||||
if isToolNameAllowed(normalizedAllow, name) || isImplicitlyAllowedSubagentTool(name) {
|
||||
effective = append(effective, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"mode": mode,
|
||||
"raw_allowlist": append([]string(nil), allowlist...),
|
||||
"inherited_tools": inherited,
|
||||
"inherited_tool_count": len(inherited),
|
||||
"effective_tools": effective,
|
||||
"effective_tool_count": len(effective),
|
||||
}
|
||||
}
|
||||
|
||||
func implicitSubagentTools() []string {
|
||||
out := make([]string, 0, 1)
|
||||
if isImplicitlyAllowedSubagentTool("skill_exec") {
|
||||
out = append(out, "skill_exec")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeSubagentStream(events []tools.SubagentRunEvent, messages []tools.AgentMessage) []map[string]interface{} {
|
||||
items := make([]map[string]interface{}, 0, len(events)+len(messages))
|
||||
for _, evt := range events {
|
||||
|
||||
@@ -161,6 +161,27 @@ func TestHandleSubagentRuntimeRegistryAndToggleEnabled(t *testing.T) {
|
||||
if !ok || len(items) < 2 {
|
||||
t.Fatalf("expected registry items, got %#v", payload["items"])
|
||||
}
|
||||
var tester map[string]interface{}
|
||||
for _, item := range items {
|
||||
if item["agent_id"] == "tester" {
|
||||
tester = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if tester == nil {
|
||||
t.Fatalf("expected tester registry item, got %#v", items)
|
||||
}
|
||||
toolVisibility, _ := tester["tool_visibility"].(map[string]interface{})
|
||||
if toolVisibility == nil {
|
||||
t.Fatalf("expected tool_visibility in tester registry item, got %#v", tester)
|
||||
}
|
||||
if toolVisibility["mode"] != "allowlist" {
|
||||
t.Fatalf("expected tester tool mode allowlist, got %#v", toolVisibility)
|
||||
}
|
||||
inherited, _ := tester["inherited_tools"].([]string)
|
||||
if len(inherited) != 1 || inherited[0] != "skill_exec" {
|
||||
t.Fatalf("expected inherited skill_exec, got %#v", tester["inherited_tools"])
|
||||
}
|
||||
|
||||
_, err = loop.HandleSubagentRuntime(context.Background(), "set_config_subagent_enabled", map[string]interface{}{
|
||||
"agent_id": "tester",
|
||||
|
||||
Reference in New Issue
Block a user