Repository navigation
isothrum 2.0.0
Highlights
2.0.0 is a major release: it removes and changes public API (every such entry below
starts with Breaking:). docs/migrating-to-2.0.md says what to
change.
- Breaking, for embedders: public API nothing used is gone (the
Hooksport, most of
isothrum-machine, test-double and builder leftovers), the process port lives at the crate root,
andpark,attachanddetachtake the operation's options. - Breaking, for the command line:
isothrum shellbecomesisothrum console <machine>,
--ssh-jumpis taken byuponly, andattachno longer stands up an ad-hoc box. - Breaking, for event readers: an event's
runandopcan be absent (on an event written
outside an operation), and thehook_failedwarning is gone. - Breaking, for every run: Isothrum reads run manifest version 1, the frozen contract
Isoloom 2.0 writes, and refuses the version 0 that 1.x read: regenerate the files first. - New operations:
update(bring a running run up to date with its regenerated manifest),
retireandconsole;isothrum completions. - Graphical consoles:
Running::graphical_consoleandisothrum screenreach a VM's screen
(VNC or RDP) on the loopback, opening an SSH tunnel where the manifest says, and a status says
each machine's console or why it has none. - CI-ready command line: Ctrl-C and SIGTERM stop the tools and exit 130/143 with a
cancelled
error; with--jsonevery failure is an event; a step's timeout counts time without progress. - Corporate environments: proxy and CA settings reach an elevated Hyper-V session and a
containerised Terraform; an owner-only home on Windows; the user'sTF_PLUGIN_CACHE_DIRis kept. - Releases you can verify: signed binaries with SBOM and provenance,
cargo binstall, CI on
every pull request, and documentation organised as tutorial, how-tos, reference and explanation. - Many fixes where unreadable tool output passed for success (Compose
configandps,
reap), and a sensitive output masked instatus.
Fixed
-
A step the run manifest marks a download runs with a download's rule. A Compose start in a
VM (docker-vm,cloud-docker) pulled its images inside the start's command, run through
vagrant sshorssh.rununder the start's limit, so a large image on a slow connection could
time the start out. Isoloom now writes that pull as a step of its own (pull-<step>) marked
"download": true: the runner lets it run while it makes progress, with no ceiling, stops it
once it shows none for itstimeout_swithdownload_stalled(its last line quoted), and
reports its failure asdownload_failed, as it does for a Compose start's pull on this
computer. -
A Vagrant start downloads its VMs' boxes before
vagrant up, with a download's rule. A box
downloaded byvagrant upcounted against theup's own limit. Before avagrant upthat
creates the run's VMs, the runner reads each machine's boxes from the manifest
(native.boxes, for the run's provider and this computer's architecture; the access machine's
left to its hot-add), skips those Vagrant has already (vagrant box list, the same provider,
version when exact and build), and adds the others in a step of their own,boxes-<step>:
vagrant box add <box> --provider <p> [--box-version <v>] [--architecture <a>], stopped with
download_stalledonce it shows no progress for 15 minutes,download_failedwith Vagrant's
reason when refused (Vagrant saying it has the box already is no failure). A manifest that
lists no boxes starts as before. -
A run whose operation's process died is failed, not ready. An operation killed midway
(SIGKILL, a crash, the computer switched off) never recorded how it ended: its record stayed
starting(orstopping,parking,resuming), so a run whose containers or VMs had come up
read asreadythough its set-up never ran, and the nextupwas refused as
already_running. Such a run now reads asfailed, and the next operation, once it holds the
run, records itfailed(itslastthe killed operation,cancelled), soupclears what
it left and starts afresh. -
A resume never brings back a run with a machine gone. A machine removed outside the runner
(docker rm -f,vagrant destroy, the hypervisor's Delete) while the run ran or was parked
was left out of a Compose resume, and a Vagrant resume made it again from its box,
unprovisioned (vagrant up --no-provision); either way the run was called ready.resume
now fails withnothing_to_resumenaming the machines (Error::PartlyGone) and records the
runfailed, and anupthat resumes parked runs (Occupied::ResumeParked) clears what is
left and starts it afresh instead. -
A Compose start pulls its images before its
up, with no time limit while they move. An
11 GB image pulled through a slow connection was still downloading when the start'supstep
ran into its limit, and the embedder could only say theuptook too long. Before a step that
runsdocker compose … up(and before the access machine's hot-add), the runner now pulls what
thatupstarts in a step of its own,pull-<step>:docker compose … pull --policy <the up's --pull, missing by default> --ignore-buildable [--include-deps] <its services>, with the
project, files, profiles and variables theuphas, its lines that step'slogevents. A pull
still moving has no ceiling; one that shows no progress for 15 minutes is stopped with the new
download_stalled(its last line quoted), and one that fails ends the start with the new
download_failedand Compose's reason, before anything starts. Theupthen runs under its own
limit. Anupthe runner doesn't read whole (an option it doesn't know), one with--pull never, and everyupon a Compose that refusespull --policypull in theupas before. -
A status read another process's action on a machine refuses is no failure. With no
operation of this runner holding the run (another process'svagrant upadding and provisioning
a VM, or one that began just after the read looked), eachstatuswas refused by Vagrant's
action lock, tried again for a minute, then failed with Vagrant's words (An action 'read_state' was attempted on the machine 'attackbox', … (Vagrant::Errors::MachineActionLockedError) (exited with code 1)), which an app polling the run showed as an error. A query now waits 10 s at most,
not again once an operation holds the run, and then reports the machines Vagrant named
busy(MachineState::Busy), the others as last read, the status not live. An operation's own
reads still wait a minute and fail with Vagrant's refusal. -
A graphical console is no longer asked for through a busy run.
graphical_consoleread the
run's Vagrant status even while an operation held the run, so a screen asked for while a VM was
being added waited a minute on Vagrant's locks, then failed with them. It reads the run as
statusdoes (as last read while an operation holds it) and refuses a machine another process
acts on at once, saying so. -
A graphical console that doesn't answer isn't handed out. A console on the loopback is
checked to accept a connection (2 s) beforegraphical_consolegives it, and refused otherwise
(its screen didn't answer at 127.0.0.1:5901). A vagrant-qemu VM started without a VNC display
(info vnc:None) is refused saying so and to restart it, not thatinfo vncnamed no server. -
An engine's sensitive output never fills a later template. A guest VM's read (
docker-vm)
and a Terraform run's guest login bound every engine output into${outputs.*}under the
engine's names, sensitive ones included, for the rest of the operation; they now bind as the
run's outputs are bound (outputs::bind: by the manifest's names, sensitive ones left out), and
only while they read. -
terraform.copy_modulecopies nothing a copy of a project never takes. It left out
.terraformand states only, so Terraform's crash logs (crash.log,crash.*.log, which can
hold a variable's secret),.gitand.vagrantwent into the run's state folder; it now
follows Isoloom'snever_copiedrule, as the project's archive does. -
A value given twice on the command line is refused (
--param A=…or--secret-from-env A
repeated: a usage error naming it), where the last one was kept without a word. -
A half-drawn VLAN is refused. A LAN with a
switchbut novlan, or the reverse, compiles to
Error::VlanIncomplete, and adeviceon a plain LAN toError::DeviceOffSwitch, where both
compiled as a plain LAN, the field dropped. -
A tool whose version can't be read is traced when the manifest sets its
min_version: it
still passes, with a warning naming it and what its probe printed. -
libvirt's KVM device is said as it is. A
/dev/kvmthat can't be opened for another reason
than permission (busy, no such device) read "add yourself to the kvm and libvirt groups". -
ISOTHRUM_LOG's value prefixes only the filter's error, not a badISOTHRUM_LOG_FORMAT's. -
An operation on a run that exists fills a param's default, refuses a required one with no
value, and leaves out one of another scope, as a start does. It took the recorded params and
the given ones only, so a param a run taken over wasn't given, or one a regenerated manifest
adds, had no value (${params.<name>}unbound, no Terraform variable), while a secret slot
fell back to its default; and a given param of another host or provider was kept. -
Runner::adoptkeeps the slot a deployment was applied in, or refuses it, and picks it under
the home's slots lock. A slot another run on the place held, or out of 1 to 99, was replaced
by a free one without a word, so a laterupdateordownapplied under names the deployment's
resources don't have; it is now refused (adopt_refused). The slot was also picked without
slots.lock, so a start in another process could take the same one meanwhile. A start and a
take-over now share one way to pick it. -
A guest delivery and a Terraform console read a machine's outputs under Isoloom's names.
The runner wrote the machine's name inaddress_<machine>andssh_user_<machine>with-as
_only, where Isoloom also writes.as_and lowers the case, so a machine named with a.
or a capital was never found. A spec names its machines in kebab-case today, so no generated
project met it; a test now holds the names to the outputs Isoloom generates. -
A secret slot for another provider isn't written into a machine. Its guest delivery took
the slot's default without asking the run's provider, where theenvand Terraform deliveries
did; the delivery still runs (the machine's set-up waits for it), with no value for the slot. -
A
ready_filecondition whoseuserfills empty logs in as root, as every other login the
runner makes does with no user, rather than as this computer's user (@host). -
A Compose config the runner can't read fails its step, saying why, rather than passing for an
empty one. Whendocker compose config --format jsonprinted something the runner couldn't
read,compose.check_ports_freechecked no port (so a taken one surfaced later as Docker's bind
error),compose.remove_stale_networksread every network of the project as stale and removed
them, andcompose.pin_portswrote no pins, so the run started on ports Docker picked and a
resume got new addresses. Each step now fails (tool_failed) and acts on nothing, saying what is
wrong: not JSON (and serde's error), noservicesobject, a port with no target or of a protocol
other than TCP or UDP (which read as TCP), and whose. -
A VirtualBox VM whose
showvminfofails before a start is skipped with why in a trace,
not in silence. -
A progress meter a tool redraws in place shows as it moves. A tool's output was cut into
lines at\nonly, so a tool redrawing its line after a carriage return (ovftool'sDisk progress: 8%while Vagrant uploads a box to ESXi, which prints no\nuntil it is done) gave
nologevent for as long as it ran. A lone\rnow ends a line too (\r\nis still one line
break), so a moving meter is also progress against its step's time limit. A redrawn line is a
logevent only when its text changed and at most once a second, so a fast meter doesn't fill
the event log; a line ended by\nalways is. -
A Terraform run whose server lost one of its resources can be destroyed. The destroy runs
without a refresh, so a resource the server no longer has (a Proxmox host reset after an apply
cut short) reaches its provider's delete, and a few deletes fail on it rather than counting it
done: bpg/proxmox's IP set failed everydownof the run, and the clearing of its leftovers
before the nextup, withno such IPSet 'iso1n0'. Adownwhose destroy printed one of the
errors the manifest'sidentity.terraform.gonenames, filled with the attributes of an
instance the state still holds, now drops that instance from the state (terraform state rm,
run as the destroy is), says which in analready_gonewarning (new in the event schema), and
destroys again, three times at most. Nothing else is dropped: any other failure is the
down's, as before. -
A run is free as soon as the operation that held it returns. Its lock was freed only by
closing the lock file, while a tool another operation started meanwhile still had the file's
handle until it ran (on Unix, a spawned process gets every open handle until itsexec). So the
next operation on the run, or a retry right after a refusal or a failure, could be refused as
locked. The lock is now unlocked, then closed, which frees it whoever still has the handle. -
A Docker CLI config that can't be read, or isn't JSON, is traced. It read as "no context
picked" without a word, so the inheritedDOCKER_HOSTandDOCKER_CONTEXTstayed with nothing
saying why. Only a config that isn't there is now taken for none, as everywhere else. -
A start with no free host port left for an ephemeral port fails, naming the port. Compose's
compose.pin_portsthen wrote no pins and passed, so the run started on ports Docker picked and a
resume got new addresses; a Vagrant start left the port out in silence, so the VM forwarded it at
itspreferrednumber, which a resume needn't get again, and the QEMU helper VM's SSH forward
fell back to 50022 the same way. Both drivers now pick by one rule (thepreferrednumber with
ISOLOOM_PUBLISH_FIXEDwhen no other run reserved it, else a free loopback port, one the IPv6
loopback doesn't hold either, as the free-port check counts a port) and fail withtool_failed
naming the port when no free one another run starting hasn't reserved is found. A loopback the
runner can't bind to look for one (no file descriptor left, a loopback that is down), which read
as no free port, now fails the pick withioand the OS's reason (a tcp port on 127.0.0.1 couldn't be bound: …); theiocode
covers a socket on this computer too. -
A failed tool says why in one wording everywhere. An argv or
ssh.runstep that failed
with nothing on its standard error said only "exited with code 1", while a driver's own call
quoted its last output lines (a login refused on stdout, as Debian on AWS does); a ready file's
failed read quoted one line, andisothrum-machinesaid "it exited with 1". Each now says the
error a machine-readable Vagrant call printed as itserror-exitrecord, else its last error
lines, else its last output lines, then how it ended ((exited with code 1),exited with a signal):isothrum_runner::Captured::reason, whichisothrum-machineuses too. Its error lines
leave out vagrant-libvirt's[fog][WARNING]lines, which every call prints: on libvirt a failure
read as fog's warnings. -
A cloud resource whose expiry tag isn't a time is said by
reap, not passed over. Every
resource a cloud's query returns carries Isoloom's expiry tag; one whose value didn't parse (empty,
a date, a typo) was left out of both the orphans andunread, so the one resource the sweep
couldn't judge was the one it never showed. It is now listed inunread(not checked:on the
command line), unless a recorded run's state covers it. -
A Vagrant record that can't be read fails the start, rather than reading as none. An
unreadablemachines/folder in the run's Vagrant state read as "nothing recorded", so a run
left on another provider was never cleared first, andvagrant uprefused it. A network whose
native name can't be filled is now traced too, rather than shown without one in silence. -
The free-port check before a Vagrant start checks the ports the VM forwards. It looked
only at a fixed port'spreferrednumber and an ephemeral port's pinned one, while the VM
forwards an ephemeral port no free one was pinned for at itspreferrednumber (as status
reports it): a taken one went unchecked, and Vagrant collided on it once the VM was imported.
The check and status now share one rule for a forwarded port's host port. -
An environment variable that isn't UTF-8 is no longer taken for one that isn't set. A
--secret-from-envvariable was refused as "isn't set", anISOTHRUM_LOGwas ignored without a
word, anISOTHRUM_HOMEsentisothrumto~/.isothrum, aDOCKER_CONFIGwas passed over for
~/.docker, and aPATHwas dropped, so ovftool wasn't found on it and the macOS ESXi relay set
PATHto its wrapper's folder followed by an empty entry. Vagrant's variables andUSERPROFILE,
which an elevated Hyper-V session is handed, were dropped the same way, so aVAGRANT_HOMEthat
isn't UTF-8 had the session pinned to the profile's boxes, and nothing was traced. A secret's
variable and the log's are now refused by name;ISOTHRUM_HOME(as--homeis),DOCKER_CONFIG
andPATHare read as paths, and the wrapper's folder goes first on thePATH(alone when there
is none); a variable the runner hands on is kept as it is and counts as set, and where it must go
on as text it is left out, and traced by name. Breaking, for embedders:RunnerConfig::path
is anOption<OsString>. -
A check its runner never reported is the one that fails. The checks a runner left out were
taken by position, after as many results as it printed: a runner that reported its second
check, or one check twice, then stopped had the wrong check failed as never reached, and the
one it never reported counted as reported. They are now matched by name. -
A UTM controller still up is no longer taken for one that is off. The controller's halt and
its wake before the checks waited only while Vagrant read itrunning, but UTM calls a running
VMstarted: a failed halt counted as done, and a controller still powering itself off was
booted at once, racing its own poweroff. They now read the state as status does, and keep
waiting while it is anything but off or absent. -
project.archiveno longer puts a Terraform state or Isoloom's other outputs in the guest's
copy. The archive left out only.git,.vagrant,.terraformand the top's
.isoloom-project*, plus what.isoloomignorelisted, so a*.tfstate(the secrets Terraform
saw, in plain text), its backups and lock, crash logs and every.isoloom/output went into the
archive a step pipes into a guest. It now leaves out exactly what the generated files' copies do
(isoloom_manifest::ignore::Rules::for_copy): Isoloom's outputs but those the step's new
with.outputsnames, and, at any depth, version control and the tools' state, in those outputs
too. The walk never enters a folder no copy takes. -
Ctrl-C or a cancelled CI job no longer leaves the tools running. Each tool runs in its own
process group (a job object on Windows), so SIGINT and SIGTERM reached onlyisothrum, and
Terraform or Vagrant kept running.isothrumnow races its command against SIGINT and SIGTERM
(Ctrl-C, Ctrl-Break, the console closed, a log-off or a shutdown on Windows): the operation is
dropped, which stops each tool's process tree, and the command exits 130 (SIGINT) or 143
(SIGTERM) once they have stopped. -
A tool cut short is asked to stop before it is ended. A cancel, Ctrl-C or a step's timeout
sent SIGKILL to the tool's process group, so a Terraform cut short lost the state it had not
saved yet. On Unix the group now gets SIGINT (Terraform, Vagrant and Compose stop cleanly on
it), and SIGKILL once the tool has exited or 30 seconds later.isothrumwaits for them
before it exits ("Waiting for the tools to stop"); a second signal ends them at once. An
embedder waits with the newSystemProcesses::stopped(is_stopping,end_stopping). On
Windows a tool, headless, is still ended at once. -
An operation cut short says so. Dropping an operation's future (an embedder's cancel, the
command line's Ctrl-C) ended its events mid-stream and left the runstarting(orstopping,
…), so the nextupsaid it was already running. Its events now end with anerrorwhose code
is the newcancelled, after thestatemove tofailed; the record saysfailed
(last.code: cancelled), so the nextupclears what it left. -
The elevated Hyper-V session no longer reads an earlier session's files. The run's
elevated/folder kept theready,exit-<n>andout-<n>.logof the operation before, which
the next one read as its own (a broker taken for started, a command for done). A session now
clearselevated/and works in its ownelevated/<token>/; the runner deletesout-<n>.log
andexit-<n>once read, and the session's folder when it ends;exit-<n>appears whole
(renamed into place). -
An elevated Hyper-V session no longer ends six hours in, and a broker that stops fails its
request at once. The broker ended itself six hours after its prompt, whatever the operation
still had to run, and a request whose broker had stopped waited for its answer until its step's
limit, then read as atimeout. A session now lasts as long as its operation; a broker that
stops other than at the session's end writes why inended, and the request in flight fails
withelevation_failedsaying so, as does every later call of the operation. A request whose
file couldn't be written, or whose answer couldn't be read, now fails the session too (the
broker waited for it and took no later request), and a failedwhoami /groupsis traced. -
The macOS WinRM proxy passes a client's close on to the guest. It shut
nc's input down,
which leaves a pipe open, so the guest never saw the client's end: each WinRM connection's
nc, its connection to the guest and the proxy's task for it stayed until the operation ended.
It closesnc's input now, as the SSH relay already did. -
SSH through a jump host works from a home whose path has a space.
sshwas given-F <home>/runs/<run>/ssh_config, and-Jhands that path to each jump host's ownsshthrough a
shell, unquoted: under macOS's~/Library/Application Supportevery jump failed at once ("Can't
open user config file …/Library/Application"), so aready_fileread through a Proxmox run's
router never answered.sshnow runs in the run's state folder with-F ssh_config. -
A
ready_filewait says why its reads fail. A read over SSH that failed was dropped
without a word, and a host that never answered failed with "didn't answer over SSH within
600s" alone. Each new reason is now areadyevent (waiting, "no answer over SSH yet: …",
ssh's last error line and its exit status, masked), and the wait's error ends with the last
one, out of time included. -
Two starts in one home no longer take the same slot. Two
ups at once (two processes) both
read the records before either recorded its slot. A start now picks its slot and records it under
an OS lock on<home>/slots.lock(freed as a run's lock is), waiting up to 30 s for another
start's pick, then failing withlocked(Error::SlotsLocked). -
Terraform uses the user's
TF_PLUGIN_CACHE_DIR. The runner replaced it with its own cache
under the home. A cache the user set is now used as it is (neither created nor narrowed); without
one, the runs share<home>/terraform-pluginsas before. -
The macOS ESXi relay keeps the user's
no_proxy. It replaced it with the loopback alone,
so an internal host the user reached directly went to their proxy; it is now the user's
no_proxy(elseNO_PROXY) pluslocalhost,127.0.0.1.http_proxyis still the relay's (WinRM
reaches the guests only through it);https_proxyis left as it is. -
--name's help no longer defines it by itself. It now says the default is the environment's
name in the manifest. -
The checks no longer run on a setup machine that is still powering itself off. Waking the
controller for the checks asked it over SSH whether it was halting, and took a failed SSH (a
controller already on its way down) for "not halting": it skipped the boot and the checks then
found it off. The controller now answers in words; no answer is waited out like a halting one,
then it is booted. The wait is bounded: each ask may go 20 s without an answer, and a
controller still running without saying it stays up after three minutes fails the step
(tool_failed) rather than be asked for up to two hours. -
A run's own access machine reaches the run through its router. On Compose,
attachstarted
the access service alone (--no-deps), without the service that sets its routes (Isoloom's
isoloom-routes-<machine>, in its network namespace): an access machine on its own network
reached nothing behind the router. Attach and detach now take the services in the machine's
namespace (network_mode: service:<machine>, read fromdocker compose config) with it, and
attach waits for them (--wait); a config the runner can't read fails them (tool_failed) rather
than taking the machine alone. -
attachjoins the chosen network by the name the run gave it, and says when it can't. On
Compose it guessed the network as<project>_<network>and swallowed every failure to join
it, so an attach reported (and its event claimed) a network its machine wasn't on. The network
must now be one of the manifest's (network_unknownotherwise, before anything starts), is
joined by its rendered native name (as status shows it), and a failure to join fails the attach
(tool_failed); only a container already on it is left as it is. A Vagrant run refuses a
chosen network (unsupported): a VM's networks are its Vagrantfile's, and the network was
ignored. -
isothrum reapfails when it couldn't search the cloud, and still says its sweep. A query of
what the cloud holds that couldn't run or be read was listed (not checked: …) but the command
passed, as for a clean sweep; it now fails. When the search for leftovers failed after the
destroys (a recorded run's record or Terraform state unreadable,io), the command printed
nothing of the sweep; and the failure was taken for said when the last destroy had failed (nothing
printed at all), or stamped with the destroyed run'srunandop. It now prints the sweep (with
--json, itsresult,orphansandunreadnull), then the failure (anerrorevent without
runnorop), and exits 1. -
A guest delivery leaves no secret on disk. The values a
guestdelivery sends wait in an
owner-onlyguest-<machine>.inin the run's state folder while SSH is retried (up to 15 minutes).
An operation cut short then (a cancel, Ctrl-C) skipped the file's removal, and a removal that
failed was only traced: the values stayed. The file is now removed however the delivery ends, and
a failure to remove it fails the step (io). The runner's crate docs list the four deliveries
(env,tf_var,state_file,guest), no longer say no value is ever stored and no longer
describe adotenvstate file;SECURITY.mdand the security model name the three owner-only
files a value is kept in (astate_filedelivery's, a guest delivery's input, an elevated
session's variables). -
A guest delivery whose machine never answers over SSH fails with
unreachable, as an
ssh.runstep whose host never answers does. Past its 15 minutes, ssh's exit status 255 read as
the tool failing (tool_failed). -
isothrumsays why a run folder's.isoloomcan't be read. Any failure to list it (a
permission refused, a file in its place) read as "no run manifest: runisoloom generate";
only a missing folder does now, and any other failure is reported as it is. -
An elevated Hyper-V call with a standard input is refused, not run without it. The
elevated broker starts each program with no input, so a call that pipes a file in ran with
none; it now fails withProcessError::Refusedbefore anything runs. -
docker compose psoutput the runner can't read no longer passes for no container. Whenps --format jsonprinted something that isn't its JSON, every machine read absent: a resume of a
parked run whose containers were there dropped its parked mark and failed with
nothing_to_resume,compose.pin_portsre-picked the pins of existing containers, and the
containers of a runningdocker-vmVM read absent. The run's status, park, resume and those steps
now fail (tool_failed); in a VM, the read counts as a missed one (tried again, then the
containers readunknown, "unreadable: …"). A port of a protocol other than TCP or UDP, which
read as TCP, is left out, and traced. -
A target this host doesn't run is named as the manifest writes it. Opening a run whose
manifest leaves this host out saidtarget "docker" on this host isn't supported…, the JSON
quotes kept; it now saystarget `docker` on this host. -
A recorded run's Terraform state that doesn't parse fails the orphan sweep.
Runner::orphansskipped such a state silently, so the resources it holds, still live, were
listed as orphans; it now fails withio, as it does for a state it can't read. -
A Vagrant that can't list its plugins is said once. When
vagrant plugin listcouldn't
run or failed,isothrum-machine's detection read no plugin as installed, so every provider
that needs one said its plugin wasn't installed. Each now says that Vagrant couldn't say which
plugins it has, with why. -
The editor's errors say their cause. A zip that couldn't be written said only "writing the
zip", and the compiler'sPortNameandYamlerrors only what they were doing; their message
(aJsError's, for the editor) now ends with the cause. -
A sensitive output is masked in a run's status. Events and results showed it as
***,
butStatusandStartedheld the value itself inOutputValue::value, andisothrum status --jsonprinted it.OutputValue::valueis now***for a sensitive output (in its JSON and its
Debugtoo). -
An output whose
formatcan't be filled is said. It quietly became an output with no
value;up,resumeandupdatenow emit anoutput_unformattedwarning (new in the event
schema) saying which format named what the run doesn't have, before the outputs. -
An operation that failed ends with its own error when the run's record can't be updated.
The record's I/O error replaced the operation's, and the closingstateevent was never
emitted. The operation now ends with its own error, after arecord_not_savedwarning (new in
the event schema) and its closingstateevent. -
The compiler addresses a LAN whose block doesn't start its last octet. It took a block's
usable addresses from its size alone (.2 on, .10 on a /24), so a machine on10.10.1.8/29or
10.10.1.128/25got.2, outside the block, and Isoloom refused the spec. The addresses are now
Isoloom's own (NetworkBlock::host):.10to.12on10.10.1.8/29,.130on10.10.1.128/25.
A block Isoloom gives machines no address on (not a network's own address, larger than a /24 or
smaller than a /29) is now the compiler'sBadCidr, which says so; it was written into a spec
Isoloom then refused. -
The compiler refuses an address no machine can take on its LAN. A machine's asked-for
address was written into the spec as it was, so.200(or the gateway's.9, the router's
.14, …) on10.10.1.8/29compiled, and Isoloom refused the spec. It is now the compiler's new
Error::AddressOutside, naming the LAN, the machine, the address and the block. A /24's
addresses below.10, which the compiler gives no machine itself, can still be asked for. -
A Cisco router drawn at an address of its own on a LAN it is the gateway of is refused. The
compiler put the first Cisco router on a plain LAN at the LAN's gateway address and dropped the
address drawn for it without a word; it is now the compiler's newError::GatewayAddress,
naming the LAN, the router, the address drawn and the gateway's. A router left without an
address there, or one that stays a member (a second router, a VLAN), is as before. -
A cloud target's manifest is found from the run's folder. Isoloom writes one manifest per
cloud (.isoloom/cloud-vm/aws/run.json), but the command line looked only for
.isoloom/<target>/run.json:isothrum up <folder>said nothing was generated, and--target cloud-vmthat it didn't exist.--target cloud-vm/awsnow names the AWS one,--target cloud-vmthe only cloud generated, and no--targetthe only manifest of any target. -
A symbolic link in a module
terraform.copy_modulecopies is refused, not followed. One to a
folder above it copied until the stack ran out, and one elsewhere brought in a file from outside
the project; the step now fails (unsupported) naming the link. -
The editor's whole-project download says when no target generates anything.
bundleon a
project no target generates a file for saidthis project has no files for `any target`; it
now says no target generates any file for the project, and that the plan'sskippedsays why. -
isothrum reap --dry-runfails when it would destroy a run. A dry run that found expired
runs and no leftover passed, though the runs it named are still standing; it now exits 1, as the
docs say a sweep with something left does. -
An instance's manifest is found from the run's folder.
isoloom generate --instance <n>
writes to.isoloom-<n>/, but the command line looked only under.isoloom/, soisothrum up <folder>said nothing was generated. It now reads.isoloom/and every instance's
.isoloom-<n>/(nfrom 1 to 99, as Isoloom writes it): an instance's manifest is named with its
folder (.isoloom-2/docker), and its target alone names it too;--target dockerstill takes
.isoloom/docker/first. The rule is the runner's newis_outputs_folder(andMANIFEST_FILE,
the manifest's file name), held to the folders Isoloom generates. -
A run's folder is the one above an outputs folder only. The project a
terraform.copy_modulecopies from, and the folder a tool run in a container gets mounted, is
the folder above the manifest's outputs folder; any folder whose name starts with.isoloom
(.isoloom-reach,.isoloomfoo) was taken for one. It is now.isoloomor an instance's
.isoloom-<n>. -
No starter goes in an instance's outputs folder. The compiler took
.isoloom/alone for
Isoloom's outputs, so a step file at.isoloom-2/x.shor.isoloomignore.shgot a starter
that no copy of the project holds; it now asks Isoloom's rule for its outputs
(ignore::OUTPUTS,/.isoloom*at the project's top). -
An event log cut mid-character no longer stops the run. The run's
events.jsonlwas read
as UTF-8 text, so a log cut short inside a character (a full disk, a killed process) failed
every later operation of the run withio, andisothrum eventswith it. Such a line now
counts as one the runner can't read (EventLog::unreadable), and each event is written in one
write, its line break included. -
A run's event log is trimmed only by the operation holding the run's lock. Each operation
trimmed a longevents.jsonl(over 4000 lines) before it locked the run, by writing the
newer half to a new file renamed over it: a second operation on a running run replaced the log
under the first, whose later events went to the file it replaced (on Windows the rename over
the first's open log could fail the second withiorather thanlocked). An operation now
opens the log to append before the lock, so alockederror is still logged, and trims it once
the run is locked. -
The run's SSH configuration reads the same to every OpenSSH. It escaped a path's spaces
with backslashes, which OpenSSH reads as escapes only from 8.7: Windows 10's 8.1, RHEL 8's 8.0
and Ubuntu 20.04's 8.2 split a key under a home whose path has a space (C:\Users\First Last).
Each path is now written between double quotes (with/on Windows, so a network home's\\
stays whole); a home whose path no release reads as written fails the SSH call withio. The
refused login's message names:, which a login takes. -
A switch named like a cable's network is refused. A switch called
link-<n>nested its
VLANs under the network of that name, which a cable then took over: the VLANs vanished from the
spec. It is now refused as a LAN of that name is (Error::ReservedName). -
A Windows starter opens each port on its transport. The PowerShell starter took a port's
number alone: a UDP port got a TCP firewall rule, a number drawn for TCP and UDP got the same
rule twice, and a machine with only 443 was told IIS was for port 80. Each rule now has its
port's transport (project-53-udp), and IIS's line names the web ports drawn. -
Provider detection without Vagrant probes nothing.
providers::detectran every
hypervisor's probe (VirtualBox's liveness check takes up to 8 s) even when told Vagrant isn't
installed, though each provider then said only that; it now answers at once. -
attach --jsonandreap --jsonprint events only. Each ended with a JSON object that
wasn't an event, on the stream documented as one event per line, andattach's showed its
image unmasked when it was a secret's value.attachnow says where the machine is in its
resultevent'sdata(masked as every event is);reapends with aresultevent, no
runnorop, whosedatais the sweep. -
A wait shows in text as it goes. The
readyevents that say what a wait is waiting for
(aready_file's "running: …", "no answer over SSH yet: …") reached--jsononly; the text
output now shows them (waiting: <condition>: <detail>). Atcpcondition emitted an empty
waitingevent every 2 s; it says once what it waits for. -
A cable from a port to itself is a
SelfLink. The compiler checked the ports first, so a
cable whose two ends were the same port failed withPortInUse(the second end read as a port
that already had a cable); it now fails withError::SelfLink, as a cable between two ports of
one node does. -
A check runner's
ENDline ends it only with both counts. Isothrum took any
isoloom-check: END …line as the runner's end, where Isoloom's reader of the protocol
(isoloom-check/1) takesEND <p> passed, <f> failedonly: a runner cut off before its counts
is now unfinished, as Isoloom reads it. Isothrum reads the lines with that reader,
isoloom_manifest::CheckLine::parse, instead of a copy of it. -
A ready file's first line is read as Isoloom reads it, with
isoloom_manifest::ReadyLine::parseinstead of a copy of theisoloom-ready/1grammar, which
also tookfailed:x(no space) for a failure and trimmed the step where Isoloom's reader doesn't. -
A tool's version is compared with the manifest's minimum in Isoloom's order. Only the numbers
were compared, so a pre-release passed for the release it leads to (1.6.0-rc.1for a minimum of
1.6.0); the version a tool prints is now read as Isoloom'sVersion, a pre-release older than
its release. -
A failed
step_okready condition says one thing. Itsreadyevent gave what the step's
success means, itsnot_readyerror "the step didn't succeed"; both now say "the step didn't
succeed, so not: ". -
A malformed
--nameexits 2, a usage error. It exited 1 with the coderun_name, where
the exit-code reference promises 2 for a value missing or malformed; itserrorevent's code
is nowusage. -
A mistyped
--paramon an existing run is refused.down,park,check,updateand
the other operations on a run that exists dropped a param the manifest doesn't take and passed
a value outside itsvaluesunchecked. They now check what is given as a start does (an
unknown name, a value outsidevalues, a param the runner sets), then replay the recorded
params. -
A Terraform preflight says what is wrong with the module it checks. With a
terraform.copy_modulestep, awith.fromthat couldn't be filled, a folder outside the
project or one that couldn't be read all read as "the Terraform module … has no.tffile:
generate the run's files again", naming the generated module beside the manifest. The fill
error,<step>.with.from: … isn't a folder inside the run's projector the I/O error is
reported now, and a module without a Terraform file is named where it is. -
The setup machine's boot and halt take the step's limit on the run's provider. Vagrant's
wake_controllerandpark_controllerread the step'stimeout_sonly, never its
provider_timeout_s, so a longer limit for ESXi never reached them. -
A
downforgets the run's pinned SSH host keys whatever the driver. Only a Terraform
run'sdownremovedknown_hostsandssh_configfrom the run's state folder, so a Compose or
Vagrant run that reaches a machine over SSH refused its recreated machine's new host key at the
same address. -
Adopting a Terraform state never leaves it in both folders. Any failed rename fell back to
a copy and a removal, so a state whose folder couldn't be written was copied into the run and
stayed where it was. The copy is now the fallback across file systems only, and a copy whose
source can't be removed is removed itself. -
An
ESXI_HOSTPORTthat isn't a port is aparam_invaliderror. It became 22, and the
macOS relays pointed at the wrong port. -
Errors that read as something else are said. A folder entry that couldn't be read (the
module copy, the runs' list, the run folders the CLI finds) was skipped; an elevated answer that
wasn't an exit code read as a program killed by a signal; a public SSH key that couldn't be
read was made again byssh-keygen, which failed on its overwrite prompt. Each is now an error
naming its cause. A VM address thatstatuscan't read (on ESXi) and a guesthostthat can't
be filled are still left out, and traced with why. -
A parked run with a member up and failing its healthcheck reads
failed, notparked.
The status counted only a healthy machine as running, whilecheckandprovisioncount an
unhealthy one too and act on the run as running. -
A VLAN drawn with a
tcis refused (Error::VlanTc). The compiler dropped it without a
word: Isoloom's VLANs have notc, and the switch's LAN they nest under was written without
one. A plain LAN and a cable keep theirs. -
A provisioning step outside the project is refused (
Error::StepPath, naming the machine and
the path). An absolute path or one with a..that leaves the project failed as
Error::Unreadable, which says the compiler wrote what Isoloom doesn't read (a bug), while the
same path as a Windows VM's playbook compiled. -
Two LANs on the same VLAN of a switch are refused (
Error::VlanTaken, naming both). They
compiled into one VLAN block, the second LAN's: the first LAN's machines moved to it without a
word, at the second LAN's addresses. -
A starter script installs with the package manager Isoloom records for its OS. The
compiler guessed it from the distribution's name, so a distribution Isoloom adds gotapt-get
whatever it installs with, and a shell step on Windows gotapt-getlines too (now none). -
A Compose or local Vagrant run answers at the address its ports are published on.
${host.<machine>}was always127.0.0.1, while the manifest'spublish.address(Isoloom's
ISOLOOM_PUBLISH_ADDRESS) was never read: a run published on a LAN address was reported, and its
tcpready conditions tried, at the loopback, where nothing listened. It is now that address,
filled; one on every interface (0.0.0.0,::, however it is written:0:0:0:0:0:0:0:0,
[::0]) answers at the loopback. -
A run manifest that isn't UTF-8 is
manifest_invalid, naming the byte. Bytes that
weren't UTF-8 inside a JSON string read as U+FFFD, so a path or a template of the contract
changed without a word. -
A driver action's or a
project.archive's error names where its step is as every other
step's does (lifecycle.up.archive.with.root, notarchive.with.root), and a Compose
action's errors name its own list rather than alwayslifecycle.up. -
A Terraform run's status shows each machine's published ports, as a Compose or Vagrant
run's does; they were always empty. An entry of the state'spublishedoutput that doesn't
read as<machine>/<port>at<address>:<port>is still left out, and now traced. -
An elevated session that fails to start says why, as every failed tool does: with its exit
code, and its output when its error output is empty (it said nothing then). -
Vagrant's own clean-ups say what they did. An orphaned VM (or its folder) was said removed
(orphan_removed) before the hypervisor was asked, and a removal it refused was only traced; a
hypervisor listing that failed (VBoxManage list vms,prlctl list) skipped the clean-up in
silence; a VirtualBox machine folder that couldn't be read read as one with nothing left; and the
destroy of the run left on another provider only had to start, its exit status unchecked.
orphan_removednow says a VM or folder once it is removed ("removed a VM an earlier run left
()"); a removal the hypervisor or the file system refuses, a hypervisor listing or a machine
folder that can't be read and a failed destroy on the other provider are each acleanup_failed
warning (new in the event schema) with why, and the operation goes on.
step_toleratedis a tolerated manifest step's alone. -
A starter file is written for every path Isoloom accepts, and only for one. A step whose file
name held..(provision/a..b.sh) got no starter, though only a..segment leaves the
project, while a path with a drive or a backslash (a Windows path) got one. The compiler now asks
Isoloom'sRelPath(no drive, no backslash, no..segment, not absolute). -
Relays to an ESXi host that can't be set up say why (
Error::Relay, codeio): a relay
that can't listen on the loopback, aPATHthat isn't UTF-8, the ovftool wrapper's folder that
can't be written. Every such failure read as an I/O error on<home>/relay. -
A check whose setup and teardown both fail says both. The teardown's error was dropped for the
setup's; it is now acheck_cleanup_failedwarning before the checks end with the setup's error. -
A one-target zip holds every one of the project's own files (
bundle_target). It kept only
those whose path or bare file name some generated file's text contained, which missed a reference
Isoloom builds from parts.
Added
-
Error::DownloadStalledandError::DownloadFailed, with the codesdownload_stalledand
download_failed: a download made before a step, in a step of its own (a Composeup's images),
that showed no progress for as long as a download may (after_s,last_line), or whose tool
failed (reason). -
A console offered as it stands now. A status's
ConsoleOfferof a running machine says why
it can't be shown now (unavailable): its hypervisor gives it no display, or it doesn't answer
where the hypervisor says (libvirt, QEMU, VirtualBox and the runner's own are asked; one through
Proxmox's API or an SSH tunnel isn't opened for a status). -
A VM's screen in its hypervisor's own app.
Running::native_console(machine)gives what
opens a running VM there (NativeConsole: the app, and its ways, best first: a program and its
arguments, a macOS app and what it opens, or anhttpsURL), never a shell line, each argument
checked: VirtualBox (VBoxManage startvm <vm> --type separate), Hyper-V (vmconnect.exe),
libvirt (virt-viewer, elseremote-viewer), VMware Fusion or Workstation on the VM's.vmx,
Parallels Desktop on its bundle, UTM, ESXi's Host Client and Proxmox VE's web console;
vagrant-qemu has no app. A status names the run's app and what of it this computer needs
(Status::native,NativeApp). -
isothrum key --jsonprints{"public_key": …};--jsonignored it. The policies list the
console,screenandkeyJSON documents as stable machine output, with their fields, and the
--jsonhelp says what each command prints. -
Deserializeonisothrum_wasm::{Plan, File, Choices}, so a Rust caller reads an export's
JSON back into them. -
A machine's graphical console, where its run manifest lists one (
machines[].consoles,
Isoloom 2.0):Running::graphical_console(machine)gives aConsole(its protocol,
VNC or RDP; itsEndpoint, a port of this computer's loopback or Proxmox VE's VNC websocket
(ProxmoxVnc: the URL, the ticket, the header that signs it in); the account and password to
sign in with), reading the port where the hypervisor keeps it:virsh domdisplayfor libvirt,
info vncon vagrant-qemu's monitor socket,VBoxManage showvminfofor VirtualBox's remote
display,vncproxyover Proxmox's API (withcurl, its credentials in an owner-only config file
piped into it and removed however the request ends, a cancel or Ctrl-C included), or an SSH tunnel it opens from the loopback (ssh -N -L, held by the
Consoleuntil it is dropped). A console the hypervisor says listens off the loopback is
refused. For VMware's VNC server, the runner chooses a free loopback port and an 8-character
password per machine before the run's first Vagrant command, in an operation that holds the
run's lock (astatusorgraphical_consoleonly reads them), keeps them in the run's
consoles.jsonuntildown, and gives them to the run's Vagrant processes in
ISOLOOM_CONSOLES, never to a command an embedder runs.MachineStatus::console
(ConsoleOffer: the protocol, ornoneand why) says each machine's in a status. The error
no_console(Error::NoConsole) says why a machine has none, isn't running, or its hypervisor
doesn't say where it is.isothrum screen <machine>prints where it answers (--show-password,
--json), and holds a tunnel open until it is stopped. -
Captured::success,Captured::reasonandCaptured::outputonisothrum_runner::Captured,
whatisothrum_runner::capturekeeps of a tool it runs (both were inprocess, now at the crate
root): whether it exited 0, why it failed, as every failed tool's error says it, and what it
printed; andDisplayonExit(exited with code 1,exited with a signal). -
Release binaries. A tag publishes
isothrumfor Linux (x86_64, aarch64; static musl),
macOS (arm64, x86_64) and Windows (x86_64) on GitHub releases, each archive with the licences of
every crate it is built from, plus a CycloneDX SBOM,SHA256SUMS, a keyless cosign signature of
it and build provenance attestations (docs/how-to/verify-a-release.md).
cargo binstall isothrum-cliinstalls them. -
CI on every push to
mainand every pull request: clippy and the tests on Linux, macOS and Windows, the
docs, the quality ratchet,cargo deny,cargo machete, typos,cargo semver-checks, the
MSRV and the WebAssembly build. Every workflow's actions are pinned to commits; Dependabot
proposes updates. -
Documentation organised by purpose (
docs/README.md): a tutorial, how-tos (CI, proxies and
CAs, Hyper-V without admin rights, moving the home, offline use, cost controls, SSH keys), the
state layout, the event stream, the supported tool versions, the architecture and the security
model; the policies (what is stable, run manifest versions, MSRV, supported releases); a README
per crate; SECURITY.md, CONTRIBUTING.md, SUPPORT.md and the Code of Conduct. -
With
--json, every failure is anerrorevent. A failure before any operation began (a
folder with no manifest, a value missing, an option clap refuses) was printed as text on
standard error; it is now anerrorevent on standard output (codeusagefor a command line
that makes no sense), so a CI job reads every outcome from one stream. -
docs/reference/:cli.md(every command and option, written from the clap grammar),
exit-codes.md(the exit codes, and every error code with its cause and remedy) and
environment.md(every variable isothrum reads or sets for its tools). Tests fail when the
first two drift from the code (ISOTHRUM_WRITE_REFERENCE=1 cargo test -p isothrum-cli referencewrites them). -
crates/isothrum-runner/schema/status.schema.json, the JSON Schema ofisothrum status --json(isothrum_runner::status_schema()), committed beside the event schema and checked for
drift the same way (cargo run -p xtask -- schemawrites both). -
isothrum completions <shell>prints a completion script (bash, zsh, fish, PowerShell,
elvish); it needs no home. -
ISOTHRUM_LOGtakes filter directives (isothrum_runner=debug,warn), not only a level, and
ISOTHRUM_LOG_FORMAT=jsonwrites one JSON object per trace line. -
Error codes
cancelledandusage(ErrorCode::Cancelled,ErrorCode::Usage). -
Event::new(seq, run, op, kind): an event an embedder writes itself (a failure no operation
reports), timed now. -
Corporate networks. The proxy variables (
HTTP_PROXY,HTTPS_PROXY,NO_PROXY, either case)
and the CA bundles (SSL_CERT_FILE,REQUESTS_CA_BUNDLE,CURL_CA_BUNDLE) reach the tools of an
elevated Hyper-V session; a containerised Terraform (macOS, Proxmox) gets them and
TF_CLI_CONFIG_FILE, with the values the runner read when it was configured, and the files they
name mounted read-only. One that isn't UTF-8 is left out where it must go on as text, and traced
by name. -
An owner-only home on Windows. A home outside the user's profile gets a protected DACL (the
account, SYSTEM, Administrators) that everything under it inherits, set withicacls; a home in
the profile keeps the profile's. -
ProcessSpec::env(name): the value a process is given for a variable. -
is_outputs_folder(name)andMANIFEST_FILE: whether a folder at a project's top is one Isoloom
writes the project's outputs under (.isoloom, an instance's.isoloom-<n>), and the file a
target's run manifest is in (run.json). -
fake::Reply::pace(featurefake): how long a scripted tool runs before each line it prints,
after itsdelay(a slow download's progress), for a test of a step's time without progress. -
AccessPick::{with_machine, with_network, with_image}: anAccessPickis built from
AccessPick::default()with them. -
Running::console_command(machine, user): the command that opens a console into any machine of
the run, a pure query that reads the run's record as status does (a Vagrant run's console gets the
host ports its start pinned). On a Terraform run:ssh -tto the machine's address as its user
(ssh_user_<machine>, elsessh_user, else root), through the run's SSH settings. On Compose:
docker compose execof the service. On Vagrant:vagrant sshof the define. A Compose service
inside the run's VM (docker-vm,cloud-docker, Proxmox) goes into that VM, then into the
service's container. A Terraform machine's console and a Compose service's start bash where the
machine has it, else sh; a Vagrant VM's isvagrant ssh's, the VM's login shell. As a named
account: withSome(user)(isothrum console --user <user> <machine>), the console opens as
that account of the machine, in its home, rather than as the one the way in logs in as (a
container's own user, Vagrant'svagrant, a module's SSH user). A container is entered withexec --userand a login shell (a Compose service in the run's VM too, through the VM); a VM, Vagrant's
or a module's, with asudo -ilogin as the account from its usual login (that login's own shell
when it is the account already). The machine must have the account, and on a VM its login must be
allowed tosudo, as a Vagrant box's and a cloud image's are. A name no Unix account has, and any
account on a Windows machine, is refused (unsupported).Noneopens it as the way in's login.
It writes nothing but the SSH configuration a way in over SSH needs, in the run's state folder,
wheresshruns: a way in that writes nothing leaves no run folder behind (status, by contrast,
readies the run's state: Compose's pinned-ports file, Terraform's plugin cache). -
Running::retire(machine, opts, events)(andisothrum retire): takes a machine out of the
running run ahead of its removal from the manifest, or its move out of the default start, and
the update after: what that update wouldn't remove itself. A Vagrant VM is destroyed alone
(vagrant destroy -f <define>: Vagrant forgets a define the Vagrantfile no longer has). A
Compose service is stopped and removed with the services in its namespace (docker compose [--profile access] rm -sf <service> <its sidecars>: an update touches no service outside the
default start, so one moved back to an on-demand machine would keep running); inside the run's
VM (docker-vm,cloud-docker, Proxmox) its container and the containers in its namespace go,
through the VM's way in. A machine a Terraform module makes is left to the update's apply. The
VM a run's containers run in (itsguest) is refused (unsupported: every container would go
with it), as is a machine there is nothing to take out of (no native name, a Compose service
with no VM to run in, an on-demand machine on a server or cloud). New operation in events:
retire. -
Running::update(andisothrum update): brings a running run up to date with its manifest,
regenerated since it started (a machine added to the run or taken out). It first restarts each
running VM whose boot-time settings changed, which Vagrant applies only when it boots a VM (synced
folders, networks, forwarded ports, size, provider settings): the run's record keeps each VM's
boot digest (the manifest'snative.boot_digest), and a VM whose digest changed is restarted
(vagrant reload <define> --no-provision, as stepreload-<machine>); a digest not known on both
sides restarts nothing. Thenup's steps run again with the params the run was started with, so
each engine converges, then the guests' secrets, the ready conditions and the outputs, as atup.
A machine that was running (runningorunhealthy) keeps what it was given: guest secrets go to
new machines only. The secrets checked are those the run's recorded provider needs, whatever
provider is given. The record then names the manifest the update ran (RunRecord::manifest, which
isothrum reapreopens) and its boot digests. The run stays ready meanwhile; a sealed run stays
sealed. New operation in events:update. -
A status while an operation holds the run, and Vagrant's machine locks. While any operation
holds the run (up,update,attach,detach,provision,park,resume, …), in this
process or another (its OS lock is held),statusdoesn't ask the tools, whose reads would
contend with the operation's: it reports the operation's state (starting), or the record's
(readywhile anupdateor anattachruns), with the machines as the runner last read them,
and the newStatus::livefalse. The runner keeps what it last read of a run in its folder
(seen.json), which adownremoves. Vagrant refuses a command at once when another process
holds a machine's action lock (MachineActionLockedError: avagrant upholds it while it
provisions, and on Hyper-V even a read of a VM's state takes it) or its index lock
(MachineLocked, taken at every read of a machine's state): avagrant global-status --pruneor
avagrant statuselsewhere could fail the run's status read, a start or adown, and a refused
vagrant upread as an earlier run's leftovers ("is locked"), so the run was destroyed and built
again. A call refused so is now tried again, two seconds apart, for up to a minute after the first
refusal, each new try said in aretryingwarning: the run's status reads, and its own Vagrant
commands (the manifest's steps; the driver's: the controller's boot and halt, a VM's restart, an
access machine's attach and a retire) when a second run does nothing twice, since a refusal can
come after Vagrant acted:destroy,halt,suspendand anupnot told to provision always;
provision,reload,resumeand a provisioninguponly when the refused try announced no
action on a machine. A refused try's lines don't count as stale markers. -
isothrum console <machine>andisothrum retire <machine>: the CLI's way to
Running::console_command(printed as one line to paste in a POSIX shell, or its JSON) and
Running::retire(with--paramand--secret-from-env, asupdate). -
ShellCommand::sh_line(): the command as one line a POSIX shell runs as it is, to paste in a
terminal:cd <its folder> && VAR=value… program args…, each word that needs it single-quoted
(ash -cscript with;or>, a JSON value), so the shell splits it back into the argv.
isothrum consoleprints it; before, the CLI joined the words unquoted and left the folder
out, so a pasted console broke. -
The CLI's
attachanddetachtake every run option:--param,--secret-from-env,
--host-label(a run recorded under another is refused,running_elsewhere) and--provider. -
The machine-setup backend holds UTM and its plugin to a version.
isothrum-machine's
provider detection reads each Vagrant plugin's version and UTM's own (its app bundle's
CFBundleShortVersionString), and reports UTM unavailable with the way out
when either is too old:vagrant_utmbefore 0.2.0 (it reads a VM's forwarded ports through a
script UTM refuses, and has nostartedstate) or UTM before 4.6.5 (the plugin refuses it). -
isothrum_runner::version_found(the version a tool prints, as Isoloom'sVersion, so a
pre-release comes before its release), which the runner's tool probes compare with the
manifest's minimum andisothrum-machineholds UTM and its plugin to (one it can't read holds
nothing back), and
isothrum_runner::vagrant_plugins(each plugin ofvagrant plugin list --machine-readable, with
its version), which the runner andisothrum-machineshare.isothrum-machinenow readsvagrant plugin list --machine-readable, not the human list. -
isothrum_runner::every_nameandfrom_wire_name: every name a unit enum is written as,
from its JSON Schema, and the value a name reads back as (the way back fromwire_name). The
command line offers Isoloom's clouds and providers from them, with no list of its own. -
Topology::vlan_lan(isothrum-compiler): the topology's key for a switch's VLAN, what a
network the spec nests under the switch's LAN came from. -
Status::any_present: whether anything of the run is there (a machine that isn't absent),
by the runner's own rule.
Changed
- A
.isoloom-reachfolder is the project's own. Isoloom no longer writes a machine's login
there, and its copy rules no longer name it, soproject.archiveandterraform.copy_module
copy one below the project's top as any other folder. - A Vagrant run offers no UTM. Isoloom's
vagrantrun manifest no longer listsutmamong its
providers (vagrant_utm gives a VM no private network), nor a UTM entry among a machine's
graphical consoles:--provider utmon such a run is refused as not one of the run's providers.
Adocker-vmrun still runs on UTM. - A drawing no target can run says so. The WASM plan's
problemshold Isoloom's
no target is possiblewith each form's reason (an access VM with ports and steps beside
containers only, say), and a target whose generation fails is inskippedwith Isoloom's error,
where a message or configuration it couldn't resolve was silently dropped. - Breaking: the Vagrant provider is Isoloom's
Provider, not a string.RunOptions::provider,
RunRecord::providerandStatus::providerareOption<Provider>, and
RunOptions::with_providertakes aProvider. Their JSON is the same wire name
(vmware_esxi), so a 1.x run's record still loads.isothrum --providertakes only a provider
Isoloom names, and says which on a typo, where any text was taken and refused deep in the Vagrant
driver. A Vagrant run asked for with no provider fails with the newError::NoProvider
(provider_refused, as before), naming the run's own providers; it readprovider: a Vagrant
run needs a provider…``, an empty name in backticks, and a list of its own. A provider that isn't
one of the run's names them too. - Breaking, for embedders:
isothrum-machine'sproviders::detectworks out whether the
account may manage Hyper-V itself, with the runner'swhoami /groupscheck through the process
runner it is given, now public asisothrum_runner::may_manage_hyperv. Itsmay_manage_hyperv
parameter is gone: the reason it existed (the check needingunsafeWin32) was wrong, and an
embedder that passedNonenever got the administrator reason. Runner::adoptis documented as the general feature it is: adopting an existing Terraform
deployment as a run. A deployment applied from a run manifest's module outside the runner
(by hand, by another tool, from a home that is gone), its local state in a folder of its own,
becomes a run of that manifest thatstatus,update,downand the reaper act on; its
rustdoc has an example, and State layout
says what moves. Its docs no longer speak of one embedder's old runtime.- Breaking: the machines of the environment a run manifest describes (not its router, jobs
or checks) have the rolemember(wastarget), as Isoloom 2.0 writes them; status and
readiness countmembermachines. - No more "lab" on the public surface. The command line's help (
attach,detach,
retire), the runner's rustdoc and the crates' keywords speak of runs; the real-run test reads
its run folder fromISOTHRUM_REAL_RUN_DIR(wasISOTHRUM_REAL_LAB); the tests' access machine
is a neutraluser. - Breaking:
Runner::eventsreturns anEventLog: itsevents, andunreadable, how many
lines of the log it couldn't read as an event. Those lines were dropped without a word, so a
1.x log'shook_failedwarnings vanished fromisothrum events, which now says how many lines
it couldn't read, on standard error. - Breaking: what was wrong with a call or a manifest value has its own error code, not
unsupported.unsupportedmeant both "this runner doesn't do it" and a mistake of the
caller's or the manifest's. Now anattach(ordetach) that names none of the run's several
on-demand machines fails with the newmachine_ambiguous(Error::MachineAmbiguous), one
whose network the run doesn't have with the newnetwork_unknown(Error::NetworkUnknown; 1.x
went on without it, its join's failure swallowed on Compose: see theattachentry under Fixed),
and a manifest value the runner can't use as written (a step'sverdictthat isn't a regular
expression, an SSH login, anssh.run'swait_s,hostorcommand, aproject.archive
without its folder, aterraform.copy_module'sfromorwhen) withmanifest_invalid
(Error::Malformed, naming the field).unsupported's message no longer says "by this runner
yet": much of what it refuses is a target's limit, not a missing feature. Also
manifest_invalidnow: a placeholder with no value in the operation (Error::Unbound, was
unsupported), atcpcondition whose port isn't a number (wasnot_ready), a guest
delivery to a machine whose address no output gives (wasunsupported) and a Terraform module
with no.tffile (wasio). - Breaking:
elevation_declinediselevation_failed(ErrorCode::ElevationFailed). The
code covered more than a declined prompt: an elevated session that didn't start, or whose
broker stopped before the operation ended. The message says which. - The runner reads its environment once, in
RunnerConfig::new. Vagrant's variables and
USERPROFILE, which it hands to an elevated Hyper-V session, were read from the process's
environment in the middle of each operation; they are read when the configuration is made, with
PATH,TF_PLUGIN_CACHE_DIRand the proxy, CA andTF_CLI_CONFIG_FILEvariables it now hands on
too (to that session, a containerised tool and the macOS relays), so an embedder that changes its
environment later builds a new one. An elevated session getsRunnerConfig::pathas itsPATH. - Breaking: Isothrum reads run manifest version 1. Isoloom froze its run manifest at
manifest_version1, with a compatibility policy (isoloom_manifest::COMPATIBILITY: changes
within a version are additive; a reader refuses what it doesn't know) and a schema published at
https://raw.githubusercontent.com/isoloom/isoloom/main/schemas/run-manifest-v1.schema.json.
Isothrum reads version 1 only: arun.jsonof version 0 (what Isoloom 1.x writes) or 2 fails
withmanifest_versionbefore anything runs. Regenerate the files with the matching Isoloom; a
run 1.x brought up is taken down with 1.x, or its files are regenerated in the same folder.
Version 1'stimeout_sandprovider_timeout_smean time without progress, as Isothrum already
counts them (below). Policies pairs each release with
the version it reads. - Breaking: an event's
runandopare optional.Event::runis anOption<String>and
Event::opanOption<Op>, and the event schema no longer requires them. Every event of an
operation has both. An event written outside one has those its writer knows. The command line's
own: a command line it refused has neither;reap's closingresultanderrorhave neither;
any other failure before an operation began hasopwhen the command is an operation, andrun
when--namenamed it (a failure ofstatus,console,eventsorkeyhas noop). - The exit codes 130 and 143. A command cancelled by SIGINT exits 130, by SIGTERM
143 (it was killed by the signal before). 0, 1 and 2 are unchanged. - Breaking: an elevated session's files. A request's variables are no longer in its
req-<n>.json: they are inenv-<n>.json, which the broker reads and deletes before it starts
the program, and each request carries the session's random token and its number, without which
the broker deletes it unrun. Only a broker of this release reads them. - Breaking: A step's timeout counts time without progress, not its total time. A docker-vm run's
docker compose up --pull missingpulling a 1.1 GB image through a slow VM network was still
downloading when its 30-minute step was stopped. A step'stimeout_s(and every limit the
runner gives its own tool calls) is now the time it may go without progress: a new output line,
on either stream, that isn't blank and isn't one of the last 64 distinct lines it printed. A
stuck pull repeating36b0bafd7f97 Downloading 1.121GB(or several layers taking turns) makes
none and is stopped at its limit; a step that prints nothing is stopped at its limit from its
start, as before. A step that keeps printing new lines stops at a ceiling, six hours after its
start (or its own limit, when that is longer): no step stops sooner than before.
Error::Timeoutnow holds aTimedOut(NoProgress { after_s, last_line }or
Ceiling { after_s }) in place ofafter_s, its message says which (step `up` showed no progress for 1800s and was stopped; its last line: `…`), and theerrorevent carries it as
timeout({"kind": "no_progress", "after_s": 1800, "last_line": "…"}, or
{"kind": "ceiling", "after_s": 21600}), secrets masked. - Breaking:
EventKind::Errorhas atimeoutfield (Option<TimedOut>, the limit a step
ran into when the code istimeout). The variant isn't#[non_exhaustive]: a pattern or a
literal written without..no longer compiles; addtimeout(None) or... - Breaking:
ShellCommand::cwdis aPathBuf, no longer anOption: a console always runs in the
run's folder. - Breaking:
Running::attach(pick, opts, events)andRunning::detach(machine, opts, events)take the
operation's options (secrets and params an attach or detach that changes the run needs; pass
RunOptions::new()for none), with the access machine's choice as anAccessPick, in the
orderretireandprovisiontake theirs. - Breaking:
Running::park(mode, opts, events)takes the operation's options, asdownandretire
do (passRunOptions::new()for none). It ran on empty options: a run recorded under another
host label was parked anyway (nowrunning_elsewhere), and its steps had no params given.
isothrum parktakes--param,--secret-from-env,--host-labeland--provider, as
downdoes. Its SSH goes through the jump the run recorded atup, as every later
operation's does. - Breaking:
isothrumtakes--ssh-jumponuponly.up,down,resume,checkand
provisionaccepted it, but only anupthat starts the run reads it (it is recorded; later operations go the
same way): given anywhere else it did nothing.RunOptions::ssh_jumpsays so. - Breaking:
RunnerConfig::newhonours the user's Docker context. It started processes with
SystemProcesses::default(), which never read the Docker CLI's config, so a runner built with
it didn't dropDOCKER_HOSTandDOCKER_CONTEXTfor a context the user picked.
SystemProcesses::default()now reads$DOCKER_CONFIG/config.json, else
~/.docker/config.json;SystemProcesses::newandSystemProcesses::for_hostare gone (use
SystemProcesses::default()). - Breaking:
isothrum-machine'sproviders::Provideris Isoloom'sisoloom_manifest::Provider,
re-exported, in place of a copy of it. It is exhaustive (amatchneeds no_arm), and
Provider::idis gone: a provider's id isisothrum_runner::wire_name(&provider), the one
helper that names a manifest value as the manifest writes it. - Breaking: The process port has one path, the crate root:
Exit,ProcessError,OnLine,
captureandCapturedjoinProcessRunner,ProcessSpecandSystemProcessesthere, and the
processmodule is internal. Thefakefeature's test doubles are
isothrum_runner::fake::{FakeProcesses, Reply, Call}(wereprocess::fake::…), andCallis
ProcessSpec(a type alias): a call is recorded as the spec it ran, its fields andline()and
env(name)as before.Captured::exitis anExit(was anOption<Exit>: a process that ran
always has one), andCapturedno longer implementsDefault. - Breaking:
AttachReportis#[non_exhaustive](the newAccessPicktoo): build an
AccessPickfromAccessPick::default()with itswith_*methods. - Breaking:
ShellCommandis#[non_exhaustive]: build one withShellCommand::new(program, args, cwd)andwith_env; its fields stay readable. isothrum-machine's reasons speak to no particular program: no "then re-check" or "run
this program as administrator" ("run elevated").- Breaking:
ProcessError::Refusedcarrieswhyin place ofarg: besides an argument a wrapper
would mangle, it now refuses an elevated call's standard input. - Breaking:
Error::PortInUse'sprotocolis aString(was&'static str), the protocol as the
manifest writes it (tcp,udp). - Breaking:
plan_json,own_files_jsonandchoices_jsonthrow if their JSON can't be written. Each
had its own stand-in ([],{}, a failed plan, else an empty string); they now return
Result<String, JsError>. JavaScript gets the same string as before, and in the case that can't
happen, anErrorsaying why. - Breaking: the compiler's
Error::ReservedLanNameisReservedName: a switch's name may not
start withlink-either. Its message says the prefix is the cables'. - Breaking:
own_files_jsonthrows for a topology that is malformed or doesn't compile.
It returned[], so a caller couldn't tell a project with no file of its own from a drawing
that failed; it now throws anErrorsaying why, asbundledoes. cargo run -p xtask -- wasm --copy-to <dir>replaces--website <dir>: it copies the
files a page serves (isothrum_wasm.js,isothrum_wasm_bg.wasm) into the folder given, not
into a site'spublic/wasm/.
Removed
- Breaking: The run-manifest values Isoloom never writes are no longer read: a
staticoutput, an expiry in
minutes from now, a dotenv state file and a WinRM guest delivery (Isoloom 2.0
removes them from the manifest). - Breaking:
Running::shell_command. The access machine's shell isRunning::console_commandof its
name (theAttachReport'smachine), which starts bash where the machine has it, else sh.
isothrum shellgoes with it: useisothrum console <machine>. - Breaking:
isothrum-machinekeeps only provider detection. Its public API is now
providers::{detect, Provider, ProviderStatus}(the crate root no longer re-exportsProvider
andProviderStatus: import them fromproviders). The download record's format goes too
(storage::{Owned, OwnedBox, OwnedImage, load, parse_box_list}): half a format, its writer in the
one program that keeps the record, which now holds all of it. Gone, with no user left: the
self-tests (selftest,BootProbe,ElevatedBoot,Vm), the system report (system,
SystemReport,DockerDenied),Dirs,ErrorandResult, the record's writing half
(storage::{path, update, Before, image_ids, box_list, claim, tracked}), and the provider helpers
parse_plugins(replaced byisothrum_runner::vagrant_plugins), andkvm_problemand
HYPERV_NEEDS_ADMIN(still used insidedetect). Theexec
module is internal (no longerpub). - Breaking: The
Hooksport (Hooks,HookError,RunView,Runner::add_hooks) and the
hook_failedwarning code (gone from the event schema): nothing implemented it. An embedder
that keeps something beside a run does it around the operation's call. - Breaking:
RunRecord::manifest_sha256andLoadedManifest::sha256: nothing read the hash. A record
written with it still loads. - Breaking:
Serializeonisothrum-machine'sProviderStatus(camelCase; 1.x derived no
Deserialize): nothing wrote it as JSON (an embedder reads its fields).Provider, Isoloom's
own, keepsSerializeandDeserialize. - Breaking:
isothrum-machine'sProvider::Proxmox. There is no Vagrant Proxmox provider in use:
detection always reported it unavailable (vagrant-proxmoxnot installed). A Proxmox run is
Terraform's. - Breaking:
isothrum-machine's cloud providers.Provider::{Aws, Azure, Gcp, DigitalOcean, Linode, Oci}andProvider::is_cloudgo: detection never reports them (a cloud run is Terraform's, not
a Vagrant provider's). - Breaking:
isothrum-machine'sProviderStatus::available: it wasreason.is_none(); read that. - Breaking:
Compiled::yaml,Compiled::specandCompiled::files.
Compiled::into_partsgives the spec's text, with the spec and the project's files. - Breaking:
Runner::in_flightis internal now: no embedder read it. - Breaking:
isothrum-wasm's Rust functions are internal:plan,bundle_bytes,
bundle_target_bytes,own_filesandchoices, withErrorandResult. Only the
#[wasm_bindgen]exports called them; those stay, as doPlan,FileandChoices, the
shape of the JSON the exports hand out. - Breaking:
LoopbackPortsandHeldPortare internal, andHostPortsis sealed, its
questions (in_use,pick_free,reachable) the runner's own. No embedder implemented
HostPorts, asked it anything or named the loopback's:RunnerConfig::newinstalls it, and a
test setsRunnerConfig::portsto thefakefeature'sFakePorts. - Breaking:
RunnerConfig::with_processesandRunnerConfig::with_ports. SetRunnerConfig::processes
andRunnerConfig::portsonRunnerConfig::new(home), as every other field is set. - Breaking: Runner API nothing used:
Running::outputs(readRunning::status'soutputs),
RunnerConfig::with_clock,JsonLines(anEventssink is a closure),FixedClock(now the
crate's own test double), andthis_host(now internal). - Breaking: More API nothing used. In
isothrum-runner: theClockport (Clock,
SystemClock,RunnerConfig::clock: the runner reads the system's clock),Recorder,
EVENTS_VERSION(an event'sv),Running::path, and the test doubles'FakePorts::take,
FakePorts::answer,Reply::afterandFakeProcesses::lines(readFakeProcesses::calls), and
From<Option<i32>>forprocess::Exit(useExit::new). Inisothrum-machine:
Provider::ALL,Provider::is_remote, andProviderStatus'sremote,hypervisor,pluginand
plugin_installed(itsreasonsays what is missing or too old). - Breaking:
Occupied::Refuse: no embedder asked for a parked run to be refused.
Occupied::ResumeParked, the default, resumes it;Occupied::Replacestarts afresh; without
Replace, a run that is up is still refused withalready_running. - Breaking: The ad-hoc access machine. The runner no longer stands up an access machine
of its own when a run declares none: no<run>-isoloom-accesscontainer on the run's network
(on this computer's Docker, inside adocker-vmVM, inside a VM Terraform made on a server or
a cloud), noisoloom-accessVM added to a VM run through Isoloom's Vagrantfile hook (gone from
Isoloom's Vagrantfiles too), and status no longer probes for one;downandparkno longer
reap one.attachanddetachon a run that declares no access machine now fail with
unsupported("this run declares no access machine"). A run's own on-demand access machine
(access: true: a Compose service in theaccessprofile, a Vagrant define) is unchanged. An
embedder adds a machine of its own as an ordinary machine of the run, withRunning::update,
Running::retireandRunning::console_command. - The desktop app's crate,
apps/desktop. The Isothrum desktop app lives in its own
repository,isoloom/isothrum-desktop, which ships it (installers, the Microsoft Store) on the
crates published from here; the copy kept here "while that move completes" had drifted from it
and had no release path. Its CI jobs, the installer workflow and its place in the xtask
dependency rule go with it. The 2026-11-08 fixes made on that copy (the editor's command names,
the hypervisor choice, UTM's state) are in the desktop repository.
Security
- tokio 1.54.1 in
Cargo.lockand in the fuzz crate'sfuzz/Cargo.lock, past RUSTSEC-2026-0386
(AsyncFd's constructors; Isothrum doesn't call them).cargo denyallows crates.io only,
Isoloom's git repository no longer, andcargo run -p xtask -- fuzz --check(a CI and
CONTRIBUTING gate) runs it over the fuzz crate's lockfile too. - rustix 1.1.6 in
Cargo.lockandfuzz/Cargo.lock, past RUSTSEC-2026-0486 (its
extended-attribute functions given a zero-length buffer), whichtempfileandtar'sxattr
reach.