Hosted macOS runners run without nested virtualization: kern
.hv_support is 0 and no binary can boot a VM there, entitlement or
not. The macos job keeps the full compile gate and asserts the probe
reports the known HVF absence with a working engine load, so any
other regression still fails; the boot e2e stays on Linux and real
Macs.
The one-command install edit had replaced the whole macOS section
instead of just the install lines, dropping the from-source sign
snippet; it is back, with the current entitlements path and
libarchive in the brew line. Homebrew's tap-trust gate now leads the
macOS install: dependencies from third-party taps stay refused until
their tap is trusted once, and libkrun ships from its own tap.
All consumers live in packaging already: the formula codesigns at
install, and the CI and release workflows codesign the built binary.
The macOS dir held nothing else.
The tap repo is a publish target like the snap store: the release
workflow rewrites the in-repo formula's url/sha256 for the release
tarball and pushes it to homebrew-ecrou. No token, no update — tags
still release.
SPEC records the two macOS facts the code bends around (bare-soname
dlopens needing startup-time DYLD paths; the hypervisor entitlement)
and the packaging decisions; README gains the brew install and the
from-source sign step.
The payload's stdout is a serial console, so fastfetch reads a tty
and decorates its output with cursor escapes; the ^OS: anchor
matched nothing and would have failed a green run. --pipe --logo
none prints a plain key-value list, and a sed strips anything left
before the grep. SPEC records the first measured apt wall time: 24 s
cache-warm, boot included.
The snap only got built at tag time, so the pack rotted between
releases; ci now packs it on every change, the same action the
release workflow uses, and boots it twice: an alpine uname check
that proves the machine virtualized under strict confinement, and
apt update + install fastfetch on ubuntu, which is T8 — a real
package install through the TSI proxy, the staged e2fsprogs, and
the exit-code path, with the wall time as the standing
re-measurement of the ln driver's throughput.
Resolution ran before the cache was consulted, so every run of
alpine:3.23, ubuntu or any floating alias hit the CDN or
changelogs.ubuntu.com, and an offline run of a cached image died in
resolution instead of booting. The boot path now asks the cache
first: an alias boots the newest cached entry of its distro, within
the series or the edge flavor it floats in, and resolves upstream
only on a miss; pull and --no-cache still resolve upstream.
Exactly-named references (Ubuntu YY.MM, known codename, Alpine X.Y.Z)
resolve offline on every path; resolve_source folds into the policy,
which lives with the cache it consults.
The design paragraphs follow the code: the rootfs crosses as a raw
ext4 image built from the tarball, ownership is real inside the
machine, provisioning rides debugfs, and the control file mints the
exit code because the engine's own channel is virtiofs-root only.
The confinement budget admits the e2fsprogs children and cp, the
snap section stages e2fsprogs and names the blk feature, and the
boot-economics row counts an image copy instead of an extraction.
The runtime runs images; building them belongs to the OCI ecosystem —
CI builds, digests, scanning — and a keyed local layer could never
offer a digest. Provision also installed packages through TSI, whose
per-stream throughput makes a real package install crawl exactly at
first use, and the bare-command shape mutated the shared cache entry
under the image's own name.
0.1 runs images: run, pull, probe — distro aliases, OCI references
with digest pins, local rootfs archives. The one honest idea provision
carried stays in SPEC's Later: provision by booting, for locked-down
hosts that cannot run any builder.
The keyed-layer design shipped: provision --spec hashes the spec with
its base reference and --layer names the entry for run --layer. The
row was describing the past as the future; what remains is eviction.
The code is the source of truth now; what stays is the part code
cannot carry — the founding rule and confinement budget (fixed: the
budget said no child process, the machine child is one), the run
contracts (exit codes rewritten to match the engine-failure and
reserved-code semantics), the trust model, the falsification tests,
and the roadmap.
Cut: the engine call list, the RunOptions listing, the CLI flags
table, the cache layout tree, the workspace/module map, the ecr
porting table, and the open questions — all answered or all mirrors
of code that answers them faster.
Fetch or refresh an image's cache entry without booting it. The
command is the CLI face of the run/pull split: runs never contact a
registry on a cached image, pull does - reporting the entry path
and whether anything transferred. --no-cache forces a redownload.
SPEC documents the command and the run/pull trust model.
A run blocked on the cache-entry flock was silent: the wait went to
the log sink, which renders nowhere by default, so the CLI read as
a hang. The Progress trait gains message() - a status line outside
the counted-operation protocol, defaulting to a no-op so existing
sinks keep compiling - and the flock wait reports what it is
waiting for. The CLI renders messages on the active bar and spins
one up when no operation is active.
Docker pulls ubuntu@sha256:... and repo:tag@sha256:...; ecrou
errored on the '@'. parse_oci_ref now peels the pin (which contains
the reference's only legal post-slash colon), validates it as
algo:hex, and the manifest is fetched by digest - the tag, when
co-present, is ignored the way docker ignores it. Pinned
references are immutable by definition: the :latest freshness
check skips them, and the cache filename carries the pin so a
tagged ref and its pin never share an entry. Pull parameters
gather into registry::OciPull.
Config::load was never called: the DNS-override story it carried
contradicts the actual resolver flow, and nothing in the CLI or
snap advertised a key. Delete the module and every mention of the
ecrou.yaml file in SPEC and the snap; the XDG_CONFIG_HOME redirect
in the command chain served only that file.
What a downloaded image is trusted to be, and where verification
stops: TLS as the only anchor for manifests, digest-checked blobs,
advisory distro checksums, cache entries trusted once written.
Concurrent runs of one image are safe, not efficient.
The run interface read like a library's. `ecrou run -it alpine` now
hands the guest a terminal: with no console output file, libkrun wires
the guest console to the process's own stdio and raw-modes the tty.
The parent saves and restores the termios around the run, because
libkrun's own restore lives in the child's exit observers, which a
SIGKILLed machine never runs. A tty run refuses to start without a
terminal on stdin and stdout, and --capture refuses to combine with
-t: the payload's streams cannot land in files and on the terminal at
once.
-i without -t keeps the machine's stdin open through pipes: libkrun
exposes the non-tty streams as virtio console ports (krun-stdin,
krun-stdout, krun-stderr), and the guest init points the payload's
stdio at whichever port nodes exist. `echo hi | ecrou run -i alpine --
cat` crosses the machine; a stream the host left on a tty has no port
node and stays on the console.
-v (--volume HOST:GUEST[:ro]) replaces --share, and -w aliases
--workdir. The global -v/--verbose loses its short flag to make room;
verbose stays reachable by its long form.
A reference starting with ./, ../ or / names a local rootfs archive:
read in place on every run, never downloaded, never cached as a
pristine entry. Provisioning over one persists into a derived cache
entry keyed by size and mtime, so the caller's file is never
overwritten and an edited archive lands in a fresh entry.
prepare_provisioned and persist were sitting there with no consumer:
the machinery for "install once, every later boot carries it" that
one-shot machines cannot do on their own - packages, users, anything
short of a share - and no way to reach it.
ecrou provision IMAGE -- COMMAND is the missing surface. It boots a
machine over a freshly prepared rootfs, runs the command, and
persists the tree into the cache entry, marked provisioned; a marked
entry is extracted as-is on every later call, the command skipped.
A failing command leaves the entry unprovisioned for the next
attempt, and --no-cache re-downloads before re-provisioning.
The engine behind run splits out as vm::run_prepared: the boot half
of run, taking the rootfs the provisioner is about to persist. run
itself is prepare plus that.
One cache fix the provision flow surfaced: a digest refresh replaced
the tarball but left the old .provisioned marker, so a fresh pristine
image would present as provisioned and skip provisioning for good.
The marker now dies with the entry it belonged to.
The console is one stream, kernel and guest mixed, so a consumer that
needs the payload's stdout and stderr apart - rhema's sandbox seam
reports them as separate fields - could not get it. With capture set,
the run adds the directory as an internal rw share (guest path
/.ecrou-io, reserved) and the init wraps the payload in a redirection
group pointing at it: the streams land host-side as DIR/stdout and
DIR/stderr, written live, owned by the host user, and there when run()
returns. The console stays on stdout for diagnostics; the CLI exposes
the flag as --capture.
RunOptions carried quiet/verbose, which are flags, not engine state.
They are now a LogLevel enum (Quiet/Normal/Verbose) mapped onto
libkrun's filter; the CLI maps its flags onto it. The forked child's
last-gasp boot diagnostics also stop going straight to stderr: they
land in the console file the parent streams, with stderr kept for the
case where the console was never configured. lib.rs now states the
crate contract: no UI in the library, progress through the sink,
logging through the facade, typed errors out.
'ecrou run alpine -- "apk add x && y"' reached the guest as one argv
element and execvp'd a program named after the whole line. One
argument containing whitespace is a quoted shell line: hand it to the
image's shell as 'sh -c'. Argv vectors stay literal, including ones
that begin with sh -c.
QEMU was the committed v1 engine with libkrun later; that order is
flipped. libkrun deletes the initramfs pack, the kernel fetch, slirp
config, and the uid mapping problem, and its VMM runs in-process, which
collapses the kill chain. Verified against the stable-1.19.x headers:
eleven-call v1 surface, krun_set_workdir native, mapped volumes dead on
Linux (tagged virtiofs plus guest init mount instead), KVM required
with QEMU as the later TCG fallback. Snap spike now stages libkrun
plus libkrunfw instead of qemu-system.
ecrou descends from ecr: the microVM runtime that survives strict snap
confinement. The founding rule (no host mounts, no namespaces, every
crossing a file or a socket) is stated with its syscall budget, sourced
from snapd's seccomp allowlist; execution model, exit-code propagation
through a private control share, kill-chain cancellation, the spike-snap
gate, and seven falsification tests round out the v1 design.