Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
befb85b
fix: software rendering, Stop/Start buttons, VNC browser icon, and wa…
Apr 8, 2026
8e44c02
docs: add user.js template and document Google OAuth / Xvfb popup fix
Apr 8, 2026
d8e52f1
feat: proper full-stack lifecycle management for Start/Stop/Restart b…
Apr 8, 2026
dc7ea77
fix: change default display mode from headless to virtual
Apr 8, 2026
937b346
fix: show live current display mode separately from startup preference
Apr 8, 2026
8fa2cf2
fix: proper button states and rename Refresh to Check Status
Apr 8, 2026
130fec4
fix: remove stray closing div that broke Alpine.js reactive button st…
Apr 8, 2026
69eadb2
feat: open default home page after startup to avoid black VNC screen
Apr 8, 2026
ddbea9a
fix: always kill stale websockify before starting fresh in _ensure_we…
Apr 8, 2026
ccc25d3
fix: VNC panel proportional resize on window resize + CSS drag-resize…
Apr 9, 2026
100c8e2
feat: proportional resize on window resize + maximize/restore button …
Apr 9, 2026
f731f1c
fix: openbox WM + window maximize + resize=remote for proper VNC pane…
Apr 9, 2026
911af56
fix: replace openbox with direct xdotool maximize; revert resize=remo…
Apr 9, 2026
37c9ce9
fix: remove broken home_tab API call, use xdotool; fix websockify tim…
Apr 9, 2026
9b3f02b
fix: add fission.autostart=false to user.js.template (fixes black coo…
Apr 9, 2026
83d56c3
fix: websockify pkill -9 + longer sleep after kill
Apr 9, 2026
685520d
feat: configurable VNC resolution via CAMOFOX_VNC_RESOLUTION (default…
Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,102 @@ Anti-detection browser integration for Agent Zero. Provides:
- Optional: `camofox` CLI for auth vault and scripting
- For visible browser mode: `xvfb`, `x11vnc`, `websockify`, and noVNC assets at `/opt/noVNC`
- The embedded viewer now runs through the normal Agent Zero URL, so users do not need to publish extra VNC ports in Docker

---

## Virtual Display Mode (Xvfb)

When running in Docker or headless Linux environments, CamoFox can use a virtual display via Xvfb for the VNC panel. Key points:

- CamoFox spawns its **own Xvfb displays** (`:100`, `:101`) in virtual mode — separate from any manually started display
- `x11vnc` must be pointed at the **CamoFox display**, not the base `:99` display
- The VNC watchdog script (`scripts/vnc-watchdog.sh`) handles this automatically

### Startup Sequence

```bash
# 1. Start base Xvfb display
nohup Xvfb :99 -screen 0 1920x1080x24 -ac -nolisten tcp > /tmp/xvfb.log 2>&1 &

# 2. Start websockify
nohup /usr/bin/python3 /usr/bin/websockify --web /opt/noVNC 6080 127.0.0.1:5999 > /tmp/websockify.log 2>&1 &

# 3. Start CamoFox server with software rendering env vars
DISPLAY=:99 LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe \
MOZ_DISABLE_OOP_COMPOSITING=1 MOZ_WEBRENDER=0 MOZ_DISABLE_RDD_SANDBOX=1 \
nohup node /usr/local/lib/node_modules/camofox-browser/dist/src/server.js > /tmp/camofox.log 2>&1 &

# 4. Toggle to virtual mode via Agent Zero tool: camofox_session toggle_display headless=virtual

# 5. Start VNC watchdog (auto-detects correct display, keeps x11vnc alive)
bash /path/to/camofox-plugin/scripts/vnc-watchdog.sh >> /tmp/camofox-watchdog.log 2>&1 &
```

---

## Google OAuth / Popup Windows in Xvfb

When using Google OAuth ("Sign in with Google") or any site that opens a popup window, the popup may render as a **black screen** in Xvfb virtual display environments. This is caused by GPU-accelerated compositing being unavailable in headless environments.

### Fix: Apply Firefox User Preferences

A `user.js` template is included in `scripts/user.js.template`. Apply it to your CamoFox browser profile:

```bash
# Find your profile directory
ls /root/.camofox/profiles/

# Copy the template (replace 'a0-agent-0' with your profile name)
cp scripts/user.js.template /root/.camofox/profiles/a0-agent-0/user.js

# Restart the browser session to apply
```

This template:
- **Disables GPU/WebRender acceleration** — fixes black rendering in Xvfb
- **Forces popup windows to open as tabs** (`browser.link.open_newwindow=3`) — prevents Google OAuth and similar popups from opening as separate black windows; they open as normal tabs instead

### Additional: Software Rendering Environment Variables

The plugin's Start button (and the startup sequence above) automatically sets these environment variables on the CamoFox server process, which are inherited by the browser:

```
LIBGL_ALWAYS_SOFTWARE=1
GALLIUM_DRIVER=llvmpipe
MOZ_DISABLE_OOP_COMPOSITING=1
MOZ_WEBRENDER=0
MOZ_DISABLE_RDD_SANDBOX=1
```

### Closing a Stuck Black Popup

If a black popup is already open and can't be closed through the browser UI:

```bash
# Install xdotool if missing
apt-get install -y xdotool

# Find and close popups on the CamoFox display
for wid in $(DISPLAY=:100 xdotool search --name '' 2>/dev/null); do
name=$(DISPLAY=:100 xdotool getwindowname $wid 2>/dev/null)
if echo "$name" | grep -qi 'google\|sign in'; then
DISPLAY=:100 xdotool windowclose $wid 2>/dev/null && echo "Closed: $name"
fi
done
```

---

## Scripts

### `scripts/vnc-watchdog.sh`

A background watchdog that monitors which display `camoufox-bin` is rendering on and auto-restarts `x11vnc` on the correct display whenever it crashes or changes. Run it after toggling to virtual display mode:

```bash
bash scripts/vnc-watchdog.sh >> /tmp/camofox-watchdog.log 2>&1 &
```

### `scripts/user.js.template`

Firefox user preferences template for fixing popup rendering in Xvfb environments. See [Google OAuth / Popup Windows in Xvfb](#google-oauth--popup-windows-in-xvfb) above.
79 changes: 75 additions & 4 deletions api/camofox_health.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""API handler: CamoFox server health check, start, and stop."""

import asyncio
import os
import shutil

Expand Down Expand Up @@ -151,14 +152,84 @@ async def _diagnose(self) -> dict:

async def _server_control(self, action: str) -> dict:
cfg = get_config()
# Try CamofoxCli first; fall back to direct subprocess control
try:
cli = CamofoxCli(
binary_path=cfg.get("binary_path") or None,
default_user=cfg.get("default_user_id", ""),
)
result = await cli.execute("server", action)
return {"ok": True, "action": action, "result": result}
except CamofoxCliNotFoundError as e:
return {"ok": False, "error": str(e)}
except CamofoxCliError as e:
return {"ok": False, "error": str(e)}
except (CamofoxCliNotFoundError, CamofoxCliError):
pass # Fall through to subprocess fallback
except Exception:
pass

# Subprocess fallback (no camofox CLI required)
if action == "stop":
try:
proc = await asyncio.create_subprocess_exec(
"pkill", "-f", "node.*server.js",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc.communicate()
# Also kill any lingering camoufox-bin processes
proc2 = await asyncio.create_subprocess_exec(
"pkill", "-f", "camoufox-bin",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await proc2.communicate()
return {"ok": True, "action": "stop", "result": "Server stopped via pkill."}
except Exception as e:
return {"ok": False, "error": f"Stop failed: {e}"}

elif action == "start":
import shutil as _shutil
# Find server.js
server_js = None
try:
result = await asyncio.create_subprocess_exec(
"npm", "root", "-g",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await result.communicate()
if result.returncode == 0:
candidate = os.path.join(stdout.decode().strip(), "camofox-browser", "dist", "src", "server.js")
if os.path.isfile(candidate):
server_js = candidate
except Exception:
pass

if not server_js:
return {"ok": False, "error": "server.js not found — cannot start server."}

port = cfg.get("server_url", "http://localhost:9377").split(":")[-1].rstrip("/")
env = os.environ.copy()
env["CAMOFOX_PORT"] = str(port)
if cfg.get("api_key"):
env["CAMOFOX_API_KEY"] = cfg["api_key"]
if cfg.get("admin_key"):
env["CAMOFOX_ADMIN_KEY"] = cfg["admin_key"]
# Software rendering env vars
env["LIBGL_ALWAYS_SOFTWARE"] = "1"
env["GALLIUM_DRIVER"] = "llvmpipe"
env["MOZ_DISABLE_OOP_COMPOSITING"] = "1"
env["MOZ_WEBRENDER"] = "0"
env["MOZ_DISABLE_RDD_SANDBOX"] = "1"

try:
await asyncio.create_subprocess_exec(
"node", server_js,
env=env,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await asyncio.sleep(4)
return {"ok": True, "action": "start", "result": "Server start command sent."}
except Exception as e:
return {"ok": False, "error": f"Start failed: {e}"}

return {"ok": False, "error": f"Unknown action: {action!r}"}
6 changes: 6 additions & 0 deletions api/camofox_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ async def _start_server(self, input: dict) -> dict:
env["CAMOFOX_API_KEY"] = api_key
if admin_key:
env["CAMOFOX_ADMIN_KEY"] = admin_key
# Software rendering — required for correct rendering in Xvfb virtual displays
env["LIBGL_ALWAYS_SOFTWARE"] = "1"
env["GALLIUM_DRIVER"] = "llvmpipe"
env["MOZ_DISABLE_OOP_COMPOSITING"] = "1"
env["MOZ_WEBRENDER"] = "0"
env["MOZ_DISABLE_RDD_SANDBOX"] = "1"

# Find server.js from installed npm package
server_js = self._find_server_js()
Expand Down
Loading