Skip to main content

Local libvirt/KVM sandbox VM manager for coding agents (Ubuntu 24.04 cloud-image, SSH, optional virtiofs share, optional nftables isolation).

Project description

Pypi PypiDownloads ReadTheDocs GithubActions Codecov

Read the Docs

https://aivm.readthedocs.io/en/latest/

Pypi

https://pypi.org/project/aivm

A small Python CLI to create and manage a local libvirt/KVM Ubuntu 24.04 VM designed for running coding agents with a stronger boundary than containers.

Current state

aivm is experimental and best understood as a local, long-lived libvirt/KVM development VM manager for agent workflows. The actively maintained daily path is:

aivm code .
aivm ssh .
aivm attach .
aivm status

The current attachment model is centered on explicit host-folder registration:

  • persistent is the default for new attachments. It uses a dedicated persistent-root virtiofs export, persisted attachment declarations, and replay helpers so attachment intent survives VM reboot/reconcile cycles.

  • shared-root is the legacy single-export path. It still uses one VM-level virtiofs export plus host/guest bind mounts, but new attachments no longer choose it unless --mode shared-root is explicit or a saved attachment already uses that mode.

  • shared is the older direct per-folder virtiofs mode and is mostly useful for simple/small attachment sets.

  • git bootstraps a guest-local Git repo and host remote plumbing. It is not a live filesystem sync engine.

The old settings-sync story has been removed for now. It was too flaky to keep as a supported workflow. Project handoff should use explicit attachments, manual Git operations, or a future redesigned synchronization feature.

What it provides

  • Dedicated libvirt NAT network per aivm configuration

  • Optional host firewall isolation via nftables

  • Ubuntu cloud-image VM provisioning via cloud-init

  • SSH + VS Code Remote-SSH workflows

  • Optional virtiofs folder sharing (explicit trust extension)

  • A single config store for defaults, VMs, networks, and attachments

Install

uv pip install .

Fast Start

Recommended for new repos:

No explicit setup is required first: if VM context is missing, aivm code . offers to run the aivm config init / aivm vm create bootstrap for you (run them yourself for the explicit, reproducible path).

aivm code .
aivm status
aivm status --sudo   # optional deeper privileged checks

aivm code . auto-selects/bootstraps VM context from the global config store (~/.config/aivm/config.toml), attaches the current folder if needed, and opens VS Code.

During setup and reconcile flows, subprocess logging is now organized around user-meaningful steps instead of isolated commands. aivm shows the current step, why it exists, a semantic summary for each planned command, and the exact command line that will run before it executes the step. Full raw commands still appear at higher verbosity.

If you prefer an explicit flow, aivm config init is required before aivm vm create.

Interactive aivm config init shows the detected defaults once, then lets you accept them, edit the generated TOML in $EDITOR/$VISUAL (falling back to nano or micro), or use a prompt-by-prompt editor. Subsequent confirmation steps show only changed values instead of repeating the full defaults table.

See also:

Status and sudo behavior

By default, aivm status avoids privileged probes. Use --sudo for network/firewall/libvirt/image checks.

Privilege modes (behavior.privilege_mode):

Both answer one question – when does aivm invoke sudo?

  • as-needed (default) probes what already works without sudo – unprivileged qemu:///system access via the libvirt group, user-writable VM storage – and uses sudo only where required.

  • always escalates every privileged-capable host operation through sudo (the classic behavior).

An unrecognized value is an error, not a silent fallback. A global no-sudo mode is not exposed because managed nftables and new host bind mounts still require root on the supported runtime.

Run aivm host permissions check to inspect the permissions used by routine VM operations, and aivm host permissions setup to establish the host-side prerequisites. Normal setup may use sudo to add you to the libvirt group; --adopt additionally runs one privileged metadata pass for each existing storage tree. Setup never changes your config: establishing a capability and choosing a policy are different acts, so privilege_mode and firewall.enabled stay yours to set. State-changing hypervisor commands keep their approval prompt even when they no longer need sudo, so destructive operations never become promptless just because escalation stopped being necessary.

Command manager defaults:

  • subprocess execution is centralized through a command manager

  • logs are grouped into step/plan previews with nested context

  • read-only sudo probes (inspect/query/status) are auto-approved by default

  • state-changing sudo steps still prompt unless --yes/--yes-sudo is set

  • approval usually happens once per grouped step, not once per command

Grouped approval does not widen privilege beyond the commands shown in the step preview. The preview is the approval boundary.

Use:

  • --yes to auto-approve all prompts

  • --yes-sudo to auto-approve only sudo prompts

When running interactively, expect step previews such as:

  • current context / breadcrumb

  • current step title

  • why the step exists

  • semantic summaries plus exact commands for the current step

  • a single approval prompt for the whole step when required

Interactive approval semantics:

  • y approves the current step only

  • a approves the current step and all later steps

  • s shows the full exact commands for the current step, then reprompts

For example, the default persistent path used by aivm ssh . / aivm code . groups attachment reconciliation into named steps such as inspecting host bind state, preparing host bind targets, ensuring the VM virtiofs mapping, syncing the persisted manifest, and mounting/verifying the bind inside the guest.

Readable previews may abbreviate long shell payloads, but the full exact commands are still available on demand in the approval prompt and are always logged when they actually run.

Config defaults:

New configs use a host-qualified default VM name derived from $HOSTNAME. For example, on a host named workstation, the generated VM name, guest hostname, and primary SSH alias are all aivm-2404-workstation. Existing explicit config values are not migrated; configs that relied on an omitted implicit name now receive the new host-qualified default.

[behavior]
yes_sudo = false
auto_approve_readonly_sudo = true  # set false for strict "prompt every sudo" mode
privilege_mode = "as-needed"       # "never" | "as-needed" | "always"

Common Workflows

VS Code and SSH

aivm vm ssh_config
aivm code .
aivm vm code --host_src .
aivm vm code .
aivm vm ssh .

Folder attachment

aivm attach .
aivm detach .
aivm vm attach --vm aivm-2404-$HOSTNAME --host_src .
aivm attach . --mode git

Attachment modes:

  • persistent (default for new attachments): the preferred persistent- attachment path. It uses a dedicated VM-level virtiofs export at /var/lib/libvirt/aivm/<vm>/persistent-root plus stable staged host binds, writes a persisted attachment manifest, installs a guest systemd replay helper at VM bootstrap, and lets boot / aivm code . / aivm ssh . repair guest-visible bind mounts from that manifest instead of rebuilding every attachment from scratch.

  • shared-root: legacy single-export behavior. One VM-level virtiofs mapping exports /var/lib/libvirt/aivm/<vm>/shared-root; each attached folder is bind-mounted under that root on host and then bind-mounted to guest_dst in guest. Existing saved shared-root attachments continue to use this mode, and new attachments can still request it with --mode shared-root.

  • shared: direct per-folder virtiofs mapping from host source to guest. This is simpler but consumes one VM virtiofs device slot per folder.

  • git: guest-local Git repo bootstrap plus host/guest remote plumbing. It does not automatically synchronize worktree contents.

In shared, shared-root, persistent, and git modes, attached folders mount to the same absolute path inside the guest by default unless --guest_dst overrides it. Running VMs are live-attached when possible. aivm code and aivm ssh remount the selected folder and best-effort restore other folders already saved for that VM after guest startup.

For persistent attachments, explicit detach updates the stored declaration and refreshes the replay manifest instead of depending on interactive teardown of the stable host-side staged bind mount. If the guest can mount the persistent-root export but the host manifest is missing, replay now fails closed instead of silently reusing stale cached guest state.

Major limitation: shared-mode folder count

Each shared folder uses a dedicated virtiofs device mapping in the VM definition. Attaching many folders can hit VM device-slot limits (for example PCI/PCIe capacity), which surfaces from libvirt as errors like No more available PCI slots during attach/restore.

shared-root and persistent reduce this pressure by using one persistent virtiofs mapping per VM and per-attachment host/guest bind mounts. Their host-side preparation is also designed to avoid mutating the ownership or permissions of the user’s source tree; aivm prepares only its own internal directories and does not recursively rewrite a bind-mounted project path.

Workarounds today:

  • detach unused shared folders

  • prefer --mode git for folders that do not need live writable host sharing

  • split large folder sets across multiple VMs

Use --mode git to keep a normal Git repo on guest disk instead of exposing a writable virtiofs share. In that mode, aivm configures the guest repo to accept host pushes via receive.denyCurrentBranch=updateInstead and registers a host-side remote pointing at the guest repo over the VM SSH alias. That remote is plumbing for explicit Git handoff; aivm no longer tries to push or pull project contents automatically for git-mode attachments.

aivm code --mode git . behavior:

  • New folder (no saved attachment): creates/uses a git-mode attachment and defaults the guest destination to the exact host path.

  • Folder previously attached in any non-git mode, including shared, shared-root, or persistent: returns an error (mode mismatch). Detach + reattach is required to switch modes.

  • aivm code . without --mode: reuses saved mode if present; otherwise creates a new persistent attachment.

Migration note:

  • persistent has become the default path for new attachments. Existing shared-root attachments keep working unchanged. Reattach a folder with aivm detach . then aivm attach . --mode persistent when you want an older saved attachment to move to the persisted replay behavior.

Mode selection behavior:

  • New folder (no saved attachment record): defaults to persistent unless --mode is explicitly set.

  • Existing folder attachment: omitting --mode reuses the saved mode for that (host folder, VM) pair.

  • Existing folder attachment + explicit different --mode: this now errors. You must explicitly detach then reattach to change mode:

aivm detach .
aivm attach . --mode git

Known issue: long-lived virtiofs FD growth (now auto-mitigated)

Host-side virtiofsd keeps one open descriptor per inode the guest caches, and guests never evict those caches on their own, so long-lived virtiofs attachments historically saturated the daemon’s fd ceiling (~1M) and ordinary traversal failed with OSError: [Errno 24] Too many open files even though ulimit -n looked fine. The dominant trigger turned out to be the guest OS itself: Ubuntu’s stock nightly updatedb sweep walks virtiofs mounts (virtiofs is missing from the default PRUNEFS), touching every shared inode every day.

aivm now installs a guest-side virtiofs guard (systemd timer) that prunes updatedb and flushes guest dentry/inode caches when the cached-inode count crosses a watermark, releasing the host descriptors before the ceiling is reached. The guard is config-driven ([virtiofs] fd_guard = true, the default): new VMs get it via cloud-init, and aivm vm update reconciles existing running VMs — installing, refreshing after config/version changes, or uninstalling when disabled — so no manual setup or host-side aivm vm flush_caches cron jobs are needed. aivm vm fdguard (default action status) shows the live state and offers direct install/uninstall.

Remaining guidance:

  • prefer fewer, narrower shared folders; detach stale attachments

  • use --mode git for repos that do not need live writable host sharing

  • aivm vm flush_caches remains as a manual recovery command

  • see docs/source/virtiofs.rst for the full mechanism, tuning knobs ([virtiofs] fd_guard*), and the incident runbook

Inventory and visibility

aivm list
aivm vm list
aivm list --section vms
aivm list --section networks
aivm list --section folders
aivm status --detail

Config-store lifecycle (explicit flow)

aivm config init
aivm vm create
aivm vm update
aivm vm edit
aivm config discover
aivm config show
aivm config edit
aivm config lint
aivm config format
aivm config paths
aivm help plan
aivm help tree
aivm help completion
aivm host doctor

Command Groups

aivm config --help
aivm host --help
aivm host image_fetch --help
aivm help --help
aivm host net --help
aivm host fw --help
aivm vm --help

Safety Notes

  • This tool assumes Linux + libvirt. It focuses on Debian/Ubuntu hosts for dependency installation.

  • Security model and threat model details: the Security Model.

  • NAT alone does not prevent VM -> LAN. Enable firewall isolation if you want “internet-only” access.

  • To allow specific VM->host or VM->blocked-LAN service ports while firewall isolation is enabled, set [firewall].allow_tcp_ports / allow_udp_ports in config (for example allow_tcp_ports = [22, 5432]).

  • virtiofs sharing is optional; it’s powerful, but it intentionally exposes that host directory to the VM.

  • aivm vm code requires VS Code’s code CLI and the Remote - SSH extension.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aivm-0.5.0.tar.gz (347.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aivm-0.5.0-py3-none-any.whl (267.0 kB view details)

Uploaded Python 3

File details

Details for the file aivm-0.5.0.tar.gz.

File metadata

  • Download URL: aivm-0.5.0.tar.gz
  • Upload date:
  • Size: 347.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for aivm-0.5.0.tar.gz
Algorithm Hash digest
SHA256 5de2bd2ce0f4c1f3f28535813ec2991b531ee312d9b25a30f84df6c741bda5db
MD5 45cf7f0093a97197332f62d4a35a8ccd
BLAKE2b-256 585a0c5c9b5ac731182f724416b87b6b401b0c286e4916f9cc987e2ac593a912

See more details on using hashes here.

Provenance

The following attestation bundles were made for aivm-0.5.0.tar.gz:

Publisher: release.yml on Erotemic/aivm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aivm-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: aivm-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 267.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for aivm-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f7d2f7fb2f61c8d778be7c538d4b1cd742fc6a626258a4919edfb32698a7e0e0
MD5 90d6888ffa62e5811d3ae390c8394b17
BLAKE2b-256 d721af055bf51f0efa4889e91b151d4d4dd5249cc2159501d9d9c01619383cd9

See more details on using hashes here.

Provenance

The following attestation bundles were made for aivm-0.5.0-py3-none-any.whl:

Publisher: release.yml on Erotemic/aivm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page