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
This commit is contained in:
parent
6407b8531f
commit
5ec7801689
11 changed files with 94 additions and 82 deletions
|
|
@ -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 != "" {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue