validate-on-real-incus
Skill47 starsChanged yesterdayApache-2.0View on GitHub
Validate an isx change end-to-end against a real Incus daemon on this Linux host -- set one up, build, branch, inspect, compare against the base commit, and time it. Use when a change touches how isx talks to Incus (instance writes, devices, network, storage, lifecycle, the proxy's view of instances), when a PR says it was only tested against FakeIncusDaemon, or when asked to validate a PR "for real".
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 24
If that job failed, find out whether the failure is in the area the change touched, and compare it with recent runs on main (gh run list …
- Very specific: line 45
Never chain anything after isx proxy start: a command queued behind it runs later, whenever the proxy stops, e.g.
- Very specific: line 68
When the change is in the build itself, a measurement-only patch that drops both incus.deviceAdd(buildName, "tun", ...) calls in …
- Very specific: line 71
Branches do not get a disk size: Every branch sets its root disk's size, which Incus turns into a btrfs qgroup limit, and the nested kernel …
- Very specific: line 141
Don't build the other side by git stash-ing the main tree: a git stash.
- Very specific: line 146
Don't ./install.sh (JVM) from a worktree: its launchers run the jar where it was built, so removing the worktree breaks the installed isx …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Validate on a real Incus
FakeIncusDaemon proves what isx asks for, not what Incus does with it. Incus has semantics a
fake easily gets wrong: a profile device cannot be removed from one instance, only masked;
PATCH merges while PUT replaces; expanded_devices differs from devices; options vary between
Incus versions. Anything that depends on those needs a real daemon.
1. Check what CI already proved
CI runs isx against a real Incus on every PR: the isx-integration-tests-native job in
.github/workflows/test-integration.yml, described in .claude/rules/ci.md.
gh pr checks <n> # did the real-Incus job pass?
gh api repos/Sanne/incus-spawn/actions/jobs/<job-id> \
--jq '.steps[] | select(.conclusion=="failure") | .name'If that job failed, find out whether the failure is in the area the change touched, and compare it
with recent runs on main (gh run list --workflow test-integration.yml) before blaming the change.
Then grep .github/scripts/ and the workflow for the feature. What CI does not exercise is what
needs checking by hand. CI also runs its own Incus version (Zabbly's latest), not the one a distro
ships, so version-dependent behaviour is not covered.
2. Set up
./install.sh # JVM build of the working tree to ~/.local/bin (fast enough)
scripts/local-incus.sh # Incus + isx init as CI does it; idempotentThis session does not gain the incus-admin group the script adds, so wrap every incus and
isx command in sg incus-admin -c "...", as CI does. Then:
sg incus-admin -c "isx proxy start" # foreground: run it ALONE in the background
sg incus-admin -c "isx build tpl-minimal --yes"Never chain anything after isx proxy start: a command queued behind it runs later, whenever the
proxy stops, e.g. a build that starts while you are tearing things down.
Inside an isx instance
The script detects a nested instance (a user namespace that maps only part of the ID space) and works around what the kernel refuses there, saying so as it goes:
- The bridge: it creates
incusbr0without Incus's firewall rules, which nftables refuses in a user namespace, and on a free10.166.N.0/24, since isx's default is the outer isx's subnet. - Container IDs: it gives Incus the top 65536 IDs this namespace maps, since the usual range is not mapped and every container would fail with "Failed to handle idmapped storage".
- The 443 redirect: it adds the redirect with nftables where
isx initcould not (no legacy iptablesnattable).
What it cannot fix:
- Templates do not build. They add a
tundevice withmode, which Incus refuses in a nested container. Test on a plain container instead:incus launch images:fedora/43 <n>, then push~/.config/incus-spawn/ca.crtto/etc/pki/ca-trust/source/anchors/and runupdate-ca-trust. Its traffic takes the same bridge DNS -> redirect -> proxy path as a branch. It has no registered address or proof, so credential enforcement relays its own authentication without injecting an account. When the change is in the build itself, a measurement-only patch that drops bothincus.deviceAdd(buildName, "tun", ...)calls inBuildCommandlets templates build here; apply it to both sides identically (see Pitfalls) and never commit it. - Branches do not get a disk size. Every branch sets its root disk's
size, which Incus turns into a btrfs qgroup limit, and the nested kernel refuses quotas (btrfs quota enablegives EPERM), so everyisx branchfails withbtrfs qgroup limit ... invalid qgroupid. For a change that does not touch disk limits, a measurement-only patch droppingupdate.device("root", "size", settings.disk())inInstanceLifecycle.configureBranchgets past it, under the same rules; anything about--diskneeds a non-nested host. - The outer proxy sits between this one and the real upstream. It intercepts the same
domains and speaks only HTTP/1.1. To reach the real host, run the proxy binary directly with
the benchmark override:
ISX_BENCH_UPSTREAM=<host>=<ip>:443 isx-proxy --gateway-ip <gw>, undersg incus-adminso it can read the Incus socket. Local DNS answers with the outer gateway, so resolve<ip>elsewhere, e.g.curl -s -H 'accept: application/dns-json' 'https://1.1.1.1/dns-query?name=<host>&type=A'.
3. Exercise the change
A branch that starts tries to attach a shell; without a TTY it says "No terminal, so no shell
opened" and exits, so isx branch is safe to script. Use --no-start and incus start when the
change is about what happens on a later start rather than at branch time. Cover every path the
change touches, typically:
- plain branch;
--cpu/--memory/--disk;--proxy-only;--airgap --account <ns>=<account>, with and without the template pinning accounts- a source with
user.incus-spawn.instance-mode=kvmpluskvm/vhost-vsockdevices (you can add them to anincus copyof a template), branched by default,--kvmand--no-kvm - the TUI (
isxinsidetmux new-session -d -x 200 -y 50, drive withtmux send-keys, read withtmux capture-pane -p), since it has its own branch flow inListCommand
What to inspect
incus config show <n> --expandedandincus query /1.0/instances/<n> | jq .expanded_devices. Always the expanded view: a device that is gone fromdevicescan still come from a profile. That is how airgap branches kept their NIC unnoticed (#813).- Behaviour from inside, not just config:
incus exec <n> -- ip -4 -br addr,ip route, and acurlto the internet as well as to an intercepted domain. - Which account the proxy serves: pin the instance to a missing account
(
incus config set <n> user.incus-spawn.account.claude=ghost), signal the proxy (kill -USR1 <pid>, the pid is in/health), and expectAccount 'ghost' is not configuredfrom that instance only. It proves the proxy maps the instance's source address to its own pins. - Raw Incus semantics, when unsure what a request will do:
incus query -X PATCH /1.0/instances/<n> -d '{"config":{"k":null}}'against a throwaway instance.
4. Compare against the base commit
A bug you find may predate the change. Build the base in a worktree ($SCRATCH being any scratch
directory outside the repo) and run the same scenario:
git worktree add "$SCRATCH/base" <base-sha>
(cd "$SCRATCH/base" && mvn -q package -DskipTests)
sg incus-admin -c "java -jar $SCRATCH/base/cli/target/quarkus-app/quarkus-run.jar branch ..."5. Time it, when the change is about speed
bench/trace-branch.sh records the daemon's events around one isx branch. Build native for both
sides (mvn package -Dnative -DskipTests -pl cli -am), each in its own worktree
($SCRATCH/base as above, $SCRATCH/head for the change), and run each tree's own
bench/trace-branch.sh: it takes the newest runner in that tree's cli/target. Alternate a few
runs of each; a single run is noise.
Compare the writes before PUT .../state, the time of the start request, and any gap after
"Stopping forkfile" (a push still in flight makes the start wait a full second).
Pitfalls
mvn verify -DskipITs=falseis not this: it runsisx initagainst the host and its template build IT has no definition to build. Use the CLI as above.pkill -f <pattern>also kills the shell whose command line contains the pattern. Kill by pid.kill <pid>ofisx proxy startstops theisx-proxyit started too (#882), butkill -9cannot: the orphan keeps the ports and the next proxy fails with "Address already in use", or a leftover older build keeps serving. Find it withsudo ss -ltnp | grep 18443and kill that pid.- Don't build the other side by
git stash-ing the main tree: agit stash; <build>; git stash popchain run in the background keeps your edits away until the job ends, and anything you edit meanwhile collides with the pop. Use a worktree per side, as in section 5. - A patch needed only to measure (e.g. to get past something this host refuses) goes into both sides' worktrees identically, and is never committed.
- Don't
./install.sh(JVM) from a worktree: its launchers run the jar where it was built, so removing the worktree breaks the installedisxand the proxy. Install from the main checkout. - Inside an isx instance, the outer isx's DNS and MITM proxy sit behind this one. A certificate
issued by "incus-spawn MITM CA" might be the outer one: check which domains the inner proxy
actually overrides (
incus network get incusbr0 raw.dnsmasq) before reading a result.
Report
Say which scenarios ran and which did not, what was compared against the base, and file anything found that the change did not cause as its own issue rather than folding it into the review.
Clean up the instances you created (incus delete -f ...).
In this repository3 other files it gives its agents.
- docs/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- site/llms.txtllms.txt
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

