The docs run on dell01 now; the apex warehack.ing SSH name points at the web
relay (no port 22) and docker-2 is superseded. Also fix the fetch bug:
'git fetch origin main' only writes FETCH_HEAD, so 'reset --hard origin/main'
fails on a fresh checkout — use bare 'git fetch origin'.
Swapped the narration voice after auditioning the American female and male
sets side by side. River also reads faster, so the recording is 3:22 rather
than 4:37, and the duration label is now a component prop instead of a
hardcoded string that could drift from the file again.
The talk exists as audio now: a synthesised narration, four and a half
minutes, on a /talk/ page with the full transcript underneath. The transcript
is the authoritative version, and the audio is preload="none" so the 2.5 MB
file is only fetched if somebody presses play. A second player sits on the
homepage, the way spicebook surfaces its pitch.
The badge follows the homestar.ink pattern: a small webring-style button in
the footer that opens a short modal about the studio. It is a Starlight footer
override, so it appears site-wide rather than only on the landing page, and it
closes on Escape or backdrop click with focus returned to the badge.
Talk script and narration text are kept in drafts/ for reuse.
The pre-publish audit caught the source distribution sweeping in the whole
docs-site tree, node_modules included: 7,178 files and 71 MB for a package
whose source is about thirty files. Excluded, which brings it back to 116 KB.
No secrets were exposed (no local .env exists), but this is exactly the case
the unpacked-sdist audit is meant to catch, and PyPI is immutable per version.
The caption named an API call, which meant nothing to a first-time reader;
it now describes what the picture actually shows, in prose rather than
monospace.
The cards were offset because CardGrid was using the stagger prop, which
deliberately drops alternate columns down the page. Removed, so the four
cards sit in an even grid.
The splash hero was an empty right column. It now shows a genuine
vm_screenshot capture of KolibriOS running Tetris, set in a window frame
and captioned with the call that produced it, so the picture demonstrates
the tool rather than illustrating it.
The frame is markup plus CSS rather than baked into the raster, so it stays
crisp at any size and follows the theme in both light and dark.
The capture itself was driven through the tools: launch_vm with the ISO,
vm_mouse_move to dismiss KolibriOS's MTRR notification and open Tetris from
the desktop, vm_send_keys to play a few pieces for colour.
Starlight, diataxis-shaped: a tutorial pair, six how-to guides, a generated
tool reference plus a configuration reference, and four explanation pages
covering architecture, sandbox isolation, see-and-drive, and the failure
handling philosophy.
The tool reference is generated from the running MCP server's own schemas, so
its 33 tools, parameters and defaults cannot drift from the code; the
generator asserts its grouping still covers exactly the live tool set.
Infrastructure follows the warehacking cookie-cutter (multi-stage Dockerfile,
Caddy serving dist with real 404 status, compose profiles for prod and dev,
Makefile with a deploy target pointed at docker-2). Two deviations worth
noting: remark-gfm is added to the MDX pipeline because Astro enables GFM for
.md but not .mdx, so tables silently rendered as run-together paragraphs; and
the card icon palette is pinned to the accent because Starlight's staggered
grid rotates through colours including purple.
Package URLs now point at the Gitea repo and this site.
- version 2026.08.17 in pyproject and __init__, kept in sync by a test
- add the MIT LICENSE file the metadata already claimed; it ships in both
the sdist and the wheel
- classifiers and Repository URL following the convention used by the other
MCP servers (git.supported.systems/MCP/<name>)
- tests/test_packaging.py guards the invariants that only bite after upload:
version drift, an unimportable console-script target, a declared license
with no file, and a Python floor that moves ahead of what we test
shutil.rmtree's error callback was renamed onerror -> onexc in 3.12 and the
two receive different third arguments, so sandbox_destroy raised TypeError on
every 3.11 run despite requires-python = ">=3.11". Route both through one
helper and test it.
Found by running the suite under 3.11 before publishing; the whole suite now
passes on 3.11 and 3.13.
The unit tests mock QMP and the guest agent, so they cannot catch a wrong
argument name, a QEMU option that stopped parsing, or a reply shape that
differs from the fake. This suite drives the real thing: the qemu-img
toolchain, lifecycle and see-and-drive on a diskless BIOS VM, adopting a
forgotten VM through attach_vm, the refusal to reuse a live socket, and the
full sandbox journey (guest exec, file round trip, live snapshot create /
restore / delete verified by guest state, screenshot) with a check that the
base image is never modified.
It immediately earned its keep: pid_matches_vm was rejecting attached VMs,
because an externally launched QEMU carries whatever -name its launcher
chose, so adopting one under a different name made it read as stopped. The
identity check now applies only to VMs we spawned.
QEMU command line:
- escape commas in every interpolated path (qopt); a path like
'data,readonly=on.qcow2' previously injected a drive option
- reject extra_args flags that breach VM isolation (host filesystem
passthrough, host block devices, spawning chardevs, -runas) and document
the parameter as operator-only
- detect duplicate host ports across port_forwards instead of failing at
QEMU launch; auto ports no longer collide with each other
Sandbox isolation:
- sandbox_vm now blocks guest-initiated traffic by default (restrict=on),
with allow_network=True to opt in. Verified end to end: with identical
guest network state, a default sandbox reaches neither a host loopback
service nor the internet, while allow_network=True reaches both
- note in the docstring that the guest agent answers before the guest has
finished booting
Bounded work per call:
- vm_serial_read seeks a 256KB window from the end instead of reading a
console log that grows without bound into memory
- cap vm_type_text length and vm_mouse_move deltas
- screenshots get a unique filename and are cleaned up, so a concurrent
capture cannot swap the frame under vm_click
Identity and liveness:
- attach_vm requires an actual unix socket and stores the resolved path
- attached VMs are judged by connecting, not by a stat that a stale socket
file would pass
- refuse to act on a PID whose cmdline proves it is a different VM
- a sandbox's base image counts as in use while its overlay is live
- fix a latent NameError in vm_mouse_move's homing branch
The success paths were fine; several failure paths drew a confident
conclusion without checking the thing they waited for.
Registry (was: any parse error killed the whole server, since load() runs
in the lifespan):
- quarantine an unreadable file and start empty instead of raising, so the
tools that stop runaway VMs keep working when bookkeeping is damaged
- skip malformed or invalidly-named records rather than failing the load;
report both through list_vms as registry_warnings
- read-modify-write under an exclusive flock so a second instance merges
instead of clobbering, with a PID-unique temp file
- drop the lifespan shutdown save, which could resurrect deleted records
- version the schema and round-trip unknown record fields
sandbox_destroy (the only tool that deletes files):
- verify the process actually died, escalating to SIGKILL, and refuse to
delete an overlay QEMU still holds open
- assert the target is inside the VM state tree before rmtree
- report cleanup errors instead of swallowing them; destroyed now reflects
what happened
Launch races:
- reserve the name before the first await so two concurrent launches cannot
race over one set of sockets
- refuse to unlink a QMP socket that is still accepting connections
- register the VM with a warning rather than orphaning it when the pidfile
is unreadable but QEMU is up
Guest agent and QMP:
- bound every guest-agent call, not just the handshake; cap max_bytes and
stop guest_file_read spinning on a zero-progress agent
- serialize QMP sessions per VM (the monitor is single-client) and say
"another operation holds it" instead of "the VM has likely exited"
- poll liveness while waiting for SHUTDOWN so a crashed VM is reported as
exited rather than as a guest ignoring ACPI
- default command timeout, with a longer bound for savevm/loadvm
- stricter snapshot tags; log destructive operations to stderr
Adds tests/test_reliability.py covering the conditions above.