Files
gooseek/backend/internal/computer/sandbox.go
home 06fe57c765 feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
2026-02-27 04:15:32 +03:00

432 lines
10 KiB
Go

package computer
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
type SandboxConfig struct {
Image string
Timeout time.Duration
MemoryLimit string
CPULimit string
NetworkMode string
WorkDir string
MaxFileSize int64
AllowNetwork bool
}
func DefaultSandboxConfig() SandboxConfig {
return SandboxConfig{
Image: "gooseek/sandbox:latest",
Timeout: 5 * time.Minute,
MemoryLimit: "512m",
CPULimit: "1.0",
NetworkMode: "none",
WorkDir: "/workspace",
MaxFileSize: 10 * 1024 * 1024,
AllowNetwork: false,
}
}
type Sandbox struct {
ID string
ContainerID string
WorkDir string
Status string
TaskID string
CreatedAt time.Time
}
type SandboxManager struct {
cfg SandboxConfig
sandboxes map[string]*Sandbox
mu sync.RWMutex
useDocker bool
}
func NewSandboxManager(cfg SandboxConfig) *SandboxManager {
if cfg.Timeout == 0 {
cfg.Timeout = 5 * time.Minute
}
if cfg.MemoryLimit == "" {
cfg.MemoryLimit = "512m"
}
if cfg.WorkDir == "" {
cfg.WorkDir = "/workspace"
}
useDocker := isDockerAvailable()
return &SandboxManager{
cfg: cfg,
sandboxes: make(map[string]*Sandbox),
useDocker: useDocker,
}
}
func isDockerAvailable() bool {
cmd := exec.Command("docker", "version")
return cmd.Run() == nil
}
func (sm *SandboxManager) Create(ctx context.Context, taskID string) (*Sandbox, error) {
sandboxID := uuid.New().String()[:8]
sandbox := &Sandbox{
ID: sandboxID,
TaskID: taskID,
Status: "creating",
CreatedAt: time.Now(),
}
if sm.useDocker {
workDir, err := os.MkdirTemp("", fmt.Sprintf("sandbox-%s-", sandboxID))
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %w", err)
}
sandbox.WorkDir = workDir
args := []string{
"create",
"--name", fmt.Sprintf("gooseek-sandbox-%s", sandboxID),
"-v", fmt.Sprintf("%s:%s", workDir, sm.cfg.WorkDir),
"-w", sm.cfg.WorkDir,
"--memory", sm.cfg.MemoryLimit,
"--cpus", sm.cfg.CPULimit,
}
if !sm.cfg.AllowNetwork {
args = append(args, "--network", "none")
}
args = append(args, sm.cfg.Image, "tail", "-f", "/dev/null")
cmd := exec.CommandContext(ctx, "docker", args...)
output, err := cmd.CombinedOutput()
if err != nil {
os.RemoveAll(workDir)
return nil, fmt.Errorf("failed to create container: %w - %s", err, string(output))
}
sandbox.ContainerID = strings.TrimSpace(string(output))
startCmd := exec.CommandContext(ctx, "docker", "start", sandbox.ContainerID)
if err := startCmd.Run(); err != nil {
sm.cleanupContainer(sandbox)
return nil, fmt.Errorf("failed to start container: %w", err)
}
} else {
workDir, err := os.MkdirTemp("", fmt.Sprintf("sandbox-%s-", sandboxID))
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %w", err)
}
sandbox.WorkDir = workDir
}
sandbox.Status = "running"
sm.mu.Lock()
sm.sandboxes[sandboxID] = sandbox
sm.mu.Unlock()
return sandbox, nil
}
func (sm *SandboxManager) Execute(ctx context.Context, sandbox *Sandbox, code string, lang string) (*SandboxResult, error) {
ctx, cancel := context.WithTimeout(ctx, sm.cfg.Timeout)
defer cancel()
startTime := time.Now()
filename, err := sm.writeCodeFile(sandbox, code, lang)
if err != nil {
return nil, err
}
var cmd *exec.Cmd
var stdout, stderr bytes.Buffer
if sm.useDocker {
runCmd := sm.getRunCommand(lang, filename)
cmd = exec.CommandContext(ctx, "docker", "exec", sandbox.ContainerID, "sh", "-c", runCmd)
} else {
cmd = sm.getLocalCommand(ctx, lang, filepath.Join(sandbox.WorkDir, filename))
}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
exitCode := 0
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else if ctx.Err() == context.DeadlineExceeded {
return &SandboxResult{
Stderr: "Execution timeout exceeded",
ExitCode: -1,
Duration: time.Since(startTime),
}, nil
}
}
files, _ := sm.collectOutputFiles(sandbox)
return &SandboxResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: exitCode,
Files: files,
Duration: time.Since(startTime),
}, nil
}
func (sm *SandboxManager) RunCommand(ctx context.Context, sandbox *Sandbox, command string) (*SandboxResult, error) {
ctx, cancel := context.WithTimeout(ctx, sm.cfg.Timeout)
defer cancel()
startTime := time.Now()
var cmd *exec.Cmd
var stdout, stderr bytes.Buffer
if sm.useDocker {
cmd = exec.CommandContext(ctx, "docker", "exec", sandbox.ContainerID, "sh", "-c", command)
} else {
cmd = exec.CommandContext(ctx, "sh", "-c", command)
cmd.Dir = sandbox.WorkDir
}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
exitCode := 0
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
}
}
return &SandboxResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: exitCode,
Duration: time.Since(startTime),
}, nil
}
func (sm *SandboxManager) WriteFile(ctx context.Context, sandbox *Sandbox, path string, content []byte) error {
if int64(len(content)) > sm.cfg.MaxFileSize {
return fmt.Errorf("file size exceeds limit: %d > %d", len(content), sm.cfg.MaxFileSize)
}
fullPath := filepath.Join(sandbox.WorkDir, path)
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
return os.WriteFile(fullPath, content, 0644)
}
func (sm *SandboxManager) ReadFile(ctx context.Context, sandbox *Sandbox, path string) ([]byte, error) {
fullPath := filepath.Join(sandbox.WorkDir, path)
return os.ReadFile(fullPath)
}
func (sm *SandboxManager) Destroy(ctx context.Context, sandbox *Sandbox) error {
sm.mu.Lock()
delete(sm.sandboxes, sandbox.ID)
sm.mu.Unlock()
if sm.useDocker && sandbox.ContainerID != "" {
sm.cleanupContainer(sandbox)
}
if sandbox.WorkDir != "" {
os.RemoveAll(sandbox.WorkDir)
}
return nil
}
func (sm *SandboxManager) cleanupContainer(sandbox *Sandbox) {
exec.Command("docker", "stop", sandbox.ContainerID).Run()
exec.Command("docker", "rm", "-f", sandbox.ContainerID).Run()
}
func (sm *SandboxManager) writeCodeFile(sandbox *Sandbox, code string, lang string) (string, error) {
var filename string
switch lang {
case "python", "py":
filename = "main.py"
case "javascript", "js", "node":
filename = "main.js"
case "typescript", "ts":
filename = "main.ts"
case "go", "golang":
filename = "main.go"
case "bash", "sh", "shell":
filename = "script.sh"
case "ruby", "rb":
filename = "main.rb"
default:
filename = "main.txt"
}
fullPath := filepath.Join(sandbox.WorkDir, filename)
if err := os.WriteFile(fullPath, []byte(code), 0755); err != nil {
return "", fmt.Errorf("failed to write code file: %w", err)
}
return filename, nil
}
func (sm *SandboxManager) getRunCommand(lang, filename string) string {
switch lang {
case "python", "py":
return fmt.Sprintf("python3 %s/%s", sm.cfg.WorkDir, filename)
case "javascript", "js", "node":
return fmt.Sprintf("node %s/%s", sm.cfg.WorkDir, filename)
case "typescript", "ts":
return fmt.Sprintf("npx ts-node %s/%s", sm.cfg.WorkDir, filename)
case "go", "golang":
return fmt.Sprintf("go run %s/%s", sm.cfg.WorkDir, filename)
case "bash", "sh", "shell":
return fmt.Sprintf("bash %s/%s", sm.cfg.WorkDir, filename)
case "ruby", "rb":
return fmt.Sprintf("ruby %s/%s", sm.cfg.WorkDir, filename)
default:
return fmt.Sprintf("cat %s/%s", sm.cfg.WorkDir, filename)
}
}
func (sm *SandboxManager) getLocalCommand(ctx context.Context, lang, filepath string) *exec.Cmd {
switch lang {
case "python", "py":
return exec.CommandContext(ctx, "python3", filepath)
case "javascript", "js", "node":
return exec.CommandContext(ctx, "node", filepath)
case "go", "golang":
return exec.CommandContext(ctx, "go", "run", filepath)
case "bash", "sh", "shell":
return exec.CommandContext(ctx, "bash", filepath)
case "ruby", "rb":
return exec.CommandContext(ctx, "ruby", filepath)
default:
return exec.CommandContext(ctx, "cat", filepath)
}
}
func (sm *SandboxManager) collectOutputFiles(sandbox *Sandbox) (map[string][]byte, error) {
files := make(map[string][]byte)
err := filepath.Walk(sandbox.WorkDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(sandbox.WorkDir, path)
if err != nil {
return nil
}
if strings.HasPrefix(relPath, "main.") || strings.HasPrefix(relPath, "script.") {
return nil
}
if info.Size() > sm.cfg.MaxFileSize {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return nil
}
files[relPath] = content
return nil
})
return files, err
}
func (sm *SandboxManager) ListSandboxes() []*Sandbox {
sm.mu.RLock()
defer sm.mu.RUnlock()
result := make([]*Sandbox, 0, len(sm.sandboxes))
for _, s := range sm.sandboxes {
result = append(result, s)
}
return result
}
func (sm *SandboxManager) GetSandbox(id string) (*Sandbox, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
s, ok := sm.sandboxes[id]
return s, ok
}
func (sm *SandboxManager) CopyToContainer(ctx context.Context, sandbox *Sandbox, src string, dst string) error {
if !sm.useDocker {
srcData, err := os.ReadFile(src)
if err != nil {
return err
}
return sm.WriteFile(ctx, sandbox, dst, srcData)
}
cmd := exec.CommandContext(ctx, "docker", "cp", src, fmt.Sprintf("%s:%s", sandbox.ContainerID, dst))
return cmd.Run()
}
func (sm *SandboxManager) CopyFromContainer(ctx context.Context, sandbox *Sandbox, src string, dst string) error {
if !sm.useDocker {
srcPath := filepath.Join(sandbox.WorkDir, src)
srcData, err := os.ReadFile(srcPath)
if err != nil {
return err
}
return os.WriteFile(dst, srcData, 0644)
}
cmd := exec.CommandContext(ctx, "docker", "cp", fmt.Sprintf("%s:%s", sandbox.ContainerID, src), dst)
return cmd.Run()
}
func (sm *SandboxManager) StreamLogs(ctx context.Context, sandbox *Sandbox) (io.ReadCloser, error) {
if !sm.useDocker {
return nil, fmt.Errorf("streaming not supported without Docker")
}
cmd := exec.CommandContext(ctx, "docker", "logs", "-f", sandbox.ContainerID)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
return stdout, nil
}