Add runtime abstraction and agent loop

Co-authored-by: codegirl007 <codegirl-007@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-17 16:47:22 +00:00
co-authored by codegirl007
parent 54dc5658c6
commit 94146fcae2
17 changed files with 1512 additions and 2 deletions
@@ -0,0 +1,266 @@
package dockerx11
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"agentbox/internal/environment"
)
const imageName = "agentbox-runtime:local"
type Config struct {
ProjectRoot string
RunID string
Output io.Writer
}
type Environment struct {
config Config
containerName string
created bool
stopped bool
mu sync.Mutex
}
func New(config Config) *Environment {
return &Environment{
config: config,
containerName: "agentbox-run-" + config.RunID,
}
}
func (e *Environment) Start(ctx context.Context) error {
if e.config.Output == nil {
e.config.Output = io.Discard
}
if _, err := e.docker(ctx, "version", "--format", "{{.Server.Version}}"); err != nil {
return fmt.Errorf("Docker is required and the daemon must be accessible: %w", err)
}
fmt.Fprintln(e.config.Output, "Creating environment...")
if err := e.dockerStream(ctx, "build", "-q", "-t", imageName, "-f",
filepath.Join(e.config.ProjectRoot, "environment", "Dockerfile"), e.config.ProjectRoot); err != nil {
return fmt.Errorf("build environment image: %w", err)
}
_, err := e.docker(ctx,
"create",
"--name", e.containerName,
"--init",
"--network=none",
"--read-only",
"--tmpfs=/tmp:rw,nosuid,nodev,size=128m",
"--cap-drop=ALL",
"--security-opt=no-new-privileges",
"--pids-limit=128",
"--memory=512m",
"--cpus=1",
imageName,
)
if err != nil {
return fmt.Errorf("create environment: %w", err)
}
e.created = true
if _, err := e.docker(ctx, "start", e.containerName); err != nil {
return fmt.Errorf("start environment: %w", err)
}
if err := e.waitFor(ctx, 10*time.Second, func() bool {
_, readyErr := e.docker(ctx, "exec", e.containerName, "test", "-f", "/tmp/agentbox-ready")
return readyErr == nil
}); err != nil {
return fmt.Errorf("wait for graphical environment: %w", err)
}
return nil
}
func (e *Environment) Launch(ctx context.Context, command environment.Command) error {
fmt.Fprintln(e.config.Output, "Uploading build...")
file, err := os.Open(command.Path)
if err != nil {
return fmt.Errorf("open application: %w", err)
}
defer file.Close()
stage := exec.CommandContext(ctx, "docker", "exec", "-i", e.containerName, "sh", "-c",
"cat > /tmp/application && chmod 0500 /tmp/application")
stage.Stdin = file
if output, err := stage.CombinedOutput(); err != nil {
return fmt.Errorf("stage application: %w: %s", err, strings.TrimSpace(string(output)))
}
fmt.Fprintln(e.config.Output, "Launching application...")
args := []string{"exec", "-d"}
for key, value := range command.Env {
args = append(args, "-e", key+"="+value)
}
args = append(args, e.containerName, "sh", "-c")
parts := []string{"exec", "/tmp/application"}
for _, arg := range command.Args {
parts = append(parts, shellQuote(arg))
}
parts = append(parts, ">/tmp/stdout.log", "2>/tmp/stderr.log")
args = append(args, strings.Join(parts, " "))
if _, err := e.docker(ctx, args...); err != nil {
return fmt.Errorf("launch application: %w", err)
}
if command.WindowTitle != "" {
var windowID string
if err := e.waitFor(ctx, 10*time.Second, func() bool {
output, searchErr := e.docker(ctx, "exec", e.containerName, "xdotool",
"search", "--name", command.WindowTitle)
if searchErr != nil {
return false
}
windowID = strings.TrimSpace(strings.Split(string(output), "\n")[0])
return windowID != ""
}); err != nil {
return fmt.Errorf("wait for application window %q: %w", command.WindowTitle, err)
}
if _, err := e.docker(ctx, "exec", e.containerName, "xdotool",
"windowactivate", "--sync", windowID); err != nil {
return fmt.Errorf("focus application window: %w", err)
}
} else {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(500 * time.Millisecond):
}
}
return nil
}
func (e *Environment) Screenshot(ctx context.Context) ([]byte, error) {
if _, err := e.docker(ctx, "exec", e.containerName, "scrot", "-o", "/tmp/screenshot.png"); err != nil {
return nil, fmt.Errorf("capture screenshot: %w", err)
}
data, err := e.docker(ctx, "exec", e.containerName, "cat", "/tmp/screenshot.png")
if err != nil {
return nil, fmt.Errorf("extract screenshot: %w", err)
}
return data, nil
}
func (e *Environment) SendInput(ctx context.Context, action environment.InputAction) error {
var args []string
switch action.Type {
case environment.KeyDown:
if action.Key == "" {
return errors.New("key_down requires key")
}
args = []string{"keydown", action.Key}
case environment.KeyUp:
if action.Key == "" {
return errors.New("key_up requires key")
}
args = []string{"keyup", action.Key}
case environment.MouseMove:
args = []string{"mousemove", strconv.Itoa(action.X), strconv.Itoa(action.Y)}
case environment.MouseDown:
args = []string{"mousedown", strconv.Itoa(action.Button)}
case environment.MouseUp:
args = []string{"mouseup", strconv.Itoa(action.Button)}
case environment.Wait:
if action.Duration < 0 {
return errors.New("wait duration cannot be negative")
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(action.Duration):
return nil
}
default:
return fmt.Errorf("unsupported input action %q", action.Type)
}
_, err := e.docker(ctx, append([]string{"exec", e.containerName, "xdotool"}, args...)...)
if err != nil {
return fmt.Errorf("send %s: %w", action.Type, err)
}
return nil
}
func (e *Environment) Logs(ctx context.Context) ([]environment.LogEntry, error) {
var entries []environment.LogEntry
for _, stream := range []string{"stdout", "stderr"} {
data, err := e.docker(ctx, "exec", e.containerName, "cat", "/tmp/"+stream+".log")
if err != nil {
continue
}
if len(data) > 0 {
entries = append(entries, environment.LogEntry{
Stream: stream, Message: string(data), Time: time.Now().UTC(),
})
}
}
return entries, nil
}
func (e *Environment) Stop(ctx context.Context) error {
e.mu.Lock()
defer e.mu.Unlock()
if !e.created || e.stopped {
return nil
}
e.stopped = true
fmt.Fprintln(e.config.Output, "Shutting environment down...")
_, stopErr := e.docker(ctx, "stop", "--time=3", e.containerName)
_, removeErr := e.docker(ctx, "rm", "-f", e.containerName)
if stopErr != nil {
return fmt.Errorf("stop environment: %w", stopErr)
}
if removeErr != nil {
return fmt.Errorf("remove environment: %w", removeErr)
}
return nil
}
func (e *Environment) waitFor(ctx context.Context, timeout time.Duration, check func() bool) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
if check() {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return errors.New("timed out")
case <-ticker.C:
}
}
}
func (e *Environment) docker(ctx context.Context, args ...string) ([]byte, error) {
command := exec.CommandContext(ctx, "docker", args...)
output, err := command.CombinedOutput()
if err != nil {
return output, fmt.Errorf("docker %s: %w: %s", args[0], err, strings.TrimSpace(string(output)))
}
return output, nil
}
func (e *Environment) dockerStream(ctx context.Context, args ...string) error {
command := exec.CommandContext(ctx, "docker", args...)
command.Stdout = e.config.Output
command.Stderr = e.config.Output
return command.Run()
}
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
}
var _ environment.Environment = (*Environment)(nil)
@@ -0,0 +1,48 @@
package environment
import (
"context"
"time"
)
type Command struct {
Path string
Args []string
Env map[string]string
WindowTitle string
}
type InputType string
const (
KeyDown InputType = "key_down"
KeyUp InputType = "key_up"
MouseMove InputType = "mouse_move"
MouseDown InputType = "mouse_down"
MouseUp InputType = "mouse_up"
Wait InputType = "wait"
)
type InputAction struct {
Type InputType `json:"type"`
Key string `json:"key,omitempty"`
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Button int `json:"button,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
}
type LogEntry struct {
Stream string `json:"stream"`
Message string `json:"message"`
Time time.Time `json:"time"`
}
type Environment interface {
Start(context.Context) error
Launch(context.Context, Command) error
Screenshot(context.Context) ([]byte, error)
SendInput(context.Context, InputAction) error
Logs(context.Context) ([]LogEntry, error)
Stop(context.Context) error
}