From ce4864cfde97fcfeed6a824d2f2f97ce1aefb349 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 6 Feb 2026 19:38:22 +0800 Subject: [PATCH] Enhance sandbox environment setup for VNC and Python compatibility - Ensure the existence of $HOME/.Xauthority for PyAutoGUI/Xlib compatibility. - Adjust PYTHONPATH to maintain access to user-installed Python packages. - Explicitly set XAUTHORITY to the correct path for X11 authentication. - Pass secrets as environment variables for Claude CLI to ensure proper access. - Refactor VNC image support check to utilize a centralized keyword list for better maintainability. --- agent/sandbox/claude/command.go | 24 ++++++++++++++++++++++++ agent/sandbox/claude/executor.go | 5 ++--- sandbox/manager.go | 23 +++++++++++++++++++---- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/agent/sandbox/claude/command.go b/agent/sandbox/claude/command.go index 0290a36e..a2bfc8fc 100644 --- a/agent/sandbox/claude/command.go +++ b/agent/sandbox/claude/command.go @@ -121,6 +121,10 @@ func BuildCommandWithContinuation(messages []agentContext.Message, opts *Options // System prompt may contain quotes, newlines, special characters that break shell quoting var bashCmd strings.Builder + // Ensure $HOME/.Xauthority exists for PyAutoGUI/Xlib (HOME=/workspace). + // Xvfb runs without auth, but Xlib requires the file to exist. + bashCmd.WriteString("touch /home/sandbox/.Xauthority 2>/dev/null; touch \"$HOME/.Xauthority\" 2>/dev/null\n") + // If we have a system prompt (first request only), write it to a temp file via heredoc first // then use --append-system-prompt-file if systemPrompt != "" { @@ -306,10 +310,30 @@ func buildEnvironment(opts *Options, systemPrompt string) map[string]string { // Session data is stored in $HOME/.claude/ (i.e., /workspace/.claude/) env["HOME"] = "/workspace" + // Fix Python user-site-packages: changing HOME from /home/sandbox to /workspace + // breaks Python's ability to find packages installed via pip --user (e.g., playwright, + // pyautogui, playwright-stealth) which live in /home/sandbox/.local/lib/pythonX.Y/site-packages/ + env["PYTHONPATH"] = "/home/sandbox/.local/lib/python3.12/site-packages" + + // Fix X11 auth: PyAutoGUI/Xlib looks for $HOME/.Xauthority, but HOME=/workspace + // so it fails to find /home/sandbox/.Xauthority created during image build. + // Explicitly set XAUTHORITY to the correct path. + env["XAUTHORITY"] = "/home/sandbox/.Xauthority" + // claude-proxy runs on localhost:3456, Claude CLI connects to it env["ANTHROPIC_BASE_URL"] = "http://127.0.0.1:3456" env["ANTHROPIC_API_KEY"] = "dummy" // Proxy doesn't verify this + // Pass secrets as environment variables for Claude CLI to use + // These are configured in package.yao sandbox.secrets (e.g., LLM_API_KEY, GITHUB_TOKEN) + // start-claude-proxy also exports them for the proxy process, but Claude CLI + // is launched via a separate docker exec, so it needs them passed explicitly here. + if len(opts.Secrets) > 0 { + for k, v := range opts.Secrets { + env[k] = v + } + } + // Note: System prompt and max_turns are passed via CLI flags in BuildCommand // CLAUDE_SYSTEM_PROMPT environment variable is NOT supported by Claude CLI // --append-system-prompt or --system-prompt flags must be used instead diff --git a/agent/sandbox/claude/executor.go b/agent/sandbox/claude/executor.go index 8a2c6087..10273830 100644 --- a/agent/sandbox/claude/executor.go +++ b/agent/sandbox/claude/executor.go @@ -1103,14 +1103,13 @@ func (e *Executor) GetVNCUrl() string { return "" } - // Check if the image supports VNC (playwright or desktop variants) imageName := e.opts.Image if imageName == "" { return "" } - // VNC is only available for playwright and desktop images - if !strings.Contains(imageName, "playwright") && !strings.Contains(imageName, "desktop") { + // Check if the image supports VNC using the shared keyword list in sandbox package + if !infraSandbox.IsVNCImage(imageName) { return "" } diff --git a/sandbox/manager.go b/sandbox/manager.go index e9236aa5..d37e4623 100644 --- a/sandbox/manager.go +++ b/sandbox/manager.go @@ -22,6 +22,10 @@ import ( "github.com/yaoapp/yao/sandbox/ipc" ) +// vncImageKeywords lists image name keywords that indicate VNC support. +// Add new keywords here when adding new VNC-capable sandbox images. +var vncImageKeywords = []string{"playwright", "desktop", "chrome"} + // execReadCloser wraps a Reader with a Closer type execReadCloser struct { *bufio.Reader @@ -324,9 +328,15 @@ func (m *Manager) createContainer(ctx context.Context, opts CreateOptions) (*Con CapDrop: []string{"ALL"}, } + // Chrome/browser images need SYS_ADMIN for namespace-based process isolation. + // Without it, Chrome renderer/GPU processes crash with error code 5. + if IsVNCImage(image) { + hostConfig.CapAdd = []string{"SYS_ADMIN"} + } + // VNC port mapping for Docker Desktop (macOS/Windows) // Only enable for VNC-capable images (playwright/desktop) when config is enabled - if m.config.VNCPortMapping && isVNCImage(image) { + if m.config.VNCPortMapping && IsVNCImage(image) { // Expose VNC ports in container config containerConfig.ExposedPorts = nat.PortSet{ "6080/tcp": struct{}{}, // noVNC websockify @@ -952,9 +962,14 @@ func (m *Manager) fixIPCSocketPermissions(ctx context.Context, containerID strin time.Sleep(50 * time.Millisecond) } -// isVNCImage checks if the image is VNC-capable (playwright or desktop variants) -func isVNCImage(imageName string) bool { - return strings.Contains(imageName, "playwright") || strings.Contains(imageName, "desktop") +// IsVNCImage checks if the image is VNC-capable based on vncImageKeywords. +func IsVNCImage(imageName string) bool { + for _, kw := range vncImageKeywords { + if strings.Contains(imageName, kw) { + return true + } + } + return false } // findAvailablePort finds an available port on the host