a73x

docs/releases.md

Ref:   Size: 20.1 KiB   History

# Releases

Tarballs and checksums for every release live at
[eitri.sh/dl](https://eitri.sh/dl/), with `/dl/latest/` pointing at the
newest. The [quickstart](quickstart.md) takes a release from download to a
running VM.

## v0.0.8

Storage that outlives the VM. A volume claim is a request for durable bytes;
the first VM that names the claim puts those bytes on a host and leaves them
there. Delete that VM, create another naming the same claim, and the data is
waiting — a checked-out repo, a database, a build cache, anything you would
have lost with the root disk. The guest sees a raw `/dev/vdc` and owns
everything above it: partition it, format it, mount it, exactly as you would a
disk you bought.

**A claim is yours; where it lands is the fleet's.** You ask for a name and a
size and get a claim back, pending. The first create that names it decides
which host materializes the bytes, and every later VM naming that claim is
placed on that host — data does not move between hosts, so the guests that use
it do not either. A create that names a different host is refused, and the
refusal says which host holds the claim.

**Nothing deletes a volume by accident.** A claim a VM still holds cannot be
deleted. A volume file the fleet does not recognize is reported and kept, never
reclaimed. A tombstoned claim's bytes go only after the grace period, and a
host that cannot answer whether a file exists is treated as a host that has not
answered — not as one whose disk is empty. Force-removing a host is the single
path that destroys volumes on purpose, and the audit record says how many it
took.

**A host that says nothing cannot vouch for its guests.** A VM whose host has
stopped reporting now reads `unreachable` rather than holding its last known
`running` or `creating` forever. The status you see is what a host actually
told the plane, and a host that went quiet says so instead of leaving stale
optimism on the page.

**Your cloud-init and eitri's both land.** A tenant document that writes files,
runs commands or sets users no longer displaces the CA trust file, the sshd
drop-in, the clock fix or the root grow that eitri needs — both parts merge,
your scalars win, and the boot gate proves it on every run rather than assuming
it.

Under it: the toolchain moved to Go 1.27, mutation testing is one make target
away and runs on every push, and a round of hardening tightened what an
unauthenticated stranger can learn from the plane, what a misconfigured agent
will start with, and which refusals name the fix.

**Known limits.**

- Volumes are attached at create and only at create. There is no detach, no
  reattach to a running VM, and no way to move one between hosts.
- The surface is the HTTP API. Volumes have no console page, no CLI command
  and no MCP tool yet; `POST /api/v1/volume-claims` and the `volume_claims`
  field on create are the whole of it.
- A volume is exactly as durable as one host's disk. There is no replication
  and no snapshot; a host that loses its disk loses every volume on it.
- Force-removing a host destroys its volumes. Their claims return to pending
  rather than staying bound to a host that is gone.
- Rolling a server back below v0.0.8 leaves a host that holds volumes stuck in
  decommissioning, with nothing said about why.
- A volume file that disappears from the host is re-created empty on the next
  attach, and nothing alerts on it.
- A host running an agent older than v0.0.8 refuses volume-bearing creates
  rather than booting the guest without its disk.

## v0.0.7

A VM can be a machine on your network. Declare a bridge on a Linux host, name
it to the agent, pick that name at create — the guest gets a second NIC on your
LAN, addressed by your own DHCP server, while keeping everything it already
had: NAT egress, the gate, published ports, its neighbours. The thing that made
you reach for multipass — a VM your other machines can just talk to, running a
service that advertises its own address — is now one field on create.

**A named network is a second life, not a different one.** The guest's private
NIC stays: eitri still reaches it there, other guests on the host still reach
it there, and outbound still leaves through the host. The second NIC is where
your network meets it. Which address a service advertises is that service's
own business.

**The address is discovered, not invented.** Your DHCP server owns that
network, so eitri watches the guest's own lease exchange and reports what was
granted — the console's **Network** row carries your name for the network and
the address the rest of it knows the guest by. A guest configured static there
is reachable all the same; eitri just has nothing to report.

**Your network is protected from its guests.** A guest cannot forge its own
reported address — eitri believes only frames that arrived from your side of
the wire — and it cannot answer DHCP onto your network at all: lease offers
from a guest are dropped at the tap, before the bridge can carry them to a
sibling, your real machines, or the switch.

**Refusals name the fix, wherever you ask from.** A create naming a network
its host does not advertise says what that host does serve; a Mac is told its
OS cannot serve named networks rather than told to set a flag its agent would
refuse; an agent given `--host-network` for a bridge that is missing refuses
to start, naming it. Over MCP, `vm_create` takes the same `network` field,
reports the discovered address, and default placement picks a host that
actually serves the network you asked for.

**Known limits.**

- Named networks are Linux-host only. A Mac agent given `--host-network`
  refuses to start; place bridged guests on a Linux host.
- A host serving named networks needs the kernel's nftables bridge family; an
  agent that cannot program it refuses to start and names the command. Hosts
  with no named networks are untouched.
- A bridged guest cannot serve DHCPv4 onto that network — deliberate, so a
  guest can never hand out leases to your real machines. DHCPv6 and router
  advertisements are not filtered: a bridged guest is a full peer, and some of
  yours may be routers on purpose.
- The private NIC keeps the default route via route metrics the default
  Ubuntu image honours; an image whose distro renders cloud-init's network
  config without netplan may order its own routes.

## v0.0.6

The distance from a token to a running VM. `eitri init` is one guided command
that takes you from a fresh access token to a first shell — it proves your
token, settles your SSH CA (uses the one you have, never blindly makes a
second), and writes the config every later command reads. After it, `eitri ssh
<vm>` needs no environment at all.

**One guided setup.** `eitri init` walks the steps and confirms each before it
acts: which plane, which tenant, which signing key. It never generates a CA when
you already have one, and it tells you plainly when a key it finds is one no
guest will trust — the failure that used to read as a bare "permission denied."

**One MCP surface.** eitri speaks MCP in exactly one place: `/mcp` on the
control plane, with a bearer token. The local stdio binary is gone — a
self-hoster gets an MCP server by hosting the plane, and there is one credential
story to keep honest instead of three.

**The console uses the width it is given.** Wide viewports get two real
columns — the facts a page reports on the left, the actions you can take on the
right — instead of an 80-column measure with a field of empty space beside it. A
guest names the CAs it trusts, baked in when it was created. An offered agent
upgrade shows itself as pending rather than a button that ignores you. A VM being
created says what it is doing — "downloading image 1.2/3.7 GiB" — instead of a
silent "creating," and a published port counts the sessions it carries.

**Refusals that tell you the fix.** A tenant with no registered CA is told the
three ways to add one, not just told no. A full host names what is holding its
capacity — and if it's a VM you just deleted, it says to wait for the teardown
rather than to delete another. A gate bound to every interface with no name to
advertise refuses to boot and says so, instead of booting into certificates no
client can verify. A sign-in that fails names the origin that broke it — usually
a browser at the wrong address for the console.

**Known limits.**

- The local stdio MCP binary is retired. If you registered it with `claude mcp
  add eitri`, point your client at `/mcp` on your plane with a personal access
  token instead.
- Self-hosters: a gate bound to a wildcard (`:2222`) now requires
  `ssh_gate_domain` and refuses to start without it — a gate has to know the name
  clients dial. Set it, or bind one concrete address.
- The first v0.0.6 plane freezes each existing guest's trusted-CA set at
  upgrade, so guests that never had a record now do — and can, for the first
  time, be flagged when a tenant's CA set moves on. A VM created before this
  shows its frozen set rather than "unrecorded."
- A mixed fleet: publishing a UDP port on a host still running a pre-v0.0.5
  agent is refused, because that agent would bind it as TCP and report it healthy
  while it carried nothing. Upgrade the host's agent, then publish the port.

## v0.0.5

A published port can carry datagrams. Expose a VM's UDP port the same way you
expose a TCP one — DNS, game servers, anything that doesn't wait for a
handshake — and eitri carries the datagrams to the guest and the replies back.
Around it, VMs got sturdier: they persist by default, they recover their clock
after a host sleeps, and a host that can't certify a guest stops being somewhere
to put one.

**UDP exposures.** A published port is TCP or UDP now, chosen at create. Traffic
on a published port rides its own path so a busy port can't starve the fleet
management the same tunnel carries.

**Every VM is persistent.** A guest that outlives a reboot is the default and
the only mode — there is no per-VM knob to lose track of. A powered-off guest is
a stopped machine you can start again, not a thing that quietly vanished.

**A guest keeps its clock through a host's sleep.** Close a laptop that's
running guests, open it hours later, and the guests step their clocks back into
line instead of sitting in the past — the state that used to make a gate admit
you and the guest deny you.

**A host that cannot certify a guest is not a place to put one.** The plane
won't schedule a VM onto a host whose agent can't sign the guest's host key — the
VM would boot unreachable. It refuses at create, naming the fix.

**Known limits.**

- Fielded hosts: the exposure session caps rest on the agent's file-descriptor
  limit, which the shipped unit raises. A host whose unit predates that line
  needs the unit reinstalled to get the higher ceiling.
- macOS hosts: every agent upgrade is a new binary, so macOS re-asks its Local
  Network / privacy permission each time. Approve it in System Settings after an
  upgrade, or the agent looks enrolled but silently can't reach its guests.

## v0.0.4

A PAT is enough. Point an LLM client anywhere on the internet at
`https://api.eitri.sh/mcp` with a personal access token and it gets the whole
eitri toolset: create a VM, run commands in it, publish a port, hand back the
address. No local install, no config file, no CA of your own. Around it, the
console is drawn in the site's own language.

**MCP.** eitri-server serves MCP at `/mcp` over streamable HTTP, authenticated
with the same bearer token the API takes; a caller sees exactly the fleet its
token can see. `api.eitri.sh` carries that one route and nothing else.

**Delegated access, not a held key.** eitri never holds a signing key for your
guests. When a token-only caller needs SSH access, you delegate it: sign the
public key eitri shows you with your own CA, on your own TTL (`ssh-keygen -s`).
Revoke it by letting it expire or by deleting it — eitri's access ends when you
say so. Because the certificate chains to a CA you already registered, VMs
created before you delegated accept it too.

**Your guests' host keys stay on your hosts.** A guest's SSH host key is born on
the machine that runs it and never leaves it; the control plane signs only
public keys. The database now holds no private keys at all — keys stored by an
earlier release are dropped the first time the new server opens it.

`vm_create` reports what it is waiting for as it waits — usually seconds,
minutes when a host downloads an image for the first time — so a long call reads
as a VM coming up rather than silence.

**Upgrades read a host bundle.** A self-upgrading agent can now be handed the
same tarball you would install by hand, verified against the release manifest
as ever. `/dl` still carries bare binaries beside the bundles for agents from
older releases.

**The console speaks the site's language.** We revamped the console:
monospace, hairline rules, light and dark mode—because we care.

**Known limits.**

- `/mcp` authenticates with a bearer token and nothing else. Browser
  connectors that sign in with OAuth—claude.ai among them—cannot reach it.
- A delegation lives in memory. A control-plane restart or redeploy drops it
  and you delegate again — one `ssh-keygen` against a public key that has not
  changed. This is a property, not a bug: eitri's access to your guests ends
  when its process does.
- An agent from an earlier release does not know how to generate a guest host
  key, so a VM it creates against a v0.0.4 control plane boots with an
  uncertified one and `eitri ssh` fails host verification with a clear error.
  Upgrade the host's agent — from the console, as usual — and re-create the VM.
- Self-hosters: v0.0.4 adds a required `key_encryption_key` to `server.json`
  (`openssl rand -hex 32`), and the server refuses to start without it. It
  encrypts the fleet's own SSH CA and gate host key, so keep it outside your
  database backups.

## v0.0.3

macOS hosts. An Apple-silicon Mac joins the fleet as a host and runs Linux
guests beside your Linux boxes (so the Mac Mini can earn its keep after all).
Around it: hosts own their guest networks, you can publish a VM's ports, and
the agent reads guest images without a conversion tool.

**Mac hosts.** An Apple-silicon Mac enrolls like any other host and runs
guests through vfkit, the signed helper that drives Apple's
Virtualization.framework. `brew install vfkit` is the one thing the agent
cannot do for you—vfkit only works carrying Apple's virtualization
entitlement, and an entitlement lives in a signature, so it comes from a
signed distribution or not at all. Everything else the agent does itself: it
installs as a LaunchAgent in the session you're already in, keeps its state in
`~/.eitri/agent`, and upgrades from the console like every other host.
Releases ship a `darwin_arm64` host bundle beside the Linux ones.

**Hosts own their guest network.** The subnet a host's guests live on is a
fact about that host, and the host is the only thing that can observe it. So
the direction reverses: a host tells the fleet which subnet its guests are on,
a joining host proposes its own—the fleet's allocation is only a suggestion—
and a Mac reports whatever subnet vmnet actually gave it. A host also refuses
a guest subnet that contains its own uplink, naming both sides and the flag
that fixes it, because that overlap takes out the very connection you'd use to
fix it.

**Published ports.** SSH goes through the gate; anything else a guest serves
needs a published port. You publish a VM's port on its host: the fleet
binds a port on the host (30000–32767 when you leave the choice to it) and
pipes it to the guest. The MCP tools speak it too, so an LLM that just
built something in a VM can publish the port and hand back the address to
dial. There is no authentication in front of a published
port—whoever can reach the host on that port reaches the service—so publish
what you're content to serve to everything on that network, and leave the rest
to the gate.

**Images, decoded in-process.** The agent reads guest images itself: qcow2,
vmdk, vhdx, vdi, parallels, vpc and asif are parsed in-process, gzip is
decompressed sparsely as it lands, and an image that is already raw is renamed
into place—there is no format to change. `qemu-img` goes with it. A Linux
host needs KVM and nothing else.

**Fixes.** A VM says which SSH key eitri put in it—the key type, its
fingerprint, its comment. A one-click create picks a default image its host's
architecture can run, instead of one image for the whole fleet. A guest's
address is judged on its own terms—does it name something reachable—rather
than against an allocation the fleet made. A lost guest's report quotes the
last thing its hypervisor said, instead of "lost" and nothing else. A backend
signals only the processes it started, so a pid recycled across a host reboot
is never someone else's process getting SIGKILL. And a host is asked whether
it can run a guest at all before a create spends anything on a download.

**Known limits.**

- Apple silicon only: there is no darwin/amd64 bundle. An Intel Mac is told
  there is no artifact for it—true, and checkable—rather than offered a
  binary nobody has booted a guest on.
- A Mac joins as a host. The control plane stays Linux; the Mac bundle
  carries no server.
- vfkit is yours to install (`brew install vfkit`); eitri cannot mirror a
  binary whose entitlement lives in Apple's signature.

## v0.0.2

One fix, worth its own release. A guest's disk was handed to cloud-hypervisor
without a declared format, and an autodetected raw image gets its sector 0
write-protected—so the first boot's partition grow only ever happened in the
kernel's memory, and the guest died in initramfs the first time it was
power-cycled. Disks are now declared raw and the partition table stays
writable.

## v0.0.1

The first release. eitri is a small cloud backed by your own hardware: one
control-plane server—hosted at eitri.sh or run yourself—an agent on every
Linux/KVM box you enroll, cloud-hypervisor microVMs, a web console, and SSH
into guests through an identity-checking jump gate.

**Fleet.** Enroll any Linux/KVM host with a one-shot join command from the
console. The agent reconciles each VM in its own worker, admits VMs against
the host's real resources, fetches cloud-hypervisor and the guest firmware
itself (sha-verified against the release), and upgrades itself from the
console—running VMs survive agent restarts and upgrades.

**VMs.** UEFI guests boot any cloud-init disk image—the guest owns its
kernel. Addressing is an embedded DHCP server with per-VM reservations. The
console shows live host metrics, per-VM status and a browser serial console;
tenant quotas bound what a tenant can create.

**Sign-in and tenancy.** The console signs in through OIDC—the bundled
`eitri-oidc` issuer for a single box, or [bring your own IdP](byo-idp.md)
(Google, Okta, Keycloak…). Every identity's first sign-in creates its own
tenant; every API request, event stream, console session and audit row is
tenant-scoped. CLI and automation authenticate with personal access tokens
minted in the console.

**SSH.** Tenants register their own user CA—eitri never holds a user
private key. `eitri ssh` self-signs a short-lived certificate, pins eitri's
host CA, and jumps the gate to `<tenant>.<vm>`; certificates are revocable
per serial. See [connecting.md](connecting.md).

**Hosted.** Don't want to run the control plane? Sign in at
[console.eitri.sh](https://console.eitri.sh) with Google and join your own
machines—the same server the tarball ships, run for you. Your hardware
still serves every VM; the hosted plane only coordinates it. Self-hosting
stays a first-class path.

**Running it.** `eitri-server` and `eitri-oidc` ship with hardened systemd
units and run as dedicated non-root users; the agent's unit documents exactly
why it runs as root. Server and issuer state live under `/var/lib`, config
under `/etc/eitri`. The server speaks plain HTTP—front it with TLS if it
leaves your LAN.

**Platforms.** Hosts: Linux amd64 and arm64 with KVM. Client CLI: Linux and
macOS, amd64 and arm64.

**Known limits.**

- One user per tenant.
- No TLS termination in the server—put a proxy in front for anything
  routable.
- Switching `oidc.issuer` after tenants exist strands their identity
  bindings; pick your issuer before inviting sign-ins.
- macOS hosts are not supported.