Commit Graph
30 Commits
Author SHA1 Message Date
vhaudiquet a49cba488b fix(progress): close the sink on failed transfers
A download that died mid-stream (read error, digest mismatch)
returned before progress.finish(), leaving the sink with an open
operation — a lingering progress bar in the CLI and a broken
begin/finish pairing for any other sink.
2026-10-29 23:56:38 +01:00
vhaudiquet e4f712b782 refactor(download): one blocking HTTP stack, no tokio runtime
The library is blocking end to end — vm::run waits on the machine
for the payload's whole lifetime — so the async interior bought no
concurrency (OCI layers download sequentially) and nothing any
consumer shape could use; each call rented a tokio runtime with
'Failed to create Tokio runtime' as a user-visible failure mode.
download.rs and registry.rs move to reqwest::blocking, folding the
runtime wrappers away; distro.rs joins the same client shape,
which also gives its metadata calls their first timeouts. The
shared client bounds the connect and, for small metadata bodies,
the whole request; transfers stay unbounded (the blocking client
has no per-read window; a stalled stream is the operator's
Ctrl-C). tokio and futures-util drop out as direct dependencies;
tokio remains transitively under reqwest.
2026-10-29 23:56:20 +01:00
vhaudiquet a758689bbe refactor(vm): split machine-process supervision into supervise.rs
vm.rs composed run options and supervised the machine process in
one file. The process half moves out: the fork that enters the VM
loop, the console relay, the wait and exit-status decode, and the
termios save/restore. run_prepared keeps the composition and calls
into the new module. No behavior change.
2026-10-29 22:54:05 +01:00
vhaudiquet 3e839cb5ca refactor(rootfs): split the tarball packer into pack.rs
rootfs.rs mixed the cache lifecycle with the tree-to-tarball
writer that persisting relies on. The packer moves to pack.rs
beside extract.rs: pack_rootfs becomes crate-internal, its
partial-cleanup test moves with it. No behavior change.
2026-10-29 22:52:08 +01:00
vhaudiquet aa95d05f41 refactor(download): split the OCI registry client into registry.rs
download.rs carried two clients in one file: the plain transfer
plumbing and a full OCI Distribution Spec client (token auth,
manifest lists, layer blobs). The registry half moves to its own
module; download.rs keeps hashing, checksum files and file
transfers. fetch_oci_digest moves with it; rootfs imports it from
the new home. No behavior change.
2026-10-29 22:50:49 +01:00
vhaudiquet bb61be4625 fix(download): bound the metadata clients like the transfers
The freshness digest check and the distro checksum fetch kept the
total 30 s request timeout the transfer clients just lost. They
move to connect + read windows, matching the one policy.
2026-10-29 22:49:09 +01:00
vhaudiquet 47c9208aab refactor(spec): drop the rhema references
The spec file is a plain provisioning spec, not a .rhema/ artifact;
the test base image needs no personal registry name.
2026-10-29 22:47:05 +01:00
vhaudiquet 1223512729 refactor(rootfs): remove the unused PreparedRootfs accessors
arch() and cache_entry() have no callers; the fields feed the
module's own flow.
2026-10-29 22:47:05 +01:00
vhaudiquet 7461b20349 refactor(krun): remove the unused kvm_available probe
No caller: the CLI probes through kvm_probe, which reports the
failure mode instead of a boolean.
2026-10-29 22:45:13 +01:00
vhaudiquet 006fce632b refactor(config): remove the unwired config module
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.
2026-10-29 22:45:04 +01:00
vhaudiquet 12148ac951 refactor(utils): remove the QEMU-era helpers
validate_memory_string predates vm::parse_memory_mib and nothing
calls it; the qemu_* name mappers and their tests served the QEMU
engine that never shipped. The Arch variants stay: they feed the
debian/OCI/Alpine name mappings and the host-arch detection.
2026-10-29 22:44:51 +01:00
vhaudiquet d9d9b1c99e docs: state the image trust model
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.
2026-10-29 22:42:28 +01:00
vhaudiquet 185ffeeb3d fix(error): let the typed variants survive the anyhow conversion
KvmUnavailable and ArchNotAvailable were raised as anyhow context
strings and wrapped into Other at the public boundary, so the
matching lib.rs documents never fired. They now ride at the top of
an anyhow chain and the manual From recovers them by downcast;
KvmUnavailable carries the open(2) error as its source instead of
swallowing it.

This changes the API: KvmUnavailable is a struct variant.
2026-10-29 22:42:21 +01:00
vhaudiquet 56af198d54 fix(rootfs): make cache writes safe against concurrent runs
Downloads, OCI bundle packing and rootfs packing all wrote through
one fixed partial name, so two processes preparing the same entry
interleaved bytes and renamed the wreckage into place for good.
Each writer now gets a unique partial file (utils::partial_path)
and the last atomic rename wins with whole bytes either way; the
layer pointer is written the same way.
2026-10-29 22:42:12 +01:00
vhaudiquet 6835056be9 fix(download): drop the total request timeout from transfers
The 300 s client timeout covered the whole body stream, so any
download slower than a couple of MB/s died mid-transfer. Connect
and per-read windows catch dead peers and stalls instead; slow
transfers now finish.
2026-10-29 22:41:59 +01:00
vhaudiquet 8ceab3130d fix(vm): save the tty before the fork and retry waitpid on EINTR
The termios save ran after fork, so a child that reached raw mode
first would be captured and later restored as-is. A caught signal
interrupted waitpid, failed the run, and left the machine running;
EINTR now retries the wait.
2026-10-29 22:41:50 +01:00
vhaudiquet 1e69044198 fix(extract): resolve whiteout paths like every other entry
Whiteout markers trusted the archive's own path, so a crafted
`.wh.` entry carrying `..` could delete host directories above
the rootfs. Both branches now go through `safe_join`, and a
`.wh..` marker, whose target is the parent directory itself, is
refused with them.
2026-10-29 22:41:43 +01:00
vhaudiquet 7c50b19c09 ci: add testing and snap release workflows
Testing runs the pre-commit trio on pushes to main and PRs.
Releasing builds the snap on plain vX tags and publishes it to the
stable store channel; rc tags fail the version gate and leave the
publish job skipped.
2026-10-29 22:08:06 +01:00
vhaudiquet 8a0521ae76 build(snap): trim the description and part comments
The description carried a confinement section SPEC.md already owns,
and the part comments narrated build steps the commands state on
their own. First run now shows a plain run instead of the microVM
proof.
2026-10-29 21:47:25 +01:00
vhaudiquet ed5786c4b5 docs: slim the agent conventions
The founding rule restated SPEC.md, which stays the source of truth
for the confinement budget. Commit bodies gain a length cap, and a
commit must build and work on its own; bisectability outranks the
one-component-per-commit split when the two collide.
2026-10-29 21:47:19 +01:00
vhaudiquet 062fdca59d docs: add a README
How to install and run the snap, and how to embed the library: one
RunOptions example covers a basic machine. The logo lives under
.github/img.
2026-10-29 21:47:14 +01:00
vhaudiquet 4a0c613ecb feat(cli): docker-style run flags and local rootfs references
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.
2026-10-29 18:08:30 +01:00
vhaudiquet 719a8209e0 build(snap): package ecrou as a strictly confined snap
core26 base, strict confinement, the kvm interface for acceleration
with home and network auto-connected. The runtime needs nothing else:
the host process never mounts a filesystem and never creates or
enters a namespace, so it already lives inside snapd's default-deny
seccomp allowlist. The built .snap artifact is ignored.
2026-10-29 18:06:54 +01:00
vhaudiquet 878ce29b8d feat(cli): provision layers from a spec file
A provision command baked by hand - `ecrou provision alpine -- apk
add ...` - cannot be reviewed or shared: the artifact worth keeping is
a diff in git, not a shell history line. A spec file is that diff.
`ecrou provision --spec image.yaml` parses base, packages and a run
block, derives the provisioning script from the base's own package
manager (apt, apk, dnf, discovered at provision time on the running
machine), and bakes the result into a cache entry keyed by the spec's
content hash: the same file never provisions twice, an edited file
never reuses the old layer, and the key hashes the raw bytes so the
layer is stable across ecrou versions.

`--layer NAME` names the entry, and `ecrou run --layer NAME` boots it
directly - no image resolution, no download; the layer is whatever
the provision call persisted under that name. A keyed entry has no
tarball of its own, so the base downloads only on the first
provisioning and every later run extracts the finished layer.
2026-10-29 18:06:50 +01:00
vhaudiquet dad5f61833 docs(spec): keyed provisioned layers under Later 2026-10-29 00:13:48 +01:00
vhaudiquet d9f9c79dc4 feat(cli): ecrou provision bakes a command into a cache 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.
2026-10-28 16:20:37 +01:00
vhaudiquet 1fb3f0c076 fix(extract): modes degrade instead of killing the unpack
With preservation on, the crate chmods every file to the mode it
ships as it extracts, and one refused bit killed the whole run:
alpine's usr/bin/chage ships setgid-shadow (02755, group shadow),
and an extracting context that cannot set the bit - an overlayfs
tmp, a confined sandbox - fails the chmod with EPERM and takes the
boot down with "failed to set permissions to 2755".

The crate's own chmod now runs with the special bits masked off,
which no filesystem refuses, and the shipped modes ride the fixups
like directory modes already do: applied after the walk, degraded
to plain rwx when the filesystem refuses them, exact everywhere
else. Unprivileged tar degrades the same way; the guest re-applies
whatever it needs. The setgid regression test pins the round trip.
2026-10-28 15:30:30 +01:00
vhaudiquet 23fcd713b6 feat(vm): capture the payload's output into a host directory
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.
2026-10-28 13:37:33 +01:00
vhaudiquet db2658f385 docs: drop the removed --arch flag from the CLI table 2026-10-28 13:20:08 +01:00
vhaudiquet 74c3744d39 fix(cli): remove the --arch flag
KVM runs only the host architecture, so the flag could not mean what
it suggested: a mismatched value downloaded a foreign rootfs that
extracted cleanly and died at boot with a cryptic guest-side exec
error. The flag's real job, picking the image variant to fetch, is
the library's; RunOptions.arch keeps that role and an empty value
still resolves to the host architecture.
2026-10-28 13:20:02 +01:00