guix containers for easily exploring historical lisp machines
  • JavaScript 51%
  • Python 26.3%
  • C 20.4%
  • Shell 1.1%
  • Makefile 0.5%
  • Other 0.7%
Find a file
2026-08-11 11:48:51 -04:00
.agents/skills/write-reimplementation-specs docs: specify program selection and window management 2026-07-19 04:46:56 -04:00
.github/workflows Publish Genera-styled museum documentation site 2026-07-26 09:12:09 -04:00
cadr-web Add M14 report publication capability 2026-08-11 11:48:51 -04:00
docs Add M14 report publication capability 2026-08-11 11:48:51 -04:00
scripts Add M14 report publication capability 2026-08-11 11:48:51 -04:00
site Add linear animated Lisp Machine tours 2026-07-26 13:41:07 -04:00
tests Add M14 report publication capability 2026-08-11 11:48:51 -04:00
.gitattributes Close CADR WebAssembly roadmap M1 2026-07-27 13:48:02 -04:00
.gitignore Add M11 selected browser audio closure 2026-08-11 05:22:30 -04:00
AGENTS.md docs: specify program selection and window management 2026-07-19 04:46:56 -04:00
manifest.scm Add Lisp machine harnesses and editor museum docs 2026-07-18 02:49:50 -04:00
README.md Publish Genera-styled museum documentation site 2026-07-26 09:12:09 -04:00

lisp-machine-container-museum

This repo launches two Lisp Machine environments in Guix containers:

  • CADR / LM-3: fully public, based on the LM-3 upstream Fossil repos
  • Open Genera: requires your own purchased opengenera2.tar.bz2

If you only want one system to start exploring, use the CADR path first.

Historical, architectural, and artifact research lives in the museum knowledge base. It includes the current analysis of Open Genera VLOD world files.

The same canonical Markdown is published as a Genera-profile GitHub Pages site:

https://htayj.github.io/lisp-machine-container-museum/

Build the site locally with Pandoc and the pinned public Genera Fonts release:

curl -L --fail \
  https://github.com/htayj/genera-fonts/releases/download/v0.1.1/Genera-fonts-latin-v0.1.1.tar.gz \
  -o /tmp/Genera-fonts-latin-v0.1.1.tar.gz
echo "a72cfaa9ed6c418ba751d4a32d3cf715b1b3e6edd44acdc144f297d0c915b3cf  /tmp/Genera-fonts-latin-v0.1.1.tar.gz" \
  | sha256sum --check
python3 scripts/build-docs-site.py \
  --font-archive /tmp/Genera-fonts-latin-v0.1.1.tar.gz
python3 -m http.server 8000 --directory _site

The generated _site/ tree is ignored. The Pages workflow builds it from docs/ and does not commit generated HTML or Genera font data to this repository. See Publishing the museum documentation site for the interface behavior, font provenance, build boundary, and validation procedure.

What you need

  • Guix installed and working on the host
  • X11 available on the host
  • for Open Genera only: a local opengenera2.tar.bz2

If X11 authentication fails, allow your user explicitly:

xhost +SI:localuser:$USER

Which emulator to use

Use CADR / LM-3 if you want:

  • a fully public system
  • the simplest first boot
  • the MIT LM-3 environment from tumbleweed.nu

Use Open Genera if you want:

  • Symbolics Genera
  • your own commercial Open Genera world
  • a closer approximation of a later Symbolics workstation environment

CADR / LM-3

Launch

./scripts/cadr-guix-container.sh --mode run

The first run will:

  1. start a Guix container with the build/runtime dependencies for usim
  2. create a local Fossil checkout in ./l
  3. cache the supporting upstream Fossil repositories in the repo root
  4. build and launch the emulator

What a successful boot looks like

A successful cold boot normally lands in a Lisp Listener. That is not a blank system; it is the expected usable prompt.

On the first run, the bootstrap and build can take a while. Later runs should be much faster.

Keyboard notes

The default CADR key mapping in usim includes:

  • F1 = System
  • F2 = Network
  • F3 = Status
  • F4 = Terminal
  • F5 = Help
  • F6 = Clear Input

If you are unsure what to do next after boot, F5 is the first key to try.

Headless computer-use harness

The computer-use harness runs usim on a private, authenticated Xvfb display and drives its real X11 window through keyboard and mouse events. It is intended for repeatable behavioral checks and evidence screenshots without taking over the host desktop.

Check the harness dependencies and prepared CADR artifacts:

./scripts/cadr-computer-use.sh doctor

Start a named session and inspect its status:

./scripts/cadr-computer-use.sh start --session research
./scripts/cadr-computer-use.sh status --session research

The first start creates private copies of the System 303 disk and source trees. A later plain start cold-boots that existing private runtime. Use --fresh to discard and recreate the private runtime from the current public base before cold booting a stopped session:

./scripts/cadr-computer-use.sh stop --session research
./scripts/cadr-computer-use.sh start --session research --fresh

Fresh preparation builds and validates a staged runtime before installing it. The state root, each session, and each completed private runtime carry harness ownership markers; the tool refuses to adopt similarly named existing directories without them. A pre-hardening session directory therefore cannot be reused automatically: preserve it as evidence or remove it manually after review, then choose a new session name.

Wait for an elapsed interval or a stable screen, then send keyboard input:

./scripts/cadr-computer-use.sh wait --session research --seconds 10
./scripts/cadr-computer-use.sh wait --session research --stable-for 3 --timeout 180
./scripts/cadr-computer-use.sh key --session research help
./scripts/cadr-computer-use.sh type --session research --enter '(+ 2 3)'

help is the portable key alias for F5; the other aliases are listed by the script's key --help output. Mouse coordinates address the 768 by 963 CADR client window. Host button 3 is the CADR head/rightmost button:

./scripts/cadr-computer-use.sh mouse --session research move 400 500
./scripts/cadr-computer-use.sh mouse --session research click 400 500 --button 3

Capture the exact client window and stop the session cleanly:

./scripts/cadr-computer-use.sh screenshot --session research --label listener
./scripts/cadr-computer-use.sh stop --session research

Screenshots, JSON provenance sidecars, private disks and source trees, saved emulator state, credentials, and logs stay in the ignored build/cadr-computer-use/<session>/ directory. stop --discard deletes that named session instead of retaining it. Current sidecars distinguish revisions in the public checkouts at start from the private sources' copy-time revisions; they also hash the private source trees, usim executable, and private microcode and symbol files used by the run. Toolchain provenance records the Guix channel, manifest hash, Python version, and resolved commands as well. Immediately before launching the shared usim executable, the supervisor reacquires the preparation lock, hashes it again, compares that value with usim_sha256_at_start, and performs the launch while still holding the lock. A mismatch fails the start. The execution-time hash is kept in session state; screenshot sidecars retain only the start-time hash.

A normal stop reports forced_stop: false and state_may_be_incomplete: false. If a process exceeds its shutdown timeout and must be killed, the session is labeled forced-stopped with both flags true; do not treat that saved emulator state as a known-complete warm-resume artifact.

Warm start --resume is experimental. In the 2026-07-17 smoke run, restoring the state written by a clean stop halted immediately at microcode ILLOP; a subsequent --fresh cold boot worked. Do not currently rely on --resume for session persistence. See the harness architecture and observation record for provenance and the exact evidence boundary.

Raw screenshots remain in the ignored session tree. A small number may be copied into the museum only after an image- and use-specific copyright review under the runtime screenshot publication policy. Curated screenshot files are outside any blanket project license.

Useful CADR commands

Validate the container toolchain without starting the GUI:

./scripts/cadr-guix-container.sh --mode run --verify

Only create the upstream checkout:

./scripts/cadr-guix-container.sh --mode run --bootstrap-only

Prepare the repositories and build usim without launching the GUI:

./scripts/cadr-guix-container.sh --mode run --prepare-only

Open a shell inside the prepared environment:

./scripts/cadr-guix-container.sh --mode shell

Update the upstream LM-3 checkout:

./scripts/cadr-guix-container.sh --mode update

LOD helpers

The repo also includes a helper around the upstream lod and diskmaker tools for inspecting world-load bands and swapping them into the disk image.

List built-in aliases:

./scripts/lod-helper.sh list-releases

Inspect the default public System 303 load band:

./scripts/lod-helper.sh inspect 303-dist

Install a different public load band into the disk image:

./scripts/lod-helper.sh install 303-dist

Built-in aliases currently include:

  • 303-dist
  • 303-cold
  • 100-dist
  • 100-cold

Open Genera

Open Genera is not included in this repo. You must supply your own purchased archive, for example:

~/opengenera2.tar.bz2

Do not use the Open Genera archive that is commonly available on well known public piracy websites. Using infringing copies is illegal. Obtain Open Genera legitimately instead, for example by purchasing it from Symbolics: http://www.symbolics-dks.com/

Nothing extracted from that archive is checked into git. Runtime files are staged under ./.lm-home/opengenera/.

One-time host network setup

Open Genera expects a host TAP device named tun0.

Create or repair it with:

sudo ./scripts/opengenera-host-net.sh up --user "$USER"

Check its current state with:

./scripts/opengenera-host-net.sh status

Despite the name, tun0 must be a TAP device, not a TUN device. The helper script handles that for you.

Writable site storage on the host

To create a local site and save a reusable world, Genera needs a writable host file service. In this repo that is expected to be an NFS export visible to the VLM at the default Open Genera host paths: HOST:/var/lib/symbolics/sys.sct/site/ and HOST:/usr/opt/VLM200/lib/symbolics/.

Prepare it with:

sudo ./scripts/opengenera-host-nfs.sh up

Check the export and the Genera-facing paths with:

./scripts/opengenera-host-nfs.sh status

This helper exports writable directories for:

  • site files under /var/lib/symbolics/sys.sct/site/
  • saved .vlod worlds under /usr/opt/VLM200/lib/symbolics/

That means the standard Genera defaults from the installation guide can be used without editing them to repo-specific paths.

If you want to remove just these dedicated exports later:

sudo ./scripts/opengenera-host-nfs.sh down

NFSv2 fallback when the host kernel only offers NFSv3/v4

Some modern host kernels expose only NFSv3/v4, but Open Genera's built-in NFS client expects NFSv2. When rpcinfo -p 127.0.0.1 does not show program 100003 version 2, use the bundled Nimble userspace NFSv2 server instead.

Start it with:

sudo ./scripts/opengenera-host-nimble.sh up

Check it with:

./scripts/opengenera-host-nimble.sh status

This helper stops conflicting kernel RPC/NFS services if needed, starts Nimble, and exports the standard Open Genera paths directly, so inside Genera use:

  • DIS-EMB-HOST:/var/lib/symbolics/sys.sct/site/ for site files
  • DIS-EMB-HOST:/usr/opt/VLM200/lib/symbolics/ for saved worlds

Launch

./scripts/opengenera-guix-container.sh --mode run --archive ~/opengenera2.tar.bz2

The first run will:

  1. start a Guix container with the required runtime tools
  2. download the public snap4.tar.gz Linux VLM runtime into ./.lm-home/opengenera/downloads/
  3. extract the official Genera-8-5.vlod, VLM_debugger, and sys.sct tree from your archive
  4. build a small local compatibility shim for modern Linux
  5. start the VLM

What a successful boot looks like

You should see the VLM POST complete and a Cold Load stream appear. The usual non-fatal early warning is:

genera (Cold Load): Unable to allocate a modifier for the Hyper key.

That warning by itself is not a failed boot.

Genera headless computer-use harness

The Genera computer-use harness runs the real VLM client on an authenticated private Xvfb display and sends keyboard and mouse events through XTEST. Unlike the ordinary launcher above, it runs the native VLM in a Bubblewrap sandbox with separate user, network, PID, IPC, mount, and hostname namespaces. The sandbox sees a read-only Guix store, the exact private X socket and two read-only launch helpers, and its writable session runtime; it does not see the host home, repository, ordinary runtime sockets, or a default network route. Its private tun0 exposes no NFS, Nimble, FTP, or other guest file service. The private Xvfb disables MIT-SHM and verifies its absence before launch, preventing the out-of-namespace X server from attaching host shared-memory segments for the VLM.

Two tracked compatibility modules remain deliberately narrow. One suppresses only the redundant legacy /sbin/ifconfig tun0 10.0.0.1 dstaddr 10.0.0.2 netmask 255.255.255.0 request after the private interface is configured; every other system(3) command retains normal libc behavior. The X module installs typed event converters only on displays returned by XOpenDisplay, replaces only zero timestamps on seven core input event types, locally handles the byte-exact typed modifier request, and pairs its typed grab suppression with the matching ungrab. Direct tracing also established that the main Genera display relays a guest X11 byte stream through write(2). Only on descriptors belonging to those wrapped Displays, the module replaces the exact observed eight-byte GrabServer-plus-GetModifierMapping request and exact 148-byte SetModifierMapping request with sequence- and reply-compatible requests accepted by the private Xvfb. The replacement extension name must first be live-verified absent. No read-family API is interposed. The globally wrapped write(2) delegates every untracked or nonmatching write byte-for-byte, including TAP, world-image, ordinary-file, and unrelated-socket writes. A separate supervised process answers one observed RFC 868 request with one raw Ethernet reply entirely inside the throwaway network namespace. The harness reports running only after it has observed both exact X substitutions, validated packet evidence, and a successful responder exit.

Check the harness tools and currently staged Open Genera inputs:

./scripts/genera-computer-use.sh doctor

Start a named session from your purchased archive and inspect its status:

./scripts/genera-computer-use.sh start \
  --archive ~/opengenera2.tar.bz2 \
  --session research \
  --fresh
./scripts/genera-computer-use.sh status --session research

The first start verifies the purchased archive and exact known Genera 8.5 world, debugger, and public snap4 VLM identities. It then places private copies of those inputs, both compatibility modules, the RFC 868 responder, generated host files, and configuration under the ignored session tree. Immediately before execution the supervisor rehashes every private artifact and tracked launch source. The ordinary launcher stages inputs but is not retained as another runtime layer. A later start with the same required --archive but without --fresh reuses the private inputs; --fresh replaces them from the verified base. Every start creates a new VLM process; the harness has no host-process snapshot or resume mechanism. A reused private world could contain an explicitly saved in-guest state, which requires separate verification.

Wait for the main client, capture it, and send input only after verifying the current screen. main is the default target, but spelling it out makes a research transcript unambiguous:

./scripts/genera-computer-use.sh wait --session research --window-kind main
./scripts/genera-computer-use.sh screenshot \
  --session research --window-kind main --label initial-display
./scripts/genera-computer-use.sh key \
  --session research --window-kind main return
./scripts/genera-computer-use.sh type \
  --session research --window-kind main 'text to type'

Open Genera changes X clients while moving from Cold Load to its main display. The harness therefore requires an exact --window-kind target on wait, key, type, mouse, and screenshot. The choices are main, cold-load, debugger, and genera-other; omitted means main. It rediscovers candidates for every operation and fails closed when zero or more than one client has the requested kind. Commands, action records, and screenshot sidecars identify the selected X window by kind, ID, title, and geometry. Mouse coordinates therefore refer to that observed client's geometry, not the fixed CADR dimensions:

./scripts/genera-computer-use.sh mouse \
  --session research --window-kind main move 600 450
./scripts/genera-computer-use.sh mouse \
  --session research --window-kind main click 600 450 --button 1

The preserved VLM Debugger normally uses the Cold Load client, so target it as cold-load. The debugger kind is a reserved title classifier for a distinct X client whose title explicitly contains VLM Debugger; D04 did not observe such a separate title and does not treat that selector as a verified VLM-debugger route.

Portable Genera-key aliases are select (F1), function (F3), suspend (F4), resume (F5), clear-input (F10), complete (F11), end (KP_End), and help (F12). Existing aliases include rubout, abort, super, return/enter, space, and escape. These are host X-key translations; the meaning of a key still depends on the active Genera input context.

For example, after choosing Emergency Break on the main System Menu, target the separate Cold Load client explicitly. The Cold Load reader activates a completed form with the Genera End key rather than Return:

./scripts/genera-computer-use.sh wait \
  --session research --window-kind cold-load
./scripts/genera-computer-use.sh screenshot \
  --session research --window-kind cold-load --label emergency-break
./scripts/genera-computer-use.sh type \
  --session research --window-kind cold-load '(+ 40 2)'
./scripts/genera-computer-use.sh key \
  --session research --window-kind cold-load end
./scripts/genera-computer-use.sh screenshot \
  --session research --window-kind cold-load --label emergency-break-result
./scripts/genera-computer-use.sh key \
  --session research --window-kind cold-load resume
./scripts/genera-computer-use.sh wait \
  --session research --window-kind main

Capture the resulting exact client window and stop the VLM:

./scripts/genera-computer-use.sh screenshot \
  --session research --window-kind main --label after-input
./scripts/genera-computer-use.sh stop --session research

On stop, the supervisor sends SIGTERM to the actual VLM process, waits for its prompt, and answers yes. A pipe trace proved that the current public VLM reads the answer and begins cleanup, including breaking its X connection. This VLM artifact then stalls while terminating its Cold Load channel, so the bounded stop finishes with SIGKILL. stop consequently returns status 2 and records forced_after_confirmed_shutdown_stall: true, forced_stop: true, and state_may_be_incomplete: true; it must not be described as orderly host shutdown.

Neither the accepted confirmation nor forced cleanup invokes Genera's in-guest Save World operation. The harness does not create a process checkpoint or expose an NFS, Nimble, or FTP service. Absent a separately verified in-guest Save World action, unsaved Lisp memory, editor buffers, definitions, and processes are discarded with the VLM process. See the linked evidence guide for the mutex-level shutdown finding and the exact scope of that observation.

Each generation has a generation-scoped run record, updated atomically while active and preserved before the next generation, plus an ordered action log. Input operations record intent before XTEST dispatch and a linked succeeded or failed-or-partial outcome afterward. Screenshots, sidecars, logs, Xauthority, configuration, and all licensed or runtime copies stay under ignored build/genera-computer-use/<session>/. Use stop --discard to delete a stopped named session. Selected runtime screenshots may be copied into the museum only after an image- and use-specific copyright review; permission or a documented U.S. fair-use basis can support publication. The raw session remains ignored, and curated screenshot files remain outside any blanket project license. See the runtime screenshot publication policy and the Genera harness architecture and evidence guide.

Useful Open Genera commands

Validate the container setup without launching the GUI:

./scripts/opengenera-guix-container.sh --mode run --verify --archive ~/opengenera2.tar.bz2

Stage the runtime without starting it:

./scripts/opengenera-guix-container.sh --mode run --prepare-only --archive ~/opengenera2.tar.bz2

Open a shell in the prepared runtime:

./scripts/opengenera-guix-container.sh --mode shell --archive ~/opengenera2.tar.bz2

Extra arguments after -- are passed through to genera itself. That lets you override the default .VLM configuration if needed.

Open Genera notes

  • Only run one Open Genera instance at a time against tun0.
  • If you launch a second instance while one is already running, you can see Device or resource busy.
  • The launcher uses your purchased world/debugger, but the Linux-hosted VLM runtime comes from the historical public snap4 package.
  • The launcher includes a small compatibility preload because the historical runtime still tries to use legacy Linux networking behavior and X11 keyboard modifier remapping that modern unprivileged/Xwayland setups reject. The same preload also answers the VLM's early RFC 868 time request locally, so no host time daemon or privileged UDP/37 listener is required.

Font recovery

Recover the public CADR font sources from a checkout of mietek/mit-cadr-system-software:

python3 scripts/extract-cadr-fonts.py \
  /path/to/mit-cadr-system-software/src/lmfont \
  --output build/fonts/cadr \
  --clean \
  --strict

This reads the historical source representations under src/lmfont; it does not inspect a CADR load band or heap. The curated public outputs and full format notes are in the CADR font recovery article.

Recover the fonts resident in your own licensed Genera world:

python3 scripts/extract-genera-fonts.py \
  .lm-home/opengenera/runtime/Genera-8-5.vlod \
  --output build/fonts/genera \
  --clean

That command writes BDF files, normalized JSON, and PNG font sheets. All Genera outputs are derived from licensed media and remain in the ignored local build tree; do not commit or redistribute them. See the Genera extraction procedure and evidence-graded font catalog.

On-line help recovery

Recover the source-integrated help from the pinned public MIT CADR System 46 tree:

python3 scripts/extract-cadr-help.py \
  --source /path/to/mit-cadr-system-software/src \
  --output docs/assets/mit-cadr-online-help \
  --clean

The checked-in result contains exact public source contexts, four standalone ZWEI artifacts, catalogs, and the source license. See the CADR help recovery article for the inclusion rule and optional metadata-only LM-3 cross-check.

Recover the installed documentation from your own licensed Genera sys.sct tree:

python3 scripts/extract-genera-help.py \
  --sys-sct .lm-home/opengenera/runtime/sys.sct \
  --output build/help/genera \
  --clean

This decodes the Document Examiner's Sage Binary databases, copies three reviewed standalone help files, and inventories source-level help declarations without executing Genera code. Every output derived from Genera remains under the ignored build/help/genera/ tree and must not be committed or redistributed. See the Genera help recovery article.

Day-to-day use

Once either machine finishes booting, the normal place to work is a Lisp Listener.

Practical first steps:

  1. Click into the Lisp Machine window.
  2. Try the help key for the emulator you launched.
  3. Treat the Listener as your REPL and command entry point.

If you stop the emulator process abruptly, unsaved in-memory state is lost.

Files and caches

The repo creates a few local working directories:

  • ./l/ for the LM-3 upstream working checkout
  • ./.lm-home/ for container home directories and runtime caches
  • ./*.fossil for cached Fossil repositories

These are intentionally ignored by git.