- Refactored the sandbox initialization process to resolve the connector before obtaining the Computer, allowing for the injection of OPENAI_PROXY_* environment variables. - Updated the GetComputer and BuildCreateOptions functions to accept an optional connector parameter for improved environment variable management. - Standardized the VNC configuration by replacing SANDBOX_VNC_ENABLED with VNC_ENABLED across Dockerfiles and related scripts for consistency. - Enhanced the VNC service startup script to check the new VNC_ENABLED variable, ensuring proper service initialization. Made-with: Cursor
231 lines
5.8 KiB
Go
231 lines
5.8 KiB
Go
package sandboxv2
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/yaoapp/gou/connector"
|
|
"github.com/yaoapp/yao/agent/sandbox/v2/types"
|
|
infra "github.com/yaoapp/yao/sandbox/v2"
|
|
)
|
|
|
|
// resolveEnvRef resolves $ENV.XXX references to os.Getenv("XXX").
|
|
func resolveEnvRef(value string) string {
|
|
if strings.HasPrefix(value, "$ENV.") {
|
|
return os.Getenv(value[5:])
|
|
}
|
|
return value
|
|
}
|
|
|
|
// BuildCreateOptions converts a SandboxConfig into the V2 infrastructure
|
|
// CreateOptions. An optional connector is used to inject OPENAI_PROXY_*
|
|
// environment variables when the connector is OpenAI-compatible (non-Anthropic).
|
|
func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspaceID string, conn ...connector.Connector) (infra.CreateOptions, error) {
|
|
opts := infra.CreateOptions{
|
|
ID: identifier,
|
|
Owner: ownerID,
|
|
Image: cfg.Computer.Image,
|
|
WorkDir: cfg.Computer.WorkDir,
|
|
User: cfg.Computer.User,
|
|
MountPath: cfg.Computer.MountPath,
|
|
MountMode: cfg.Computer.MountMode,
|
|
WorkspaceID: workspaceID,
|
|
Labels: cfg.Labels,
|
|
}
|
|
|
|
if opts.Labels == nil {
|
|
opts.Labels = make(map[string]string)
|
|
}
|
|
|
|
// Lifecycle policy
|
|
switch cfg.Lifecycle {
|
|
case "oneshot":
|
|
opts.Policy = infra.OneShot
|
|
case "session":
|
|
opts.Policy = infra.Session
|
|
case "longrunning":
|
|
opts.Policy = infra.LongRunning
|
|
case "persistent":
|
|
opts.Policy = infra.Persistent
|
|
default:
|
|
opts.Policy = infra.OneShot
|
|
}
|
|
|
|
// Timeouts
|
|
if cfg.IdleTimeout != "" {
|
|
d, err := time.ParseDuration(cfg.IdleTimeout)
|
|
if err != nil {
|
|
return opts, fmt.Errorf("idle_timeout: %w", err)
|
|
}
|
|
opts.IdleTimeout = d
|
|
}
|
|
if cfg.MaxLifetime != "" {
|
|
d, err := time.ParseDuration(cfg.MaxLifetime)
|
|
if err != nil {
|
|
return opts, fmt.Errorf("max_lifetime: %w", err)
|
|
}
|
|
opts.MaxLifetime = d
|
|
}
|
|
if cfg.StopTimeout != "" {
|
|
d, err := time.ParseDuration(cfg.StopTimeout)
|
|
if err != nil {
|
|
return opts, fmt.Errorf("stop_timeout: %w", err)
|
|
}
|
|
opts.StopTimeout = d
|
|
}
|
|
|
|
// Memory (string like "4g" → bytes)
|
|
if cfg.Computer.Memory != "" {
|
|
mem, err := parseMemory(cfg.Computer.Memory)
|
|
if err != nil {
|
|
return opts, fmt.Errorf("memory: %w", err)
|
|
}
|
|
opts.Memory = mem
|
|
}
|
|
|
|
opts.CPUs = cfg.Computer.CPUs
|
|
|
|
// VNC
|
|
opts.VNC = cfg.Computer.VNC.Enabled
|
|
|
|
// Ports
|
|
for _, p := range cfg.Computer.Ports {
|
|
opts.Ports = append(opts.Ports, infra.PortMapping{
|
|
ContainerPort: p.Port,
|
|
HostPort: p.HostPort,
|
|
Protocol: p.Protocol,
|
|
})
|
|
}
|
|
|
|
// NodeID (host mode pre-selection)
|
|
if cfg.NodeID != "" {
|
|
opts.NodeID = cfg.NodeID
|
|
}
|
|
|
|
// Merge environment + secrets into CreateOptions.Env.
|
|
// Secrets override environment for same-name keys.
|
|
// $ENV.XXX references are resolved at runtime.
|
|
envSize := len(cfg.Environment) + len(cfg.Secrets)
|
|
if envSize > 0 {
|
|
opts.Env = make(map[string]string, envSize)
|
|
for k, v := range cfg.Environment {
|
|
opts.Env[k] = resolveEnvRef(v)
|
|
}
|
|
for k, v := range cfg.Secrets {
|
|
opts.Env[k] = resolveEnvRef(v)
|
|
}
|
|
}
|
|
|
|
if opts.Env == nil {
|
|
opts.Env = make(map[string]string)
|
|
}
|
|
|
|
// Inject OPENAI_PROXY_* when connector is OpenAI-compatible (non-Anthropic).
|
|
// The a2o proxy inside the container translates Anthropic API → OpenAI API.
|
|
if len(conn) > 0 && conn[0] != nil && !conn[0].Is(connector.ANTHROPIC) {
|
|
injectProxyEnv(opts.Env, conn[0])
|
|
}
|
|
|
|
// Inject VNC_* environment variables from config.
|
|
if cfg.Computer.VNC.Enabled {
|
|
opts.Env["VNC_ENABLED"] = "true"
|
|
if cfg.Computer.VNC.Password != "" {
|
|
opts.Env["VNC_PASSWORD"] = resolveEnvRef(cfg.Computer.VNC.Password)
|
|
}
|
|
if cfg.Computer.VNC.Resolution != "" {
|
|
opts.Env["VNC_RESOLUTION"] = cfg.Computer.VNC.Resolution
|
|
}
|
|
if cfg.Computer.VNC.ViewOnly {
|
|
opts.Env["VNC_VIEW_ONLY"] = "true"
|
|
}
|
|
}
|
|
|
|
return opts, nil
|
|
}
|
|
|
|
// injectProxyEnv extracts backend URL, model, and API key from an
|
|
// OpenAI-compatible connector's settings and writes them as OPENAI_PROXY_*
|
|
// environment variables into env.
|
|
func injectProxyEnv(env map[string]string, conn connector.Connector) {
|
|
settings := conn.Setting()
|
|
if settings == nil {
|
|
return
|
|
}
|
|
|
|
if host, ok := settings["host"].(string); ok && host != "" {
|
|
env["OPENAI_PROXY_BACKEND"] = host
|
|
}
|
|
if model, ok := settings["model"].(string); ok && model != "" {
|
|
env["OPENAI_PROXY_MODEL"] = model
|
|
}
|
|
if key, ok := settings["key"].(string); ok && key != "" {
|
|
env["OPENAI_PROXY_API_KEY"] = key
|
|
}
|
|
|
|
// Forward extra options as JSON.
|
|
extra := make(map[string]interface{})
|
|
for k, v := range settings {
|
|
switch k {
|
|
case "host", "model", "key", "proxy", "type":
|
|
continue
|
|
default:
|
|
extra[k] = v
|
|
}
|
|
}
|
|
if len(extra) > 0 {
|
|
if data, err := json.Marshal(extra); err == nil {
|
|
env["OPENAI_PROXY_OPTIONS"] = string(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseMemory converts a human-readable memory string to bytes.
|
|
// Supported formats: "4GB", "4G", "4g", "512MB", "512M", "512m", "1024KB", "1024K", "1024".
|
|
func parseMemory(s string) (int64, error) {
|
|
if len(s) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
upper := strings.ToUpper(s)
|
|
var num string
|
|
var multiplier int64
|
|
|
|
switch {
|
|
case strings.HasSuffix(upper, "GB"):
|
|
num = s[:len(s)-2]
|
|
multiplier = 1 << 30
|
|
case strings.HasSuffix(upper, "MB"):
|
|
num = s[:len(s)-2]
|
|
multiplier = 1 << 20
|
|
case strings.HasSuffix(upper, "KB"):
|
|
num = s[:len(s)-2]
|
|
multiplier = 1 << 10
|
|
case strings.HasSuffix(upper, "TB"):
|
|
num = s[:len(s)-2]
|
|
multiplier = 1 << 40
|
|
case strings.HasSuffix(upper, "G"):
|
|
num = s[:len(s)-1]
|
|
multiplier = 1 << 30
|
|
case strings.HasSuffix(upper, "M"):
|
|
num = s[:len(s)-1]
|
|
multiplier = 1 << 20
|
|
case strings.HasSuffix(upper, "K"):
|
|
num = s[:len(s)-1]
|
|
multiplier = 1 << 10
|
|
case strings.HasSuffix(upper, "T"):
|
|
num = s[:len(s)-1]
|
|
multiplier = 1 << 40
|
|
default:
|
|
num = s
|
|
multiplier = 1
|
|
}
|
|
|
|
var val float64
|
|
if _, err := fmt.Sscanf(num, "%f", &val); err != nil {
|
|
return 0, fmt.Errorf("invalid memory value %q", s)
|
|
}
|
|
return int64(val * float64(multiplier)), nil
|
|
}
|