Skip to content

Repository files navigation

ash

ash is a CLI for spawning, attaching to, suspending, resuming, mounting into, and deleting NixOS agent VMs through virtle.

Most documentation lives in the command help and generated command pages:

Install / run

Build with flakes:

nix build github:0xferrous/ash
./result/bin/ash --help

Run directly:

nix run github:0xferrous/ash -- --help

Quickstart

Start a reusable background VM:

ash spawn --name work -f ../my-nix#agent

Attach to it:

ash attach work

Follow its logs:

ash logs -f work

Use Kitty's SSH kitten for an attached session:

ash attach --kitty work

Forward Wayland and X11 applications from the guest to the host with Waypipe:

ash attach --waypipe work

Waypipe and Kitty can be combined:

ash attach --waypipe --kitty work

Start and attach immediately, keeping the VM after SSH exits:

ash spawn --name work -f ../my-nix#agent --attach --keep

Copy files between the host and a running VM:

ash cp work ./input.txt ~/workspace/input.txt
ash cp --from guest work ~/workspace/output.txt ./output.txt

Use -r to copy directories and -v to print a successful transfer.

Mount a host directory into a running VM:

ash mount work ~/dev/project

Unmount it:

ash umount work ~/dev/project

Suspend and resume a background VM:

ash stop --suspend work
ash resume work

Stop a background VM:

ash stop work

Inspect and list VM state, or interactively delete stopped VM state and cached store images:

ash inspect work
ash ls
ash ls --cache
ash rm

Reset a stopped VM's strategy-specific Nix store state:

ash stop work
ash rebuild-db work
ash attach --spawn work

Configuration

Ash uses ASH_NAME as its XDG application namespace, defaulting to ash. It reads $XDG_CONFIG_HOME/$ASH_NAME/config.toml, falling back to ~/.config/$ASH_NAME/config.toml, and stores VM state and caches below the matching XDG_STATE_HOME and XDG_CACHE_HOME namespaces. For example, ASH_NAME=nash selects ~/.config/nash, ~/.local/state/nash, and ~/.cache/nash. --config overrides the configuration file directly.

See example_config.toml for the global and space mount formats. Set global.memory to configure VM memory in MiB; it defaults to 4096. Set global.kitty = true to use kitten ssh by default for spawned and attached sessions; an explicit --kitty also enables it.

VMs attach to the private host bridge ash0 through /run/wrappers/bin/qemu-bridge-helper; override these with global.network_bridge and global.qemu_bridge_helper. The host must create the bridge and authorize it in /etc/qemu/bridge.conf.

Ash passes ash.mdns-host=<dns-label> and ash.mdns-mac=<stable-mac> on the guest kernel command line. A compatible guest can use these values to publish <dns-label>.ash.local; the agent NixOS configuration uses Avahi for this. Names that are not already lowercase DNS labels receive an eight-hex digest suffix after normalization. The host must have .local mDNS resolution enabled, and multicast UDP 5353 must pass over the VM bridge.

Every VM exposes exactly two directory shares: shares-ro and shares-rw. Workspace, cwd, configured spaces, and runtime mounts are staged beneath these roots and bind-mounted at their final guest paths. A VM state directory has the following relevant layout; entries marked as conditional are created only when that feature is used:

<state-dir>/
|-- ash-state.toml
|-- virtle.toml
|-- shares/
|   |-- ro/                              -> shares-ro
|   |   |-- system/
|   |   |   |-- nix-store/              # shared strategy
|   |   |   `-- guest-store-state/      # shared strategy
|   |   `-- mounts/
|   |       |-- spaces/<tag>/            # configured read-only mounts
|   |       `-- hotmounts/<id>/          # runtime read-only mounts
|   `-- rw/                              -> shares-rw
|       |-- system/
|       |   |-- guest-store-state/       # shared strategy
|       |   |-- guest-store-upper/       # shared strategy
|       |   `-- guest-store-work/        # shared strategy
|       `-- mounts/
|           |-- workspace/
|           |-- cwd/                     # with --mount-cwd
|           |-- spaces/<tag>/            # configured writable mounts
|           `-- hotmounts/<id>/          # runtime writable mounts
|-- persist.img
|-- nix-store.img                        # image strategy
`-- virtle_state/                        # sockets, locks, runtime files

The guest mounts the two roots at /run/ash/shares/ro and /run/ash/shares/rw; Ash then bind-mounts individual staged children at their requested guest destinations. Ash leaves the consolidated shares' daemon argument arrays unset so Virtle supplies its standard virtiofsd arguments. The default Nix store strategy is shared; it stages the host /nix/store at shares/ro/system/nix-store, with lower-store metadata and writable overlay state under the corresponding system directories. Guests using this strategy must mount shares-ro during stage 1 and bind the staged store at /nix/store. Pass --shares-ro-socket PATH to reuse an existing daemon socket; --ro-store-socket remains a compatibility alias.

To use a private image-backed store instead:

[global.nix_store]
strategy = "image"
image_size_mib = 32768

Override these defaults for an individual VM when spawning it:

ash spawn --name private --nix-store-strategy image \
  --nix-store-image-size-mib 65536 -f ../my-nix#agent

The overrides are saved as nix_store_strategy and nix_store_image_size_mib in that VM's ash-state.toml and reused by later spawns and regeneration.

Ash caches one closure-sized ext4 base image for each selected toplevel under $XDG_CACHE_HOME/$ASH_NAME/nix-store-images (or ~/.cache/$ASH_NAME/nix-store-images, with ASH_NAME defaulting to ash). New VM state uses a sparse reflink/CoW clone of that base image when the host filesystem supports it, then grows the clone to the VM's configured capacity. Different image-size settings therefore reuse the same cached closure without rebuilding or fully copying the store image. Each cached base has a neighboring <cache-key>.toml sidecar, and each writable VM image has nix-store.toml. These record the exact closure and registration, closure sizes, creation and last-use timestamps, cache lineage, and deduplicated flake URL, lock hash, and effective input-override provenance. Provenance is informational and does not affect cache identity. The writable clone is labeled nix-store and does not expose the host store to the guest. Ash appends ash.nix-store=image or ash.nix-store=shared to the kernel command line so guests can select the matching stage-1 mount layout. Image-capable guests must mount the nix-store label at /nix; Ash loads the Nix database through QGA after boot. Increasing image_size_mib grows an existing stopped VM's filesystem automatically. When the selected closure changes, Ash imports only missing immutable store paths into the stopped VM's existing image and retains guest-added and older paths. Shrinking the image or migrating a legacy image created with mke2fs -d requires ash rebuild-db NAME, which discards guest-added store paths.

Select a space with a repeatable --space/-s option:

ash spawn --name work -s ash -f ../my-nix#agent

Use repeatable --override-input NAME=FLAKE options to override inputs while Ash evaluates and builds the selected flake:

ash spawn --name work -f ../my-nix#agent \
  --override-input ash=path:../ash

Overrides are saved with named VM state and reused by later regeneration. Relative path references are saved as absolute paths.

For a new VM, omitting --space applies no configured spaces. For an existing named VM, it reuses the saved space list. Spaces can compose other spaces with extends = ["base", ...]; extended spaces are evaluated recursively before the extending space.

A space may also define files = ["HOST_PATH", "HOST_PATH:GUEST_PATH", ...]. Ash reads each regular host file when generating virtle.toml and emits it as a Virtle write_files entry, preserving its permission mode. Host and guest ~ expansion follows the mount rules. Files written below the guest user's home are owned by that user; other destinations retain Virtle's default ownership.

Agent Portal

The repository also builds a standalone OCaml implementation of the Agent-box Portal protocol:

  • agent-portal-host — host-side capability broker
  • agent-portal-cli — diagnostic and direct API client
  • ash-dbus-proxy — experimental filtered host D-Bus/vsock bridge
  • gh — transparent GitHub CLI wrapper
  • wl-paste — transparent image-clipboard wrapper

Portal uses MessagePack over a permission-restricted Unix socket or Linux AF_VSOCK stream and remains wire-compatible with the Rust Agent-box implementation. Its code is isolated under lib/portal/, bin/agent-portal-*, and wrappers/; it does not depend on the Ash VM implementation.

See PORTAL.md for configuration, security behavior, and wrapper compatibility. With an enabled [portal] section, Ash either manages a per-VM vsock Portal process or injects the endpoint of a user-managed global Portal into the guest.

More detail

Use command help:

ash spawn --help
ash resume --help
ash mount --help

About

ash is a CLI for spawning, attaching to, suspending, resuming, mounting into, and deleting NixOS agent VMs through virtle.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages