Imported from olafkfreund/nixarchy-microvm (
AGENTS.md). Install upstream withnpx skills add olafkfreund/nixarchy-microvm. Copyright stays with the author.
AGENTS.md
Instructions for any AI agent working in this repository: Claude Code, Codex, Copilot,
Gemini or others. CLAUDE.md and .github/copilot-instructions.md point here. This
file is the single source. When anything disagrees with it, this file wins.
What this repository is
nixarchy.microvm is an Omarchy shell plugin written in
Quickshell QML. It manages the two kinds of NixOS MicroVM that
nixarchy has, in one list, through two
surfaces:
- a bar widget, whose popup sits under the glyph (
Panel.qml); - a full-screen keyboard menu (
Menu.qml).
The two kinds:
- disposable VMs from
nixarchy vm(state under$XDG_STATE_HOME/nixarchy/microvm/<name>/, no root, no rebuild); - permanent machines declared as
programs.nixarchy.services.microvm.machines.<name>, each amicrovm@<name>system unit.
It lists both, and can start, stop, restart, open the console or logs, create, edit
and delete them. A permanent VM is written as one line into
~/.config/nixarchy/apps.nix through nixarchy.pkg's writers; applying is the user's
own nixarchy-apply in a terminal. Optional AI assist asks the default agent for a
form, never for an action.
flake.nix packages the plugin for NixOS and nixarchy. The user guide is
docs/usage.md. The design is in intent/, spec/ and plan/.
Layout
| Path | Owns |
|---|---|
Model.js |
All logic: parsing CLI, systemd and apps.nix output, rows, validation, the snippet grammar, the agent prompt and reply, and every command's argv. Pure .pragma library with no QML, tested under Node. |
schema.json |
The JSON Schema handed to the agent. One copy, read at runtime and by the tests. |
qmldir |
Declares MicrovmState a singleton, so the bar and the menu share one instance. |
MicrovmState.qml |
Data, polling, feature detection, the operation lock, the stream log, the agent call, and every Process. |
MicrovmView.qml |
Interaction: modes (list / form / review / log), cursor, filter, confirmations, keys. Shared by both surfaces. |
VmList.qml, CreateForm.qml, LogView.qml, ShortcutSheet.qml |
Drawing pieces used by the view. |
Panel.qml |
The bar widget host: glyph, KeyboardPanel popup, and IPC target nixarchy.microvm.bar. |
Menu.qml |
The full-screen menu host (manifest kind menu). It scales the view 1.45×. |
manifest.json |
Plugin id nixarchy.microvm, kinds menu + bar-widget, keepLoaded: true, settings schema. |
flake.nix |
The package (an explicit files list, copied as real files) and checks.<system>.default. |
share/omarchy-menu.jsonc |
The Omarchy menu row users paste in. |
tests/ |
Node tests for Model.js (tests/run.js). |
docs/ |
The user guide and the GitHub Pages site. docs/upstream.md tracks the two upstream issues. |
intent/, spec/, plan/ |
Design artifacts for each task. See Workflow. |
Commands
node tests/run.js # Model tests
nix flake check # tests + manifest, entry points, no symlinks, no pacman/yay, no hex colours
nix flake check --all-systems --no-build # aarch64 evaluates
nix build # the plugin folder, exactly as nixarchy links it
omarchy plugin validate "$(readlink -f result)"
To see the repo the way omarchy plugin add would, validate a fresh clone rather than
the working tree. The result link that nix build leaves behind is a symlink, so
validating . fails once you have built:
d=$(mktemp -d) && git clone -q . "$d/p" && rm -rf "$d/p/.git" && omarchy plugin validate "$d/p"
Verifying live (on a nixarchy desktop)
- Install a copy with the shell stopped (a symlinked checkout does not
reload on
rescanPlugins). Changing a plugin folder, or savingshell.json, while the shell runs makes it reload live, and that reload blanks the bar (nixarchy#847). Stop, swap, start:
The first time, runwhile quickshell kill -p "$OMARCHY_PATH/shell" --any-display >/dev/null 2>&1; do :; done rm -rf ~/.config/omarchy/plugins/nixarchy.microvm cp -rL result ~/.config/omarchy/plugins/nixarchy.microvm chmod -R u+w ~/.config/omarchy/plugins/nixarchy.microvm omarchy-restart-shell # nothing left to kill, so it only starts the shellomarchy plugin enable nixarchy.microvmbeforeomarchy-restart-shell: it writesshell.json. Putting the owner's plugin back (the Home Manager link) goes in the same order. - Wait for the shell until
omarchy-shell shell pinganswers. - Check the log and the bar. Get the instance from
qs list --all, then runqs log -i <instance>. The bar is healthy whenqs log -i <instance> | grep -c pluginBarApiForis 0 andqs ipc -p "$OMARCHY_PATH/shell" show | grep -cx 'target omarchy.bar'is 1. A blank bar (only a chevron) means restart the shell before anything else: the ai-mirror control dialog is drawn in the bar, so every control request lapses unseen while it is blank. - Open each surface:
- the menu:
omarchy-shell shell toggle nixarchy.microvm '{}', or'{"create":true}'to open straight into the form; - the popup:
omarchy shell nixarchy.microvm.bar open.
- the menu:
- Confirm what is up with
hyprctl layers -j. The menu's namespace isnixarchy-microvm-menu. - Test VMs: disposable ones named
t1,t2and so on, removed withnixarchy vm rmwhen done. A permanent test machine is onep1line inapps.nix, removed withnixarchy-opt-remove programs.nixarchy.services.microvm.machines.p1. Never runnixarchy-applyas part of a test unless the owner asked for a rebuild.
Retaking the captures
Real captures only, and never of anything but the plugin, demo-* VMs, the
host's own permanent machine and the wallpaper:
- Stage. Run
docs/capture.sh --setup. It refuses if anydemo-*VM already exists, createsdemo-shellanddemo-python, and savesapps.nix,services.nix,shell.jsonandomarchy-menu.jsoncunder$XDG_RUNTIME_DIR. It writes nothing toapps.nix: the permanent row in the captures is whatever machine the host declares. Snapshot the two nixarchy files and note do-not-disturb (omarchy-shell notifications isDnd) and the workspace on each monitor; turn do-not-disturb on (setDnd true). - Clear the screen. Put the monitor the popup lands on (the bar that
holds the widget) and the focused monitor (where the menu opens) on empty
workspaces with
hyprctl dispatch 'hl.dsp.focus({ workspace = "23" })'; a plainhyprctl dispatch workspace Nis ignored by the workspace groups. Park the pointer withhl.dsp.cursor.move, then reopen the surface, or a hover tooltip stays in the shot. - Drive the surfaces.
- Open them with IPC, and send keys with
wtype, only while a plugin layer is up:wtypetypes into whatever has focus. A stray Enter on the list row under the cursor starts that VM in a terminal, which then takes the keys; it happened once here. Right before sending, check that your ai-mirror grant is held andhyprctl layers -jshowsomarchy-keyboard-panelornixarchy-microvm-menu. - Send the whole take as one
wtypeprocess undertimeout, with-sdelays, rather than one call per key. Send-as-k minus: a lone-makeswtyperead standard input and hang. ai-mirror before its #26 fix refuses to type into a layer-shell panel ("typing must name the window"), so it can grant control but not type here. hyprctl layers -jsays which monitor the menu chose.- The owner must not be using the desktop. If a workspace changes under you, stop.
- Take stills with
docs/capture.sh --shot NAME X,Y WxH, cropped to the card; the popup card is 456 px wide, the menu card 1028 px. - Record with
wl-screenrec -g …. It refuses a region that crosses an output edge by even one pixel. - The SSH key list shows the owner's public keys: clear the field before a still, or crop it out.
- Open them with IPC, and send keys with
- Encode. WebM (VP9,
-crf 40) and MP4 (H.264,-crf 28). Look at every still, and at a frame sheet of every video (ffmpeg -vf fps=1,scale=…,tile=…), before committing. - Tear down. Run
docs/capture.sh --teardown(it stops and removes only the recordeddemo-*VMs,demo-newfrom the recording included, and restores any of the four files that changed; an untouchedshell.jsonis left alone, since writing it blanks the bar), restore do-not-disturb and the workspaces, check the bar (Verifying live, step 3), and diffapps.nixandservices.nixagainst the snapshot.docs/img/must stay under 8 MB, and CI enforces it: it ships inside everyomarchy plugin addclone.
Rules
Each rule records a real failure or a hard constraint:
- No symlinks anywhere in the repository.
omarchy plugin addclones this repo as the plugin folder, andomarchy-plugin-validaterefuses any symlink inside it. That is whyCLAUDE.mdimportsAGENTS.mdinstead of linking to it. - No hardcoded colours. Use
Color.*,Style.*andBorder.*tokens, so themes switch cleanly.nix flake checkfails on"#rrggbb". - No
pacmanoryay, not even in comments. nixarchy fails the rebuild on them. - A new runtime file goes in the
fileslist inflake.nix, or it is not in the package. - Run external commands by name from
PATH. The one exception is nixarchy.pkg's adapter, which is not onPATHand is resolved at$XDG_CONFIG_HOME/omarchy/plugins/nixarchy.pkg/bin/nixarchy-pkg, and every feature that needs it is hidden when it is missing. A missing command fails silently inside a QMLProcess, so document it as a requirement. - Argv arrays only, never
sh -c. Every command is built inModel.jsas an array and returnsnullon invalid input. - The agent's reply is data for the form and nothing else. It is parsed with
the schema, converted per field with the field's own type, validated as if typed,
shown to the user, and confirmed by them. Nothing in it is executed, and the call
itself runs
claudewith--restricted --strict-mcp-config --tools "". - Every permanent-VM write goes through nixarchy.pkg's writers.
opt set,opt replace,nixarchy-opt-removeandnixarchy-service-enable, never a file write, never/var/lib/microvms, never a unit file, never the flake. Applying isnixarchy-applyin a terminal the user watches. - The snippet grammar is the only Nix this repository emits, and
Model.parseMachineSnippetmust keep reading everythingModel.machineSnippetwrites (parse(emit(f)) == fis a test). A line a user changed by hand outside the grammar is shown read-only, never rewritten. - Every
nixarchy vmfeature is detected, not assumed.list --jsonby the output's first byte,run --detach,consoleandset-templatebynixarchy-vm help. A key that needs a missing feature is absent, not broken. - Disposable state is never touched except through
nixarchy-vm. No file under the state directory is written by this plugin. - One mutation at a time, via the singleton. Start, stop, restart, create, edit and delete are refused while another mutation runs, from either surface. Listing, console, logs, copy, apply and the agent call never lock.
- Lists read through a QObject
varproperty are Qt sequence wrappers, not JS arrays. Checklength, notArray.isArray(seeModel.settingsFor). - The surfaces are keep-loaded.
open()resets the view and then focuses whatever belongs to the final mode, viaQt.callLater. It never touches the stream or the log. - Nothing polls while every surface is closed. The bar's slow poll for the glyph is the only exception.
- Logic goes in
Model.js, with a Node test. Keep QML to drawing and wiring. - A user-visible change updates
docs/usage.mdand the README in the same PR.
Workflow
Any task that is tracked as an issue, or that touches more than one file, goes
through three artifacts named with the slug YYYY-MM-DD-<issue>-<slug>. Typos,
lock bumps and one-line config changes are exempt.
intent/<slug>.md(why), committed asstatus: draft. Stop for the owner's review.- After approval,
spec/<slug>.md(what). Stop. - After approval,
plan/<slug>.md(how, self-contained). Stop. - Implement only once the plan is
status: approved.
- Never approve an artifact yourself.
- Record each approval as its own commit, for example
docs(plan): approve <slug> (#N). - Make one commit per plan step, and cite the step.
- If the work deviates from the plan, update
plan/in the same commit as the code. - The PR links all three artifacts and closes the issue. Review compares the diff
to
plan/.
Branches are named feat|fix|docs/<issue>-<slug>. Commit subjects use Conventional
Commits (feat:, fix:, docs:, build:, ci:, refactor:), each with the
issue number.
Known follow-ups
- codex as an assist agent, once a no-tools mode is verified by behaviour.
- Disposable rename, per-VM memory and cores, and
rfor disposable VMs. - The upstream issues in
docs/upstream.md: every key they unlock is already wired and hidden until the feature is detected. - Wire p620 to this flake.