From ec34f488dfc293bf3fa3034b275c5aa89678c208 Mon Sep 17 00:00:00 2001 From: viettranx Date: Wed, 11 Mar 2026 17:33:28 +0700 Subject: [PATCH] feat(tools): add tool alias registry for Claude Code skill compatibility Add alias support to the tool Registry so Claude Code skills can reference Anthropic tool names (Read, Write, Edit, Bash, etc.) and have them resolve to GoClaw canonical tools (read_file, write_file, edit, exec, etc.). - Registry: aliases map, RegisterAlias(), resolve(), Aliases() - Policy engine: auto-includes alias defs when canonical tool passes filter - System prompt: alias entries in coreToolSummaries + missing use_skill - Legacy toolAliases migrated to Registry.RegisterAlias() at startup --- cmd/gateway.go | 22 ++++++++++++ internal/agent/systemprompt.go | 12 +++++++ internal/tools/policy.go | 31 ++++++++++++++-- internal/tools/registry.go | 65 ++++++++++++++++++++++++++++++---- 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/cmd/gateway.go b/cmd/gateway.go index 6dd69276..9dbbaaf3 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -536,6 +536,28 @@ func runGateway() { toolsReg.Register(tools.NewMessageTool()) slog.Info("session + message tools registered") + // Register legacy tool aliases (backward-compat names from policy.go). + for alias, canonical := range tools.LegacyToolAliases() { + toolsReg.RegisterAlias(alias, canonical) + } + + // Register Claude Code tool aliases so Claude Code skills work without modification. + // LLM calls alias name → registry resolves to canonical tool → executes. + for alias, canonical := range map[string]string{ + "Read": "read_file", + "Write": "write_file", + "Edit": "edit", + "Bash": "exec", + "WebFetch": "web_fetch", + "WebSearch": "web_search", + "Agent": "spawn", + "Skill": "use_skill", + "ToolSearch": "mcp_tool_search", + } { + toolsReg.RegisterAlias(alias, canonical) + } + slog.Info("tool aliases registered", "count", len(toolsReg.Aliases())) + // Allow read_file to access skills directories and CLI workspaces (outside workspace). // Skills can live in ~/.goclaw/skills/, ~/.agents/skills/, ~/.goclaw/skills-store/, etc. // CLI workspaces live in ~/.goclaw/cli-workspaces/ (agent working files). diff --git a/internal/agent/systemprompt.go b/internal/agent/systemprompt.go index f861dbd5..fc9a550e 100644 --- a/internal/agent/systemprompt.go +++ b/internal/agent/systemprompt.go @@ -64,6 +64,7 @@ var coreToolSummaries = map[string]string{ "web_fetch": "Fetch and extract content from a URL", "cron": "Manage scheduled jobs and reminders", "skill_search": "Search available skills by keyword (weather, translate, github, etc.)", + "use_skill": "Invoke a skill by name and follow its instructions", "mcp_tool_search": "Search for available MCP external integration tools by keyword", "browser": "Browse web pages interactively", "tts": "Convert text to speech audio", @@ -86,6 +87,17 @@ var coreToolSummaries = map[string]string{ "delegate_search": "Search for agents by expertise to find the right delegation target", "team_tasks": "Manage team task board (list, create, complete, cancel tasks)", "team_message": "Send messages to teammates (progress updates, questions)", + + // Claude Code tool aliases — enable Claude Code skills without modification + "Read": "Alias for read_file — Read file contents", + "Write": "Alias for write_file — Create or overwrite files", + "Edit": "Alias for edit — Edit a file by replacing exact text matches", + "Bash": "Alias for exec — Run shell commands", + "WebFetch": "Alias for web_fetch — Fetch and extract content from a URL", + "WebSearch": "Alias for web_search — Search the web", + "Agent": "Alias for spawn — Spawn a subagent or delegate to another agent", + "Skill": "Alias for use_skill — Invoke a skill by name", + "ToolSearch": "Alias for mcp_tool_search — Search for available MCP tools", } // BuildSystemPrompt constructs the full system prompt with all sections. diff --git a/internal/tools/policy.go b/internal/tools/policy.go index c617f8e6..c286530f 100644 --- a/internal/tools/policy.go +++ b/internal/tools/policy.go @@ -54,14 +54,20 @@ var toolProfiles = map[string][]string{ "full": {}, // empty = no restrictions } -// Tool aliases map alternative names to canonical names. -var toolAliases = map[string]string{ +// Legacy tool aliases — migrated to Registry.RegisterAlias() at startup. +// Kept as seed data only; resolveAlias() is no longer used. +var legacyToolAliases = map[string]string{ "bash": "exec", "apply-patch": "apply_patch", "edit_file": "edit", "sessions_spawn": "spawn", } +// LegacyToolAliases returns legacy aliases for registration into the Registry. +func LegacyToolAliases() map[string]string { + return legacyToolAliases +} + // Subagent deny lists — tools subagents cannot use. var subagentDenyList = []string{ "exec", // subagents should not shell out — main agent can still exec @@ -107,11 +113,30 @@ func (pe *PolicyEngine) FilterTools( } // Resolve aliases and build definitions + allowedSet := make(map[string]bool, len(allowed)) var defs []providers.ToolDefinition for _, name := range allowed { canonical := resolveAlias(name) if tool, ok := registry.Get(canonical); ok { defs = append(defs, ToProviderDef(tool)) + allowedSet[canonical] = true + } + } + + // Add registry aliases for allowed canonical tools + for alias, canonical := range registry.Aliases() { + if !allowedSet[canonical] { + continue + } + if tool, ok := registry.Get(canonical); ok { + defs = append(defs, providers.ToolDefinition{ + Type: "function", + Function: providers.ToolFunctionSchema{ + Name: alias, + Description: tool.Description(), + Parameters: tool.Parameters(), + }, + }) } } @@ -322,7 +347,7 @@ func unionWithSpec(current []string, allTools []string, spec []string) []string } func resolveAlias(name string) string { - if canonical, ok := toolAliases[name]; ok { + if canonical, ok := legacyToolAliases[name]; ok { return canonical } return name diff --git a/internal/tools/registry.go b/internal/tools/registry.go index 5fb54a36..95c789f5 100644 --- a/internal/tools/registry.go +++ b/internal/tools/registry.go @@ -13,6 +13,7 @@ import ( // Registry manages tool registration and execution. type Registry struct { tools map[string]Tool + aliases map[string]string // alias name → canonical tool name mu sync.RWMutex rateLimiter *ToolRateLimiter // nil = no rate limiting scrubbing bool // scrub credentials from output (default true) @@ -21,6 +22,7 @@ type Registry struct { func NewRegistry() *Registry { return &Registry{ tools: make(map[string]Tool), + aliases: make(map[string]string), scrubbing: true, // enabled by default } } @@ -42,12 +44,44 @@ func (r *Registry) Register(tool Tool) { r.tools[tool.Name()] = tool } -// Get returns a tool by name. +// RegisterAlias maps an alias name to a canonical tool name. +// Rejected if alias collides with an existing real tool. +func (r *Registry) RegisterAlias(alias, canonical string) { + r.mu.Lock() + defer r.mu.Unlock() + if _, exists := r.tools[alias]; exists { + slog.Warn("alias conflicts with registered tool", "alias", alias, "canonical", canonical) + return + } + r.aliases[alias] = canonical +} + +// Aliases returns a copy of the alias map. +func (r *Registry) Aliases() map[string]string { + r.mu.RLock() + defer r.mu.RUnlock() + cp := make(map[string]string, len(r.aliases)) + maps.Copy(cp, r.aliases) + return cp +} + +// resolve looks up a tool by name, checking real tools first, then aliases. +func (r *Registry) resolve(name string) (Tool, bool) { + if t, ok := r.tools[name]; ok { + return t, true + } + if canonical, ok := r.aliases[name]; ok { + t, ok := r.tools[canonical] + return t, ok + } + return nil, false +} + +// Get returns a tool by name (checks real tools first, then aliases). func (r *Registry) Get(name string) (Tool, bool) { r.mu.RLock() defer r.mu.RUnlock() - t, ok := r.tools[name] - return t, ok + return r.resolve(name) } // Unregister removes a tool from the registry by name. @@ -70,7 +104,7 @@ func (r *Registry) Execute(ctx context.Context, name string, args map[string]any // making tool instances thread-safe for concurrent execution. func (r *Registry) ExecuteWithContext(ctx context.Context, name string, args map[string]any, channel, chatID, peerKind, sessionKey string, asyncCB AsyncCallback) *Result { r.mu.RLock() - tool, ok := r.tools[name] + tool, ok := r.resolve(name) r.mu.RUnlock() if !ok { @@ -127,18 +161,33 @@ func (r *Registry) ExecuteWithContext(ctx context.Context, name string, args map } // ProviderDefs returns tool definitions for LLM provider APIs. +// Includes alias definitions (same params/description, alias name). func (r *Registry) ProviderDefs() []providers.ToolDefinition { r.mu.RLock() defer r.mu.RUnlock() - defs := make([]providers.ToolDefinition, 0, len(r.tools)) + defs := make([]providers.ToolDefinition, 0, len(r.tools)+len(r.aliases)) for _, tool := range r.tools { defs = append(defs, ToProviderDef(tool)) } + for alias, canonical := range r.aliases { + tool, ok := r.tools[canonical] + if !ok { + continue + } + defs = append(defs, providers.ToolDefinition{ + Type: "function", + Function: providers.ToolFunctionSchema{ + Name: alias, + Description: tool.Description(), + Parameters: tool.Parameters(), + }, + }) + } return defs } -// List returns all registered tool names. +// List returns all registered canonical tool names (excludes aliases). func (r *Registry) List() []string { r.mu.RLock() defer r.mu.RUnlock() @@ -156,7 +205,7 @@ func (r *Registry) Count() int { return len(r.tools) } -// Clone creates a shallow copy of the registry with all registered tools. +// Clone creates a shallow copy of the registry with all registered tools and aliases. // The clone shares the rate limiter (thread-safe) and scrubbing setting. // Used by subagent toolsFactory so subagents inherit parent tools (web_fetch, web_search, etc.). func (r *Registry) Clone() *Registry { @@ -164,9 +213,11 @@ func (r *Registry) Clone() *Registry { defer r.mu.RUnlock() clone := &Registry{ tools: make(map[string]Tool, len(r.tools)), + aliases: make(map[string]string, len(r.aliases)), rateLimiter: r.rateLimiter, scrubbing: r.scrubbing, } maps.Copy(clone.tools, r.tools) + maps.Copy(clone.aliases, r.aliases) return clone }