Skip to content
radiolabmePublic

About

Koto VM System #1

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

koto

Koto creates a reproducible Ubuntu (Jammy 22.04) development environment that you can access through your web browser. It's designed for situations where you need a consistent Linux environment but you're working on macOS, or when you want to isolate your development work from your main system.

The whole thing runs locally in a VM, but feels almost like working with remote development containers. You get a full desktop, VS Code both in the browser and as a desktop app, and all the usual development tools pre-installed.

Features

  • Deterministic provisioning with Ansible
  • Modular shell orchestration (retry, lint preflight, quiet + JSON modes)
  • Append-only provisioning event log (attempt-start / attempt-end / task-result)
  • Native JSON summary including structured failures[] (host, name, uuid, status, message)
  • Optional pruning of event history via KOTO_EVENT_PRUNE
  • Replayable artifacts (you can regenerate summaries from the event log only)

Getting Started

The fastest way to get started:

git clone <repository-url>
cd koto
make setup PROFILE=malta   # Or rely on the default: make setup

make setup creates the VM, starts it, and runs provisioning. If you skip PROFILE, it defaults to crete (4 CPU, 8GB RAM). Set KOTO_PROFILE=malta in your environment to change that default. Prefer the shell wrapper? ./koto.sh setup now inspects existing VMs and walks you through starting, re-provisioning, recreating, or destroying them instead of failing silently—and on a fresh create it will prompt you to choose a hardware profile before proceeding (press ENTER for crete).

Prefer to break the steps apart?

make vm PROFILE=malta      # Create the VM only
make vm-start              # Boot it
make provision             # Run Ansible provisioning

Available profiles: bouvet, bali, malta, crete (default), sicily, honshu. Run make vm-profiles or ./koto.sh profiles to see the resource details for each. See lima/README.md for deeper customization.

After the VM is created and provisioned:

./scripts/start-web-portal.sh

Open http://localhost:8000 and you'll see a simple portal with links to the various ways you can access your development environment.

VM Management

All VM templating and creation logic is in the ./lima directory:

make vm-profiles        # List available hardware profiles
make vm-status          # Show current VM status
make vm-shell           # Open shell in VM
make vm-stop            # Stop VM
make vm-start           # Start VM
make vm-delete          # Delete VM
make vm-recreate        # Delete and recreate VM

For more details on the modular Lima structure, hardware profiles, and customization options, see lima/README.md.

Development Environment

This project uses a Lima-only approach with zero host dependencies except Lima itself. All tools (Python, Ansible, jq, git, etc.) run inside the VM.

Host Requirements: Only Lima (~100 MB)

All dependencies run inside the VM - Python, Ansible, jq, git, curl, wget are installed automatically in the VM during provisioning.

What You Get

The VM runs Ubuntu 22.04 (Jammy) with XFCE4 desktop delivered via a lightweight TigerVNC session (no display manager). You can access it several ways:

The ./vm directory in this project becomes /home/user inside the VM, so files you create in the VM show up on your host system and vice versa.

Why This Exists

Sometimes you need a Linux environment but you're on macOS. Sometimes you want to try things without messing up your main system. Sometimes you're working on a team and everyone needs the exact same development environment.

This gives you that without Docker's file system performance issues, without the complexity of manual VM setup, and without needing to learn a bunch of container orchestration tools.

The Architecture

The setup uses Lima (lightweight VM manager) with QEMU for virtualization. Ansible handles installing and configuring everything inside the VM. The web portal is just a simple Python HTTP server serving static HTML.

Desktop & VNC Design

Instead of a traditional display manager (e.g. LightDM), the desktop role provisions:

  • XFCE4 + essentials (terminal, file manager, Firefox, tooling)
  • TigerVNC server launched as a per-user systemd unit (vncserver@user)
  • Optional passwordless localhost-only VNC (default) for quick local dev
  • Switchable password mode (set -e vnc_passwordless=false or edit defaults)

This reduced crash surface (LightDM greeter issues, race conditions at boot) and made restarts idempotent and fast.

VNC Configuration & Desktop Variables

Variable Default Purpose
vnc_passwordless false Disable VNC auth (still bound to 127.0.0.1). Fast local access.
vnc_password user password Password written to ~/.vnc/passwd when auth enabled.
vnc_geometry 1920x1080 Desktop resolution; override per run.
vnc_depth 24 Color depth (lower to save bandwidth).
vnc_shared true Allow multiple clients to attach simultaneously.
vnc_display 1 Primary display number (:1 → tcp/5901).
vnc_extra_displays [] Additional displays (e.g. [2,3] → :2, :3).
vnc_enable_clipboard true Enables bidirectional clipboard via autocutsel.
vnc_memlock_limit 8M Systemd LimitMEMLOCK for VNC session processes (reduces Firefox warnings).
vnc_health_check true Wait/verify port availability after start.
vnc_randr_modes (list) Allowed dynamic resize modes (enables noVNC resize + xrandr).

Start (or re-run) with custom settings:

ansible-playbook -i inventory playbooks/main.yml --tags desktop \
	-e vnc_passwordless=false -e vnc_password='ChangeMe!' \
	-e vnc_geometry=1680x1050 -e vnc_extra_displays='[2,3]' \
	-e vnc_memlock_limit=16M -e desktop_debug_firefox=true

noVNC proxies the TigerVNC session on port 6080; direct VNC connects to localhost:5901 (primary) and :5902, :5903, etc. for any extras.

Dynamic resizing: with vnc_randr_modes set, you can use the noVNC “Resize remote to browser” button or run xrandr -s <mode> inside the session to switch among those resolutions.

Reset a wedged display (cleans stale pid/log):

./scripts/reset-vnc.sh 1   # or 2/3 if using extra displays

Target only VNC unit changes during development:

ansible-playbook -i inventory playbooks/main.yml --tags vnc-unit

Health checks can be disabled:

ansible-playbook -i inventory playbooks/main.yml --tags desktop -e vnc_health_check=false

Clipboard sync can be disabled (saves two tiny background processes):

ansible-playbook -i inventory playbooks/main.yml --tags vnc-setup -e vnc_enable_clipboard=false

Software Bill of Materials (SBOM)

You can generate an SBOM bundle for the running VM that includes:

  • System packages (root filesystem) in Syft JSON, SPDX tag-value, and CycloneDX JSON
  • Python dependency snapshot (pip freeze + Syft parse)
  • code-server extension inventory

Run:

make sbom

Outputs are placed under vm/workspace/sbom/<timestamp>/ (from host) / ~/workspace/sbom/<timestamp>/ (inside VM). Each run includes:

File Purpose
system.syft.json Detailed package inventory (Syft native)
system.spdx SPDX tag-value for license/compliance tooling
system.cdx.json CycloneDX JSON for supply chain scanners
python-requirements.txt Raw pip freeze output
python.syft.json Parsed Python dependency SBOM (if freeze succeeded)
code-server-extensions.txt List of installed code-server extensions
MANIFEST.txt Index / provenance metadata

Syft is auto-installed in the VM if missing (downloaded once per VM). You can archive one of these directories as an audit artifact or feed the files into vulnerability scanners.

Everything installs locally to this project directory. You don't need Node.js, Python, or any particular development tools on your host system - the setup script downloads what it needs and keeps it contained here.

If you want to get rid of everything, just delete this directory.

If you want to get rid of everything, just delete this directory.

Customization

The Ansible playbooks are in playbooks/ and are pretty straightforward if you want to add software or change configurations. The VM configuration is in the Lima YAML files if you want to adjust memory, CPU, or port mappings.

The default credentials are intentionally simple (user/password) since this is meant for local development. If you're exposing this outside your machine, change them.

File Sharing

Files you put in ./vm/ appear as /home/user/ inside the VM. This works both ways - create a file in the VM and it shows up on your host system.

The sharing uses Lima's built-in 9p mounting, which is fast enough for most development work but not quite as fast as native filesystem access.

Remote Access

If you install Tailscale inside the VM (it's pre-installed, just needs sudo tailscale up), you can access your development environment from anywhere. The web interfaces work fine over Tailscale.

Platform Support

Works on macOS (Intel and Apple Silicon) and Linux (x86_64 and arm64). The setup script detects your platform and installs the right binaries.

Windows isn't supported because Lima doesn't run on Windows. If you're on Windows, use WSL2 and run this inside the WSL2 environment.

Troubleshooting

If something breaks, you can check status with either ./koto.sh status or make status. limactl shell koto gets you a shell inside the VM to inspect services directly.

Check desktop/VNC specifically:

limactl shell koto systemctl status vncserver@1.service
limactl shell koto journalctl -u vncserver@1.service --no-pager -n 80

Common desktop issues:

Symptom Likely Cause Fix
Black screen on connect Session not started yet Wait 2-3s or restart systemctl restart vncserver@1.service
Password rejected Using passwordless mode Re-run with -e vnc_passwordless=false
Geometry not applied Old session still running Run ./scripts/reset-vnc.sh 1 or restart service
noVNC cannot connect WebSocket proxy race Refresh; verify service + port (5901) listening

The Ansible playbooks are idempotent, so you can re-run ./scripts/run-playbooks.sh if something gets misconfigured.

If you really mess things up, ./koto.sh destroy (add -y for non-interactive) will destroy the VM so you can start over. Running ./koto.sh setup afterwards will guide you through reusing or recreating the VM; combine it with -y to automatically start and provision an existing instance without prompts.

Provisioning Event Log

Each provisioning run creates a temp results directory (mktemp). Inside it:

results_dir/
	attempt-1.raw
	attempt-2.raw   # only if retries occur
	events/
		000001.attempt-start.jsonl
		000002.task-result.jsonl
		000003.attempt-end.jsonl
		...
	summary.json (if you redirect --json-file)

Event objects (JSONL lines) share a common envelope:

{"ts":"2025-09-25T18:49:35Z","kind":"attempt-start","attempt":1,"status":"started"}
{"ts":"2025-09-25T18:49:37Z","kind":"task-result","attempt":1,"host":"koto-host","task_name":"Install common dependencies","task_uuid":"...","status":"failed","message":"Package install error"}
{"ts":"2025-09-25T18:49:37Z","kind":"attempt-end","attempt":1,"status":"failed"}

The final JSON summary (when --json is used) folds these into a structured object with failure_count and failures[].

Pruning

Set KOTO_EVENT_PRUNE=N to retain only the most recent N event JSONL files after a run. Example:

KOTO_EVENT_PRUNE=500 scripts/run-playbooks.sh --json

Omit the variable to keep the full history. Pruning happens after summary emission.

Replay

Because the event log is append-only, you can derive new summaries without re-running Ansible:

jq -s 'map(select(.kind=="task-result" and (.status=="failed" or .status=="unreachable"))) | length' results_dir/events/*.task-result.jsonl

A simple replay helper script can be added if needed.

Test Harness Playbook

For a controlled failure (without touching the main provisioning flow) use:

ansible-playbook -i inventory playbooks/test/fail.yml

Trigger an intentional failure:

KOTO_TEST_FAIL=1 ansible-playbook -i inventory playbooks/test/fail.yml

This emits task-result events you can inspect to validate failure reporting and pruning.


This project started because setting up consistent development environments is annoying and Docker on macOS has performance issues with file sharing. Lima + Ansible solved that, but the setup was complicated enough that it needed automation.

The result is something that feels a bit like GitHub Codespaces or GitPod, but running entirely on your local machine—now with a leaner, more resilient desktop path (no display manager) for faster restarts and fewer moving parts.

About

Koto VM System #1

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages