A local VPS fleet you can drive from a browser. QEMU/KVM, no root, no libvirt, Python stdlib only.
python3 vpsd/vps.py doctor # check the host
python3 vpsd/vps.py create web1 --start
python3 vpsd/vps.py list
python3 vpsd/vps.py console web1 # serial console, Ctrl-] detaches
python3 vpsd/vps.py ssh web1 -- uname -a
python3 vpsd/api.py --port 8905 # fleet UI + web console at :8905
A graphical guest can be watched and driven without a browser and without anything running inside it, because QEMU will do both:
python3 vpsd/vps.py screenshot tr4 /tmp/g.ppm # the framebuffer, as QEMU sees it
python3 vpsd/vps.py type tr4 driver @tab driver @ret # log in at the greeter
python3 vpsd/vps.py click tr4 640 400 # absolute tablet, so screenshot px == click px
That pair reaches what ssh cannot: the display manager before anyone has logged in, a session that failed to start, a boot menu, single-user mode. ssh tells you what a shell says is true; screenshot shows you the screen. QEMU writes PPM — magick /tmp/g.ppm /tmp/g.png if you want to look at it.
It builds nothing. The images it runs come from T&R, which is the distro; this is the thing that runs it. The two do not import each other.
No root anywhere in the normal path. /dev/kvm is usually 0666, so
guests run as you. The price is user-mode (SLIRP) networking with recorded port forwards instead of bridges — instances are reachable from the host, and not yet from each other.
No libvirt. One QEMU process per instance, supervised by pid + a QMP
socket, with all state on disk as JSON. Anything that can read a directory can report on the fleet, which is what makes the web UI trivial.
Instances are cheap. Every disk is a qcow2 overlay on one shared golden
image, so spawning costs kilobytes. A T&R server instance settles at ~51 MB.
vpsd/api.py serves the fleet as JSON, lifecycle actions, and two ways into a guest:
`/console/<name>` — the serial console over SSE, in xterm.js. The broker
is the sole client of QEMU's chardev socket (it only accepts one) and fans out to viewers, connecting lazily so vps.py console still works when the web console is closed. Scrollback is primed from QEMU's own log, so you see the boot you were not present for.
`/desktop/<name>` — the framebuffer over noVNC, through a WebSocket→TCP
proxy implemented here because websockify is one more system dependency than ~80 lines of RFC 6455 framing is worth.
Both are built to be dropped into an <iframe>, which is how they end up inside RAVIO's drive-in screen as tabs.
Mutations require an x-parkvps header, which a cross-origin form cannot send, so it forces a preflight the origin allowlist then refuses. Binding to localhost is not protection: localhost is reachable from any page you happen to be browsing.
A SLIRP `hostfwd` TCP connect proves nothing. QEMU accepts the host side
before knowing whether the guest is listening, so "port open" reports ready for a guest still in the boot loader. Read the SSH banner instead.
Readiness probing is not free to the guest. Reading sshd's banner and
closing makes sshd log an error, and FreeBSD's syslog sends errors to /dev/console — so polling the fleet flooded every guest's serial console at exactly the poll rate. Readiness is cached per pid.
Replaying a recording into a live terminal is not display-only. xterm.js
answers any query it is fed and the answer goes to the guest as input, so a ESC[6n captured during boot got answered minutes later at a shell prompt and executed. Scrollback is stripped of terminal queries; live output is not.
A serial socket does not buffer, so a boot failure before you attach is
invisible — and those are the ones you cannot attach during. QEMU's chardev logfile= keeps the scrollback.
**socket.create_connection(addr, 5) sets the timeout on the resulting
socket**, not just on connecting, so an idle VNC session died after five seconds of perfectly normal silence and reported a clean disconnect.
A guest needs a `usb-tablet` for the desktop to be usable. VNC speaks
absolute pointer positions; with only a PS/2 mouse, press/move/release land in three different places and every drag fails.
Full detail, including the FreeBSD-specific traps, is in docs/spec/README.md.
Works, and has no test suite — everything here was verified by running it, which is not the same thing. strip_queries is the only unit-tested piece.