feat: config, OpenAI-compatible streaming provider and safe tools
This commit is contained in:
commit
c1c7b0e005
13 changed files with 1031 additions and 0 deletions
183
internal/tools/files.go
Normal file
183
internal/tools/files.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/crypto/openclaw/internal/llm"
|
||||
)
|
||||
|
||||
// Workspace confines file access to a root directory using os.Root
|
||||
// (Go 1.24+), which rejects "..", absolute paths and symlink escapes.
|
||||
type Workspace struct {
|
||||
Dir string
|
||||
MaxBytes int64
|
||||
}
|
||||
|
||||
func (w Workspace) open() (*os.Root, error) {
|
||||
if err := os.MkdirAll(w.Dir, 0o750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.OpenRoot(w.Dir)
|
||||
}
|
||||
|
||||
func cleanRel(p string) (string, error) {
|
||||
if p == "" {
|
||||
p = "."
|
||||
}
|
||||
if filepath.IsAbs(p) {
|
||||
return "", errors.New("path must be relative to the workspace")
|
||||
}
|
||||
return filepath.Clean(p), nil
|
||||
}
|
||||
|
||||
type pathArgs struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// ---- file_read
|
||||
|
||||
type FileRead struct{ Workspace }
|
||||
|
||||
func (FileRead) Spec() llm.ToolSpec {
|
||||
return llm.ToolSpec{Name: "file_read", Description: "Read a text file inside the workspace.",
|
||||
Parameters: obj(map[string]any{"path": map[string]any{"type": "string"}}, "path")}
|
||||
}
|
||||
|
||||
func (f FileRead) Call(_ context.Context, raw json.RawMessage) (string, error) {
|
||||
var a pathArgs
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err := cleanRel(a.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err := f.open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer root.Close()
|
||||
fh, err := root.Open(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fh.Close()
|
||||
b, err := io.ReadAll(io.LimitReader(fh, f.MaxBytes+1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if int64(len(b)) > f.MaxBytes {
|
||||
return string(b[:f.MaxBytes]) + "\n[truncated]", nil
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// ---- file_write
|
||||
|
||||
type FileWrite struct{ Workspace }
|
||||
|
||||
func (FileWrite) Spec() llm.ToolSpec {
|
||||
return llm.ToolSpec{Name: "file_write", Description: "Create or overwrite a text file inside the workspace.",
|
||||
Parameters: obj(map[string]any{"path": map[string]any{"type": "string"}, "content": map[string]any{"type": "string"}}, "path", "content")}
|
||||
}
|
||||
|
||||
func (f FileWrite) Call(_ context.Context, raw json.RawMessage) (string, error) {
|
||||
var a pathArgs
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if int64(len(a.Content)) > f.MaxBytes {
|
||||
return "", fmt.Errorf("content exceeds %d bytes", f.MaxBytes)
|
||||
}
|
||||
p, err := cleanRel(a.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if p == "." {
|
||||
return "", errors.New("path required")
|
||||
}
|
||||
root, err := f.open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer root.Close()
|
||||
if dir := filepath.Dir(p); dir != "." {
|
||||
if err := mkdirAll(root, dir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
fh, err := root.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o640)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fh.Close()
|
||||
if _, err := fh.WriteString(a.Content); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("wrote %d bytes to %s", len(a.Content), p), nil
|
||||
}
|
||||
|
||||
func mkdirAll(root *os.Root, dir string) error {
|
||||
cur := ""
|
||||
for _, part := range strings.Split(filepath.ToSlash(dir), "/") {
|
||||
cur = filepath.Join(cur, part)
|
||||
if err := root.Mkdir(cur, 0o750); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- file_list
|
||||
|
||||
type FileList struct{ Workspace }
|
||||
|
||||
func (FileList) Spec() llm.ToolSpec {
|
||||
return llm.ToolSpec{Name: "file_list", Description: "List entries of a directory inside the workspace (default: root).",
|
||||
Parameters: obj(map[string]any{"path": map[string]any{"type": "string"}})}
|
||||
}
|
||||
|
||||
func (f FileList) Call(_ context.Context, raw json.RawMessage) (string, error) {
|
||||
var a pathArgs
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return "", err
|
||||
}
|
||||
p, err := cleanRel(a.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
root, err := f.open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer root.Close()
|
||||
d, err := root.Open(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer d.Close()
|
||||
ents, err := d.ReadDir(500)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return "", err
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, e := range ents {
|
||||
name := e.Name()
|
||||
if e.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
sb.WriteString(name + "\n")
|
||||
}
|
||||
if sb.Len() == 0 {
|
||||
return "(empty)", nil
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue