package execute import ( "bytes" "context" "encoding/json" "fmt" "os" "os/exec" "regexp" "sync" "syscall" "time" "ollie/internal/sandbox" "ollie/pkg/tools" ) // Server runs code in a sandboxed environment. type Server struct { // Confirm is an optional function called before executing sensitive operations. // If it returns false, the operation is denied. Confirm func(string) bool // workdir is the working directory for sandboxed commands. If empty, // the process working directory is used. wdMu sync.RWMutex workdir string // rate limiting state (per-Server) rateLimitMu sync.Mutex validationFailures int lastFailure time.Time blockedUntil time.Time } // Decl returns a factory for an execute Server with the given working directory. func Decl(workdir string) func() tools.Server { return func() tools.Server { return New(workdir) } } // ListTools implements tools.Server, returning the ToolInfo definitions for the execute_* built-in tools. func (e *Server) ListTools() ([]tools.ToolInfo, error) { return []tools.ToolInfo{ { Name: "execute_code", Description: "Run inline code in a sandboxed environment.", InputSchema: json.RawMessage(`{ "type": "object", "required": ["code"], "properties": { "code": {"type": "string", "description": "Code to execute."}, "language": {"type": "string", "description": "Language interpreter (default: bash)."}, "timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."}, "sandbox": {"type": "string", "description": "Sandbox name (default: default)."} } }`), }, { Name: "execute_tool", Description: "Run a named tool script from " + ToolsPath() + " in a sandboxed environment. Supported languages: bash, python3 (detected from shebang). Use this only for named scripts, not for inline shell commands.", InputSchema: json.RawMessage(`{ "type": "object", "required": ["tool"], "properties": { "tool": {"type": "string", "description": "Name of the tool script in the tools directory."}, "args": {"type": "array", "items": {"type": "string"}, "description": "Arguments for the tool script."}, "timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."}, "sandbox": {"type": "string", "description": "Sandbox name (default: default)."} } }`), }, { Name: "execute_pipe", Description: "Run a pipeline of steps, piping stdout of each into stdin of the next. Use {code: \"cmd --flags\"} for shell commands; use {tool, args} only for named scripts in " + ToolsPath() + ".", InputSchema: json.RawMessage(`{ "type": "object", "required": ["pipe"], "properties": { "pipe": { "type": "array", "items": { "type": "object", "properties": { "tool": {"type": "string"}, "args": {"type": "array", "items": {"type": "string"}}, "code": {"type": "string"} } } }, "timeout": {"type": "integer", "description": "Timeout in seconds (default: 30)."}, "sandbox": {"type": "string", "description": "Sandbox name (default: default)."} } }`), }, }, nil } // CallTool implements tools.Server. func (e *Server) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { result, err := e.Dispatch(ctx, tool, args) if err != nil { return json.Marshal(map[string]string{"error": err.Error()}) } return json.Marshal(map[string]any{ "content": []map[string]string{{"type": "text", "text": result}}, }) } // New creates a new Server with the given working directory. func New(workdir string) *Server { return &Server{workdir: workdir} } // SetWorkDir updates the working directory used for subsequent command executions. func (e *Server) SetWorkDir(dir string) { e.wdMu.Lock() e.workdir = dir e.wdMu.Unlock() } var whitespacePattern = regexp.MustCompile(`\s+`) const ( maxFailures = 5 blockDuration = 30 * time.Second failureWindow = 60 * time.Second ) func (e *Server) checkRateLimit() error { e.rateLimitMu.Lock() defer e.rateLimitMu.Unlock() now := time.Now() if now.Before(e.blockedUntil) { remaining := e.blockedUntil.Sub(now).Round(time.Second) return fmt.Errorf("rate limited: too many validation failures, blocked for %v", remaining) } return nil } func (e *Server) recordValidationFailure() { e.rateLimitMu.Lock() defer e.rateLimitMu.Unlock() now := time.Now() if now.Sub(e.lastFailure) > failureWindow { e.validationFailures = 0 } e.validationFailures++ e.lastFailure = now if e.validationFailures >= maxFailures { e.blockedUntil = now.Add(blockDuration) e.validationFailures = 0 } } // Execute runs code in a sandbox and returns combined stdout+stderr. func (e *Server) Execute(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool) (string, error) { if timeout <= 0 { timeout = 30 } if !trusted { if err := e.ValidateCode(code); err != nil { return "", err } } cfg, err := loadLayeredConfig(sandboxName) if err != nil { return "", err } e.wdMu.RLock() workDir := e.workdir e.wdMu.RUnlock() if workDir == "" { workDir, _ = os.Getwd() } ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() var cmd *exec.Cmd var interpreter []string switch language { case "bash", "": interpreter = []string{"bash", "-c", code} case "python3", "python": interpreter = []string{"python3", "-c", code} default: return "", fmt.Errorf("unsupported language: %s (supported: bash, python3)", language) } wrapped := sandbox.WrapCommand(cfg, interpreter, workDir) cmd = exec.CommandContext(ctx, wrapped[0], wrapped[1:]...) cmd.Dir = workDir cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.Cancel = func() error { if cmd.Process != nil { syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) } return nil } var outputBuf bytes.Buffer lw := &limitedWriter{w: &outputBuf, limit: 10 * 1024 * 1024} cmd.Stdout = lw cmd.Stderr = lw err = cmd.Run() output := outputBuf.Bytes() if lw.truncated { output = append(output, []byte("\n[output truncated at 10MB]")...) } if ctx.Err() == context.DeadlineExceeded { return "", fmt.Errorf("execution timeout after %d seconds", timeout) } if err != nil { return string(output), fmt.Errorf("execution failed: %v\nOutput: %s", err, string(output)) } return string(output), nil } // Close implements tools.Server (no-op). func (e *Server) Close() {} var _ tools.Server = (*Server)(nil) // compile-time interface check