Merge pull request #1453 from trheyi/main

Enhance sandbox environment setup for VNC and Python compatibility
This commit is contained in:
Max 2026-02-06 20:09:54 +08:00 committed by GitHub
commit e14722f1fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 45 additions and 7 deletions

View file

@ -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

View file

@ -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 ""
}

View file

@ -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