- Project infrastructure: go.mod, Makefile, .gitignore, README - Configuration system with YAML parsing and env overrides - Structured logging with slog - AI provider interface with Ollama, OpenAI, Anthropic implementations - Provider manager with runtime switching - Agent orchestrator with ReAct loop (reason-act-observe) - Tool registry with JSON Schema descriptions - Terminal tool: shell command execution with safety controls - Filesystem tools: read, write, list with path access control - Conversation memory with session management - Web UI server with WebSocket streaming - Modern dark theme UI with glassmorphism, animations - Frontend: WebSocket client, markdown rendering, tool call display
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
package filesystem
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/ai"
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/config"
|
|
"git.bebrik.xyz/Dan4ick/ZovOS_AI/internal/tools"
|
|
)
|
|
|
|
// RegisterTools registers filesystem-related tools in the registry.
|
|
func RegisterTools(registry *tools.Registry, cfg config.FilesystemToolConfig, log *slog.Logger) {
|
|
if !cfg.Enabled {
|
|
return
|
|
}
|
|
|
|
registry.Register(tools.ToolDef{
|
|
Tool: ai.Tool{
|
|
Name: "read_file",
|
|
Description: "Read the contents of a file at the given path.",
|
|
Parameters: map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"path": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Absolute path to the file to read",
|
|
},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args json.RawMessage) (string, error) {
|
|
var a struct{ Path string `json:"path"` }
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkAllowed(a.Path, cfg.AllowedPaths); err != nil {
|
|
return "", err
|
|
}
|
|
data, err := os.ReadFile(a.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
content := string(data)
|
|
if len(content) > 50000 {
|
|
content = content[:50000] + "\n... (file truncated)"
|
|
}
|
|
return content, nil
|
|
},
|
|
})
|
|
|
|
registry.Register(tools.ToolDef{
|
|
Tool: ai.Tool{
|
|
Name: "write_file",
|
|
Description: "Write content to a file, creating it if it doesn't exist.",
|
|
Parameters: map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"path": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Absolute path to the file",
|
|
},
|
|
"content": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Content to write to the file",
|
|
},
|
|
},
|
|
"required": []string{"path", "content"},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args json.RawMessage) (string, error) {
|
|
var a struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
}
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkAllowed(a.Path, cfg.AllowedPaths); err != nil {
|
|
return "", err
|
|
}
|
|
dir := filepath.Dir(a.Path)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.WriteFile(a.Path, []byte(a.Content), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("File written successfully: %s (%d bytes)", a.Path, len(a.Content)), nil
|
|
},
|
|
})
|
|
|
|
registry.Register(tools.ToolDef{
|
|
Tool: ai.Tool{
|
|
Name: "list_directory",
|
|
Description: "List contents of a directory.",
|
|
Parameters: map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"path": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Path to the directory to list",
|
|
},
|
|
},
|
|
"required": []string{"path"},
|
|
},
|
|
},
|
|
Handler: func(ctx context.Context, args json.RawMessage) (string, error) {
|
|
var a struct{ Path string `json:"path"` }
|
|
if err := json.Unmarshal(args, &a); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkAllowed(a.Path, cfg.AllowedPaths); err != nil {
|
|
return "", err
|
|
}
|
|
entries, err := os.ReadDir(a.Path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var sb strings.Builder
|
|
for _, e := range entries {
|
|
info, _ := e.Info()
|
|
typeStr := "file"
|
|
if e.IsDir() {
|
|
typeStr = "dir "
|
|
}
|
|
size := int64(0)
|
|
if info != nil {
|
|
size = info.Size()
|
|
}
|
|
sb.WriteString(fmt.Sprintf("[%s] %s (%d bytes)\n", typeStr, e.Name(), size))
|
|
}
|
|
if sb.Len() == 0 {
|
|
return "(empty directory)", nil
|
|
}
|
|
return sb.String(), nil
|
|
},
|
|
})
|
|
|
|
log.Info("filesystem tools registered")
|
|
}
|
|
|
|
func checkAllowed(path string, allowedPaths []string) error {
|
|
if len(allowedPaths) == 0 {
|
|
return nil // no restrictions
|
|
}
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return fmt.Errorf("resolving path: %w", err)
|
|
}
|
|
for _, allowed := range allowedPaths {
|
|
if strings.HasPrefix(absPath, allowed) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("access denied: path %s is not in allowed directories", path)
|
|
}
|