mirror of
https://github.com/hanzoai/runtime.git
synced 2026-10-30 18:23:13 +00:00
Every sandbox was created privileged under the daemon's default runtime, and the setting that would have changed it defaulted to an empty string that silently meant runc. For a service whose whole premise is running code somebody else wrote, that is not a boundary. An org sets its isolation and the create request carries it, so a sandbox records what it was made with: changing the policy moves new sandboxes and leaves running ones alone. gvisor is the default. firecracker is delivered through kata's shim, which is why it is one name rather than two. runc is refused unless the runner names that org, and an empty list names nobody. Absent means the default, unknown is a refusal, and a runtime the daemon does not have is a refusal. Nothing substitutes quietly. The runner took an organization in a field called UserId with a note beside it saying so. Deciding who may have runc through a field named after something else is not reviewable. Privileged is replaced by dropping everything and granting the eight the daemon uses. Nothing the sandbox does broke; what stopped working is mount, mknod, raw sockets, chroot, writing to proc, and seeing the host's disks. AUDIT_WRITE is among them because without it sudo works and complains every time. The creation histogram's lowest bucket was a tenth of a second, so the metric could not observe the claim the product makes about its own speed. It can now, and it carries the isolation, so comparing them is a query. Co-authored-by: Hanzo Dev <[email protected]>
523 lines
13 KiB
Go
523 lines
13 KiB
Go
// Copyright 2025 Hanzo Industries Inc.
|
|
// SPDX-License-Identifier: AGPL-3.0
|
|
|
|
package computeruse
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hanzoai/runtime/apps/daemon/pkg/toolbox/computeruse"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type Process struct {
|
|
Name string
|
|
Command string
|
|
Args []string
|
|
User string
|
|
Priority int
|
|
Env map[string]string
|
|
LogFile string
|
|
ErrFile string
|
|
AutoRestart bool
|
|
cmd *exec.Cmd
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
mu sync.Mutex
|
|
running bool
|
|
}
|
|
|
|
type ComputerUse struct {
|
|
processes map[string]*Process
|
|
mu sync.RWMutex
|
|
configDir string
|
|
}
|
|
|
|
var _ computeruse.IComputerUse = &ComputerUse{}
|
|
|
|
func (c *ComputerUse) Initialize() (*computeruse.Empty, error) {
|
|
c.processes = make(map[string]*Process)
|
|
// Create config directory for logs
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return new(computeruse.Empty), fmt.Errorf("failed to get home directory: %v", err)
|
|
}
|
|
c.configDir = filepath.Join(homeDir, ".runtime", "computeruse")
|
|
err = os.MkdirAll(c.configDir, 0755)
|
|
if err != nil {
|
|
return new(computeruse.Empty), fmt.Errorf("failed to create config directory: %v", err)
|
|
}
|
|
|
|
// Start a D-Bus session and set env vars globally
|
|
cmd := exec.Command("dbus-launch")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
log.Errorf("Failed to start dbus-launch: %v", err)
|
|
} else {
|
|
for _, line := range strings.Split(string(output), "\n") {
|
|
if strings.HasPrefix(line, "DBUS_SESSION_BUS_ADDRESS=") || strings.HasPrefix(line, "DBUS_SESSION_BUS_PID=") {
|
|
parts := strings.SplitN(line, ";", 2)
|
|
for _, part := range parts {
|
|
kv := strings.SplitN(part, "=", 2)
|
|
if len(kv) == 2 {
|
|
os.Setenv(strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
c.initializeProcesses(homeDir)
|
|
|
|
return new(computeruse.Empty), nil
|
|
}
|
|
|
|
func (c *ComputerUse) Start() (*computeruse.Empty, error) {
|
|
// Set DISPLAY environment variable in the main process
|
|
display := os.Getenv("DISPLAY")
|
|
if display == "" {
|
|
display = ":0"
|
|
}
|
|
os.Setenv("DISPLAY", display)
|
|
log.Infof("Set DISPLAY environment variable to: %s", display)
|
|
|
|
// Start all processes in order of priority
|
|
c.startAllProcesses()
|
|
|
|
// Check process status after starting
|
|
status, err := c.GetProcessStatus()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get process status after start: %v", err)
|
|
}
|
|
|
|
// Check if all required processes are running
|
|
required := []string{"xvfb", "xfce4", "x11vnc", "novnc"}
|
|
var failed []string
|
|
for _, name := range required {
|
|
if s, ok := status[name]; !ok || !s.Running {
|
|
failed = append(failed, name)
|
|
}
|
|
}
|
|
|
|
if len(failed) > 0 {
|
|
return nil, fmt.Errorf("failed to start: %v", failed)
|
|
}
|
|
|
|
return new(computeruse.Empty), nil
|
|
}
|
|
|
|
func (c *ComputerUse) initializeProcesses(homeDir string) {
|
|
// Get environment variables from Dockerfile or use defaults
|
|
vncResolution := os.Getenv("VNC_RESOLUTION")
|
|
if vncResolution == "" {
|
|
vncResolution = "1024x768"
|
|
}
|
|
|
|
vncPort := os.Getenv("VNC_PORT")
|
|
if vncPort == "" {
|
|
vncPort = "5901"
|
|
}
|
|
|
|
display := os.Getenv("DISPLAY")
|
|
if display == "" {
|
|
display = ":0"
|
|
}
|
|
|
|
// Get user from environment, fallback to RUNTIME_SANDBOX_USER or default to "root" (just in case, but should not happen)
|
|
user := os.Getenv("VNC_USER")
|
|
if user == "" {
|
|
user = os.Getenv("RUNTIME_SANDBOX_USER")
|
|
if user == "" {
|
|
user = "root"
|
|
}
|
|
}
|
|
|
|
// Get D-Bus session address from environment
|
|
dbusAddress := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
|
|
|
|
// Process 1: Xvfb (X Virtual Framebuffer)
|
|
c.processes["xvfb"] = &Process{
|
|
Name: "xvfb",
|
|
Command: "/usr/bin/Xvfb",
|
|
Args: []string{display, "-screen", "0", vncResolution + "x24"},
|
|
User: user,
|
|
Priority: 100,
|
|
AutoRestart: true,
|
|
Env: map[string]string{
|
|
"DISPLAY": display,
|
|
},
|
|
LogFile: filepath.Join(c.configDir, "xvfb.log"),
|
|
ErrFile: filepath.Join(c.configDir, "xvfb.err"),
|
|
}
|
|
|
|
// Process 2: xfce4 (Desktop Environment)
|
|
c.processes["xfce4"] = &Process{
|
|
Name: "xfce4",
|
|
Command: "/usr/bin/startxfce4",
|
|
Args: []string{},
|
|
User: user,
|
|
Priority: 200,
|
|
AutoRestart: true,
|
|
Env: map[string]string{
|
|
"DISPLAY": display,
|
|
"HOME": homeDir,
|
|
"USER": user,
|
|
"DBUS_SESSION_BUS_ADDRESS": dbusAddress,
|
|
},
|
|
LogFile: filepath.Join(c.configDir, "xfce4.log"),
|
|
ErrFile: filepath.Join(c.configDir, "xfce4.err"),
|
|
}
|
|
|
|
// Process 3: x11vnc (VNC Server)
|
|
c.processes["x11vnc"] = &Process{
|
|
Name: "x11vnc",
|
|
Command: "/usr/bin/x11vnc",
|
|
Args: []string{"-display", display, "-forever", "-shared", "-rfbport", vncPort},
|
|
User: user,
|
|
Priority: 300,
|
|
AutoRestart: true,
|
|
Env: map[string]string{
|
|
"DISPLAY": display,
|
|
},
|
|
LogFile: filepath.Join(c.configDir, "x11vnc.log"),
|
|
ErrFile: filepath.Join(c.configDir, "x11vnc.err"),
|
|
}
|
|
|
|
// Process 4: novnc (Web-based VNC client)
|
|
c.processes["novnc"] = &Process{
|
|
Name: "novnc",
|
|
Command: "/usr/share/novnc/utils/novnc_proxy",
|
|
Args: []string{"--vnc", "localhost:" + vncPort},
|
|
User: user,
|
|
Priority: 400,
|
|
AutoRestart: true,
|
|
Env: map[string]string{
|
|
"DISPLAY": display,
|
|
},
|
|
LogFile: filepath.Join(c.configDir, "novnc.log"),
|
|
ErrFile: filepath.Join(c.configDir, "novnc.err"),
|
|
}
|
|
}
|
|
|
|
func (c *ComputerUse) startAllProcesses() {
|
|
// Sort processes by priority and start them
|
|
processes := c.getProcessesByPriority()
|
|
|
|
for _, process := range processes {
|
|
go c.startProcess(process)
|
|
// Wait a bit between starting processes to ensure proper initialization
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
}
|
|
|
|
func (c *ComputerUse) getProcessesByPriority() []*Process {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
var processes []*Process
|
|
for _, p := range c.processes {
|
|
processes = append(processes, p)
|
|
}
|
|
|
|
// Sort by priority (lower number = higher priority)
|
|
for i := 0; i < len(processes)-1; i++ {
|
|
for j := i + 1; j < len(processes); j++ {
|
|
if processes[i].Priority > processes[j].Priority {
|
|
processes[i], processes[j] = processes[j], processes[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return processes
|
|
}
|
|
|
|
func (c *ComputerUse) startProcess(process *Process) {
|
|
process.mu.Lock()
|
|
if process.running {
|
|
process.mu.Unlock()
|
|
return
|
|
}
|
|
process.running = true
|
|
process.mu.Unlock()
|
|
|
|
for {
|
|
log.Infof("Starting process: %s", process.Name)
|
|
|
|
// Create context for the process
|
|
process.ctx, process.cancel = context.WithCancel(context.Background())
|
|
|
|
// Create command
|
|
process.cmd = exec.CommandContext(process.ctx, process.Command, process.Args...)
|
|
|
|
// Set environment variables
|
|
if len(process.Env) > 0 {
|
|
process.cmd.Env = os.Environ()
|
|
for key, value := range process.Env {
|
|
process.cmd.Env = append(process.cmd.Env, fmt.Sprintf("%s=%s", key, value))
|
|
}
|
|
}
|
|
|
|
// Set up logging
|
|
if process.LogFile != "" {
|
|
logFile, err := os.OpenFile(process.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
log.Errorf("Failed to open log file for %s: %v", process.Name, err)
|
|
} else {
|
|
process.cmd.Stdout = logFile
|
|
defer logFile.Close()
|
|
}
|
|
}
|
|
|
|
if process.ErrFile != "" {
|
|
errFile, err := os.OpenFile(process.ErrFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
log.Errorf("Failed to open error file for %s: %v", process.Name, err)
|
|
} else {
|
|
process.cmd.Stderr = errFile
|
|
defer errFile.Close()
|
|
}
|
|
}
|
|
|
|
// Start the process
|
|
err := process.cmd.Start()
|
|
if err != nil {
|
|
log.Errorf("Failed to start process %s: %v", process.Name, err)
|
|
if !process.AutoRestart {
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
continue
|
|
}
|
|
|
|
log.Infof("Process %s started with PID: %d", process.Name, process.cmd.Process.Pid)
|
|
|
|
// Wait for the process to finish
|
|
err = process.cmd.Wait()
|
|
if err != nil {
|
|
log.Errorf("Process %s exited with error: %v", process.Name, err)
|
|
} else {
|
|
log.Infof("Process %s exited normally", process.Name)
|
|
}
|
|
|
|
// Check if we should restart
|
|
if !process.AutoRestart {
|
|
break
|
|
}
|
|
|
|
log.Infof("Restarting process %s in 5 seconds...", process.Name)
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
|
|
process.mu.Lock()
|
|
process.running = false
|
|
process.mu.Unlock()
|
|
}
|
|
|
|
func (c *ComputerUse) Stop() (*computeruse.Empty, error) {
|
|
log.Info("Stopping all computer use processes...")
|
|
|
|
c.mu.RLock()
|
|
processes := make([]*Process, 0, len(c.processes))
|
|
for _, p := range c.processes {
|
|
processes = append(processes, p)
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
// Stop processes in reverse priority order
|
|
for i := len(processes) - 1; i >= 0; i-- {
|
|
process := processes[i]
|
|
c.stopProcess(process)
|
|
}
|
|
|
|
return new(computeruse.Empty), nil
|
|
}
|
|
|
|
func (c *ComputerUse) stopProcess(process *Process) {
|
|
process.mu.Lock()
|
|
defer process.mu.Unlock()
|
|
|
|
if !process.running {
|
|
return
|
|
}
|
|
|
|
log.Infof("Stopping process: %s", process.Name)
|
|
|
|
// Cancel the context to stop the process
|
|
if process.cancel != nil {
|
|
process.cancel()
|
|
}
|
|
|
|
// Kill the process if it's still running
|
|
if process.cmd != nil && process.cmd.Process != nil {
|
|
err := process.cmd.Process.Kill()
|
|
if err != nil {
|
|
log.Errorf("Failed to kill process %s: %v", process.Name, err)
|
|
}
|
|
}
|
|
|
|
process.running = false
|
|
}
|
|
|
|
func (c *ComputerUse) GetProcessStatus() (map[string]computeruse.ProcessStatus, error) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
status := make(map[string]computeruse.ProcessStatus)
|
|
for name, process := range c.processes {
|
|
process.mu.Lock()
|
|
processStatus := computeruse.ProcessStatus{
|
|
Running: false,
|
|
Priority: process.Priority,
|
|
AutoRestart: process.AutoRestart,
|
|
}
|
|
|
|
if process.cmd != nil && process.cmd.Process != nil {
|
|
// Check if the process is still alive
|
|
if err := process.cmd.Process.Signal(syscall.Signal(0)); err == nil {
|
|
processStatus.Running = true
|
|
processStatus.Pid = &process.cmd.Process.Pid
|
|
}
|
|
}
|
|
|
|
process.mu.Unlock()
|
|
status[name] = processStatus
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// IsProcessRunning checks if a specific process is running
|
|
func (c *ComputerUse) IsProcessRunning(req *computeruse.ProcessRequest) (bool, error) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
process, exists := c.processes[req.ProcessName]
|
|
if !exists {
|
|
return false, fmt.Errorf("process %s not found", req.ProcessName)
|
|
}
|
|
|
|
process.mu.Lock()
|
|
defer process.mu.Unlock()
|
|
return process.running, nil
|
|
}
|
|
|
|
// RestartProcess restarts a specific process
|
|
func (c *ComputerUse) RestartProcess(req *computeruse.ProcessRequest) (*computeruse.Empty, error) {
|
|
c.mu.RLock()
|
|
process, exists := c.processes[req.ProcessName]
|
|
c.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return nil, fmt.Errorf("process %s not found", req.ProcessName)
|
|
}
|
|
|
|
// Stop the process first
|
|
c.stopProcess(process)
|
|
|
|
// Wait a moment for cleanup
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Start the process again
|
|
go c.startProcess(process)
|
|
|
|
return new(computeruse.Empty), nil
|
|
}
|
|
|
|
// GetProcessLogs returns the logs for a specific process
|
|
func (c *ComputerUse) GetProcessLogs(req *computeruse.ProcessRequest) (string, error) {
|
|
c.mu.RLock()
|
|
process, exists := c.processes[req.ProcessName]
|
|
c.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return "", fmt.Errorf("process %s not found", req.ProcessName)
|
|
}
|
|
|
|
if process.LogFile == "" {
|
|
return "", fmt.Errorf("no log file configured for process %s", req.ProcessName)
|
|
}
|
|
|
|
content, err := os.ReadFile(process.LogFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read log file for %s: %v", req.ProcessName, err)
|
|
}
|
|
|
|
return string(content), nil
|
|
}
|
|
|
|
// GetProcessErrors returns the error logs for a specific process
|
|
func (c *ComputerUse) GetProcessErrors(req *computeruse.ProcessRequest) (string, error) {
|
|
c.mu.RLock()
|
|
process, exists := c.processes[req.ProcessName]
|
|
c.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return "", fmt.Errorf("process %s not found", req.ProcessName)
|
|
}
|
|
|
|
if process.ErrFile == "" {
|
|
return "", fmt.Errorf("no error file configured for process %s", req.ProcessName)
|
|
}
|
|
|
|
content, err := os.ReadFile(process.ErrFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read error file for %s: %v", req.ProcessName, err)
|
|
}
|
|
|
|
return string(content), nil
|
|
}
|
|
|
|
func (c *ComputerUse) GetStatus() (*computeruse.StatusResponse, error) {
|
|
// Get the current process status
|
|
processStatus, err := c.GetProcessStatus()
|
|
if err != nil {
|
|
return &computeruse.StatusResponse{
|
|
Status: "error",
|
|
}, err
|
|
}
|
|
|
|
// Check if all required processes are running
|
|
requiredProcesses := []string{"xvfb", "xfce4", "x11vnc", "novnc"}
|
|
allRunning := true
|
|
|
|
for _, processName := range requiredProcesses {
|
|
if status, exists := processStatus[processName]; !exists || !status.Running {
|
|
allRunning = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if allRunning {
|
|
return &computeruse.StatusResponse{
|
|
Status: "active",
|
|
}, nil
|
|
}
|
|
|
|
// Check if any processes are running
|
|
anyRunning := false
|
|
for _, status := range processStatus {
|
|
if status.Running {
|
|
anyRunning = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if anyRunning {
|
|
return &computeruse.StatusResponse{
|
|
Status: "partial",
|
|
}, nil
|
|
}
|
|
|
|
return &computeruse.StatusResponse{
|
|
Status: "inactive",
|
|
}, nil
|
|
}
|