Experimental, non-authoritative material. This is a personal working project created partly with emerging AI tools. It is not a verified security product or professional advice. Test it without important data or credentials first. Corrections are welcome. Use it at your own risk and cost. Full disclaimer.
KVM-Agent creates a disposable graphical Ubuntu desktop for autonomous coding
agents. The host runs only Ubuntu's KVM/libvirt stack and virt-manager; Codex,
Claude Code, OpenCode, Aider, Ollama, project commands, and their third-party
installers run inside the VM.
The repository has one setup and recovery command:
./setup-kvm-agent.shThe script installs the host virtualization stack, authenticates an official
Ubuntu cloud image, creates the VM, installs a minimal Ubuntu desktop, installs
the five requested tools plus the current official GitHub CLI, and waits for
the result. Day-to-day operation happens through the familiar virt-manager
GUI.
An opt-in reduced formal-methods environment is available:
./setup-kvm-agent.sh --formal-methodsIt adds Lean 4, Isabelle/HOL, GHC, Cabal, Haskell Language Server, HLint, VS Code, and the official Lean and Haskell extensions inside the same graphical guest. It does not restore the older repository architecture or its larger prover collection.
An additional opt-in manager/worker profile can prepare VMs on different physical hosts to submit long jobs over Tailscale or WireGuard plus ordinary OpenSSH. It is intentionally not enabled for normal users; see Cross-host manager/worker VMs.
An opt-in research-journal profile can also be added to already-running VMs. It records meaningful agent events and generates evidence-based daily, weekly, and monthly JSON plus static HTML without recreating the guest; see Automatic research journals.
The same command can resume interrupted finalization or replace a disposable VM. A small removal helper remains available when removal without rebuilding is wanted:
./setup-kvm-agent.sh --finalize-existing --name kvm-agent
./setup-kvm-agent.sh --replace-existing --name kvm-agent
./setup-kvm-agent.sh --resize-existing --name kvm-agent --memory 24576 --vcpus 12
./remove-kvm-agent.shIt does not remove the shared verified Ubuntu image cache, host packages, or additional disks that a user attached manually.
flowchart TB
H["Trusted Ubuntu host account"]
M["virt-manager and system libvirt"]
subgraph V["Disposable Ubuntu 26.04 desktop VM"]
D["GNOME desktop and terminal"]
A["Codex · Claude Code · OpenCode · Aider"]
O["Ollama on 127.0.0.1"]
F["Optional Lean · Isabelle/HOL · Haskell · VS Code"]
D --> A
D --> O
D --> F
end
P["Chosen remote provider or local model"]
H --> M
M -->|"KVM + local SPICE console"| V
A -->|"only after user configuration"| P
O -->|"local weights or Ollama Cloud"| P
The GUI does not replace or bypass KVM: virt-manager is a graphical client
for the same system libvirt/KVM boundary. The host account remains trusted and
can control the VM. Coding agents are never installed on the host.
Several systems assign identifiers to the same VM. They are technically independent, but using different names for one machine is an easy source of operator error. Recommended convention: choose one globally unique VM name and reuse it as the libvirt name, guest hostname, Tailscale device name, and Mac SSH alias.
| Name | Recommended example | Used by |
|---|---|---|
| Libvirt VM name and guest hostname | vm-workstation-01 |
--name, virsh, kvm-agent-host |
| Tailscale device name | vm-workstation-01 |
MagicDNS and Machines page |
| Mac SSH alias | vm-workstation-01 |
ssh and scp on macOS |
| Guest login | agent |
Linux and OpenSSH |
| Tailscale tag | tag:development or a composite swarm tag |
Access policy; describes trust/role, not machine identity |
The physical host's informal name is separate, and a Tailscale tag should describe a security role or group rather than replace the machine name. See Secure remote access before connecting several machines.
From the ordinary Ubuntu host account, the script:
- installs KVM, libvirt,
virt-manager,virt-install, and supporting Ubuntu packages withsudo; - adds that host account to the
libvirtgroup; - starts libvirt's standard NAT network;
- downloads Ubuntu 26.04's released amd64 cloud image and verifies it against Ubuntu's GPG-signed SHA-256 manifest;
- asks for a local GUI password and creates a dedicated recovery SSH key;
- creates a graphical VM with SPICE, virtio video, clipboard integration, an Ubuntu desktop, and no host directory share;
- installs GitHub CLI from GitHub's official APT repository, downloads and
runs the official Codex, Claude Code, OpenCode, and Ollama installers inside
the guest, and installs Aider in a per-user
uvenvironment; and - when
--formal-methodsis selected, installs Lean throughelan, Isabelle2025-2/HOL from its checksum-verified official Linux archive, GHC/Cabal/HLS through GHCup, HLint through Cabal, and VS Code with the official Lean and Haskell extensions; - when
--swarm-roleis selected, installs either Tailscale or WireGuard support, a dedicated manager SSH key and/or a locked-down non-sudo worker account, plus helpers for safe Tailscale naming, host-key verification, fixed SSH/rsync access, and remote-job lifecycle management; Tailscale authentication and manager-key authorization remain explicit human steps; - installs a host-side
kvm-agent-hosthelper, a guest controller-key helper, and an OpenSSH baseline that denies password, root, agent, X11, tunnel, and port-forwarding access by default; - before any vendor installer runs, configures a guest firewall that denies unsolicited inbound traffic and, by default, outbound traffic to private and link-local address ranges, while leaving internet access open; and
- verifies each command, keeps Ollama bound to guest loopback
(
127.0.0.1:11434), disables future cloud-init runs, and destroys the cloud-init seed once provisioning is done.
It deliberately does not:
- install an agent, Node.js package, Python agent package, or Ollama on the host;
- sign in to OpenAI, Anthropic, GitHub, Ollama Cloud, or another service;
- download Ollama model weights;
- mount the host home directory or a project directory in the guest;
- configure USB passthrough, SSH-agent forwarding, or a LAN-facing VM console;
- enroll a VM into Tailscale, configure WireGuard peers, or connect physical hosts to a swarm overlay;
- choose a model provider; or
- install Agda, Rocq/OCaml, HOL4, HOL Light, Mathlib, or the Archive of Formal Proofs.
The reduced formal-methods environment remains optional, so users who only need an agent VM do not pay its download, disk, and provisioning cost.
The supported primary path is:
| Component | Supported configuration |
|---|---|
| Host | Ubuntu 24.04 or 26.04 LTS, x86-64 |
| Guest | Ubuntu 26.04 LTS, amd64 |
| Firmware | Intel VT-x or AMD-V enabled |
| Host privilege | The invoking account can use sudo |
| Network | Internet access during initial provisioning |
| Display | Local graphical Ubuntu session for virt-manager |
| Disk | 120 GiB guest virtual disk by default; at least 12 GiB free on the host, or 30 GiB with --formal-methods |
| Memory | Dynamically allocated: 75% of host RAM, capped at 32 GiB, while retaining at least 2 GiB for the host; at least 8 GiB is recommended |
The default memory is 75% of host RAM, capped at 32 GiB while retaining at
least 2 GiB for the host. The default vCPU count is 75% of the host's logical
CPUs, capped at 16. Thus a 16 GiB/8-thread host normally gives the guest about
12 GiB and 6 vCPUs, while a 64 GiB/32-thread host gives it 32 GiB and 16
vCPUs. Explicit --memory and --vcpus values still override these defaults.
New VMs are built from Ubuntu's released ubuntu-26.04-server-cloudimg-amd64.img.
The setup verifies the signed image manifest, passes 26.04 into early guest
provisioning, reads /etc/os-release again over the managed recovery channel,
and refuses final cleanup if the guest does not report Ubuntu 26.04. The
verified release is also recorded in
/var/lib/kvm-agent/installed-versions.txt.
Some Ubuntu 24.04 hosts have a libosinfo database that predates the
ubuntu26.04 identifier. In that case the script clearly reports that it is
using ubuntu24.04 only as compatible virtual-hardware metadata for
virt-install. This does not select or install Ubuntu 24.04: the disk URL,
signed checksum, early guest check, and final guest check all remain pinned to
26.04.
Updating this repository does not change an already-created VM. Check an existing guest from the trusted Ubuntu host with:
kvm-agent-host ssh YOUR_VM_NAME cat /etc/os-releaseIf it reports 24.04, copy the work you intend to keep out of the VM and review
it before using --replace-existing, which deliberately deletes and recreates
the selected guest. Follow the guarded migration procedure in
Troubleshooting.
Download or clone this repository, then run:
cd YOUR_AGENT_VM_DIRECTORY
chmod +x setup-kvm-agent.sh
./setup-kvm-agent.shYOUR_AGENT_VM_DIRECTORY is a placeholder. A Git clone is normally named
agent-vm; a downloaded ZIP may extract as agent-vm-main.
Do not run sudo ./setup-kvm-agent.sh. Run it as the host account that will
use virt-manager; the script invokes sudo only for the operations that need
it.
The script asks for a password for the guest's agent account. This password is
for the local graphical login. SSH password login remains disabled. The account
has passwordless sudo inside the disposable guest so the agents can be highly
autonomous without receiving host privilege.
Initial provisioning commonly takes 20–60 minutes. Installing the desktop,
upgrading Ubuntu, or reaching upstream download services may take longer on a
slow machine. With --formal-methods, the large Isabelle, Lean, GHC, HLS, and
VS Code downloads plus the HLint build may take several hours. The host
terminal waits by default and enforces a six-hour upper bound for that profile.
When the script finishes, log out of the Ubuntu host and back in if it added
your account to libvirt for the first time. Then run:
virt-manager --connect qemu:///systemDouble-click kvm-agent, open its graphical console, and log in as agent with
the password chosen during setup. In the guest terminal:
codex
claude
opencode
aider
ollama --version
gh --versionEach coding agent performs its own first-run authentication or provider setup.
Do that only after reading Credential handling.
For a private repository, protected main, project-scoped deploy key,
fine-grained API token, and issue-to-pull-request routine, follow
GitHub integration for a local coding-agent VM.
With --formal-methods, the same guest also supports:
code
lean --version
lake --version
isabelle jedit
ghc --version
cabal --version
haskell-language-server-wrapper --version
hlint --versionSee Reduced formal-methods environment for the exact scope, editor behavior, and update model.
--name NAME Libvirt VM name and guest Linux hostname
(default for new VMs only: kvm-agent)
--user NAME Guest login name (default: agent)
--memory MB Guest RAM in MiB
--vcpus NUMBER Guest virtual CPUs
--disk GB Guest virtual disk size (default: 120)
--no-wait Return after starting the VM
--allow-lan Permit egress to private/link-local address ranges; UFW
remains enabled and continues to deny unsolicited inbound
traffic. Only for an internal mirror or model endpoint
--formal-methods Add Lean, Isabelle/HOL, Haskell tooling, VS Code, and the
official Lean/Haskell extensions inside the guest
--allow-remote-editor
Opt in to client-initiated local SSH forwarding for a
remote editor; agent and X11 forwarding stay disabled
--swarm-role ROLE Prepare the guest as "manager", "worker", or "both"
--swarm-network N Use "tailscale" (default) or "wireguard" for swarm traffic
--add-swarm ROLE Add a swarm role to an already-provisioned managed VM
--add-journal Add automatic research journals to an existing managed VM
--harden-existing Reapply the current SSH baseline to a named existing VM
--journal-project P
Initialize guest-side Git project P; may be repeated
--journal-backend B
Use evidence (default), claude, or codex reporting
--journal-allow-remote-reporting
Consent to sending bounded project metadata to the selected
claude/codex provider; required for either remote backend
--journal-timezone Z
Use IANA timezone Z (default: Etc/UTC)
--resize-existing Change persistent RAM and/or vCPU allocation of a powered-
off existing VM without deleting it
--replace-existing Remove the selected existing VM after exact-name
confirmation, then build it again
--finalize-existing
Resume verified final cleanup of an existing VM
The 120 GiB default is a guest-visible maximum, not 120 GiB allocated
immediately on the host: qcow2 grows as the guest writes data. Before any
large installation, setup explicitly grows and verifies the root partition
and filesystem. It also keeps 512 MiB of emergency space during provisioning
so a failed download or package build can release that space instead of
leaving the graphical login unusable. The host must have at least 12 GiB free
for the base profile or 30 GiB for --formal-methods; setup checks this before
--replace-existing removes the old VM.
Large installers and archives, including the Isabelle distribution, are staged
in a protected directory on the guest root filesystem. They are not stored in
Ubuntu's RAM-backed /run filesystem, and partial downloads are removed
automatically on failure.
--no-wait returns before provisioning finishes, so it cannot immediately
remove the cloud-init seed or disable future cloud-init runs. Complete those
steps later with the repository helper; it waits for successful provisioning,
verifies the guest marker, disables cloud-init, performs any required update
reboot, proves that a new boot completed, rediscovers a changed DHCP address,
and removes the seed:
./setup-kvm-agent.sh --finalize-existing --name NAMEFor example:
./setup-kvm-agent.sh \
--name agent-project-01 \
--memory 16384 \
--vcpus 8 \
--formal-methodsVM names use lowercase letters, numbers, and hyphens. By default, the script
refuses to replace an existing libvirt domain or disk. --replace-existing
shows the exact removal plan, requires the VM name to be typed, retains the
shared Ubuntu cache and any manually attached extra disks, then rebuilds.
Memory and vCPU allocation can be changed without deleting the VM or its disk. Shut the guest down normally, then run, for example:
./setup-kvm-agent.sh \
--resize-existing \
--name kvm-agent \
--memory 24576 \
--vcpus 12Either --memory or --vcpus may be omitted. The helper changes the persistent
libvirt configuration and the new values apply at the next start. It refuses a
running VM, a VM with a managed-save image, RAM that leaves less than 2 GiB for
the host, or more vCPUs than the host reports. If virt-manager/libvirt has saved
the running state, start the VM and perform a normal full shutdown first; this
prevents an old saved state from restoring the previous resource configuration.
No guest filesystem, cloud-init state, or project data is changed. A vCPU count
is the number of virtual logical CPUs visible to the
guest, not a guaranteed exclusive CPU quota; host scheduling still determines
actual execution time.
Libvirt can sometimes hot-plug resources into specially prepared running guests, but increasing maximum memory or CPU topology live is not uniformly supported. KVM-Agent therefore uses the predictable powered-off path instead.
Most users can ignore this feature. --swarm-role manager|worker|both prepares
an initial VM, and --add-swarm adds a role later; these roles do not rename
the VM, so separate hosts may both keep the default name kvm-agent. Tailscale
is the default and raw WireGuard is optional. Provisioning does not enroll
devices or authorize peers automatically. Read
Cross-host manager/worker VMs before enabling it, including the
directional-access and risk guidance.
To retrofit an already-provisioned, running VM from its physical host:
./setup-kvm-agent.sh \
--add-journal \
--name kvm-agent \
--journal-project /home/agent/YOUR_PROJECTYOUR_PROJECT is a placeholder, and the path is inside the guest. Repeat
--journal-project for multiple
repositories in the same VM. This operation does not rebuild the guest. It
adds agent-neutral event instructions, canonical JSON plus static HTML
reports, and persistent 07:00 daily, Saturday weekly, and first-of-month
timers. The safe default is deterministic evidence-only reporting with no
model-provider data transfer. Remote Claude/Codex enrichment is an explicit
opt-in with a separate consent flag and falls back to evidence-only on failure.
OpenCode agents may record events but are not used as unattended reporters.
Read
Automatic research journals for the layout, event commands,
security boundary, and backend behavior.
If setup reports that the guest did not become reachable after its update
reboot, but the desktop and tools work, do not recreate the VM and do not type
the individual SSH and virsh cleanup commands. Run:
./setup-kvm-agent.sh --finalize-existing --name kvm-agentThe helper verifies /var/lib/kvm-agent/provisioned through the recovery key
before changing cloud-init or touching the seed. It creates and verifies the
cloud-init disable marker before requesting an update reboot. Because
systemctl reboot is asynchronous, a successful SSH connection alone is not
accepted as proof that reboot finished: the helper waits for the kernel boot ID
to change and re-queries libvirt DHCP leases instead of trusting the pre-reboot
address. It then verifies both the running and persistent device configurations
before deleting the exact managed seed file. SSH and cloud-init checks are
polled without cloud-init status --wait; each SSH invocation has a hard
deadline, so a connected but blocked guest cannot make the helper hang
indefinitely. It is also the supported completion path after --no-wait.
Shut the guest down normally, then run:
./remove-kvm-agent.sh --name kvm-agentThe helper displays the libvirt domain, attached storage, exact managed image paths, recovery SSH directory, and log it will remove. Type the exact VM name to confirm. It removes the domain, its main disk, any leftover cloud-init seed, and its host-side recovery data. It retains the verified Ubuntu base-image cache and virtualization packages, so a later rebuild does not repeat the host installation or image download.
Use --dry-run to inspect the plan. The helper refuses to remove a running VM;
shut it down first, or use --force only when accepting the same filesystem
corruption risk as pulling a physical machine's power cable. Extra storage
attached manually is reported but never deleted automatically.
Use virt-manager to start, stop, pause, clone, snapshot, resize, and view the
guest. Full-screen mode makes it feel like an ordinary second Ubuntu machine.
The VM is not configured to start automatically with the host.
A good working cycle is:
- create or restore a clean snapshot;
- clone the private repository with a project-only deploy key and add only the revocable credentials needed for that project;
- write a bounded GitHub issue, then start the local agent with its issue number;
- let the agent work on an
agent/...branch, run checks, push that branch, and open a pull request; - review CI, the diff, provenance, and discussion on GitHub, then merge
mainyourself; and - discard with
remove-kvm-agent.sh, or roll back the VM, when its state is no longer trusted.
See Daily operation for snapshots, updates, SSH recovery,
data transfer, tmux for long-running agent sessions, and terminal recovery
after an interrupted SSH connection. See
GitHub integration for the complete repository
setup and issue-to-pull-request contract.
The VM has a separate filesystem and no shared host directory. Setup installs a host helper with the recovery key, current-address discovery, host-key pinning, and all forwarding disabled. On the physical Ubuntu host, send a project with:
kvm-agent-host push kvm-agent ./my-project Work/Pull a result into the automatically created quarantine directory:
kvm-agent-host pull kvm-agent Work/agent-result.patchReplace kvm-agent with the real libvirt VM name. The helper initiates both
directions from the trusted host; never copy its private recovery key into the
guest or enable SSH-agent forwarding. Pulls strip executable permission and
refuse device, special, and symbolic-link entries before placing data
in quarantine.
For a separate trusted Mac, including a hardened ~/.ssh/config, dedicated
key, Tailscale roles, and Mac-initiated scp, follow
Secure access from an Ubuntu host or macOS controller.
Treat anything copied out of a potentially compromised guest as untrusted. Review it in a quarantine directory before executing it, building it, opening it as an IDE workspace, or moving it into an important repository. A small reviewable patch is preferable to copying an entire working tree. For stronger assurance, shut the guest down and extract from its virtual disk read-only instead; see Daily operation.
KVM sharply reduces the consequences of an agent mistake, but it is not a proof of safety. A compromised guest can read everything placed in that guest, use its network connection and provider credentials, attack the hypervisor, and present malicious text or clipboard content to the host user.
The default VM has outbound internet access because provisioning and remote
model providers require it. It has no port forwarded from the LAN, and its own
firewall blocks private and link-local destination ranges. This normally covers
the libvirt host, other guests, and a physical LAN; locally routed public
addresses are not covered. The host can still reach the guest on libvirt's
private network. That firewall lives inside the guest, so an agent with sudo
can remove it — see
SECURITY.md for what is enforced outside the guest and what is
only a default. The SPICE console has no TCP listener; virt-manager reaches it
through libvirt. SPICE clipboard integration is enabled for usability, so do not
move secrets through the clipboard.
The host account that runs virt-manager holds libvirt group membership,
which is equivalent to host root. On Ubuntu that authority is ambient and cannot
be downgraded to a per-session prompt without reconfiguring libvirt's socket
authentication, so prefer a dedicated VM host over a machine that is also your
browsing and mail workstation. See SECURITY.md.
The installer URLs intentionally follow the current official release channels. That makes the one-script workflow maintainable but not bit-for-bit reproducible. Those installers still run only inside an empty, credential-free guest. Organizations that require exact artifact review should replace the moving installers with an internally approved golden image or pinned bundle.
Read SECURITY.md before adding confidential source code, long-lived keys, production data, or expensive API credentials.
- Daily operation
- GitHub integration for a local coding-agent VM
- Secure access from an Ubuntu host or macOS controller
- Troubleshooting
- Agent tools and model services
- Reduced formal-methods environment
- Cross-host manager/worker VMs
- Automatic research journals
This is an experimental reference implementation, not an independently audited
security product. The script has static and mocked workflow tests in this
repository; creating a real VM still depends on host firmware, Ubuntu mirrors,
libvirt, and moving third-party installers. Report failures with the host
release, script options, cloud-init status --long, and the relevant tail of
/var/log/kvm-agent-provision.log or /var/log/kvm-agent-swarm.log.