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 }