Sign inSign up

zigadev/novnc-kit:0.2.0

Manifest digest

sha256:479864cc2ec67db4c671568e1d3cfe84cb7263b3e92d788501ceee9228dba88b

Last pushed

3 days by zigadev

Type

Sandbox Kit

Manifest digest

sha256:479864cc2ec67db4c671568e1d3cfe84cb7263b3e92d788501ceee9228dba88b

yaml
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