feat(exec): 本地执行 Provider + 放权终端沙箱(白名单/工作目录/超时/输出截断/默认关)+ 文件沙箱;config 增 ExecSandbox(默认关)
This commit is contained in:
1 parent
88dc12466d
commit
430fc4666b
11 files changed
+1199
-20
No files matched your search
@@ -0,0 +1,99 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
||||
)
|
||||
|
||||
// FileSandbox confines file read, write, and list access to a set of allowed
|
||||
// root directories (DevDocs/design/04-运行时.md §8, design/11 §5). Every path is
|
||||
// validated by [Resolve] before use, which rejects absolute paths outside the
|
||||
// roots, relative paths that escape them, and symlinks that resolve outside
|
||||
// them.
|
||||
type FileSandbox struct {
|
||||
mu sync.RWMutex
|
||||
roots []string
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewFileSandbox builds a FileSandbox confined to roots. Raw roots are made
|
||||
// absolute and cleaned. The logger is required.
|
||||
func NewFileSandbox(roots []string, log *slog.Logger) (*FileSandbox, error) {
|
||||
if log == nil {
|
||||
return nil, ErrNilLogger
|
||||
}
|
||||
return &FileSandbox{
|
||||
roots: absCleaners(roots),
|
||||
log: logger.WithSource(log, "runtime/exec"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetRoots atomically replaces the allowed roots. It enables a live config
|
||||
// reload without racing in-flight Resolve calls.
|
||||
func (s *FileSandbox) SetRoots(roots []string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.roots = absCleaners(roots)
|
||||
}
|
||||
|
||||
// Resolve validates path against the current allowed roots and returns its
|
||||
// absolute, symlink-resolved location, or an error if it would escape. A
|
||||
// relative path is resolved against the first allowed root.
|
||||
func (s *FileSandbox) Resolve(path string) (string, error) {
|
||||
roots := s.rootsLocked()
|
||||
if len(roots) == 0 {
|
||||
return "", ErrNoAllowedRoot
|
||||
}
|
||||
return resolveInsideRoot(path, roots)
|
||||
}
|
||||
|
||||
// ReadFile reads a file confined to the allowed roots.
|
||||
func (s *FileSandbox) ReadFile(path string) ([]byte, error) {
|
||||
abs, err := s.Resolve(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("exec: read %s: %w", abs, err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WriteFile writes data to a path confined to the allowed roots.
|
||||
func (s *FileSandbox) WriteFile(path string, data []byte, perm os.FileMode) error {
|
||||
abs, err := s.Resolve(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(abs, data, perm); err != nil {
|
||||
return fmt.Errorf("exec: write %s: %w", abs, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns the directory entries of a directory confined to the allowed
|
||||
// roots.
|
||||
func (s *FileSandbox) List(dir string) ([]os.DirEntry, error) {
|
||||
abs, err := s.Resolve(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(abs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("exec: list %s: %w", abs, err)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *FileSandbox) rootsLocked() []string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
roots := make([]string, len(s.roots))
|
||||
copy(roots, s.roots)
|
||||
return roots
|
||||
}
|
||||
Reference in new issue
Block a user