yao/agent/sandbox/v2/lifecycle.go
Max 5ec7801689 feat(sandbox/v2): enhance connector integration and VNC configuration
- 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
2026-03-10 23:28:31 +08:00

156 lines
4.2 KiB
Go

package sandboxv2
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"github.com/yaoapp/gou/connector"
agentContext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/sandbox/v2/types"
infra "github.com/yaoapp/yao/sandbox/v2"
)
// BuildIdentifier determines the Computer identifier based on lifecycle policy
// and optional metadata override. Returns "" for oneshot (always new).
func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID string, metadata map[string]any) string {
if cfg.Lifecycle == "oneshot" {
return ""
}
// Custom identifier from metadata takes precedence.
if metadata != nil {
if cid, ok := metadata["computer_id"].(string); ok && cid != "" {
return fmt.Sprintf("%s-%s", ownerID, cid)
}
}
switch cfg.Lifecycle {
case "session":
return fmt.Sprintf("%s-%s", ownerID, chatID)
case "longrunning", "persistent":
return fmt.Sprintf("%s-%s", ownerID, assistantID)
default:
return ""
}
}
// GetComputer obtains or creates a Computer for the current request.
// An optional connector may be passed to inject OPENAI_PROXY_* env vars.
// Returns the Computer, the resolved identifier, and any error.
func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *infra.Manager, conn ...connector.Connector) (infra.Computer, string, error) {
ownerID := resolveOwnerID(ctx)
identifier := BuildIdentifier(cfg, ownerID, ctx.ChatID, ctx.AssistantID, ctx.Metadata)
// Fill runtime fields.
cfg.Owner = ownerID
cfg.ID = identifier
workspaceID := ""
if ctx.Metadata != nil {
if ws, ok := ctx.Metadata["workspace_id"].(string); ok && ws != "" {
workspaceID = ws
}
}
if workspaceID == "" {
workspaceID = ownerID
}
cfg.WorkspaceID = workspaceID
// Host mode: no image → host computer.
if cfg.Computer.Image == "" {
cfg.Kind = "host"
nodeID := cfg.NodeID
if nodeID == "" {
return nil, identifier, fmt.Errorf("host mode requires a nodeID (set in sandbox.yao or workspace)")
}
host, err := manager.Host(context.Background(), nodeID)
if err != nil {
return nil, identifier, fmt.Errorf("get host computer: %w", err)
}
host.BindWorkplace(workspaceID)
return host, identifier, nil
}
cfg.Kind = "box"
// Reuse: non-empty identifier → try Get first.
if identifier != "" {
box, err := manager.Get(context.Background(), identifier)
if err == nil && box != nil {
box.BindWorkplace(workspaceID)
return box, identifier, nil
}
}
// Create new box.
var c connector.Connector
if len(conn) > 0 {
c = conn[0]
}
createOpts, err := BuildCreateOptions(cfg, identifier, ownerID, workspaceID, c)
if err != nil {
return nil, identifier, fmt.Errorf("build create options: %w", err)
}
// Oneshot with empty identifier: generate a random one.
if createOpts.ID == "" {
createOpts.ID = randomID()
identifier = createOpts.ID
cfg.ID = identifier
}
box, err := manager.Create(context.Background(), createOpts)
if err != nil {
return nil, identifier, fmt.Errorf("create computer: %w", err)
}
return box, identifier, nil
}
// LifecycleAction performs the post-request lifecycle operation based on policy.
// Called in defer after executeSandboxStream completes.
func LifecycleAction(ctx context.Context, cfg *types.SandboxConfig, computer infra.Computer, manager *infra.Manager) {
if computer == nil || cfg == nil {
return
}
info := computer.ComputerInfo()
switch cfg.Lifecycle {
case "oneshot":
if info.Kind == "box" && manager != nil {
if err := manager.Remove(ctx, cfg.ID); err != nil {
log.Printf("[sandbox/v2] oneshot remove %s: %v", cfg.ID, err)
}
}
case "session", "longrunning":
if info.Kind == "box" && manager != nil {
manager.Heartbeat(cfg.ID, false, 0) // active=false: request finished, start idle timer
}
case "persistent":
// No action — persistent boxes survive indefinitely.
}
}
// resolveOwnerID returns teamID if available, otherwise userID.
func resolveOwnerID(ctx *agentContext.Context) string {
if ctx.Authorized != nil {
if ctx.Authorized.TeamID != "" {
return ctx.Authorized.TeamID
}
if ctx.Authorized.UserID != "" {
return ctx.Authorized.UserID
}
}
return "anonymous"
}
func randomID() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}