chore: initial commit
This commit is contained in:
111
internal/tools/filetool/filetool.go
Normal file
111
internal/tools/filetool/filetool.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package filetool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"laodingbot/internal/logger"
|
||||
)
|
||||
|
||||
type Tool struct {
|
||||
allowedDirs []string
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func New(allowedDirs []string, log *logger.Logger) *Tool {
|
||||
normalized := make([]string, 0, len(allowedDirs))
|
||||
for _, dir := range allowedDirs {
|
||||
abs, err := filepath.Abs(strings.TrimSpace(dir))
|
||||
if err == nil {
|
||||
normalized = append(normalized, filepath.Clean(abs))
|
||||
}
|
||||
}
|
||||
if log != nil {
|
||||
log.Infof("file tool initialized allowed_dirs=%d", len(normalized))
|
||||
}
|
||||
return &Tool{allowedDirs: normalized, log: log}
|
||||
}
|
||||
|
||||
func (t *Tool) Name() string { return "file" }
|
||||
|
||||
func (t *Tool) Description() string {
|
||||
return "File operations with command format: read <path> | write <path>\\n<content>"
|
||||
}
|
||||
|
||||
func (t *Tool) Call(_ context.Context, input string) (string, error) {
|
||||
input = strings.TrimSpace(input)
|
||||
if t.log != nil {
|
||||
t.log.Debugf("file tool call input_len=%d", len(input))
|
||||
}
|
||||
if strings.HasPrefix(input, "read ") {
|
||||
path := strings.TrimSpace(strings.TrimPrefix(input, "read "))
|
||||
resolved, err := t.resolveAllowed(path)
|
||||
if err != nil {
|
||||
if t.log != nil {
|
||||
t.log.Warnf("file read denied path=%s err=%v", path, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
b, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
if t.log != nil {
|
||||
t.log.Errorf("file read failed path=%s err=%v", resolved, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if t.log != nil {
|
||||
t.log.Infof("file read success path=%s bytes=%d", resolved, len(b))
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(input, "write ") {
|
||||
parts := strings.SplitN(input, "\n", 2)
|
||||
if len(parts) < 2 {
|
||||
return "", fmt.Errorf("write requires content in second line")
|
||||
}
|
||||
path := strings.TrimSpace(strings.TrimPrefix(parts[0], "write "))
|
||||
resolved, err := t.resolveAllowed(path)
|
||||
if err != nil {
|
||||
if t.log != nil {
|
||||
t.log.Warnf("file write denied path=%s err=%v", path, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(resolved), 0o755); err != nil {
|
||||
if t.log != nil {
|
||||
t.log.Errorf("file write mkdir failed path=%s err=%v", resolved, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(resolved, []byte(parts[1]), 0o644); err != nil {
|
||||
if t.log != nil {
|
||||
t.log.Errorf("file write failed path=%s err=%v", resolved, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if t.log != nil {
|
||||
t.log.Infof("file write success path=%s bytes=%d", resolved, len(parts[1]))
|
||||
}
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unsupported file command")
|
||||
}
|
||||
|
||||
func (t *Tool) resolveAllowed(path string) (string, error) {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
abs = filepath.Clean(abs)
|
||||
for _, allowed := range t.allowedDirs {
|
||||
if strings.HasPrefix(abs, allowed+string(filepath.Separator)) || abs == allowed {
|
||||
return abs, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("path not allowed: %s", path)
|
||||
}
|
||||
85
internal/tools/shelltool/shelltool.go
Normal file
85
internal/tools/shelltool/shelltool.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package shelltool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"laodingbot/internal/logger"
|
||||
)
|
||||
|
||||
type Tool struct {
|
||||
allowedCommands map[string]struct{}
|
||||
workDir string
|
||||
timeout time.Duration
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func New(allowed []string, workDir string, timeout time.Duration, log *logger.Logger) *Tool {
|
||||
set := make(map[string]struct{}, len(allowed))
|
||||
for _, c := range allowed {
|
||||
cmd := strings.TrimSpace(c)
|
||||
if cmd != "" {
|
||||
set[cmd] = struct{}{}
|
||||
}
|
||||
}
|
||||
absDir, err := filepath.Abs(workDir)
|
||||
if err != nil {
|
||||
absDir = workDir
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = 15 * time.Second
|
||||
}
|
||||
if log != nil {
|
||||
log.Infof("shell tool initialized allowed_commands=%d work_dir=%s timeout=%s", len(set), absDir, timeout)
|
||||
}
|
||||
return &Tool{allowedCommands: set, workDir: absDir, timeout: timeout, log: log}
|
||||
}
|
||||
|
||||
func (t *Tool) Name() string { return "shell" }
|
||||
|
||||
func (t *Tool) Description() string {
|
||||
return "Execute allowlisted shell commands in Linux"
|
||||
}
|
||||
|
||||
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if trimmed == "" {
|
||||
if t.log != nil {
|
||||
t.log.Warnf("shell tool rejected empty command")
|
||||
}
|
||||
return "", fmt.Errorf("empty command")
|
||||
}
|
||||
|
||||
parts := strings.Fields(trimmed)
|
||||
base := parts[0]
|
||||
if _, ok := t.allowedCommands[base]; !ok {
|
||||
if t.log != nil {
|
||||
t.log.Warnf("shell command denied command=%s", base)
|
||||
}
|
||||
return "", fmt.Errorf("command not allowed: %s", base)
|
||||
}
|
||||
if t.log != nil {
|
||||
t.log.Infof("shell command start command=%s args=%d", base, len(parts)-1)
|
||||
}
|
||||
|
||||
runCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(runCtx, base, parts[1:]...)
|
||||
cmd.Dir = t.workDir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if t.log != nil {
|
||||
t.log.Errorf("shell command failed command=%s err=%v output_bytes=%d", base, err, len(out))
|
||||
}
|
||||
return string(out), err
|
||||
}
|
||||
if t.log != nil {
|
||||
t.log.Debugf("shell command success command=%s output_bytes=%d", base, len(out))
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
49
internal/tools/types.go
Normal file
49
internal/tools/types.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"laodingbot/internal/logger"
|
||||
)
|
||||
|
||||
type Tool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Call(ctx context.Context, input string) (string, error)
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
tools map[string]Tool
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func NewRegistry(log *logger.Logger) *Registry {
|
||||
return &Registry{tools: make(map[string]Tool), log: log}
|
||||
}
|
||||
|
||||
func (r *Registry) Register(tool Tool) {
|
||||
r.tools[tool.Name()] = tool
|
||||
if r.log != nil {
|
||||
r.log.Infof("registered tool name=%s", tool.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) (Tool, bool) {
|
||||
t, ok := r.tools[name]
|
||||
if r.log != nil {
|
||||
if ok {
|
||||
r.log.Debugf("resolved tool name=%s", name)
|
||||
} else {
|
||||
r.log.Warnf("tool not found name=%s", name)
|
||||
}
|
||||
}
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (r *Registry) List() []Tool {
|
||||
out := make([]Tool, 0, len(r.tools))
|
||||
for _, t := range r.tools {
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user