diff --git a/cmd/picoclaw/internal/gateway/helpers.go b/cmd/picoclaw/internal/gateway/helpers.go
index a06625dc9..8734352d8 100644
--- a/cmd/picoclaw/internal/gateway/helpers.go
+++ b/cmd/picoclaw/internal/gateway/helpers.go
@@ -24,6 +24,7 @@ import (
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
+ cron_tool "github.com/sipeed/picoclaw/pkg/tools/cron"
"github.com/sipeed/picoclaw/pkg/voice"
)
@@ -232,14 +233,15 @@ func setupCronTool(
cronService := cron.NewCronService(cronStorePath, nil)
// Create and register CronTool
- cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
- agentLoop.RegisterTool(cronTool)
-
- // Set the onJob handler
- cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
- result := cronTool.ExecuteJob(context.Background(), job)
- return result, nil
- })
-
+ if cfg.Tools.Cron.Enabled {
+ cronTool := cron_tool.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, cfg)
+ agentLoop.RegisterTool(cronTool)
+
+ // Set the onJob handler
+ cronService.SetOnJob(func(job *cron.CronJob) (string, error) {
+ result := cronTool.ExecuteJob(context.Background(), job)
+ return result, nil
+ })
+ }
return cronService
}
diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go
index a6fd365c7..1b20a2aac 100644
--- a/pkg/agent/instance.go
+++ b/pkg/agent/instance.go
@@ -47,13 +47,7 @@ func NewAgentInstance(
fallbacks := resolveAgentFallbacks(agentCfg, defaults)
restrict := defaults.RestrictToWorkspace
- toolsRegistry := tools.NewToolRegistry()
- toolsRegistry.Register(tools.NewReadFileTool(workspace, restrict))
- toolsRegistry.Register(tools.NewWriteFileTool(workspace, restrict))
- toolsRegistry.Register(tools.NewListDirTool(workspace, restrict))
- toolsRegistry.Register(tools.NewExecToolWithConfig(workspace, restrict, cfg))
- toolsRegistry.Register(tools.NewEditFileTool(workspace, restrict))
- toolsRegistry.Register(tools.NewAppendFileTool(workspace, restrict))
+ toolsRegistry := tools.NewToolRegistry(cfg, workspace, restrict)
sessionsDir := filepath.Join(workspace, "sessions")
sessionsManager := session.NewSessionManager(sessionsDir)
diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go
index 693f2227b..5ed27ad05 100644
--- a/pkg/agent/loop.go
+++ b/pkg/agent/loop.go
@@ -23,9 +23,10 @@ import (
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/providers"
"github.com/sipeed/picoclaw/pkg/routing"
- "github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
+ "github.com/sipeed/picoclaw/pkg/tools/message"
+ "github.com/sipeed/picoclaw/pkg/tools/subagent"
"github.com/sipeed/picoclaw/pkg/utils"
)
@@ -92,63 +93,31 @@ func registerSharedTools(
continue
}
- // Web tools
- if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
- BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
- BraveMaxResults: cfg.Tools.Web.Brave.MaxResults,
- BraveEnabled: cfg.Tools.Web.Brave.Enabled,
- TavilyAPIKey: cfg.Tools.Web.Tavily.APIKey,
- TavilyBaseURL: cfg.Tools.Web.Tavily.BaseURL,
- TavilyMaxResults: cfg.Tools.Web.Tavily.MaxResults,
- TavilyEnabled: cfg.Tools.Web.Tavily.Enabled,
- DuckDuckGoMaxResults: cfg.Tools.Web.DuckDuckGo.MaxResults,
- DuckDuckGoEnabled: cfg.Tools.Web.DuckDuckGo.Enabled,
- PerplexityAPIKey: cfg.Tools.Web.Perplexity.APIKey,
- PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
- PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled,
- Proxy: cfg.Tools.Web.Proxy,
- }); searchTool != nil {
- agent.Tools.Register(searchTool)
- }
- agent.Tools.Register(tools.NewWebFetchToolWithProxy(50000, cfg.Tools.Web.Proxy))
-
- // Hardware tools (I2C, SPI) - Linux only, returns error on other platforms
- agent.Tools.Register(tools.NewI2CTool())
- agent.Tools.Register(tools.NewSPITool())
-
// Message tool
- messageTool := tools.NewMessageTool()
- messageTool.SetSendCallback(func(channel, chatID, content string) error {
- msgBus.PublishOutbound(bus.OutboundMessage{
- Channel: channel,
- ChatID: chatID,
- Content: content,
+ if cfg.Tools.Message.Enabled {
+ messageTool := message.NewMessageTool()
+ messageTool.SetSendCallback(func(channel, chatID, content string) error {
+ msgBus.PublishOutbound(bus.OutboundMessage{
+ Channel: channel,
+ ChatID: chatID,
+ Content: content,
+ })
+ return nil
})
- return nil
- })
- agent.Tools.Register(messageTool)
-
- // Skill discovery and installation tools
- registryMgr := skills.NewRegistryManagerFromConfig(skills.RegistryConfig{
- MaxConcurrentSearches: cfg.Tools.Skills.MaxConcurrentSearches,
- ClawHub: skills.ClawHubConfig(cfg.Tools.Skills.Registries.ClawHub),
- })
- searchCache := skills.NewSearchCache(
- cfg.Tools.Skills.SearchCache.MaxSize,
- time.Duration(cfg.Tools.Skills.SearchCache.TTLSeconds)*time.Second,
- )
- agent.Tools.Register(tools.NewFindSkillsTool(registryMgr, searchCache))
- agent.Tools.Register(tools.NewInstallSkillTool(registryMgr, agent.Workspace))
+ agent.Tools.Register(messageTool)
+ }
// Spawn tool with allowlist checker
- subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus)
- subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
- spawnTool := tools.NewSpawnTool(subagentManager)
- currentAgentID := agentID
- spawnTool.SetAllowlistChecker(func(targetAgentID string) bool {
- return registry.CanSpawnSubagent(currentAgentID, targetAgentID)
- })
- agent.Tools.Register(spawnTool)
+ if cfg.Tools.Spawn.Enabled {
+ subagentManager := subagent.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus)
+ subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
+ spawnTool := subagent.NewSpawnTool(subagentManager)
+ currentAgentID := agentID
+ spawnTool.SetAllowlistChecker(func(targetAgentID string) bool {
+ return registry.CanSpawnSubagent(currentAgentID, targetAgentID)
+ })
+ agent.Tools.Register(spawnTool)
+ }
}
}
@@ -178,7 +147,7 @@ func (al *AgentLoop) Run(ctx context.Context) error {
defaultAgent := al.registry.GetDefaultAgent()
if defaultAgent != nil {
if tool, ok := defaultAgent.Tools.Get("message"); ok {
- if mt, ok := tool.(*tools.MessageTool); ok {
+ if mt, ok := tool.(*message.MessageTool); ok {
alreadySent = mt.HasSentInRound()
}
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index ca5803c35..9306e6978 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -452,6 +452,7 @@ type PerplexityConfig struct {
}
type WebToolsConfig struct {
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_WEB_ENABLED"`
Brave BraveConfig `json:"brave"`
Tavily TavilyConfig `json:"tavily"`
DuckDuckGo DuckDuckGoConfig `json:"duckduckgo"`
@@ -461,19 +462,53 @@ type WebToolsConfig struct {
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_TOOLS_WEB_PROXY"`
}
-type CronToolsConfig struct {
- ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
+type CronToolConfig struct {
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_CRON_ENABLED"`
+ ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
+}
+
+type ToolConfig struct {
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_ENABLED"` // Default env var, can be overridden per tool
}
type ExecConfig struct {
+ Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_EXEC_ENABLED"`
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
}
type ToolsConfig struct {
- Web WebToolsConfig `json:"web"`
- Cron CronToolsConfig `json:"cron"`
- Exec ExecConfig `json:"exec"`
+ // Web tools
+ Web WebToolsConfig `json:"web"`
+
+ // Cron tools
+ Cron CronToolConfig `json:"cron"`
+
+ // File tools
+ ReadFile ToolConfig `json:"read_file" env:"PICOCLAW_TOOLS_READ_FILE_ENABLED"`
+ WriteFile ToolConfig `json:"write_file" env:"PICOCLAW_TOOLS_WRITE_FILE_ENABLED"`
+ EditFile ToolConfig `json:"edit_file" env:"PICOCLAW_TOOLS_EDIT_FILE_ENABLED"`
+ AppendFile ToolConfig `json:"append_file" env:"PICOCLAW_TOOLS_APPEND_FILE_ENABLED"`
+ ListDir ToolConfig `json:"list_dir" env:"PICOCLAW_TOOLS_LIST_DIR_ENABLED"`
+
+ // Exec tool
+ Exec ExecConfig `json:"exec"`
+
+ // Skills tools
+ FindSkills ToolConfig `json:"find_skills" env:"PICOCLAW_TOOLS_FIND_SKILLS_ENABLED"`
+ InstallSkill ToolConfig `json:"install_skill" env:"PICOCLAW_TOOLS_INSTALL_SKILL_ENABLED"`
+
+ // Subagent tools
+ Spawn ToolConfig `json:"spawn" env:"PICOCLAW_TOOLS_SPAWN_ENABLED"`
+
+ // Message tool
+ Message ToolConfig `json:"message" env:"PICOCLAW_TOOLS_MESSAGE_ENABLED"`
+
+ // Hardware tools
+ I2C ToolConfig `json:"i2c" env:"PICOCLAW_TOOLS_I2C_ENABLED"`
+ SPI ToolConfig `json:"spi" env:"PICOCLAW_TOOLS_SPI_ENABLED"`
+
+ // Skills configuration (registry, cache, etc.)
Skills SkillsToolsConfig `json:"skills"`
}
diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go
index cf799140d..60431608b 100644
--- a/pkg/config/defaults.go
+++ b/pkg/config/defaults.go
@@ -293,12 +293,53 @@ func DefaultConfig() *Config {
MaxResults: 5,
},
},
- Cron: CronToolsConfig{
+ Cron: CronToolConfig{
+ Enabled: true,
ExecTimeoutMinutes: 5,
},
+ // File tools - each individually configurable
+ ReadFile: ToolConfig{
+ Enabled: true,
+ },
+ WriteFile: ToolConfig{
+ Enabled: true,
+ },
+ EditFile: ToolConfig{
+ Enabled: false,
+ },
+ AppendFile: ToolConfig{
+ Enabled: false,
+ },
+ ListDir: ToolConfig{
+ Enabled: false,
+ },
+ // Exec tool
Exec: ExecConfig{
+ Enabled: true,
EnableDenyPatterns: true,
},
+ // Skills tools
+ FindSkills: ToolConfig{
+ Enabled: true,
+ },
+ InstallSkill: ToolConfig{
+ Enabled: true,
+ },
+ // Subagent tools
+ Spawn: ToolConfig{
+ Enabled: true,
+ },
+ // Message tool
+ Message: ToolConfig{
+ Enabled: true,
+ },
+ // Hardware tools
+ I2C: ToolConfig{
+ Enabled: false,
+ },
+ SPI: ToolConfig{
+ Enabled: false,
+ },
Skills: SkillsToolsConfig{
Registries: SkillsRegistriesConfig{
ClawHub: ClawHubRegistryConfig{
diff --git a/pkg/tools/append_file/append_file.go b/pkg/tools/append_file/append_file.go
new file mode 100644
index 000000000..855869b8d
--- /dev/null
+++ b/pkg/tools/append_file/append_file.go
@@ -0,0 +1,77 @@
+package append_file
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io/fs"
+
+ "github.com/sipeed/picoclaw/pkg/tools/common"
+)
+
+type AppendFileTool struct {
+ fs common.FileSystem
+}
+
+func NewAppendFileTool(workspace string, restrict bool) *AppendFileTool {
+ var fs common.FileSystem
+ if restrict {
+ fs = &common.SandboxFs{Workspace: workspace}
+ } else {
+ fs = &common.HostFs{}
+ }
+ return &AppendFileTool{fs: fs}
+}
+
+func (t *AppendFileTool) Name() string {
+ return "append_file"
+}
+
+func (t *AppendFileTool) Description() string {
+ return "Append content to the end of a file"
+}
+
+func (t *AppendFileTool) Parameters() map[string]any {
+ return map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "path": map[string]any{
+ "type": "string",
+ "description": "The file path to append to",
+ },
+ "content": map[string]any{
+ "type": "string",
+ "description": "The content to append",
+ },
+ },
+ "required": []string{"path", "content"},
+ }
+}
+
+func (t *AppendFileTool) Execute(ctx context.Context, args map[string]any) *common.ToolResult {
+ path, ok := args["path"].(string)
+ if !ok {
+ return common.ErrorResult("path is required")
+ }
+
+ content, ok := args["content"].(string)
+ if !ok {
+ return common.ErrorResult("content is required")
+ }
+
+ if err := appendFile(t.fs, path, content); err != nil {
+ return common.ErrorResult(err.Error())
+ }
+ return common.SilentResult(fmt.Sprintf("Appended to %s", path))
+}
+
+// appendFile reads the existing content (if any) via sysFs, appends new content, and writes back.
+func appendFile(sysFs common.FileSystem, path, appendContent string) error {
+ content, err := sysFs.ReadFile(path)
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
+ return err
+ }
+
+ newContent := append(content, []byte(appendContent)...)
+ return sysFs.WriteFile(path, newContent)
+}
diff --git a/pkg/tools/append_file/append_file_test.go b/pkg/tools/append_file/append_file_test.go
new file mode 100644
index 000000000..4e10aaede
--- /dev/null
+++ b/pkg/tools/append_file/append_file_test.go
@@ -0,0 +1,103 @@
+package append_file
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// TestAppendFileTool_AppendToExisting verifies appending to an existing file
+func TestAppendFileTool_AppendToExisting(t *testing.T) {
+ tmpDir := t.TempDir()
+ testFile := filepath.Join(tmpDir, "test.txt")
+ os.WriteFile(testFile, []byte("Hello World"), 0o644)
+
+ tool := NewAppendFileTool(tmpDir, true)
+ ctx := context.Background()
+ args := map[string]any{
+ "path": testFile,
+ "content": "\nAppended text",
+ }
+
+ result := tool.Execute(ctx, args)
+
+ assert.False(t, result.IsError, "Expected success, got error: %s", result.ForLLM)
+ assert.True(t, result.Silent, "Expected Silent=true for AppendFile")
+
+ content, err := os.ReadFile(testFile)
+ assert.NoError(t, err)
+ assert.Contains(t, string(content), "Appended text")
+ assert.Contains(t, string(content), "Hello World")
+}
+
+// TestAppendFileTool_AppendToNonExistent verifies appending to a non-existent file creates it
+func TestAppendFileTool_AppendToNonExistent(t *testing.T) {
+ tmpDir := t.TempDir()
+ testFile := filepath.Join(tmpDir, "newfile.txt")
+
+ tool := NewAppendFileTool(tmpDir, true)
+ ctx := context.Background()
+ args := map[string]any{
+ "path": testFile,
+ "content": "First content",
+ }
+
+ result := tool.Execute(ctx, args)
+
+ assert.False(t, result.IsError, "Expected success, got error: %s", result.ForLLM)
+
+ content, err := os.ReadFile(testFile)
+ assert.NoError(t, err)
+ assert.Equal(t, "First content", string(content))
+}
+
+// TestAppendFileTool_MissingPath verifies error handling for missing path
+func TestAppendFileTool_MissingPath(t *testing.T) {
+ tool := NewAppendFileTool("", false)
+ ctx := context.Background()
+ args := map[string]any{
+ "content": "Some content",
+ }
+
+ result := tool.Execute(ctx, args)
+
+ assert.True(t, result.IsError)
+ assert.Contains(t, result.ForLLM, "path is required")
+}
+
+// TestAppendFileTool_MissingContent verifies error handling for missing content
+func TestAppendFileTool_MissingContent(t *testing.T) {
+ tool := NewAppendFileTool("", false)
+ ctx := context.Background()
+ args := map[string]any{
+ "path": "/tmp/test.txt",
+ }
+
+ result := tool.Execute(ctx, args)
+
+ assert.True(t, result.IsError)
+ assert.Contains(t, result.ForLLM, "content is required")
+}
+
+// TestAppendFileTool_RestrictedMode verifies access control
+func TestAppendFileTool_RestrictedMode(t *testing.T) {
+ tmpDir := t.TempDir()
+ testFile := filepath.Join(tmpDir, "test.txt")
+ os.WriteFile(testFile, []byte("Original"), 0o644)
+
+ tool := NewAppendFileTool(tmpDir, true)
+ ctx := context.Background()
+
+ // Try to append to a file outside the workspace
+ args := map[string]any{
+ "path": "/etc/passwd",
+ "content": "Malicious content",
+ }
+
+ result := tool.Execute(ctx, args)
+
+ assert.True(t, result.IsError)
+}
diff --git a/pkg/tools/filesystem.go b/pkg/tools/common/filesystem.go
similarity index 58%
rename from pkg/tools/filesystem.go
rename to pkg/tools/common/filesystem.go
index 03d461dcc..7590db276 100644
--- a/pkg/tools/filesystem.go
+++ b/pkg/tools/common/filesystem.go
@@ -1,7 +1,6 @@
-package tools
+package common
import (
- "context"
"fmt"
"io/fs"
"os"
@@ -13,7 +12,7 @@ import (
)
// validatePath ensures the given path is within the workspace if restrict is true.
-func validatePath(path, workspace string, restrict bool) (string, error) {
+func ValidatePath(path, workspace string, restrict bool) (string, error) {
if workspace == "" {
return path, fmt.Errorf("workspace is not defined")
}
@@ -83,183 +82,18 @@ func isWithinWorkspace(candidate, workspace string) bool {
return err == nil && filepath.IsLocal(rel)
}
-type ReadFileTool struct {
- fs fileSystem
-}
-
-func NewReadFileTool(workspace string, restrict bool) *ReadFileTool {
- var fs fileSystem
- if restrict {
- fs = &sandboxFs{workspace: workspace}
- } else {
- fs = &hostFs{}
- }
- return &ReadFileTool{fs: fs}
-}
-
-func (t *ReadFileTool) Name() string {
- return "read_file"
-}
-
-func (t *ReadFileTool) Description() string {
- return "Read the contents of a file"
-}
-
-func (t *ReadFileTool) Parameters() map[string]any {
- return map[string]any{
- "type": "object",
- "properties": map[string]any{
- "path": map[string]any{
- "type": "string",
- "description": "Path to the file to read",
- },
- },
- "required": []string{"path"},
- }
-}
-
-func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
- path, ok := args["path"].(string)
- if !ok {
- return ErrorResult("path is required")
- }
-
- content, err := t.fs.ReadFile(path)
- if err != nil {
- return ErrorResult(err.Error())
- }
- return NewToolResult(string(content))
-}
-
-type WriteFileTool struct {
- fs fileSystem
-}
-
-func NewWriteFileTool(workspace string, restrict bool) *WriteFileTool {
- var fs fileSystem
- if restrict {
- fs = &sandboxFs{workspace: workspace}
- } else {
- fs = &hostFs{}
- }
- return &WriteFileTool{fs: fs}
-}
-
-func (t *WriteFileTool) Name() string {
- return "write_file"
-}
-
-func (t *WriteFileTool) Description() string {
- return "Write content to a file"
-}
-
-func (t *WriteFileTool) Parameters() map[string]any {
- return map[string]any{
- "type": "object",
- "properties": map[string]any{
- "path": map[string]any{
- "type": "string",
- "description": "Path to the file to write",
- },
- "content": map[string]any{
- "type": "string",
- "description": "Content to write to the file",
- },
- },
- "required": []string{"path", "content"},
- }
-}
-
-func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
- path, ok := args["path"].(string)
- if !ok {
- return ErrorResult("path is required")
- }
-
- content, ok := args["content"].(string)
- if !ok {
- return ErrorResult("content is required")
- }
-
- if err := t.fs.WriteFile(path, []byte(content)); err != nil {
- return ErrorResult(err.Error())
- }
-
- return SilentResult(fmt.Sprintf("File written: %s", path))
-}
-
-type ListDirTool struct {
- fs fileSystem
-}
-
-func NewListDirTool(workspace string, restrict bool) *ListDirTool {
- var fs fileSystem
- if restrict {
- fs = &sandboxFs{workspace: workspace}
- } else {
- fs = &hostFs{}
- }
- return &ListDirTool{fs: fs}
-}
-
-func (t *ListDirTool) Name() string {
- return "list_dir"
-}
-
-func (t *ListDirTool) Description() string {
- return "List files and directories in a path"
-}
-
-func (t *ListDirTool) Parameters() map[string]any {
- return map[string]any{
- "type": "object",
- "properties": map[string]any{
- "path": map[string]any{
- "type": "string",
- "description": "Path to list",
- },
- },
- "required": []string{"path"},
- }
-}
-
-func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
- path, ok := args["path"].(string)
- if !ok {
- path = "."
- }
-
- entries, err := t.fs.ReadDir(path)
- if err != nil {
- return ErrorResult(fmt.Sprintf("failed to read directory: %v", err))
- }
- return formatDirEntries(entries)
-}
-
-func formatDirEntries(entries []os.DirEntry) *ToolResult {
- var result strings.Builder
- for _, entry := range entries {
- if entry.IsDir() {
- result.WriteString("DIR: " + entry.Name() + "\n")
- } else {
- result.WriteString("FILE: " + entry.Name() + "\n")
- }
- }
- return NewToolResult(result.String())
-}
-
-// fileSystem abstracts reading, writing, and listing files, allowing both
+// FileSystem abstracts reading, writing, and listing files, allowing both
// unrestricted (host filesystem) and sandbox (os.Root) implementations to share the same polymorphic interface.
-type fileSystem interface {
+type FileSystem interface {
ReadFile(path string) ([]byte, error)
WriteFile(path string, data []byte) error
ReadDir(path string) ([]os.DirEntry, error)
}
-// hostFs is an unrestricted fileReadWriter that operates directly on the host filesystem.
-type hostFs struct{}
+// HostFs is an unrestricted fileReadWriter that operates directly on the host filesystem.
+type HostFs struct{}
-func (h *hostFs) ReadFile(path string) ([]byte, error) {
+func (h *HostFs) ReadFile(path string) ([]byte, error) {
content, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
@@ -273,33 +107,33 @@ func (h *hostFs) ReadFile(path string) ([]byte, error) {
return content, nil
}
-func (h *hostFs) ReadDir(path string) ([]os.DirEntry, error) {
+func (h *HostFs) ReadDir(path string) ([]os.DirEntry, error) {
return os.ReadDir(path)
}
-func (h *hostFs) WriteFile(path string, data []byte) error {
+func (h *HostFs) WriteFile(path string, data []byte) error {
// Use unified atomic write utility with explicit sync for flash storage reliability.
// Using 0o600 (owner read/write only) for secure default permissions.
return fileutil.WriteFileAtomic(path, data, 0o600)
}
-// sandboxFs is a sandboxed fileSystem that operates within a strictly defined workspace using os.Root.
-type sandboxFs struct {
- workspace string
+// SandboxFs is a sandboxed FileSystem that operates within a strictly defined workspace using os.Root.
+type SandboxFs struct {
+ Workspace string
}
-func (r *sandboxFs) execute(path string, fn func(root *os.Root, relPath string) error) error {
- if r.workspace == "" {
+func (r *SandboxFs) execute(path string, fn func(root *os.Root, relPath string) error) error {
+ if r.Workspace == "" {
return fmt.Errorf("workspace is not defined")
}
- root, err := os.OpenRoot(r.workspace)
+ root, err := os.OpenRoot(r.Workspace)
if err != nil {
return fmt.Errorf("failed to open workspace: %w", err)
}
defer root.Close()
- relPath, err := getSafeRelPath(r.workspace, path)
+ relPath, err := getSafeRelPath(r.Workspace, path)
if err != nil {
return err
}
@@ -307,7 +141,7 @@ func (r *sandboxFs) execute(path string, fn func(root *os.Root, relPath string)
return fn(root, relPath)
}
-func (r *sandboxFs) ReadFile(path string) ([]byte, error) {
+func (r *SandboxFs) ReadFile(path string) ([]byte, error) {
var content []byte
err := r.execute(path, func(root *os.Root, relPath string) error {
fileContent, err := root.ReadFile(relPath)
@@ -328,7 +162,7 @@ func (r *sandboxFs) ReadFile(path string) ([]byte, error) {
return content, err
}
-func (r *sandboxFs) WriteFile(path string, data []byte) error {
+func (r *SandboxFs) WriteFile(path string, data []byte) error {
return r.execute(path, func(root *os.Root, relPath string) error {
dir := filepath.Dir(relPath)
if dir != "." && dir != "/" {
@@ -381,7 +215,7 @@ func (r *sandboxFs) WriteFile(path string, data []byte) error {
})
}
-func (r *sandboxFs) ReadDir(path string) ([]os.DirEntry, error) {
+func (r *SandboxFs) ReadDir(path string) ([]os.DirEntry, error) {
var entries []os.DirEntry
err := r.execute(path, func(root *os.Root, relPath string) error {
dirEntries, err := fs.ReadDir(root.FS(), relPath)
diff --git a/pkg/tools/result.go b/pkg/tools/common/result.go
similarity index 99%
rename from pkg/tools/result.go
rename to pkg/tools/common/result.go
index b13055b1c..71d2ff122 100644
--- a/pkg/tools/result.go
+++ b/pkg/tools/common/result.go
@@ -1,4 +1,4 @@
-package tools
+package common
import "encoding/json"
diff --git a/pkg/tools/base.go b/pkg/tools/common/types.go
similarity index 89%
rename from pkg/tools/base.go
rename to pkg/tools/common/types.go
index 770d8cb04..5901f0cb7 100644
--- a/pkg/tools/base.go
+++ b/pkg/tools/common/types.go
@@ -1,6 +1,8 @@
-package tools
+package common
-import "context"
+import (
+ "context"
+)
// Tool is the interface that all tools must implement.
type Tool interface {
@@ -68,14 +70,3 @@ type AsyncTool interface {
// The callback will be called from a goroutine and should handle thread-safety if needed.
SetCallback(cb AsyncCallback)
}
-
-func ToolToSchema(tool Tool) map[string]any {
- return map[string]any{
- "type": "function",
- "function": map[string]any{
- "name": tool.Name(),
- "description": tool.Description(),
- "parameters": tool.Parameters(),
- },
- }
-}
diff --git a/pkg/tools/common/web.go b/pkg/tools/common/web.go
new file mode 100644
index 000000000..d930d5599
--- /dev/null
+++ b/pkg/tools/common/web.go
@@ -0,0 +1,64 @@
+package common
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "regexp"
+ "strings"
+ "time"
+)
+
+const (
+ UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
+)
+
+// Pre-compiled regexes for HTML text extraction
+var (
+ ReScript = regexp.MustCompile(``)
- reStyle = regexp.MustCompile(`