feat: config, OpenAI-compatible streaming provider and safe tools

This commit is contained in:
OpenClaw Dev 2026-10-25 03:10:13 +01:00
commit c1c7b0e005
13 changed files with 1031 additions and 0 deletions

183
internal/tools/files.go Normal file
View 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
}