Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/types/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ type WebSocketMessage struct {
Data json.RawMessage `json:"data,omitempty"`
}

// SidecarMount describes an additional sidecar image to mount into the task container.
type SidecarMount struct {
Image string `json:"image"` // Docker image to pull.
MountPath string `json:"mount_path"` // Path to mount the sidecar filesystem in the task container.
ReadWrite bool `json:"read_write"` // If false (default), the mount is read-only.
}

// TaskAssignmentMessage is sent from server to worker when a task is available
type TaskAssignmentMessage struct {
TaskID string `json:"task_id"`
Expand All @@ -30,6 +37,8 @@ type TaskAssignmentMessage struct {
SidecarImage string `json:"sidecar_image,omitempty"`
// EnvVars contains environment variables to set in the container (e.g. WARP_API_KEY, GITHUB_ACCESS_TOKEN)
EnvVars map[string]string `json:"env_vars,omitempty"`
// AdditionalSidecars is a list of extra sidecar images to mount into the task container.
AdditionalSidecars []SidecarMount `json:"additional_sidecars,omitempty"`
}

// TaskClaimedMessage is sent from worker to server after successfully claiming a task
Expand Down
71 changes: 70 additions & 1 deletion internal/worker/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,12 @@ func (w *Worker) executeTaskInDocker(ctx context.Context, assignment *types.Task
}
}

// Prepare additional sidecar volumes (e.g., xvfb for computer use).
additionalSidecarBinds, err := w.prepareAdditionalSidecars(ctx, dockerClient, assignment.AdditionalSidecars)
if err != nil {
return err
}

envVars := []string{
fmt.Sprintf("TASK_ID=%s", task.ID),
"GIT_TERMINAL_PROMPT=0",
Expand Down Expand Up @@ -501,7 +507,9 @@ func (w *Worker) executeTaskInDocker(ctx context.Context, assignment *types.Task
binds := []string{
fmt.Sprintf("%s:/agent:ro", volumeName),
}
// Add user-configured volumes
// Add additional sidecar volumes.
binds = append(binds, additionalSidecarBinds...)
// Add user-configured volumes.
binds = append(binds, w.config.Volumes...)

hostConfig := &container.HostConfig{
Expand Down Expand Up @@ -693,6 +701,67 @@ func (w *Worker) copySidecarFilesystemToVolume(ctx context.Context, dockerClient
return nil
}

// prepareAdditionalSidecars pulls each additional sidecar image, creates a Docker volume
// from its filesystem, and returns the list of bind mount strings to add to the container.
func (w *Worker) prepareAdditionalSidecars(ctx context.Context, dockerClient *client.Client, sidecars []types.SidecarMount) ([]string, error) {
var binds []string
seenMountPaths := make(map[string]bool)

for _, sidecar := range sidecars {
if sidecar.Image == "" {
return nil, fmt.Errorf("additional sidecar has empty image")
}
if sidecar.MountPath == "" {
return nil, fmt.Errorf("additional sidecar %s has empty mount path", sidecar.Image)
}
if seenMountPaths[sidecar.MountPath] {
return nil, fmt.Errorf("duplicate mount path %s for additional sidecar %s", sidecar.MountPath, sidecar.Image)
}
seenMountPaths[sidecar.MountPath] = true

log.Infof(ctx, "Preparing additional sidecar: image=%s, mount=%s", sidecar.Image, sidecar.MountPath)

// Additional sidecar images are public, so no auth is needed.
if err := w.pullImage(ctx, sidecar.Image, ""); err != nil {
return nil, fmt.Errorf("failed to pull additional sidecar image %s: %w", sidecar.Image, err)
}

digest, err := w.getImageDigest(ctx, sidecar.Image)
if err != nil {
return nil, fmt.Errorf("failed to get digest for additional sidecar image %s: %w", sidecar.Image, err)
}

volumeName := sanitizeVolumeName(sidecar.Image, digest)
log.Debugf(ctx, "Using volume %s for additional sidecar %s", volumeName, sidecar.Image)

_, err = dockerClient.VolumeInspect(ctx, volumeName)
if err == nil {
log.Debugf(ctx, "Reusing existing volume %s for additional sidecar", volumeName)
} else {
log.Infof(ctx, "Creating new Docker volume: %s", volumeName)
if _, err := dockerClient.VolumeCreate(ctx, volume.CreateOptions{Name: volumeName}); err != nil {
return nil, fmt.Errorf("failed to create volume for additional sidecar %s: %w", sidecar.Image, err)
}

if err := w.copySidecarFilesystemToVolume(ctx, dockerClient, sidecar.Image, volumeName); err != nil {
// Clean up the empty volume so it isn't silently reused on retry.
if removeErr := dockerClient.VolumeRemove(ctx, volumeName, false); removeErr != nil {
log.Warnf(ctx, "Failed to clean up volume %s after copy failure: %v", volumeName, removeErr)
}
return nil, fmt.Errorf("failed to copy additional sidecar %s to volume: %w", sidecar.Image, err)
}
}

mode := ":ro"
if sidecar.ReadWrite {
// Docker defaults to read-write when no mode suffix is provided.
mode = ""
}
binds = append(binds, fmt.Sprintf("%s:%s%s", volumeName, sidecar.MountPath, mode))
}
return binds, nil
}

func (w *Worker) sendTaskClaimed(taskID string) error {
claimed := types.TaskClaimedMessage{
TaskID: taskID,
Expand Down