From 5ec780168919e2e49a2dec30cec0db42269ebec6 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 10 Mar 2026 23:28:31 +0800 Subject: [PATCH] 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 --- agent/assistant/sandbox_v2.go | 21 +++++---- agent/sandbox/v2/claude/runner.go | 61 ++----------------------- agent/sandbox/v2/lifecycle.go | 10 ++++- agent/sandbox/v2/options.go | 67 +++++++++++++++++++++++++++- sandbox/docker/browser/Dockerfile | 2 +- sandbox/docker/chrome/Dockerfile | 2 +- sandbox/docker/desktop/Dockerfile | 2 +- sandbox/docker/vnc/entrypoint-vnc.sh | 2 +- sandbox/manager.go | 3 +- sandbox/vncproxy/proxy.go | 4 +- tai/sandbox/docker_core.go | 2 +- 11 files changed, 94 insertions(+), 82 deletions(-) diff --git a/agent/assistant/sandbox_v2.go b/agent/assistant/sandbox_v2.go index a6e2b97e..4a58b4cc 100644 --- a/agent/assistant/sandbox_v2.go +++ b/agent/assistant/sandbox_v2.go @@ -39,15 +39,22 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) stdCtx := ctx.Context - // 1. Obtain Computer. - computer, identifier, err := sandboxv2.GetComputer(ctx, cfg, manager) + // 1. Resolve connector (before Computer so proxy env vars can be injected). + conn, _, err := ast.GetConnector(ctx, opts) + if err != nil && cfg.Runner.Name != "yao" { + closeLoadingV2(ctx, loadingMsgID, "sandbox.failed") + return nil, nil, nil, "", fmt.Errorf("get connector: %w", err) + } + + // 2. Obtain Computer (passes connector for OPENAI_PROXY_* env injection). + computer, identifier, err := sandboxv2.GetComputer(ctx, cfg, manager, conn) if err != nil { closeLoadingV2(ctx, loadingMsgID, "sandbox.failed") return nil, nil, nil, "", fmt.Errorf("getComputer failed: %w", err) } _ = identifier - // 2. Get Runner. + // 3. Get Runner. runner, err := sandboxv2.Get(cfg.Runner.Name) if err != nil { sandboxv2.LifecycleAction(stdCtx, cfg, computer, manager) @@ -55,14 +62,6 @@ func (ast *Assistant) initSandboxV2(ctx *context.Context, opts *context.Options) return nil, nil, nil, "", fmt.Errorf("get runner %q: %w", cfg.Runner.Name, err) } - // 3. Resolve connector. - conn, _, err := ast.GetConnector(ctx, opts) - if err != nil && cfg.Runner.Name != "yao" { - sandboxv2.LifecycleAction(stdCtx, cfg, computer, manager) - closeLoadingV2(ctx, loadingMsgID, "sandbox.failed") - return nil, nil, nil, "", fmt.Errorf("get connector: %w", err) - } - // 4. Resolve skills directory. skillsDir := "" if ast.Path != "" { diff --git a/agent/sandbox/v2/claude/runner.go b/agent/sandbox/v2/claude/runner.go index ea9516ab..fac6c522 100644 --- a/agent/sandbox/v2/claude/runner.go +++ b/agent/sandbox/v2/claude/runner.go @@ -24,7 +24,6 @@ const ( // ClaudeRunner implements the Runner interface for Claude CLI (mode=cli). type ClaudeRunner struct { mode string - proxyReady bool hasMCP bool mcpToolPattern string // e.g. "mcp__yao__*,mcp__github__*" servicePort int @@ -60,30 +59,6 @@ func (r *ClaudeRunner) Prepare(ctx context.Context, req *types.PrepareRequest) e }) } - // Runner-specific: write proxy config and start proxy (for non-anthropic connectors). - if req.Connector != nil && !req.Connector.Is(connector.ANTHROPIC) { - setting := req.Connector.Setting() - host, _ := setting["host"].(string) - key, _ := setting["key"].(string) - model, _ := setting["model"].(string) - if host != "" && key != "" { - proxyJSON := buildProxyConfig(host, key, model, setting) - steps = append(steps, types.PrepareStep{ - Action: "file", - Path: ".yao/proxy.json", - Content: proxyJSON, - Once: true, - }) - steps = append(steps, types.PrepareStep{ - Action: "exec", - Cmd: "which start-claude-proxy && start-claude-proxy || true", - Once: true, - IgnoreError: true, - }) - r.proxyReady = true - } - } - // Runner-specific: write MCP config. if len(req.MCPServers) > 0 { r.hasMCP = true @@ -174,7 +149,6 @@ func (r *ClaudeRunner) Stream(ctx context.Context, req *types.StreamRequest, han } // Cleanup kills any remaining claude processes. -// mode=service: don't kill the service daemon (lifecycle manages it), only clean proxy. // mode=cli: kill all claude CLI processes. func (r *ClaudeRunner) Cleanup(ctx context.Context, computer infra.Computer) error { if computer == nil { @@ -185,10 +159,6 @@ func (r *ClaudeRunner) Cleanup(ctx context.Context, computer infra.Computer) err computer.Exec(ctx, []string{"sh", "-c", "pkill -f 'claude' || true"}) } - if r.proxyReady { - computer.Exec(ctx, []string{"sh", "-c", "pkill -f 'claude-proxy' || true"}) - } - return nil } @@ -320,32 +290,9 @@ func (r *ClaudeRunner) buildCLICommand(req *types.StreamRequest, isContinuation return []string{"bash", "-c", bash.String()}, env } -// buildProxyConfig creates the claude-proxy configuration JSON. -func buildProxyConfig(host, key, model string, setting map[string]any) []byte { - backendURL := connector.BuildAPIURL(host, "/chat/completions") - config := map[string]any{ - "backend": backendURL, - "api_key": key, - "model": model, - } - opts := make(map[string]any) - for k, v := range setting { - switch k { - case "host", "key", "model", "azure", "capabilities": - continue - default: - opts[k] = v - } - } - if len(opts) > 0 { - config["options"] = opts - } - data, _ := json.MarshalIndent(config, "", " ") - return data -} - // buildMCPConfig creates the .mcp.json for Claude CLI based on declared servers. -// Each server delegates to "tai call" which bridges stdio JSON-RPC to Yao gRPC. +// Each server delegates to "tai mcp" which implements the standard MCP protocol +// over stdio and bridges to Yao gRPC with authentication. // Connection is configured via env vars (YAO_GRPC_ADDR, YAO_TOKEN, etc.) // injected by the sandbox infrastructure at container start. func buildMCPConfig(servers []types.MCPServer) []byte { @@ -357,13 +304,13 @@ func buildMCPConfig(servers []types.MCPServer) []byte { } mcpServers[name] = map[string]any{ "command": "tai", - "args": []string{"call"}, + "args": []string{"mcp"}, } } if len(mcpServers) == 0 { mcpServers["yao"] = map[string]any{ "command": "tai", - "args": []string{"call"}, + "args": []string{"mcp"}, } } config := map[string]any{"mcpServers": mcpServers} diff --git a/agent/sandbox/v2/lifecycle.go b/agent/sandbox/v2/lifecycle.go index 84680a1c..afe8604e 100644 --- a/agent/sandbox/v2/lifecycle.go +++ b/agent/sandbox/v2/lifecycle.go @@ -7,6 +7,7 @@ import ( "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" @@ -37,8 +38,9 @@ func BuildIdentifier(cfg *types.SandboxConfig, ownerID, chatID, assistantID stri } // 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) (infra.Computer, string, 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) @@ -84,7 +86,11 @@ func GetComputer(ctx *agentContext.Context, cfg *types.SandboxConfig, manager *i } // Create new box. - createOpts, err := BuildCreateOptions(cfg, identifier, ownerID, workspaceID) + 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) } diff --git a/agent/sandbox/v2/options.go b/agent/sandbox/v2/options.go index df313a24..2ceb3392 100644 --- a/agent/sandbox/v2/options.go +++ b/agent/sandbox/v2/options.go @@ -1,11 +1,13 @@ 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" ) @@ -19,8 +21,9 @@ func resolveEnvRef(value string) string { } // BuildCreateOptions converts a SandboxConfig into the V2 infrastructure -// CreateOptions. Pure runtime mapping — no file-system or DSL access. -func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspaceID string) (infra.CreateOptions, error) { +// 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, @@ -116,9 +119,69 @@ func BuildCreateOptions(cfg *types.SandboxConfig, identifier, ownerID, workspace } } + 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) { diff --git a/sandbox/docker/browser/Dockerfile b/sandbox/docker/browser/Dockerfile index eace8b69..611e35f0 100644 --- a/sandbox/docker/browser/Dockerfile +++ b/sandbox/docker/browser/Dockerfile @@ -76,7 +76,7 @@ ENV DISPLAY=:99 ENV VNC_PORT=5900 ENV NOVNC_PORT=6080 ENV RESOLUTION=1920x1080x24 -ENV SANDBOX_VNC_ENABLED=true +ENV VNC_ENABLED=true ENV SANDBOX_DESKTOP=fluxbox # Node.js environment - ensure global modules are accessible diff --git a/sandbox/docker/chrome/Dockerfile b/sandbox/docker/chrome/Dockerfile index 7d456a1b..b7ba373d 100644 --- a/sandbox/docker/chrome/Dockerfile +++ b/sandbox/docker/chrome/Dockerfile @@ -127,7 +127,7 @@ ENV DISPLAY=:99 ENV VNC_PORT=5900 ENV NOVNC_PORT=6080 ENV RESOLUTION=1920x1080x24 -ENV SANDBOX_VNC_ENABLED=true +ENV VNC_ENABLED=true ENV SANDBOX_DESKTOP=fluxbox # Node.js environment diff --git a/sandbox/docker/desktop/Dockerfile b/sandbox/docker/desktop/Dockerfile index aaa26c87..64d6aeed 100644 --- a/sandbox/docker/desktop/Dockerfile +++ b/sandbox/docker/desktop/Dockerfile @@ -85,7 +85,7 @@ ENV DISPLAY=:99 ENV VNC_PORT=5900 ENV NOVNC_PORT=6080 ENV RESOLUTION=1920x1080x24 -ENV SANDBOX_VNC_ENABLED=true +ENV VNC_ENABLED=true ENV SANDBOX_DESKTOP=xfce # Set hostname for XFCE panel display ENV HOSTNAME="Yao Sandbox" diff --git a/sandbox/docker/vnc/entrypoint-vnc.sh b/sandbox/docker/vnc/entrypoint-vnc.sh index 62e48f50..a25a2e14 100644 --- a/sandbox/docker/vnc/entrypoint-vnc.sh +++ b/sandbox/docker/vnc/entrypoint-vnc.sh @@ -5,7 +5,7 @@ # ============================================ # VNC Services Startup # ============================================ -if [ "$SANDBOX_VNC_ENABLED" = "true" ]; then +if [ "$VNC_ENABLED" = "true" ]; then echo "[Entrypoint] Starting VNC services..." /usr/local/bin/start-vnc.sh & # Wait for VNC to initialize diff --git a/sandbox/manager.go b/sandbox/manager.go index 83dd6588..133995bb 100644 --- a/sandbox/manager.go +++ b/sandbox/manager.go @@ -350,8 +350,7 @@ func (m *Manager) createContainer(ctx context.Context, opts CreateOptions) (*Con "6080/tcp": struct{}{}, // noVNC websockify "5900/tcp": struct{}{}, // VNC } - // Enable SANDBOX_VNC_ENABLED environment variable - containerConfig.Env = append(containerConfig.Env, "SANDBOX_VNC_ENABLED=true") + containerConfig.Env = append(containerConfig.Env, "VNC_ENABLED=true") // Map to random available ports on 127.0.0.1 hostConfig.PortBindings = nat.PortMap{ diff --git a/sandbox/vncproxy/proxy.go b/sandbox/vncproxy/proxy.go index f968f1d9..fc77c53c 100644 --- a/sandbox/vncproxy/proxy.go +++ b/sandbox/vncproxy/proxy.go @@ -323,10 +323,8 @@ func (p *Proxy) checkVNCEnabled(ctx context.Context, containerName string) bool return false } - // Check environment variables for VNC_ENABLED or SANDBOX_VNC_ENABLED for _, env := range info.Config.Env { - if strings.HasPrefix(env, "SANDBOX_VNC_ENABLED=true") || - strings.HasPrefix(env, "VNC_ENABLED=true") { + if strings.HasPrefix(env, "VNC_ENABLED=true") { return true } } diff --git a/tai/sandbox/docker_core.go b/tai/sandbox/docker_core.go index a0e77581..16f950cc 100644 --- a/tai/sandbox/docker_core.go +++ b/tai/sandbox/docker_core.go @@ -59,7 +59,7 @@ func (d *dockerCore) create(ctx context.Context, opts CreateOptions, addVNCPorts shmSize = 256 * 1024 * 1024 } hostCfg.ShmSize = shmSize - cfg.Env = append(cfg.Env, "SANDBOX_VNC_ENABLED=true") + cfg.Env = append(cfg.Env, "VNC_ENABLED=true") if addVNCPorts { for _, p := range []int{6080, 5900} {