package sandboxv2 import ( "fmt" "strings" "time" "github.com/yaoapp/kun/str" "github.com/yaoapp/yao/agent/sandbox/v2/types" infra "github.com/yaoapp/yao/sandbox/v2" ) // BuildCreateOptions converts a SandboxConfig into the V2 infrastructure // CreateOptions. Connector config injection is handled separately via the // a2o HTTP API (POST /config) after the container starts. func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspaceID string) (infra.CreateOptions, error) { opts := infra.CreateOptions{ ID: identifier, Owner: ownerID, NodeID: cfg.NodeID, 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, DisplayName: cfg.DisplayName, } 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 opts.IdleTimeout == 0 { switch opts.Policy { case infra.Session: opts.IdleTimeout = infra.DefaultSessionIdleTimeout case infra.LongRunning: opts.IdleTimeout = infra.DefaultLongRunningIdleTimeout } } 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] = str.EnvVar(v) } for k, v := range cfg.Secrets { opts.Env[k] = str.EnvVar(v) } } if opts.Env == nil { opts.Env = make(map[string]string) } // 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"] = str.EnvVar(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 } // 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 }