From 867d7846be9577e137993900511aa03695aa631f Mon Sep 17 00:00:00 2001 From: Oanakiaja <281723571@qq.com> Date: Wed, 28 Jan 2026 22:25:30 +0800 Subject: [PATCH 1/2] feat(commandline): add aiosandbox tool for remote sandbox execution --- .../tool/commandline/aiosandbox/README.md | 186 +++++++++ .../tool/commandline/aiosandbox/sandbox.go | 373 ++++++++++++++++++ .../commandline/aiosandbox/sandbox_test.go | 261 ++++++++++++ .../commandline/examples/aiosandbox/main.go | 135 +++++++ components/tool/commandline/go.mod | 1 + components/tool/commandline/go.sum | 2 + 6 files changed, 958 insertions(+) create mode 100644 components/tool/commandline/aiosandbox/README.md create mode 100644 components/tool/commandline/aiosandbox/sandbox.go create mode 100644 components/tool/commandline/aiosandbox/sandbox_test.go create mode 100644 components/tool/commandline/examples/aiosandbox/main.go diff --git a/components/tool/commandline/aiosandbox/README.md b/components/tool/commandline/aiosandbox/README.md new file mode 100644 index 000000000..8e357c30b --- /dev/null +++ b/components/tool/commandline/aiosandbox/README.md @@ -0,0 +1,186 @@ +# AIO Sandbox + +AIO Sandbox is an implementation of the `commandline.Operator` interface that provides a remote sandboxed environment using the [AIO Sandbox](https://github.com/agent-infra/sandbox). + +## Features + +- **Remote Execution**: Execute commands in a cloud-based sandbox environment +- **Session Persistence**: Optionally maintain shell session state across commands +- **File Operations**: Read, write, and manage files in the sandbox +- **Secure**: Path traversal protection and isolated execution environment +- **Compatible**: Works seamlessly with eino's `StrReplaceEditor` and `PyExecutor` tools + +## Installation + +```bash +go get github.com/cloudwego/eino-ext/components/tool/commandline/aiosandbox +``` + +## Usage + +### Basic Usage + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/cloudwego/eino-ext/components/tool/commandline/aiosandbox" +) + +func main() { + ctx := context.Background() + + // Create AIO Sandbox + sandbox, err := aiosandbox.NewAIOSandbox(ctx, &aiosandbox.Config{ + BaseURL: "https://xxxx.apigateway-cn-beijing.volceapi.com", + Token: "your-api-token", + WorkDir: "/workspace", + Timeout: 120, + KeepSession: true, + }) + if err != nil { + log.Fatal(err) + } + defer sandbox.Close(ctx) + + // Execute a command + output, err := sandbox.RunCommand(ctx, []string{"python3", "--version"}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Python version: %s\n", output.Stdout) + + // Write a file + err = sandbox.WriteFile(ctx, "/workspace/hello.py", `print("Hello, World!")`) + if err != nil { + log.Fatal(err) + } + + // Read a file + content, err := sandbox.ReadFile(ctx, "/workspace/hello.py") + if err != nil { + log.Fatal(err) + } + fmt.Printf("File content: %s\n", content) +} +``` + +### Integration with Eino Tools + +```go +package main + +import ( + "context" + "log" + + "github.com/cloudwego/eino-ext/components/tool/commandline" + "github.com/cloudwego/eino-ext/components/tool/commandline/aiosandbox" + "github.com/cloudwego/eino/components/tool" +) + +func main() { + ctx := context.Background() + + // Create AIO Sandbox + sandbox, err := aiosandbox.NewAIOSandbox(ctx, &aiosandbox.Config{ + BaseURL: "https://xxxx.apigateway-cn-beijing.volceapi.com", + WorkDir: "/workspace", + KeepSession: true, + }) + if err != nil { + log.Fatal(err) + } + defer sandbox.Close(ctx) + + // Create StrReplaceEditor with AIO Sandbox + editor, err := commandline.NewStrReplaceEditor(ctx, &commandline.EditorConfig{ + Operator: sandbox, + }) + if err != nil { + log.Fatal(err) + } + + // Create PyExecutor with AIO Sandbox + pyExecutor, err := commandline.NewPyExecutor(ctx, &commandline.PyExecutorConfig{ + Command: "python3", + Operator: sandbox, + }) + if err != nil { + log.Fatal(err) + } + + // Use tools in your agent + tools := []tool.BaseTool{editor, pyExecutor} + _ = tools +} +``` + +## Configuration + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `BaseURL` | string | (required) | AIO Sandbox API endpoint | +| `Token` | string | (required) | Authentication token | +| `WorkDir` | string | `/tmp` | Working directory in sandbox | +| `Timeout` | float64 | `60.0` | Command timeout in seconds | +| `KeepSession` | bool | `false` | Reuse shell sessions for stateful execution | + +## API Reference + +### Methods + +#### `NewAIOSandbox(ctx, config) (*AIOSandbox, error)` +Creates a new AIO Sandbox instance. + +#### `RunCommand(ctx, command []string) (*CommandOutput, error)` +Executes a command in the sandbox. + +#### `ReadFile(ctx, path string) (string, error)` +Reads file content from the sandbox. + +#### `WriteFile(ctx, path string, content string) error` +Writes content to a file in the sandbox. + +#### `Exists(ctx, path string) (bool, error)` +Checks if a path exists. + +#### `IsDirectory(ctx, path string) (bool, error)` +Checks if a path is a directory. + +#### `Close(ctx) error` +Releases sandbox resources. + +#### `SetWorkDir(dir string)` +Updates the working directory. + +#### `GetSessionID() string` +Returns the current shell session ID. + +## Comparison with DockerSandbox + +| Feature | AIOSandbox | DockerSandbox | +|---------|------------|---------------| +| Dependency | Remote API | Local Docker | +| Setup | API credentials | Docker installed | +| Isolation | Cloud sandbox | Container | +| Session | API-managed | Container lifecycle | +| Resource limits | Server-side | Local config | +| Network access | Server-controlled | Local config | + +## Testing + +Run unit tests: +```bash +go test ./... +``` + +Run integration tests (requires AIO Sandbox credentials): +```bash +export AIO_SANDBOX_BASE_URL=https://xxxx.apigateway-cn-beijing.volceapi.com +go test -v ./... +``` diff --git a/components/tool/commandline/aiosandbox/sandbox.go b/components/tool/commandline/aiosandbox/sandbox.go new file mode 100644 index 000000000..54e098c36 --- /dev/null +++ b/components/tool/commandline/aiosandbox/sandbox.go @@ -0,0 +1,373 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aiosandbox + +import ( + "context" + "fmt" + "net/http" + "net/url" + "path/filepath" + "strings" + "sync" + + "github.com/cloudwego/eino-ext/components/tool/commandline" + + sandboxsdk "github.com/agent-infra/sandbox-sdk-go" + "github.com/agent-infra/sandbox-sdk-go/client" + "github.com/agent-infra/sandbox-sdk-go/option" +) + +const ( + defaultWorkDir = "/tmp" + defaultTimeout = 60.0 +) + +// Config defines the configuration for AIO Sandbox. +type Config struct { + // BaseURL is the AIO Sandbox API endpoint. + // Required. + BaseURL string + + // Token is the authentication token for AIO Sandbox API. + // Optional. If not provided, requests will be made without authentication. + Token string + + // WorkDir is the working directory inside the sandbox. + // Default: "/tmp" + WorkDir string + + // Timeout is the default command execution timeout in seconds. + // Default: 60 + Timeout float64 + + // KeepSession indicates whether to reuse shell sessions for stateful execution. + // When enabled, environment variables and working directory changes persist across commands. + // Default: true + KeepSession bool +} + +func (c *Config) setDefaults() { + if c.WorkDir == "" { + c.WorkDir = defaultWorkDir + } + if c.Timeout <= 0 { + c.Timeout = defaultTimeout + } +} + +// AIOSandbox implements commandline.Operator interface using AIO Sandbox API. +// It provides a remote sandboxed environment for executing commands and file operations. +type AIOSandbox struct { + config *Config + client *client.Client + sessionID string + mu sync.RWMutex +} + +// NewAIOSandbox creates a new AIO Sandbox operator. +// +// Example: +// +// sandbox, err := aiosandbox.NewAIOSandbox(ctx, &aiosandbox.Config{ +// BaseURL: "https://api.aio-sandbox.com", +// Token: "your-api-token", +// WorkDir: "/workspace", +// Timeout: 120, +// KeepSession: true, +// }) +func NewAIOSandbox(ctx context.Context, config *Config) (*AIOSandbox, error) { + if config == nil { + return nil, fmt.Errorf("config is required") + } + if config.BaseURL == "" { + return nil, fmt.Errorf("BaseURL is required") + } + + // Copy config to avoid external mutation + cfg := *config + cfg.setDefaults() + + // Parse the base URL to extract query parameters + parsedURL, err := url.Parse(cfg.BaseURL) + if err != nil { + return nil, fmt.Errorf("invalid BaseURL: %w", err) + } + + // Build base URL without query parameters + baseURL := fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, parsedURL.Path) + + opts := []option.RequestOption{ + option.WithBaseURL(baseURL), + } + + // Add query parameters if present in the original URL + if len(parsedURL.RawQuery) > 0 { + opts = append(opts, option.WithQueryParameters(parsedURL.Query())) + } + + // Set up authentication header if token is provided + if cfg.Token != "" { + authHeader := http.Header{} + authHeader.Set("Authorization", "Bearer "+cfg.Token) + opts = append(opts, option.WithHTTPHeader(authHeader)) + } + + c := client.NewClient(opts...) + + sandbox := &AIOSandbox{ + config: &cfg, + client: c, + } + + // Create initial session if KeepSession is enabled + if cfg.KeepSession { + if err := sandbox.createSession(ctx); err != nil { + return nil, fmt.Errorf("failed to create initial session: %w", err) + } + } + + return sandbox, nil +} + +// createSession creates a new shell session. +func (s *AIOSandbox) createSession(ctx context.Context) error { + resp, err := s.client.Shell.CreateSession(ctx, &sandboxsdk.ShellCreateSessionRequest{ + ExecDir: sandboxsdk.String(s.config.WorkDir), + }) + if err != nil { + return err + } + + data := resp.GetData() + if data == nil { + return fmt.Errorf("empty response data") + } + + s.mu.Lock() + s.sessionID = data.GetSessionId() + s.mu.Unlock() + return nil +} + +// RunCommand executes a command in the sandbox. +// Implements commandline.Operator interface. +func (s *AIOSandbox) RunCommand(ctx context.Context, command []string) (*commandline.CommandOutput, error) { + cmd := strings.Join(command, " ") + if cmd == "" { + return nil, fmt.Errorf("command is empty") + } + + req := &sandboxsdk.ShellExecRequest{ + Command: cmd, + ExecDir: sandboxsdk.String(s.config.WorkDir), + Timeout: sandboxsdk.Float64(s.config.Timeout), + } + + // Use existing session if available + s.mu.RLock() + sessionID := s.sessionID + s.mu.RUnlock() + + if s.config.KeepSession && sessionID != "" { + req.Id = sandboxsdk.String(sessionID) + } + + resp, err := s.client.Shell.ExecCommand(ctx, req) + if err != nil { + return nil, fmt.Errorf("shell exec failed: %w", err) + } + + data := resp.GetData() + if data == nil { + return nil, fmt.Errorf("empty response data") + } + + // Update session ID if changed + if s.config.KeepSession && data.GetSessionId() != "" { + s.mu.Lock() + s.sessionID = data.GetSessionId() + s.mu.Unlock() + } + + output := &commandline.CommandOutput{ + Stdout: ptrToString(data.GetOutput()), + } + + // Map status to exit code and stderr + status := data.GetStatus() + switch status { + case sandboxsdk.BashCommandStatusCompleted: + if data.GetExitCode() != nil { + output.ExitCode = *data.GetExitCode() + } + case sandboxsdk.BashCommandStatusHardTimeout, sandboxsdk.BashCommandStatusNoChangeTimeout: + output.ExitCode = 124 // standard timeout exit code + output.Stderr = fmt.Sprintf("command timeout: %s", status) + case sandboxsdk.BashCommandStatusTerminated: + output.ExitCode = 137 // 128 + SIGKILL(9) + output.Stderr = "command was terminated" + case sandboxsdk.BashCommandStatusRunning: + // Command is still running (async mode) + output.Stderr = "command is still running" + default: + output.Stderr = fmt.Sprintf("unexpected status: %s", status) + } + + return output, nil +} + +// ReadFile reads file content from the sandbox. +// Implements commandline.Operator interface. +func (s *AIOSandbox) ReadFile(ctx context.Context, path string) (string, error) { + resolvedPath, err := s.resolvePath(path) + if err != nil { + return "", err + } + + resp, err := s.client.File.ReadFile(ctx, &sandboxsdk.FileReadRequest{ + File: resolvedPath, + }) + if err != nil { + return "", fmt.Errorf("read file failed: %w", err) + } + + data := resp.GetData() + if data == nil { + return "", fmt.Errorf("empty response data") + } + + return data.GetContent(), nil +} + +// WriteFile writes content to a file in the sandbox. +// Implements commandline.Operator interface. +func (s *AIOSandbox) WriteFile(ctx context.Context, path string, content string) error { + resolvedPath, err := s.resolvePath(path) + if err != nil { + return err + } + + _, err = s.client.File.WriteFile(ctx, &sandboxsdk.FileWriteRequest{ + File: resolvedPath, + Content: content, + }) + if err != nil { + return fmt.Errorf("write file failed: %w", err) + } + + return nil +} + +// IsDirectory checks if the path is a directory. +// Implements commandline.Operator interface. +func (s *AIOSandbox) IsDirectory(ctx context.Context, path string) (bool, error) { + resolvedPath, err := s.resolvePath(path) + if err != nil { + return false, err + } + + resp, err := s.client.File.ListPath(ctx, &sandboxsdk.FileListRequest{ + Path: resolvedPath, + Recursive: sandboxsdk.Bool(false), + }) + if err != nil { + // Path doesn't exist or is not a directory + return false, nil + } + + // If we can list it without error, it's a directory + return resp.GetData() != nil, nil +} + +// Exists checks if the path exists. +// Implements commandline.Operator interface. +func (s *AIOSandbox) Exists(ctx context.Context, path string) (bool, error) { + resolvedPath, err := s.resolvePath(path) + if err != nil { + return false, err + } + + // Try to read file info + _, err = s.client.File.ReadFile(ctx, &sandboxsdk.FileReadRequest{ + File: resolvedPath, + StartLine: sandboxsdk.Int(0), + EndLine: sandboxsdk.Int(1), + }) + if err == nil { + return true, nil + } + + // Check if it's a directory + isDir, _ := s.IsDirectory(ctx, path) + return isDir, nil +} + +// Close terminates the shell session and releases resources. +func (s *AIOSandbox) Close(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.sessionID != "" { + // Kill any running processes in the session + _, _ = s.client.Shell.KillProcess(ctx, &sandboxsdk.ShellKillProcessRequest{ + Id: s.sessionID, + }) + s.sessionID = "" + } + + return nil +} + +// SetWorkDir updates the working directory for subsequent operations. +func (s *AIOSandbox) SetWorkDir(dir string) { + s.mu.Lock() + defer s.mu.Unlock() + s.config.WorkDir = dir +} + +// GetSessionID returns the current shell session ID. +func (s *AIOSandbox) GetSessionID() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.sessionID +} + +// resolvePath safely resolves a path, preventing path traversal attacks. +func (s *AIOSandbox) resolvePath(path string) (string, error) { + // Check for path traversal attempts + if strings.Contains(path, "..") { + return "", fmt.Errorf("path contains potentially unsafe pattern") + } + + if filepath.IsAbs(path) { + return path, nil + } + + return filepath.Join(s.config.WorkDir, path), nil +} + +// ptrToString safely converts a string pointer to string. +func ptrToString(s *string) string { + if s == nil { + return "" + } + return *s +} + +// Compile-time check that AIOSandbox implements Operator interface +var _ commandline.Operator = (*AIOSandbox)(nil) diff --git a/components/tool/commandline/aiosandbox/sandbox_test.go b/components/tool/commandline/aiosandbox/sandbox_test.go new file mode 100644 index 000000000..5e8727bb2 --- /dev/null +++ b/components/tool/commandline/aiosandbox/sandbox_test.go @@ -0,0 +1,261 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aiosandbox + +import ( + "context" + "os" + "testing" +) + +func getTestConfig() *Config { + baseURL := os.Getenv("AIO_SANDBOX_BASE_URL") + if baseURL == "" { + return nil + } + + // Token is optional + token := os.Getenv("AIO_SANDBOX_TOKEN") + + return &Config{ + BaseURL: baseURL, + Token: token, + WorkDir: "/tmp", + Timeout: 60, + KeepSession: true, + } +} + +func TestNewAIOSandbox(t *testing.T) { + t.Run("missing config", func(t *testing.T) { + _, err := NewAIOSandbox(context.Background(), nil) + if err == nil { + t.Error("expected error for nil config") + } + }) + + t.Run("missing BaseURL", func(t *testing.T) { + _, err := NewAIOSandbox(context.Background(), &Config{ + Token: "test-token", + }) + if err == nil { + t.Error("expected error for missing BaseURL") + } + }) + + t.Run("without Token should work", func(t *testing.T) { + // Token is optional, so this should not return an error for missing token + // It will fail on createSession if KeepSession is true, but that's expected + _, err := NewAIOSandbox(context.Background(), &Config{ + BaseURL: "https://example.com", + KeepSession: false, // Disable session to avoid network call + }) + // Will fail due to network, but not due to missing token + if err != nil && err.Error() == "Token is required" { + t.Error("Token should be optional") + } + }) +} + +func TestConfigDefaults(t *testing.T) { + cfg := &Config{ + BaseURL: "https://example.com", + Token: "test-token", + } + cfg.setDefaults() + + if cfg.WorkDir != defaultWorkDir { + t.Errorf("expected WorkDir %s, got %s", defaultWorkDir, cfg.WorkDir) + } + if cfg.Timeout != defaultTimeout { + t.Errorf("expected Timeout %f, got %f", defaultTimeout, cfg.Timeout) + } +} + +func TestResolvePath(t *testing.T) { + sandbox := &AIOSandbox{ + config: &Config{ + WorkDir: "/workspace", + }, + } + + tests := []struct { + name string + path string + expected string + wantErr bool + }{ + { + name: "absolute path", + path: "/tmp/test.txt", + expected: "/tmp/test.txt", + wantErr: false, + }, + { + name: "relative path", + path: "test.txt", + expected: "/workspace/test.txt", + wantErr: false, + }, + { + name: "path traversal", + path: "../etc/passwd", + expected: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := sandbox.resolvePath(tt.path) + if (err != nil) != tt.wantErr { + t.Errorf("resolvePath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.expected { + t.Errorf("resolvePath() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestPtrToString(t *testing.T) { + tests := []struct { + name string + input *string + expected string + }{ + { + name: "nil pointer", + input: nil, + expected: "", + }, + { + name: "non-nil pointer", + input: strPtr("hello"), + expected: "hello", + }, + { + name: "empty string pointer", + input: strPtr(""), + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ptrToString(tt.input) + if got != tt.expected { + t.Errorf("ptrToString() = %v, want %v", got, tt.expected) + } + }) + } +} + +func strPtr(s string) *string { + return &s +} + +// Integration tests - run only when AIO_SANDBOX_BASE_URL and AIO_SANDBOX_TOKEN are set +func TestIntegration(t *testing.T) { + cfg := getTestConfig() + if cfg == nil { + t.Skip("Skipping integration tests: AIO_SANDBOX_BASE_URL not set") + } + + ctx := context.Background() + + sandbox, err := NewAIOSandbox(ctx, cfg) + if err != nil { + t.Fatalf("Failed to create sandbox: %v", err) + } + defer sandbox.Close(ctx) + + t.Run("RunCommand", func(t *testing.T) { + output, err := sandbox.RunCommand(ctx, []string{"echo", "hello"}) + if err != nil { + t.Fatalf("RunCommand failed: %v", err) + } + if output.ExitCode != 0 { + t.Errorf("expected exit code 0, got %d", output.ExitCode) + } + }) + + t.Run("WriteAndReadFile", func(t *testing.T) { + testContent := "Hello, AIO Sandbox!" + testPath := "/tmp/test_file.txt" + + err := sandbox.WriteFile(ctx, testPath, testContent) + if err != nil { + t.Fatalf("WriteFile failed: %v", err) + } + + content, err := sandbox.ReadFile(ctx, testPath) + if err != nil { + t.Fatalf("ReadFile failed: %v", err) + } + + if content != testContent { + t.Errorf("expected content %q, got %q", testContent, content) + } + }) + + t.Run("Exists", func(t *testing.T) { + exists, err := sandbox.Exists(ctx, "/tmp/test_file.txt") + if err != nil { + t.Fatalf("Exists failed: %v", err) + } + if !exists { + t.Error("expected file to exist") + } + + exists, err = sandbox.Exists(ctx, "/tmp/nonexistent_file.txt") + if err != nil { + t.Fatalf("Exists failed: %v", err) + } + if exists { + t.Error("expected file to not exist") + } + }) + + t.Run("IsDirectory", func(t *testing.T) { + isDir, err := sandbox.IsDirectory(ctx, "/tmp") + if err != nil { + t.Fatalf("IsDirectory failed: %v", err) + } + if !isDir { + t.Error("expected /tmp to be a directory") + } + }) + + t.Run("SessionPersistence", func(t *testing.T) { + // Set an environment variable + _, err := sandbox.RunCommand(ctx, []string{"export TEST_VAR=hello"}) + if err != nil { + t.Fatalf("RunCommand failed: %v", err) + } + + // Check if it persists (only works with KeepSession=true) + output, err := sandbox.RunCommand(ctx, []string{"echo $TEST_VAR"}) + if err != nil { + t.Fatalf("RunCommand failed: %v", err) + } + + // Note: This test verifies session persistence behavior + t.Logf("Session persistence test output: %s", output.Stdout) + }) +} diff --git a/components/tool/commandline/examples/aiosandbox/main.go b/components/tool/commandline/examples/aiosandbox/main.go new file mode 100644 index 000000000..a5eb23599 --- /dev/null +++ b/components/tool/commandline/examples/aiosandbox/main.go @@ -0,0 +1,135 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino-ext/components/tool/commandline" + "github.com/cloudwego/eino-ext/components/tool/commandline/aiosandbox" +) + +func main() { + ctx := context.Background() + + // Get configuration from environment variables + baseURL := os.Getenv("AIO_SANDBOX_BASE_URL") + token := os.Getenv("AIO_SANDBOX_TOKEN") + + if baseURL == "" || token == "" { + log.Fatal("Please set AIO_SANDBOX_BASE_URL and AIO_SANDBOX_TOKEN environment variables") + } + + // Create AIO Sandbox + sandbox, err := aiosandbox.NewAIOSandbox(ctx, &aiosandbox.Config{ + BaseURL: baseURL, + Token: token, + WorkDir: "/tmp", + Timeout: 120, + KeepSession: true, // Enable session persistence for stateful operations + }) + if err != nil { + log.Fatalf("Failed to create sandbox: %v", err) + } + defer sandbox.Close(ctx) + + fmt.Println("=== AIO Sandbox Example ===") + fmt.Printf("Session ID: %s\n\n", sandbox.GetSessionID()) + + // Example 1: Execute a simple command + fmt.Println("1. Executing 'echo hello'...") + output, err := sandbox.RunCommand(ctx, []string{"echo", "hello"}) + if err != nil { + log.Fatalf("RunCommand failed: %v", err) + } + fmt.Printf(" Output: %s", output.Stdout) + fmt.Printf(" Exit Code: %d\n\n", output.ExitCode) + + // Example 2: Write a Python script + fmt.Println("2. Writing a Python script...") + pythonCode := `#!/usr/bin/env python3 +import sys +print(f"Hello from Python {sys.version_info.major}.{sys.version_info.minor}!") +print("Arguments:", sys.argv[1:]) +` + err = sandbox.WriteFile(ctx, "/workspace/hello.py", pythonCode) + if err != nil { + log.Fatalf("WriteFile failed: %v", err) + } + fmt.Println(" Script written to /workspace/hello.py\n") + + // Example 3: Execute the Python script + fmt.Println("3. Executing Python script...") + output, err = sandbox.RunCommand(ctx, []string{"python3", "/workspace/hello.py", "arg1", "arg2"}) + if err != nil { + log.Fatalf("RunCommand failed: %v", err) + } + fmt.Printf(" Output: %s\n", output.Stdout) + + // Example 4: Read file content + fmt.Println("4. Reading file content...") + content, err := sandbox.ReadFile(ctx, "/workspace/hello.py") + if err != nil { + log.Fatalf("ReadFile failed: %v", err) + } + fmt.Printf(" Content:\n%s\n", content) + + // Example 5: Check file existence + fmt.Println("5. Checking file existence...") + exists, err := sandbox.Exists(ctx, "/workspace/hello.py") + if err != nil { + log.Fatalf("Exists failed: %v", err) + } + fmt.Printf(" /workspace/hello.py exists: %v\n\n", exists) + + // Example 6: Check if path is directory + fmt.Println("6. Checking directory...") + isDir, err := sandbox.IsDirectory(ctx, "/workspace") + if err != nil { + log.Fatalf("IsDirectory failed: %v", err) + } + fmt.Printf(" /workspace is directory: %v\n\n", isDir) + + // Example 7: Use with eino tools + fmt.Println("7. Creating eino tools with AIO Sandbox...") + + // StrReplaceEditor + editor, err := commandline.NewStrReplaceEditor(ctx, &commandline.EditorConfig{ + Operator: sandbox, + }) + if err != nil { + log.Fatalf("Failed to create editor: %v", err) + } + editorInfo, _ := editor.Info(ctx) + fmt.Printf(" Editor tool: %s\n", editorInfo.Name) + + // PyExecutor + pyExecutor, err := commandline.NewPyExecutor(ctx, &commandline.PyExecutorConfig{ + Command: "python3", + Operator: sandbox, + }) + if err != nil { + log.Fatalf("Failed to create PyExecutor: %v", err) + } + pyInfo, _ := pyExecutor.Info(ctx) + fmt.Printf(" PyExecutor tool: %s\n", pyInfo.Name) + + fmt.Println("\n=== Example completed successfully ===") +} diff --git a/components/tool/commandline/go.mod b/components/tool/commandline/go.mod index 142645c7c..5e0c25cb2 100644 --- a/components/tool/commandline/go.mod +++ b/components/tool/commandline/go.mod @@ -3,6 +3,7 @@ module github.com/cloudwego/eino-ext/components/tool/commandline go 1.23.0 require ( + github.com/agent-infra/sandbox-sdk-go v0.0.3 github.com/bytedance/mockey v1.2.14 github.com/cloudwego/eino v0.6.0 github.com/docker/docker v28.0.4+incompatible diff --git a/components/tool/commandline/go.sum b/components/tool/commandline/go.sum index 32977aa16..3830cf535 100644 --- a/components/tool/commandline/go.sum +++ b/components/tool/commandline/go.sum @@ -2,6 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/agent-infra/sandbox-sdk-go v0.0.3 h1:RUDol4LX2txAIhp1iVaMsvRybW/Xi90lIlyqTysN+3Q= +github.com/agent-infra/sandbox-sdk-go v0.0.3/go.mod h1:LcR1ZvwCSafPmNj1+RA6myQRMHWs1DZL7HoPMFJGSDo= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= From d883296a5bfba7511345ac36dee416d3f47f7b36 Mon Sep 17 00:00:00 2001 From: Oanakiaja <281723571@qq.com> Date: Wed, 28 Jan 2026 22:40:20 +0800 Subject: [PATCH 2/2] fix(commandline): remove redundant newline in fmt.Println --- components/tool/commandline/examples/aiosandbox/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/tool/commandline/examples/aiosandbox/main.go b/components/tool/commandline/examples/aiosandbox/main.go index a5eb23599..a88b8674f 100644 --- a/components/tool/commandline/examples/aiosandbox/main.go +++ b/components/tool/commandline/examples/aiosandbox/main.go @@ -73,7 +73,7 @@ print("Arguments:", sys.argv[1:]) if err != nil { log.Fatalf("WriteFile failed: %v", err) } - fmt.Println(" Script written to /workspace/hello.py\n") + fmt.Println(" Script written to /workspace/hello.py") // Example 3: Execute the Python script fmt.Println("3. Executing Python script...")