 8d9b62daf3
			
		
	
	8d9b62daf3
	
	
	
		
			
			This commit implements Phase 2 of the CHORUS Task Execution Engine development plan, providing a comprehensive execution environment abstraction layer with Docker container sandboxing support. ## New Features ### Core Sandbox Interface - Comprehensive ExecutionSandbox interface with isolated task execution - Support for command execution, file I/O, environment management - Resource usage monitoring and sandbox lifecycle management - Standardized error handling with SandboxError types and categories ### Docker Container Sandbox Implementation - Full Docker API integration with secure container creation - Transparent repository mounting with configurable read/write access - Advanced security policies with capability dropping and privilege controls - Comprehensive resource limits (CPU, memory, disk, processes, file handles) - Support for tmpfs mounts, masked paths, and read-only bind mounts - Container lifecycle management with proper cleanup and health monitoring ### Security & Resource Management - Configurable security policies with SELinux, AppArmor, and Seccomp support - Fine-grained capability management with secure defaults - Network isolation options with configurable DNS and proxy settings - Resource monitoring with real-time CPU, memory, and network usage tracking - Comprehensive ulimits configuration for process and file handle limits ### Repository Integration - Seamless repository mounting from local paths to container workspaces - Git configuration support with user credentials and global settings - File inclusion/exclusion patterns for selective repository access - Configurable permissions and ownership for mounted repositories ### Testing Infrastructure - Comprehensive test suite with 60+ test cases covering all functionality - Docker integration tests with Alpine Linux containers (skipped in short mode) - Mock sandbox implementation for unit testing without Docker dependencies - Security policy validation tests with read-only filesystem enforcement - Resource usage monitoring and cleanup verification tests ## Technical Details ### Dependencies Added - github.com/docker/docker v28.4.0+incompatible - Docker API client - github.com/docker/go-connections v0.6.0 - Docker connection utilities - github.com/docker/go-units v0.5.0 - Docker units and formatting - Associated Docker API dependencies for complete container management ### Architecture - Interface-driven design enabling multiple sandbox implementations - Comprehensive configuration structures for all sandbox aspects - Resource usage tracking with detailed metrics collection - Error handling with retryable error classification - Proper cleanup and resource management throughout sandbox lifecycle ### Compatibility - Maintains backward compatibility with existing CHORUS architecture - Designed for future integration with Phase 3 Core Task Execution Engine - Extensible design supporting additional sandbox implementations (VM, process) This Phase 2 implementation provides the foundation for secure, isolated task execution that will be integrated with the AI model providers from Phase 1 in the upcoming Phase 3 development. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			482 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			482 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package execution
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| func TestNewDockerSandbox(t *testing.T) {
 | |
| 	sandbox := NewDockerSandbox()
 | |
| 
 | |
| 	assert.NotNil(t, sandbox)
 | |
| 	assert.NotNil(t, sandbox.environment)
 | |
| 	assert.Empty(t, sandbox.containerID)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_Initialize(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := NewDockerSandbox()
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	// Create a minimal configuration
 | |
| 	config := &SandboxConfig{
 | |
| 		Type:         "docker",
 | |
| 		Image:        "alpine:latest",
 | |
| 		Architecture: "amd64",
 | |
| 		Resources: ResourceLimits{
 | |
| 			MemoryLimit:   512 * 1024 * 1024, // 512MB
 | |
| 			CPULimit:      1.0,
 | |
| 			ProcessLimit:  50,
 | |
| 			FileLimit:     1024,
 | |
| 		},
 | |
| 		Security: SecurityPolicy{
 | |
| 			ReadOnlyRoot:      false,
 | |
| 			NoNewPrivileges:   true,
 | |
| 			AllowNetworking:   false,
 | |
| 			IsolateNetwork:    true,
 | |
| 			IsolateProcess:    true,
 | |
| 			DropCapabilities:  []string{"ALL"},
 | |
| 		},
 | |
| 		Environment: map[string]string{
 | |
| 			"TEST_VAR": "test_value",
 | |
| 		},
 | |
| 		WorkingDir: "/workspace",
 | |
| 		Timeout:    30 * time.Second,
 | |
| 	}
 | |
| 
 | |
| 	err := sandbox.Initialize(ctx, config)
 | |
| 	if err != nil {
 | |
| 		t.Skipf("Docker not available or image pull failed: %v", err)
 | |
| 	}
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	// Verify sandbox is initialized
 | |
| 	assert.NotEmpty(t, sandbox.containerID)
 | |
| 	assert.Equal(t, config, sandbox.config)
 | |
| 	assert.Equal(t, StatusRunning, sandbox.info.Status)
 | |
| 	assert.Equal(t, "docker", sandbox.info.Type)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_ExecuteCommand(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name           string
 | |
| 		cmd            *Command
 | |
| 		expectedExit   int
 | |
| 		expectedOutput string
 | |
| 		shouldError    bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "simple echo command",
 | |
| 			cmd: &Command{
 | |
| 				Executable: "echo",
 | |
| 				Args:       []string{"hello world"},
 | |
| 			},
 | |
| 			expectedExit:   0,
 | |
| 			expectedOutput: "hello world\n",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "command with environment",
 | |
| 			cmd: &Command{
 | |
| 				Executable:  "sh",
 | |
| 				Args:        []string{"-c", "echo $TEST_VAR"},
 | |
| 				Environment: map[string]string{"TEST_VAR": "custom_value"},
 | |
| 			},
 | |
| 			expectedExit:   0,
 | |
| 			expectedOutput: "custom_value\n",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "failing command",
 | |
| 			cmd: &Command{
 | |
| 				Executable: "sh",
 | |
| 				Args:       []string{"-c", "exit 1"},
 | |
| 			},
 | |
| 			expectedExit: 1,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "command with timeout",
 | |
| 			cmd: &Command{
 | |
| 				Executable: "sleep",
 | |
| 				Args:       []string{"2"},
 | |
| 				Timeout:    1 * time.Second,
 | |
| 			},
 | |
| 			shouldError: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			result, err := sandbox.ExecuteCommand(ctx, tt.cmd)
 | |
| 
 | |
| 			if tt.shouldError {
 | |
| 				assert.Error(t, err)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			require.NoError(t, err)
 | |
| 			assert.Equal(t, tt.expectedExit, result.ExitCode)
 | |
| 			assert.Equal(t, tt.expectedExit == 0, result.Success)
 | |
| 
 | |
| 			if tt.expectedOutput != "" {
 | |
| 				assert.Equal(t, tt.expectedOutput, result.Stdout)
 | |
| 			}
 | |
| 
 | |
| 			assert.NotZero(t, result.Duration)
 | |
| 			assert.False(t, result.StartTime.IsZero())
 | |
| 			assert.False(t, result.EndTime.IsZero())
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_FileOperations(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	// Test WriteFile
 | |
| 	testContent := []byte("Hello, Docker sandbox!")
 | |
| 	testPath := "/tmp/test_file.txt"
 | |
| 
 | |
| 	err := sandbox.WriteFile(ctx, testPath, testContent, 0644)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Test ReadFile
 | |
| 	readContent, err := sandbox.ReadFile(ctx, testPath)
 | |
| 	require.NoError(t, err)
 | |
| 	assert.Equal(t, testContent, readContent)
 | |
| 
 | |
| 	// Test ListFiles
 | |
| 	files, err := sandbox.ListFiles(ctx, "/tmp")
 | |
| 	require.NoError(t, err)
 | |
| 	assert.NotEmpty(t, files)
 | |
| 
 | |
| 	// Find our test file
 | |
| 	var testFile *FileInfo
 | |
| 	for _, file := range files {
 | |
| 		if file.Name == "test_file.txt" {
 | |
| 			testFile = &file
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	require.NotNil(t, testFile)
 | |
| 	assert.Equal(t, "test_file.txt", testFile.Name)
 | |
| 	assert.Equal(t, int64(len(testContent)), testFile.Size)
 | |
| 	assert.False(t, testFile.IsDir)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_CopyFiles(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	// Create a temporary file on host
 | |
| 	tempDir := t.TempDir()
 | |
| 	hostFile := filepath.Join(tempDir, "host_file.txt")
 | |
| 	hostContent := []byte("Content from host")
 | |
| 
 | |
| 	err := os.WriteFile(hostFile, hostContent, 0644)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Copy from host to container
 | |
| 	containerPath := "container:/tmp/copied_file.txt"
 | |
| 	err = sandbox.CopyFiles(ctx, hostFile, containerPath)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Verify file exists in container
 | |
| 	readContent, err := sandbox.ReadFile(ctx, "/tmp/copied_file.txt")
 | |
| 	require.NoError(t, err)
 | |
| 	assert.Equal(t, hostContent, readContent)
 | |
| 
 | |
| 	// Copy from container back to host
 | |
| 	hostDestFile := filepath.Join(tempDir, "copied_back.txt")
 | |
| 	err = sandbox.CopyFiles(ctx, "container:/tmp/copied_file.txt", hostDestFile)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Verify file exists on host
 | |
| 	backContent, err := os.ReadFile(hostDestFile)
 | |
| 	require.NoError(t, err)
 | |
| 	assert.Equal(t, hostContent, backContent)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_Environment(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	// Test getting initial environment
 | |
| 	env := sandbox.GetEnvironment()
 | |
| 	assert.Equal(t, "test_value", env["TEST_VAR"])
 | |
| 
 | |
| 	// Test setting additional environment
 | |
| 	newEnv := map[string]string{
 | |
| 		"NEW_VAR": "new_value",
 | |
| 		"PATH":    "/custom/path",
 | |
| 	}
 | |
| 
 | |
| 	err := sandbox.SetEnvironment(newEnv)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Verify environment is updated
 | |
| 	env = sandbox.GetEnvironment()
 | |
| 	assert.Equal(t, "new_value", env["NEW_VAR"])
 | |
| 	assert.Equal(t, "/custom/path", env["PATH"])
 | |
| 	assert.Equal(t, "test_value", env["TEST_VAR"]) // Original should still be there
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_WorkingDirectory(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	// Test getting initial working directory
 | |
| 	workDir := sandbox.GetWorkingDirectory()
 | |
| 	assert.Equal(t, "/workspace", workDir)
 | |
| 
 | |
| 	// Test setting working directory
 | |
| 	newWorkDir := "/tmp"
 | |
| 	err := sandbox.SetWorkingDirectory(newWorkDir)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Verify working directory is updated
 | |
| 	workDir = sandbox.GetWorkingDirectory()
 | |
| 	assert.Equal(t, newWorkDir, workDir)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_ResourceUsage(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	// Get resource usage
 | |
| 	usage, err := sandbox.GetResourceUsage(ctx)
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Verify usage structure
 | |
| 	assert.NotNil(t, usage)
 | |
| 	assert.False(t, usage.Timestamp.IsZero())
 | |
| 	assert.GreaterOrEqual(t, usage.CPUUsage, 0.0)
 | |
| 	assert.GreaterOrEqual(t, usage.MemoryUsage, int64(0))
 | |
| 	assert.GreaterOrEqual(t, usage.MemoryPercent, 0.0)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_GetInfo(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	info := sandbox.GetInfo()
 | |
| 
 | |
| 	assert.NotEmpty(t, info.ID)
 | |
| 	assert.Contains(t, info.Name, "chorus-sandbox")
 | |
| 	assert.Equal(t, "docker", info.Type)
 | |
| 	assert.Equal(t, StatusRunning, info.Status)
 | |
| 	assert.Equal(t, "docker", info.Runtime)
 | |
| 	assert.Equal(t, "alpine:latest", info.Image)
 | |
| 	assert.False(t, info.CreatedAt.IsZero())
 | |
| 	assert.False(t, info.StartedAt.IsZero())
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_Cleanup(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := setupTestSandbox(t)
 | |
| 
 | |
| 	// Verify sandbox is running
 | |
| 	assert.Equal(t, StatusRunning, sandbox.info.Status)
 | |
| 	assert.NotEmpty(t, sandbox.containerID)
 | |
| 
 | |
| 	// Cleanup
 | |
| 	err := sandbox.Cleanup()
 | |
| 	require.NoError(t, err)
 | |
| 
 | |
| 	// Verify sandbox is destroyed
 | |
| 	assert.Equal(t, StatusDestroyed, sandbox.info.Status)
 | |
| }
 | |
| 
 | |
| func TestDockerSandbox_SecurityPolicies(t *testing.T) {
 | |
| 	if testing.Short() {
 | |
| 		t.Skip("Skipping Docker integration test in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := NewDockerSandbox()
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	// Create configuration with strict security policies
 | |
| 	config := &SandboxConfig{
 | |
| 		Type:         "docker",
 | |
| 		Image:        "alpine:latest",
 | |
| 		Architecture: "amd64",
 | |
| 		Resources: ResourceLimits{
 | |
| 			MemoryLimit:   256 * 1024 * 1024, // 256MB
 | |
| 			CPULimit:      0.5,
 | |
| 			ProcessLimit:  10,
 | |
| 			FileLimit:     256,
 | |
| 		},
 | |
| 		Security: SecurityPolicy{
 | |
| 			ReadOnlyRoot:        true,
 | |
| 			NoNewPrivileges:     true,
 | |
| 			AllowNetworking:     false,
 | |
| 			IsolateNetwork:      true,
 | |
| 			IsolateProcess:      true,
 | |
| 			DropCapabilities:    []string{"ALL"},
 | |
| 			RunAsUser:           "1000",
 | |
| 			RunAsGroup:          "1000",
 | |
| 			TmpfsPaths:          []string{"/tmp", "/var/tmp"},
 | |
| 			MaskedPaths:         []string{"/proc/kcore", "/proc/keys"},
 | |
| 			ReadOnlyPaths:       []string{"/etc"},
 | |
| 		},
 | |
| 		WorkingDir: "/workspace",
 | |
| 		Timeout:    30 * time.Second,
 | |
| 	}
 | |
| 
 | |
| 	err := sandbox.Initialize(ctx, config)
 | |
| 	if err != nil {
 | |
| 		t.Skipf("Docker not available or security policies not supported: %v", err)
 | |
| 	}
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	// Test that we can't write to read-only filesystem
 | |
| 	result, err := sandbox.ExecuteCommand(ctx, &Command{
 | |
| 		Executable: "touch",
 | |
| 		Args:       []string{"/test_readonly"},
 | |
| 	})
 | |
| 	require.NoError(t, err)
 | |
| 	assert.NotEqual(t, 0, result.ExitCode) // Should fail due to read-only root
 | |
| 
 | |
| 	// Test that tmpfs is writable
 | |
| 	result, err = sandbox.ExecuteCommand(ctx, &Command{
 | |
| 		Executable: "touch",
 | |
| 		Args:       []string{"/tmp/test_tmpfs"},
 | |
| 	})
 | |
| 	require.NoError(t, err)
 | |
| 	assert.Equal(t, 0, result.ExitCode) // Should succeed on tmpfs
 | |
| }
 | |
| 
 | |
| // setupTestSandbox creates a basic Docker sandbox for testing
 | |
| func setupTestSandbox(t *testing.T) *DockerSandbox {
 | |
| 	sandbox := NewDockerSandbox()
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	config := &SandboxConfig{
 | |
| 		Type:         "docker",
 | |
| 		Image:        "alpine:latest",
 | |
| 		Architecture: "amd64",
 | |
| 		Resources: ResourceLimits{
 | |
| 			MemoryLimit:   512 * 1024 * 1024, // 512MB
 | |
| 			CPULimit:      1.0,
 | |
| 			ProcessLimit:  50,
 | |
| 			FileLimit:     1024,
 | |
| 		},
 | |
| 		Security: SecurityPolicy{
 | |
| 			ReadOnlyRoot:      false,
 | |
| 			NoNewPrivileges:   true,
 | |
| 			AllowNetworking:   true, // Allow networking for easier testing
 | |
| 			IsolateNetwork:    false,
 | |
| 			IsolateProcess:    true,
 | |
| 			DropCapabilities:  []string{"NET_ADMIN", "SYS_ADMIN"},
 | |
| 		},
 | |
| 		Environment: map[string]string{
 | |
| 			"TEST_VAR": "test_value",
 | |
| 		},
 | |
| 		WorkingDir: "/workspace",
 | |
| 		Timeout:    30 * time.Second,
 | |
| 	}
 | |
| 
 | |
| 	err := sandbox.Initialize(ctx, config)
 | |
| 	if err != nil {
 | |
| 		t.Skipf("Docker not available: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return sandbox
 | |
| }
 | |
| 
 | |
| // Benchmark tests
 | |
| func BenchmarkDockerSandbox_ExecuteCommand(b *testing.B) {
 | |
| 	if testing.Short() {
 | |
| 		b.Skip("Skipping Docker benchmark in short mode")
 | |
| 	}
 | |
| 
 | |
| 	sandbox := &DockerSandbox{}
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	// Setup minimal config for benchmarking
 | |
| 	config := &SandboxConfig{
 | |
| 		Type:         "docker",
 | |
| 		Image:        "alpine:latest",
 | |
| 		Architecture: "amd64",
 | |
| 		Resources: ResourceLimits{
 | |
| 			MemoryLimit:  256 * 1024 * 1024,
 | |
| 			CPULimit:     1.0,
 | |
| 			ProcessLimit: 50,
 | |
| 		},
 | |
| 		Security: SecurityPolicy{
 | |
| 			NoNewPrivileges: true,
 | |
| 			AllowNetworking: true,
 | |
| 		},
 | |
| 		WorkingDir: "/workspace",
 | |
| 		Timeout:    10 * time.Second,
 | |
| 	}
 | |
| 
 | |
| 	err := sandbox.Initialize(ctx, config)
 | |
| 	if err != nil {
 | |
| 		b.Skipf("Docker not available: %v", err)
 | |
| 	}
 | |
| 	defer sandbox.Cleanup()
 | |
| 
 | |
| 	cmd := &Command{
 | |
| 		Executable: "echo",
 | |
| 		Args:       []string{"benchmark test"},
 | |
| 	}
 | |
| 
 | |
| 	b.ResetTimer()
 | |
| 	for i := 0; i < b.N; i++ {
 | |
| 		_, err := sandbox.ExecuteCommand(ctx, cmd)
 | |
| 		if err != nil {
 | |
| 			b.Fatalf("Command execution failed: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| } |