sha256:479864cc2ec67db4c671568e1d3cfe84cb7263b3e92d788501ceee9228dba88b
Last pushed
3 days by zigadev
Type
Sandbox Kit
Manifest digest
sha256:479864cc2ec67db4c671568e1d3cfe84cb7263b3e92d788501ceee9228dba88b
schemaVersion: "2"
kind: mixin
name: novnc
displayName: Playwright with noVNC
description: A virtual desktop and noVNC access for the browser installed by the Playwright kit.
agentInstructions:
content: |
Requires the Playwright kit. Start with `remote-browser start`, then
check `remote-browser status`. Get the VNC password with
`remote-browser password`. Stop with `remote-browser stop`.
noVNC listens on 0.0.0.0:6080. Ask the user to publish it on their host:
sbx ports <sandbox-name> --publish 127.0.0.1:6080:6080/tcp4
Then open http://127.0.0.1:6080/vnc.html?autoconnect=1&resize=scale.
Use the same authenticated browser through Playwright connectOverCDP
at http://127.0.0.1:9222. Do not launch a separate browser for logged-in work.
The profile is in ~/.local/share/remote-browser/profile. It survives browser
restarts within this sandbox. Recreating the sandbox requires backing up
and restoring the profile if the home directory is not persisted.
The display server allows headed Chromium despite the Playwright kit's
headless-only instructions. Browser traffic uses HTTPS_PROXY when set.
permissions:
network:
allow:
- archive.ubuntu.com:80
- security.ubuntu.com:80
- ports.ubuntu.com:80
- download.docker.com:443
setup:
install:
- command: |
set -euo pipefail
apt-get update
# Chromium needs GTK at runtime to display native upload file choosers.
gtk3_package=libgtk-3-0
if apt-cache show libgtk-3-0t64 >/dev/null 2>&1; then
gtk3_package=libgtk-3-0t64
fi
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends xvfb x11vnc novnc websockify openbox x11-utils python3 curl "$gtk3_package" libgtk-4-1
install -d -m 755 /opt/remote-browser
cat > /opt/remote-browser/browser.cjs <<'REMOTE_BROWSER_FILE'
const { chromium } = require('playwright');
(async () => {
const context = await chromium.launchPersistentContext(require('path').join(require('os').homedir(), '.local/share/remote-browser/profile'), {
headless: false,
viewport: null,
proxy: process.env.HTTPS_PROXY ? {server: process.env.HTTPS_PROXY, bypass: 'localhost,127.0.0.1,[::1]'} : undefined,
args: ['--no-sandbox', '--disable-dev-shm-usage', '--remote-debugging-address=127.0.0.1', '--remote-debugging-port=9222', '--window-size=1440,900', '--ozone-platform=x11'],
});
const page = context.pages()[0] || await context.newPage();
if (page.url() === 'about:blank') await page.setContent('<title>Remote browser ready</title><h1>Remote browser ready</h1><p>Enter your app URL in the address bar and log in. This browser saves your session for later automation.</p>');
console.log('Browser ready; CDP at http://127.0.0.1:9222');
context.on('close', () => process.exit(0));
for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, async () => { await context.close(); process.exit(0); });
})().catch(error => {console.error(error); process.exit(1);});
REMOTE_BROWSER_FILE
cat > /opt/remote-browser/supervisor.py <<'REMOTE_BROWSER_FILE'
import os, subprocess, time, signal
from pathlib import Path
base = Path.home() / '.local/share/remote-browser'
env = dict(os.environ, DISPLAY=':99')
env.pop('WAYLAND_DISPLAY', None)
env.pop('SBX_NO_DISPLAY', None)
processes = []
def start(name, args):
log = open(base / (name + '.log'), 'a')
process = subprocess.Popen(args, env=env, stdout=log, stderr=subprocess.STDOUT)
processes.append(process)
return process
def stop(*_):
for p in reversed(processes):
if p.poll() is None: p.terminate()
for p in reversed(processes):
try: p.wait(timeout=8)
except subprocess.TimeoutExpired: p.kill()
raise SystemExit
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
(base / 'supervisor.pid').write_text(str(os.getpid()))
try:
start('display', ['Xvfb', ':99', '-screen', '0', '1440x900x24', '-nolisten', 'tcp'])
for _ in range(50):
if subprocess.run(['xdpyinfo', '-display', ':99'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0: break
time.sleep(.1)
else: raise RuntimeError('Display failed to start')
start('desktop', ['openbox'])
start('vnc', ['x11vnc', '-display', ':99', '-localhost', '-rfbport', '5900', '-rfbauth', str(base/'vnc.pass'), '-forever', '-shared', '-repeat', '-xkb', '-noxdamage'])
start('novnc', ['websockify', '--web=/usr/share/novnc', '0.0.0.0:6080', '127.0.0.1:5900'])
start('browser', ['node', '/opt/remote-browser/browser.cjs'])
while all(p.poll() is None for p in processes): time.sleep(1)
finally:
stop()
REMOTE_BROWSER_FILE
cat > /usr/local/bin/remote-browser <<'REMOTE_BROWSER_FILE'
#!/bin/bash
set -euo pipefail
base="$HOME/.local/share/remote-browser"
mkdir -p "$base"
chmod 700 "$base"
case "${1:-status}" in
start)
if [[ -f "$base/supervisor.pid" ]] && kill -0 "$(cat "$base/supervisor.pid")" 2>/dev/null; then echo 'Already running'; exit 0; fi
if [[ ! -f "$base/vnc.pass" ]]; then
python3 - <<'PASSWORD'
import secrets, subprocess
from pathlib import Path
base = Path.home() / '.local/share/remote-browser'
p = base / 'password.txt'
p.write_text(secrets.token_hex(4) + '\n')
p.chmod(0o600)
subprocess.run(['x11vnc', '-storepasswd', p.read_text().strip(), str(base/'vnc.pass')], check=True)
PASSWORD
fi
nohup setsid python3 /opt/remote-browser/supervisor.py >> "$base/supervisor.log" 2>&1 < /dev/null &
echo "Starting browser. Run remote-browser status to check readiness."
;;
stop)
if [[ -f "$base/supervisor.pid" ]]; then kill "$(cat "$base/supervisor.pid")" 2>/dev/null || true; fi
;;
password) cat "$base/password.txt" ;;
status)
curl --noproxy '*' -fsS http://127.0.0.1:6080/vnc.html > /dev/null
curl --noproxy '*' -fsS http://127.0.0.1:9222/json/version
;;
*) echo 'Usage: remote-browser {start|stop|status|password}'; exit 1 ;;
esac
REMOTE_BROWSER_FILE
chmod 755 /usr/local/bin/remote-browser
user: "0"
description: Install the virtual desktop, noVNC, and browser launcher