Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail Ckclackatoo PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

Peiso

The Peios image builder — from a small spec naming an edition to a composed root, medium repository, packed initramfs, UKI and bootable ISO, without privilege.

Single-page view · as markdown

Building a Peios image

Peios / Peiso / Building images

peiso is the Peios image builder. Given a spec of a few lines — which edition, where its packages come from, a couple of flags — it produces a bootable installation medium: a UEFI ISO carrying a live Peios and everything needed to install it onto a disk.

It is a user-facing tool. Building a slightly customised Peios image is meant to be an ordinary thing to do, not a project: you should never have to list packages, name registry seeds, or run anything as root.

The release is a package #

The thing peiso builds from is not a list. A Peios release is an edition package — dev.peios.peios-experimental today, discoverable to image builds through its peios-experimental compatibility capability — whose version is the OS version, whose dependency closure is the base system (kernel, initramfs, services, firmware, the installer), and which ships the system's identity (/usr/lib/os-release) and a small data file, release.toml, stating what the release asks of a system beyond its packages. Editions explains that package in full.

peiso's job is therefore small: resolve that one package, compose its closure, and add what makes the result a medium rather than an installed system. An installed system moves to the next release by upgrading the same package, through upgrade-peios.

What peiso adds to an edition #

An edition describes an installed Peios. A live medium needs three more things, and they are peiso's, not the edition's:

  • The boot flavour. first_boot = "live" (the default) adds live-boot, whose initramfs hook finds the medium and mounts the live root from it. An installed system carries disk-boot instead; the installer swaps one for the other on the target. Neither belongs in the edition, because which one a system carries is a property of the medium it boots from.
  • The medium repository. The installer takes the target's boot packages from a small signed repository carried on the ISO. peiso publishes it per build, with a throwaway signing key.
  • Devtools. dwe = true adds the Developer Workstation Environment: dwed running as SYSTEM, reachable over vsock. It makes the image ownable, which is why it is a property of a development medium and can never be installed as a mere package.

Everything else in the image comes from the edition.

Where the stages happen #

flowchart LR
    S["spec<br/>(edition, source)"] --> R["root/<br/>(peipkg compose)"]
    R --> M["repo/<br/>(medium repository)"]
    R --> Q["seeds<br/>(release.toml → autoapply.d)"]
    Q --> I["initramfs cpio<br/>(mkirf)"]
    I --> F["rootfs.squashfs"]
    I --> U["UKI<br/>(mkuki)"]
    F --> O["ISO"]
    U --> O
    M --> O

The root is composed by peipkg's compose library — the same code as peipkg-compose, driven in-process. The initramfs and the UKI are packed by mkirf and mkuki, the applets shipped inside the root, so the tools that build the boot artifacts are the ones the running system carries. The build pipeline walks every stage.

No privilege #

Nothing in a peiso build runs as root. The old builder chrooted into the composed root to run the shipped applets; peiso runs them on the host under the root's own dynamic loader instead. Where a package would have a security.* attribute set on a file — the kernel's firmware signature, security.peios.sig — compose hands the signature to peiso and peiso writes it into the squashfs directly, where placing an attribute needs no capability. The composed root/ on the build host is only an intermediate; the image is the artifact, and the image is complete.

That property is what lets a build run on any machine, in CI, or by any user, and it is why sudo appears nowhere in this anthology.

What it is not #

peiso does not build packages (that is pekit), does not resolve or fetch them itself (compose does), and does not decide what a Peios contains (the edition does). A spec can add packages, files, first-boot scripts, features and medium contents on top of the edition, adjust the packers, and choose the boot flavour and devtools — Customising an image — and every addition is recorded beside the build. What the release is remains the edition package's to say.

Quick start

Peios / Peiso / Building images

This page builds the Peios Experimental medium and boots it. It assumes a checkout of the Peios tree with a populated signed package repository (pkgs/_repo2_), which supplies the edition and its closure.

What you need on the build host #

  • peiso, built from peiso/ in the tree (go build -o peiso .), or on your PATH.
  • squashfs-tools, xorriso, dosfstools and mtools — the host-side packers. Everything Peios-specific (the initramfs and UKI tools) is taken from the root being built.
  • For booting: QEMU with KVM and the OVMF firmware.

No root. If any step asks for it, that is a bug.

The spec #

dist/release/experimental.toml:

[[packages.repository]]
url = "file://../../pkgs/_repo2_"
trust_anchors = ["63977c7be45624999b88bac5aa55ab5280656ee076617a285c87602a0d980602"]
keys = ["../../pkgs/dev-signing.pub"]

[baseline]
edition = "Experimental"
package = "dev.peios.peios-experimental"
source_date = "2026-06-22T00:00:00Z"

The explicit package selects dev.peios.peios-experimental; without a version, the newest available version wins. The trust anchor authenticates repository metadata, and keys supplies the package-signing public key. The spec lists every key.

Build #

cd dist/release
make iso

peiso reports each stage:

resolving peios-experimental
resolved dev.peios.peios-experimental 2026.8-1 (… packages)
composing dist/peios-experimental-2026.8/root
resolving medium packages
publishing dist/peios-experimental-2026.8/repo
medium repo: 2 packages, trust anchor 6e50…
staged seed authd-policy
…
seeds: 15 staged
packing initramfs -> …/root/system/boot/initramfs.cpio.zst
squashing root -> …/rootfs.squashfs
squashed with 4414 signature attribute(s)
building UKI -> …/root/boot/efi/EFI/BOOT/BOOTX64.EFI
building ISO -> dist/peios-experimental-2026.8/peios-experimental-2026.8.iso
dist/peios-experimental-2026.8/peios-experimental-2026.8.iso

The last line is the ISO. Everything the build made is beside it, in dist/peios-experimental-2026.8/ — see Running a build for the layout.

Boot #

make boot

runs QEMU with UEFI firmware, the ISO attached as a virtio disk, and the serial console on your terminal. You will see the kernel, then prelude (the initramfs PID 1) running its hooks — live-boot finds the medium and mounts the live root — then peinit bringing up the services the release's seeds define, and finally a login prompt. The image autologs in a development account.

Open the web desktop #

make iso includes GXWI, the Fenestra compositor, the fenesh desktop shell, Gexora file explorer and the Hello demonstration app. Their packages supply binaries, application declarations, icons and registry seeds; no development share or checkout is needed in the guest.

After make boot reaches the console, open https://127.0.0.1:7780/ on the host. The live image has the peios development account with no password. Fenestra and its apps run as the logged-on principal using a RemoteInteractive session. Installed systems use their provisioned account instead.

GXWI serves HTTPS with a certificate the machine makes for itself, so the browser warns before the first sign-in. Before going past the warning, check the certificate's SHA-256 fingerprint against the machine's own: sign in at the VM's console (the live image's peios account has no password) and run cat /var/state/gxwi/certificate.sha256. A live medium makes a new key every boot, so the warning comes back each time. installerd attempts to carry the key into the installed system, but a skipped or failed transfer does not fail the installation. Verify any changed fingerprint through the installed machine's trusted console before entering credentials. See certificate carry-over. Plain http:// is answered with a redirect to https:// and nothing else.

The Experimental edition enables TCP 7780 on all guest interfaces and admits it through the guest packet policy. The Makefile's host port forwards bind to localhost; that protects the QEMU host listener, not a machine booted directly from the ISO or a VM configured with bridged networking.

The edition opts into gxwid-service, gxwi-config, gxwi-network, fenestra-config and fenesh-config. Machine\Software\GXWI selects the listener and compositor; Machine\Software\Fenestra selects the shell. Package installation alone does not activate these policy seeds. The GXWI service retains SeImpersonatePrivilege so it can hand the authenticated principal's token to peinit for the session job.

SSH remains available on host port 2222 after provisioning a permitted key or password credential. GXWI does not currently offer SSH-key authentication.

The same Makefile has boot-dwe, boot-install and boot-installed; Running a build covers them.

Change something #

Two things are yours to change in the spec without touching anything else:

[devtools]
dwe = true            # a development medium; builds to …-2026.8-dwe/

[registry]
remove = ["lpsd-first-account"]   # drop a seed the release would apply
add    = ["my-service"]           # add one a package in the closure ships

[[package]]
name = "org.gnu.bash"             # carry a package the edition does not

To change what the release is, change the edition package (pkgs/dev.peios.peios-experimental/) and republish it; the spec does not change. Editions explains why that line is drawn where it is.

The build pipeline

Peios / Peiso / Building images

peiso iso runs seven stages. peiso root runs only the first. Each stage consumes what earlier ones produced, and everything lands in one build directory, dist/peios-<edition>-<version>[-dwe]/ — The build directory lists its contents.

1. Resolve and compose the root #

peiso writes a compose manifest naming the edition package (at the spec's version, or any), the medium's extras (live-boot for first_boot = "live", none at all for "none"; peios-dwe when dwe = true), the spec's package sources, and one named root, initramfs at boot/initramfs.

A file:// source that holds a repository descriptor is declared as a repository; one that does not (the local pool, pkgs/_pkgsOut_) joins as local packages. Then peiso resolves — a lock — and reads the edition's resolved version from it, because the build directory is named after that version and the spec may not have said it. Only then does it compose, from the lock, into root/. The manifest and lock are kept beside the root: they record exactly what was resolved.

Compose writes each repository it drew from into the root as lcl/conf/peipkg/source<N>.repo, so that the system goes on drawing from it. peiso then takes the file:// ones out again. Such a source is a path on the machine that ran the build, and on the image it names nothing. It could not be left as a harmless leftover either: peipkg refuses to resolve while any configured repository has no usable index, so a repository nothing can refresh would stop every install, upgrade and removal on the live system and on what is installed from it — the installer's own boot package swap among them. An http(s):// source stays, and is the image's to add with peipkg repo add source<N> like any repository it carries. The build says which sources it left out.

Two things about the composed tree:

  • It is complete but attribute-less. Where a package carries a signature sidecar for a non-ELF file (device firmware: <file>.peios.sig), compose would normally set the target's security.peios.sig attribute — which needs CAP_SYS_ADMIN. peiso asks compose to record each one instead (sidecars.jsonl), and writes them into the squashfs at stage 5. The root/ on the build host therefore carries no security attributes; the image does.
  • The initramfs is a second root inside it. Packages the initramfs runs — prelude, its hooks, the module subset — are placed there by the edition's cross-root dependency edges, not by anything peiso does.

Compose is given the composer's half of the Special System Package exemption: dev.peios.fsbase lays down the mountpoint tree the layout rules otherwise protect, and a root with no /proc is not a root.

2. Publish the medium repository #

The installer swaps the live boot packages for the disk ones on the target, and it takes dev.peios.disk-boot and dev.peios.disk-boot-irf from a repository carried on the medium — a live image cannot carry them installed, because live-boot-irf and dev.peios.disk-boot-irf conflict in the initramfs.

peiso resolves those two packages against the same source scan the root's lock came from — not merely the same source declarations, the same gathered universe, so a package republished mid-build cannot give the root and the medium different revisions. It publishes them into repo/ with a key generated for this build, and writes root/lcl/conf/peipkg/peios-medium.repo naming that key as the trust anchor and file:///media/peios/repo as the base URL — where live-boot mounts the medium.

The key is a throwaway and its private half is never written anywhere. A key shipped beside the repository it signs proves nothing about it — tampering with the ISO tampers with both — so its only job is to satisfy the "required" signature policy that peipkg rightly insists on. The repository's descriptor also lists the keys the packages were signed with (keys in the spec): a repository accepts a package only if its signer is in the descriptor.

3. Stage the registry seeds #

Packages ship registry seed masters under /usr/share/regim/, and installing a package applies none of them: which seeds a system runs is policy, not payload. The edition states that policy in /usr/share/peios/release.toml. peiso reads it from the composed root, applies the spec's [registry] add and remove, and copies each named seed into one of three queues under root/lcl/policy/:

QueueFromApplied by
autoapply.d/autoapplyevery system
autoapply.live.d/live_autoapply, plus dwed-service for a DWE mediumthe boot medium only
autoapply.install.d/install_autoapplythe installed machine only

Three, not one, because an installer copies the shipped image verbatim: what is staged for the medium arrives on the disk unless something takes it off again. A development account whose password ships in the image belongs on one and not the other; so does first-boot setup, the other way round.

peiso also places the autorun script that drains the first two — reg apply --dir /lcl/policy/autoapply.d --once-delete, then the same for autoapply.live.d — which peinit runs at boot before it plans its services, so the services the seeds define start on the very boot that creates them. Base before live, so a live seed can override a value the base one set.

autoapply.install.d/ is deliberately absent from the drain: applying it on the medium would start a first-boot flow on a machine nobody has installed yet. The installer promotes it into the target's autoapply.d/ and deletes autoapply.live.d/.

A seed the spec names that no package in the root ships is an error, not a warning. So is a seed named in two queues.

Medium-only packages #

peiso also writes /lcl/policy/medium-only-packages into the image. Both installerd and the legacy peios-install script use it to remove packages selected only for the medium before installing disk-boot. A DWE image carries:

peios-medium-packages-v1
root dev.peios.live-boot
initramfs dev.peios.live-boot-irf
root dev.peios.dwed

The first line identifies the format. Each remaining nonempty line names one of the two supported roots and a package name; duplicate entries, unknown roots and unknown formats are errors. Plain images omit the DWE row. peiso does not mark arbitrary [[package]] additions for removal.

The copied list remains on the target while the package swap is incomplete. A retry removes only listed packages still installed, and a package-database error stops the operation. Package failures preserve the medium repository's configuration so a retry can still install disk-boot.

After the swap succeeds, the installer atomically replaces the list with the single newline-terminated line peios-medium-packages-complete-v1, then removes the medium repository and the checkpoint. A retry of this completed state only finishes cleanup: repository removal may already have deleted the configuration needed by repo add, so it must not repeat trust setup or package installation. Repair boot files recognises either checkpoint state even if live-boot has already been removed. Registry seed queue handling is unchanged.

Media built before this list existed use the historical two boot-package removals. Rebuild a DWE medium with the updated peiso and installers to remove its DWE package automatically; updating an installer alone cannot infer the medium's original package choices.

4. Pack the initramfs #

mkirf packs root/boot/initramfs/ into root/system/boot/initramfs.cpio.zst — inside the root, where an installed system keeps its own, so the squashfs carries it. Two subtrees are excluded: var/state/peipkg (the initramfs root's package database) and lcl/conf/peipkg (its repository configuration). Both belong to the real root.

mkirf is the copy in the root being built, run on the host under the root's own dynamic loader (usr/lib/<triplet>/ld-linux-*.so.2 --library-path … --argv0 mkirf …/usr/bin/peiosutils). No chroot, no privilege, and no second implementation of the tool the running system uses.

5. Squash the root #

The whole root/ becomes rootfs.squashfs (zstd). peiso streams the tree to mksquashfs -tar as a PAX tar rather than pointing it at the directory, for three reasons that all come down to the same property — the image is what a privileged build would have produced:

  • every entry is root:root, though the build user owns the files on disk;
  • every recorded signature from stage 1 becomes the file's security.peios.sig attribute, written into the squashfs by name — a squashfs stores whatever attributes the tar names;
  • every mtime is the spec's source_date, and entries are emitted in sorted order, so the image is reproducible.

A recorded signature whose file is not in the root is an error: a stale record must not ship unsigned firmware.

6. Build the UKI #

mkuki — again the shipped copy, run the same way — bundles the kernel from root/usr/lib/modules/<release>/, the cpio from stage 4, and the command line live-boot ships (root/usr/share/live-boot/cmdline) into one EFI binary at root/boot/efi/EFI/BOOT/BOOTX64.EFI. It runs after the squashfs so the image does not carry the ESP tree.

That command line names no console=. As on any distribution, the kernel chooses: the display's virtual terminal on a machine that has one, the serial port otherwise, and /dev/console follows the choice, so the login prompt appears wherever the kernel put its messages. A boot that wants the serial line regardless says so at boot time through the UKI stub's SMBIOS channel, which is what the dist/release targets do.

7. Author the ISO #

root/boot/efi/ becomes a FAT32 image (mkfs.vfat + mcopy, no mount) at boot/efi.img inside the ISO's data area, beside rootfs.squashfs and repo/. xorriso names that one file twice: as the El Torito boot image, which is how firmware boots an optical drive (a physical disc, or an ISO a hypervisor attaches as a CD-ROM), and as the EFI System Partition in a GPT, which is how firmware boots the same bytes written to a USB stick or attached as a disk. The image has to sit inside the ISO9660 volume rather than be appended after it: El Torito cannot record a boot image larger than 32 MiB, and firmware reads the missing size as "to the end of the volume" — an appended partition starts exactly there and comes to nothing. The result is peios-<edition>-<version>[-dwe].iso, label PEIOS, which UEFI firmware boots directly either way: the UKI's initramfs runs live-boot, which finds the medium by that label, mounts the squashfs over an overlay, and pivots.

What each stage needs #

StageRunsFrom
composepeipkg's compose libraryin-process
medium repositorypeipkg's repopub libraryin-process
seedsfile copiesin-process
initramfsmkirfthe root, via its loader
squashfsmksquashfsbuild host
UKImkukithe root, via its loader
ISOmkfs.vfat, mcopy, xorrisobuild host

Nothing runs as root, and nothing runs inside the root.

Running a build

Peios / Peiso / Building images

The commands #

peiso root <spec.toml>... [--out <dir>] [--overwrite]
peiso iso  <spec.toml>... [--out <dir>] [--overwrite]

peiso root composes the spec's edition into a root tree and stops — useful for inspecting what a release contains, or for a root that will be packed some other way. --out names the directory; without it the root goes to the same place iso would put it.

peiso iso runs the whole pipeline and prints the ISO's path on stdout as its last line; progress goes to stderr. Both commands work from any current directory — relative paths inside a spec resolve against that spec's own directory, and the output directory dist/peios-…/ is relative to where you run.

A build refuses an output path where a root already sits. --overwrite replaces it and rebuilds — the rebuild loop after republishing a package — but replaces only something recognisably a composed root (it carries the package database) or an empty directory, never an arbitrary directory the path happens to name. make iso passes it, so rebuilding is the default there.

Both also take more than one spec, layering them left to right, so a statement of where packages come from can be shared across many builds that each name only what they build. The spec has the rules.

There is no --version, --edition or --dwe flag. Everything that shapes an image is in the spec, so a build is reproducible from the file alone.

The Makefile #

dist/release/Makefile wraps the commands and the QEMU invocations that boot their output:

Target
make isobuild peiso from the tree, then peiso iso experimental.toml
make iso-devthe same with the dev.toml layer: a low squashfs level, so the squash stage takes a fraction of the time and the image is somewhat larger — the edit-boot loop's target, never a release. It writes the same build directory as iso.
make iso-dwethe same from experimental-dwe.toml
make bootboot the newest plain build: UEFI, serial console (the Makefile appends console=ttyS0 through the stub's SMBIOS channel; the image itself names none), GXWI available at https://127.0.0.1:7780/ on the host
make boot-cdboot the newest plain build with the medium attached as an optical drive, the way a hypervisor attaches an ISO
make boot-dweboot the newest DWE build with a vsock device (DWE_CID, default 3)
make boot-quiet / boot-breakadd peios.quiet=2 / rd.break to the command line via SMBIOS
make boot-vgaboot with the framebuffer in a window and console=tty0 last, so the surface is drawn on a VT — what a real machine shows. The serial log stays on stdio. Combine with any target by hand: make boot-installed GRAPHICS='-display gtk -vga std' SMBIOS=…
make boot-installboot the DWE build with a blank 8 GiB disk.img attached as /dev/vdb
make boot-installedboot disk.img alone — no medium
make clean-disk / make cleanstart the install test over / remove every build directory

The build directory a target uses is the newest ../peios-experimental-<version>[-dwe]/; only peiso knows a build's resolved version, so the Makefile looks rather than guesses.

Before signing in to GXWI, follow the Quick start's certificate check.

Driving the running system #

dist/release/drive.py boots a build by the direct kernel path, waits for the login prompt, feeds a script to the console, and prints what the guest said:

./drive.py --no-disk probe.sh                 # in the live system
./drive.py --installed probe.sh               # on the installed disk
./drive.py --build ../peios-experimental-2026.9 --no-disk probe.sh

It talks to the ordinary autologin session — an administrator, not SYSTEM. For anything that needs SYSTEM, use a DWE build and dwe exec.

The legacy peios-install test #

An installation medium is only proven by installing from it and booting what it produced. The test below exercises the older peios-install script through DWE. For ordinary console or browser installation, follow Install Peios on a PC and Installing to disk: the current installerd daemon performs privileged disk operations as SYSTEM for its unprivileged surfaces and does not require this DWE test path.

The legacy script is supported only from a SYSTEM session. Its ordinary cp -ax must read SYSTEM-only source files such as /boot/initramfs/var/state/secrets; direct installation from an administrator session is unsupported. The console administrator can open the raw disk, and the PEI-549 kernel change separately allows partition maintenance through SeManageVolumePrivilege. DWE provides the SYSTEM session used below:

make iso-dwe
make boot-install          # leave it running

then, from another terminal:

DWE_TARGET=vsock:3 dwe exec -- peios-install --yes --whole-disk /dev/vdb

The script partitions and formats the disk, copies the live root, establishes trust in the medium repository on the target, replaces live-boot with disk-boot in both the root and its initramfs, writes a command line with the root filesystem's UUID, and rebuilds the target's initramfs and UKI. Then:

make boot-installed

boots the disk with nothing else attached. The initramfs hook is now disk-boot's (mount-root-disk.sh); the login prompt on the serial console is the installed system. An installed system moves to the next release with upgrade-peios.

Note

With a rebuilt medium containing the updated peiso and installers, both installation paths remove the medium-selected dev.peios.dwed package from the target. Older media without the medium-only package list keep the historical boot-package-only cleanup. The live-only DWE service seed is discarded in either case. See medium-only packages.

Variants #

A variant is another spec file. experimental-dwe.toml is the Experimental spec plus [devtools] dwe = true, and builds into its own …-dwe/ directory so it never overwrites the plain image. A spec that pins version = "2026.8" reproduces a release after newer ones exist; one that names a different [[packages.repository]] builds from another source.

Customising an image

Peios / Peiso / Building images

peiso's job is to make a custom Peios image an ordinary thing to build. The release stays what the edition says it is; everything you add on top is yours, and peiso writes it all down in customisations.toml beside the build so two images from the same release differ auditably rather than silently.

This page goes through the additions in the order the build applies them. All of them are optional.

Extra packages #

[[package]]
name    = "org.gnu.bash"
version = ">= 5.2"        # optional

Resolved together with the edition's closure from the spec's sources, so a conflict fails the build. See The spec.

Files #

[[file]]
src  = "overlay/motd"                        # relative to the spec
dest = "usr/etc/motd"                        # inside the root
sddl = "O:SYG:SYD:(A;;GA;;;SY)(A;;FR;;;WD)" # optional

The file is copied into the composed root before anything is packed, so a dest under boot/initramfs/ lands in the initramfs — a hook you are still writing, say — and one under lcl/ lands in the operator tree. A file at a dest the release already has replaces it.

A program stays a program: a file whose source anyone can execute is injected with mode 0755, and any other with 0644. The mode means nothing more than that on Peios, but Linux still refuses to execute a file with no execute bit, whatever its descriptor allows.

sddl gives the file a security descriptor. Peios has no file modes; the descriptor is the whole of a file's access control. peiso compiles the SDDL and writes it into the squashfs as the file's security.peios.sd attribute — the same route as the firmware signatures, and just as privilege-free. Without sddl the file gets whatever the mount synthesises for an object with no descriptor, which follows its parent. The initramfs carries no attributes at all, so sddl on a file under boot/initramfs/ is refused.

Note

An injected file has no owning package. Nothing verifies it, nothing upgrades it, and peipkg does not know it exists. That is fine for a motd, a hook under development or a site's own policy file; the moment a file matters, make it a package.

First-boot scripts #

[[autorun]]
src  = "scripts/setup.sh"
name = "50-setup"         # optional; default: the file's name

The script is placed, executable, in /lcl/policy/autorun.d/, which peinit runs in order after the registry is serving and before it plans its services. peiso's own entries there are 10-apply-seeds.sh (the release's registry seeds) and 20-features.sh; a name that sorts after those runs once the release's policy is in place, which is what a script usually wants.

Whether a script runs once is the script's business. The idiom is to end with rm -- "$0" — on an installed system that is once; on the live medium the removal is an overlay whiteout that does not survive a reboot, so the script runs again against the fresh tmpfs registry, which is usually what a live image needs. The script runs with peinit's identity at that phase, not as a logged-in user.

Features #

[[feature]]
name  = "dynamic-boot"
state = "enable"          # or "install"; default enable

A feature is set-up a package's scripts do beyond its files. This generates 20-features.sh, which runs feat add <name> (install then enable) or feat install <name> per entry, in order, and removes itself. The feature's definition must be in the root — usually by a [[package]] that ships it (dev.peios.feat-dynamic-boot here). Services a feature creates start on the same boot, since autoruns finish before peinit enumerates them.

The dynamic-boot definition creates mkirf and mkuki foreground watcher services. It deliberately follows the unique kernel below /usr/lib/modules rather than recording the release present when the feature was installed, so a kernel package upgrade is adopted automatically. The UKI watcher prefers the installed system command line at /lcl/etc/boot/cmdline; a live image that has not been installed falls back to /usr/share/live-boot/cmdline.

Files on the medium #

[[medium]]
src  = "docs/"            # a file or a directory
dest = "docs"

Placed in the ISO's data area beside rootfs.squashfs and repo/. The live system mounts the medium at /media/peios, so this appears at /media/peios/docs. It is on the stick, not in the system: use it for payloads an installer or a script will read from the medium, or for things a person should find on it.

The packers #

[squashfs]
compression = "zstd"                     # any mksquashfs -comp; default zstd
exclude     = ["usr/share/doc"]          # root-relative; default none

[initramfs]
exclude = ["var/state/peipkg", "lcl/conf/peipkg"]   # the default; replaces it

[boot]
cmdline_extra = "loglevel=7 ignore_loglevel"   # appended to live-boot's command line

initramfs.exclude replaces the default list rather than adding to it, so an explicit [] packs everything; the two defaults are the initramfs root's package database and repository configuration, which belong to the real root. boot.cmdline_extra is appended to the command line the boot package ships and baked into the UKI — there is no full replacement, because the shipped line carries init=, and losing it quietly is how a custom image fails to boot.

The shipped line runs the kernel at loglevel=4, so the console carries kernel errors and worse and peinit's own boot narrative stays legible. The example above is the common override: it puts the full kernel log back. ignore_loglevel is the token that does the work — while it is set the kernel prints every message whatever loglevel says — so raising the number alone will not undo the default.

Note

The kernel's loglevel and peinit's peios.quiet are independent settings that both sound like "how noisy is the boot". loglevel governs what the kernel prints; peios.quiet governs what peinit prints. Quieting one does nothing to the other.

Not a knob: the ISO's label. live-boot finds the medium by it.

The record #

Every build writes customisations.toml beside the root:

# What this spec added beyond the release. Written by peiso.

[[file]]
src  = "/home/me/site/overlay/motd"
dest = "usr/etc/motd"
sddl = "O:SYG:SYD:(A;;GA;;;SY)(A;;FR;;;WD)"

[[autorun]]
src  = "/home/me/site/scripts/setup.sh"
name = "50-setup"

[[feature]]
name  = "dynamic-boot"
state = "enable"

Together with compose.lock.toml (every package that was resolved) it is the complete answer to "what is in this image that is not in the release".

The spec

Peios / Peiso / Reference

A spec is a TOML file. Unknown keys are errors. Relative paths resolve against the spec file's directory, not the current directory — which is also what lets several specs be layered into one.

[[packages.repository]]
url  = "file://../../pkgs/_pkgsOut_/"
keys = ["../../pkgs/dev-signing.pub"]

[baseline]
edition     = "Experimental"
package     = "dev.peios.peios-experimental"
version     = "2026.8"
first_boot  = "live"
source_date = "2026-06-22T00:00:00Z"

[devtools]
dwe = false

[registry]
add    = []
remove = []

[[package]]
name    = "org.gnu.bash"
version = ">= 5.2"
root    = ""

[[file]]
src  = "overlay/motd"
dest = "usr/etc/motd"
sddl = "O:SYG:SYD:(A;;GA;;;SY)(A;;FR;;;WD)"

[[autorun]]
src  = "scripts/setup.sh"
name = "50-setup"

[[feature]]
name  = "dynamic-boot"
state = "enable"

[[medium]]
src  = "docs/"
dest = "docs"

[squashfs]
compression = "zstd"
exclude     = []

[initramfs]
exclude = ["var/state/peipkg", "lcl/conf/peipkg"]

[boot]
cmdline_extra = ""

Layering #

A build may name several specs, and they are layered left to right:

peiso root shared.toml profiles/kernel-only/peiso.toml

Each file is read and resolved on its own first, against its own directory. A file:// repository URL, a [[file]] src, an [[autorun]] script: each means what it says where it is written, so a layer stays movable and two layers in different directories can each name paths beside themselves.

Only the layering as a whole has to be a complete spec. The two requirements no single file can be held to — an edition, and at least one repository — are checked after the merge, and the error names every layer rather than guessing which should have carried the key. So this is a spec:

# shared.toml — where packages come from. A property of the checkout.
[[packages.repository]]
url  = "file://../pkgs/_pkgsOut_/"
keys = ["../pkgs/dev-signing.pub"]
# profiles/kernel-only/peiso.toml — what is being built. A property of
# this one build.
[baseline]
edition    = "kernel-only"
first_boot = "none"

…while neither file is one alone. That split is the point: many builds can share one statement of where packages come from without repeating it, and without a build's own file having to restate anything it does not care about.

What a layer does to the one below #

ScalarsA layer that states one overrides; a layer silent about one leaves it alone. edition, package, version, first_boot, source_date, squashfs.compression, squashfs.level, boot.cmdline_extra, devtools.dwe.
ListsAccumulate in layering order: [[packages.repository]], [[package]], [[file]], [[autorun]], [[feature]], [[medium]], registry.add, registry.remove, squashfs.exclude. Repositories are searched in that order, so a base layer's sources come before a later layer's additions.
initramfs.excludeReplaces rather than accumulates. Absent and empty already mean different things there — the default list, and nothing — and appending would leave a layer no way to write either.

A single spec is just a layering of one, so nothing above changes what one file on its own means.

[[packages.repository]] #

Where packages come from. At least one is required; several are searched together, in order.

Key
urlrequiredA file:// path or an http(s):// URL. A relative file:// path is resolved against the spec's directory. A file:// directory holding a repository descriptor (repo.json) is used as a repository; one holding only .peipkg files is used as a directory of packages.
keysoptionalPublic key files (raw 32 bytes or PEM) of the signers of this source's packages. The medium repository must list a package's signer to carry it, and a plain directory of packages publishes no keys of its own — so a pool source needs this.
trust_anchorsoptionalKey fingerprints to verify a repository source against. Ignored for a directory of packages.

A source is where the build gets its packages, and only sometimes where the image will get more. An http(s):// repository is written into the image's repository configuration as source<N>, numbered in declaration order. A file:// one is not: it is a path on the build machine. See the build pipeline.

[baseline] #

Key
editionrequiredThe edition name as written on the box: "Experimental".
packageoptionalThe concrete package identity backing the edition, such as "dev.peios.peios-experimental". New qualified editions should always state it. If omitted for an older spec, Peiso derives the historical name as peios- plus the edition lowercased with whitespace replaced by hyphens: "Pro Desktop" → peios-pro-desktop.
versionoptionalThe edition version to build, e.g. "2026.8". Absent means the newest available. Written without the packaging revision; 2026.8 matches 2026.8-1 and any later revision.
first_bootoptional, default "live"What the medium boots into. "live" adds live-boot to the root. "none" says the composition is not a medium at all and adds no boot flavour — for a root something else will boot, or make bootable its own way; peiso iso refuses it. "install" is reserved and currently refused.
source_dateoptionalAn RFC 3339 timestamp stamped on everything the build produces — compose's source date, the repository indexes, every squashfs entry, the ISO. Absent means the build time, which is not reproducible.

[devtools] #

Key
dweoptional, default falseAdd the Developer Workstation Environment: dev.peios.dwed in the root and the live-only dwed-service seed. The package is listed as medium-only and removed from the installed target by either installer. The build directory and ISO gain a -dwe suffix. A DWE image is ownable by whoever can reach its vsock; it is for development.

[registry] #

Adjust the seeds the edition auto-applies. Names are seed names — login-console, not login-console.reg — resolved to /usr/share/regim/<name>.reg in the composed root.

Key
addoptionalSeeds to stage in addition to the release's. Each must be shipped by a package in the root; a name nothing ships is an error.
removeoptionalSeeds from the release's list (or from add, or the DWE seed) not to stage.

The starting list comes from the edition's release.toml, not from the spec. A spec never has to name a seed the release already applies.

[[package]] #

A package to carry beyond the edition. Repeat the table for each.

Key
namerequiredThe package name, resolved from the spec's sources with its dependencies.
versionoptionalA constraint in peipkg's grammar — "5.2", ">= 5.2", ">= 5.2, < 6". Absent means any version, newest wins.
rootoptional"initramfs" to place the package in the initramfs root — an extra boot hook, say. Absent means the main root.

An addition rides on the same resolution as the edition, so a package that conflicts with the edition's closure fails the build rather than quietly displacing something. There is no way to remove a package: the edition's closure is what makes the image a Peios, and everything else is here only because a [[package]] put it there.

[[file]] #

A file injected into the root, outside any package. Customising an image discusses the cost.

Key
srcrequiredHost path, relative to the spec; must exist and be a file.
destrequiredPath inside the root: relative (a leading / is dropped), cleaned, and not escaping the root.
sddloptionalThe file's security descriptor in SDDL, written into the image as security.peios.sd. Refused for a dest under boot/initramfs/.

[[autorun]] #

A script placed executable in /lcl/policy/autorun.d/.

Key
srcrequiredHost path, relative to the spec.
nameoptional, default the file's nameThe entry's name in the directory; ordering is by name. peiso's own entries are 10-apply-seeds.sh and 20-features.sh.

[[feature]] #

Key
namerequiredA feature under /libexec/features/, present in the root.
stateoptional, default "enable""enable" runs feat add (install then enable); "install" runs feat install only.

[[medium]] #

A file or directory placed in the ISO's data area, reachable at /media/peios/<dest> on the live system.

Key
srcrequiredHost path, relative to the spec; a file or a directory. Symlinks are refused — an ISO9660 data area cannot carry them.
destrequiredPath on the medium, relative and cleaned.

[squashfs] #

Key
compressionoptional, default "zstd"Passed to mksquashfs -comp.
leveloptional, default the compressor's ownPassed to mksquashfs -Xcompression-level; must be a level the chosen compressor accepts. A low level trades image size for build time — the knob a dev layer sets, never a release.
excludeoptionalRoot-relative paths left out of the image (a directory takes its subtree).

[initramfs] #

Key
excludeoptional, default ["var/state/peipkg", "lcl/conf/peipkg"]Globs, relative to the initramfs root, that mkirf leaves out. Setting it replaces the default; [] excludes nothing.

[boot] #

Key
cmdline_extraoptionalAppended to the kernel command line the boot package ships, and baked into the UKI. There is no full replacement.

What the spec cannot say #

No package list, no output paths, no ISO label, no full command-line replacement. Everything the spec adds — packages, files, scripts, features, medium contents — is an addition to a release, recorded in customisations.toml; nothing in it can redefine what the release is, and nothing in it can quietly produce a different image from the same file. What a Peios contains is the edition package's to say; peiso builds what it says, plus what you asked for on top.

The build directory

Peios / Peiso / Reference

Every build writes to one directory, named for what was resolved: dist/peios-<edition>-<version>/, with -dwe appended for a development medium. The version is the edition package's without its packaging revision — 2026.8, not 2026.8-1. The directory is relative to where peiso is run; peiso root --out overrides the root's location only.

A build refuses to run if root/ already exists; --overwrite replaces it (only a composed root or an empty directory — never anything else the output path may name). make iso passes the flag, so rebuilding in place is its normal behaviour.

EntryWritten by
compose.tomlcompose stageThe generated compose manifest: sources, the initramfs root, the edition and extra package requests. Marked "do not edit".
compose.lock.tomlcompose stageThe resolved closure: every package, version, root, source and hash. What the image is, exactly.
root/compose stageThe composed system. root/boot/initramfs/ is the initramfs root; root/var/state/peipkg/ its database; root/lcl/conf/peipkg/ its repository configuration.
sidecars.jsonlcompose stageThe signature sidecars compose recorded instead of stamping: one {path, blob} per line, path relative to root/. Consumed by the squashfs stage.
customisations.tomlcustomise stageEvery file, autorun and feature the spec added, with the host paths they came from.
repo/medium stageThe medium repository: descriptor, indexes, keys, and the disk-boot packages. Copied onto the ISO as repo/.
root/lcl/conf/peipkg/peios-medium.repomedium stageThe client configuration for it, anchored on this build's key.
root/lcl/policy/autoapply.d/*.regseeds stageThe staged seeds.
root/lcl/policy/autorun.d/10-apply-seeds.shseeds stageThe autorun that drains them at boot.
root/system/boot/initramfs.cpio.zstinitramfs stageThe packed initramfs, inside the root.
rootfs.squashfssquashfs stageThe live root filesystem, with signature attributes. Outside root/ so it cannot contain itself.
root/boot/efi/EFI/BOOT/BOOTX64.EFIUKI stageThe unified kernel image. root/boot/efi/ is the ESP tree.
peios-<edition>-<version>[-dwe].isoISO stageThe medium.

The Makefile adds its own files beside the spec, not in the build directory: disk.img (the install target), ovmf-vars.fd (firmware variables, reset on every boot), and console.log from drive.py.

Only the ISO is needed to boot or install. root/ and rootfs.squashfs are kept because they are what you look at when something is wrong; compose.lock.toml is kept because it is the provenance.

Editions

Peios / Peiso / Editions and upgrades

Peios has no channels and no release cadence. It is a rolling release with a full archive: every version of every package ever published stays installable. What it has instead of a channel is an edition — a package that says what a Peios is.

The edition package #

dev.peios.peios-experimental is the edition today. It retains peios-experimental as a compatibility capability for image composition and as the bounded name of pre-qualification installations. It is an ordinary peipkg package with an unusual job:

  • Its version is the OS version. dev.peios.peios-experimental 2026.8-12 is Peios 2026.8. The package writes /usr/lib/os-release (VERSION_ID="2026.8", VARIANT_ID=experimental, PRETTY_NAME="Peios 2026.8 Experimental") and the /usr/etc/os-release compatibility symlink, so a system's identity and its release are one fact, tracked in the package database.
  • Its dependencies are the base system. Kernel, modules and firmware; the initramfs — prelude, its hooks, the module subset and its own base tree, each placed IN initramfs explicitly; the service manager, registry, identity and networking daemons; the installer. Installing the package into an empty root yields a bootable Peios.
  • It provides peios-release. Depend on that virtual name to mean "some Peios"; depend on the edition to mean that edition.
  • It ships release.toml: what the release asks of a system beyond its packages, as data.
  • It declares an alternate upgrade path. peipkg refuses to install or upgrade it by name, holds it back from an upgrade of everything, and prints the message it carries: To upgrade Peios use the upgrade-peios command. The reasons are below.

"Experimental" is a name, not a channel. It marks that Peios is not yet production-ready and says nothing about cadence. When enough exists for the split to make sense it is replaced by peios-pro, peios-minimal, peios-desktop and so on — each its own package, each providing peios-release, each shipping its own os-release.

What the edition deliberately does not contain #

Three things a live medium needs are absent from the edition, because they are properties of the medium:

  • live-boot and disk-boot. Which boot packages a system carries depends on what it boots from. The installer swaps one for the other on the target; an edition that named either would be installable on only one kind of medium.
  • peios-dwe. The Developer Workstation Environment makes a machine ownable. It is added by peiso for a development medium and could never be a thing that arrives by installing a package.
  • Anything the installer is not. peios-install is in the edition: an installed system can install another.

Pinning #

Experimental's dependencies are >= floors — a floor lets a package move without republishing the edition on every change, which is right for a pre-alpha. A production edition pins exact versions, and then something useful follows: peipkg upgrade cannot move a pinned dependency past what the edition allows, so a real release upgrades as a unit, through upgrade-peios, never piecemeal.

Why the package manager will not move it #

Installing a package on Peios cannot change system policy. A package delivers files; which services run and which policies apply is decided by registry seeds that a package can only offer. That is the deliberate weakness that lets packages be installed freely.

Moving from one release to the next is more than a package upgrade: the new release's seeds have to be reconciled, and that is precisely the thing peipkg must not do. So the edition declares, in its manifest, alternate_upgrade = { message = "To upgrade Peios use the upgrade-peios command." } (PSPU §5.18). The declaration grants nothing — a package cannot name a command for the consumer to run, let alone run one. Its whole effect is that peipkg stops and shows the message, with a warning that whatever the message names sits outside the package manager's protections. The person runs upgrade-peios, which drives peipkg with --bypass-alternate-upgrade and then does the reconciling itself.

Where an edition lives #

The recipe is pkgs/dev.peios.peios-experimental/ in the Peios tree: a pekit.toml whose build writes os-release and release.toml, and a package.pekit.toml with the dependency table. Changing what Peios contains is a change there, followed by a republish. peiso and its spec do not change.

release.toml

Peios / Peiso / Editions and upgrades

An edition package ships /usr/share/peios/release.toml. It states what the release asks of a system beyond its packages. peipkg never reads it; two tools do:

  • peiso, when composing an image, to stage the seeds into the medium.
  • upgrade-peios, to stage and apply the installed release's autoapply seeds, including when an ordinary CLI package request is a no-op or declined. Read the upgrade warning and preview procedure before invoking it.

Both act as the operator, deliberately; the package manager acts as neither.

Contents #

This example shows the three queues, including the GXWI desktop seeds listed in the Experimental quick start. It is not an exhaustive release manifest: inspect the selected edition's /usr/share/peios/release.toml for its complete lists.

# Example registry queues. Read by peiso and upgrade-peios.
[registry]
autoapply = [
  "port-reservations",
  "eudev-service",
  "authd-service",
  "authd-policy",
  "lpsd-service",
  "lpsd-authd-registration",
  "login-console",
  "eventd-config",
  "eventd-service",
  "netd-service",
  "netd-default-profile",
  "resolvd-service",
  "resolvd-port",
  "gxwid-service",
  "gxwi-config",
  "gxwi-network",
  "fenestra-config",
  "fenesh-config",
]

# The boot medium applies these and an installed machine does not.
live_autoapply = [
  "lpsd-first-account",
]

# Only an installed machine applies these.
install_autoapply = [
  "oobe-service",
]

Three lists, because a seed has a place #

A seed can belong to the medium, to the machine an installer writes, or to both. The distinction exists because an installer copies the shipped image verbatim: whatever is staged onto the medium arrives on the disk too, unless something takes it off again.

Two opposite cases make that matter, and before the split neither could be said:

  • lpsd-first-account provisions the development account whose password ships in the image and is therefore public. Right on a live medium; a hole on an installed machine — which used to get it, and then crash the provisioner against its own account on every subsequent boot.
  • oobe-service runs first-boot setup, which asks for a real account. Right on an installed machine; on the medium it would take the console away from the installer.
KeyStaged intoApplied by
autoapplylcl/policy/autoapply.d/every system, on first boot
live_autoapplylcl/policy/autoapply.live.d/the boot medium only
install_autoapplylcl/policy/autoapply.install.d/the installed machine only

Each names a master a package in the edition's closure ships at /usr/share/regim/<name>.reg; listing it is what opts it in. A reader treats a name nothing ships as an error, and a name in two lists as an error too — it would be staged and drained twice, and in every case it means the release said something it did not mean.

peinit's drain applies the first two, base before live, so a live seed can override a value the base one set: the medium is the special case, and the special case goes last. It never touches the third. The installer is what promotes autoapply.install.d/ into the target's autoapply.d/ and deletes autoapply.live.d/ — the act that turns a copy of a medium into a system.

Nothing else is defined yet. Unknown keys are rejected by both readers, so the file can grow without silently meaning nothing to an older tool.

What is not here #

  • dwed-service. The DWE seed is peiso's to add, for a development medium only; a release never applies it. peiso stages it into the live queue, on the same reasoning that puts the development account there: a machine installed from a DWE medium is a machine, and a vsock control channel into it is not something the operator asked for.
  • Anything a medium adds. live-boot and the medium repository are the image's, not the release's.
  • Seeds that were removed between releases. A seed present in 2026.8 and absent in 2026.9 is not un-applied by anything today; the registry keeps its history and that is where the record lives. This is the same limitation an edition has with packages it stops depending on, and it is deferred to the same future mechanism.

Why a file, and not a package field #

peipkg's manifest is the wrong place for this, on purpose. A field the package manager acted on — "apply these seeds when I am installed" — would let any package change system policy by being installed, which is the one thing Peios packages must not be able to do. As a file that only deliberate tools read, the list is inert until someone who has chosen to build an image or upgrade a system acts on it.

Upgrading Peios

Peios / Peiso / Editions and upgrades

Use upgrade-peios to move the edition package and reconcile the release's registry seeds. First inspect the installed release and preview the package plan:

upgrade-peios --status
upgrade-peios --check

--check does not apply the package plan or stage or apply registry seeds. Planning can still refresh repository metadata for peipkg's trust-freshness checks. It is a preview against the inputs available then, not a reserved plan for a later invocation.

Warning

Plain upgrade-peios can reapply policy even when no package changes. In the source versions described below, a package plan with nothing to do, or a package prompt you decline, returns success to the updater. It then stages the installed release's seeds and, on the live root without --on-reboot, applies the seed queue. Do not run the plain command just to look for updates, or rely on declining its package prompt to leave registry policy alone.

When you intend both the package update and seed reconciliation, run:

upgrade-peios

Re-running can finish seed work left by an interruption, but it also copies the installed release's seed masters into the queue again. Review the release's policy changes and preserve important local configuration before reapplying them. A failed package transaction needs the package recovery procedure, not an assumption that every interrupted step completed.

What happens #

  1. Find the edition. upgrade-peios reads ID and VARIANT_ID from /usr/lib/os-release — the file the edition package itself wrote — and derives the edition package's name from them: VARIANT_ID=experimental is dev.peios.peios-experimental. A system whose ID is not peios, or that has no variant, is refused (exit 2).
  2. Move the edition package. It runs peipkg upgrade dev.peios.peios-experimental --bypass-alternate-upgrade, passing --yes and --allow-stale through when given. The upgrade pulls the release closure with it. A peipkg failure is exit 3, and the updater stops before its seed steps. In ordinary CLI mode, peipkg exit 0 also includes a no-op or a declined prompt; those outcomes still reach the next step.
  3. Stage the installed release's seeds. The installed release.toml is read, whether or not the package version changed; each seed it names is copied from /usr/share/regim/ into /lcl/policy/autoapply.d/, and the drain script is placed in /lcl/policy/autorun.d/ if it is missing. A seed the release names that nothing ships is exit 4.
  4. Apply the queue. Unless staging for boot, reg apply --dir /lcl/policy/autoapply.d --once-delete --yes applies the queued seeds and removes successfully applied files. This command is run when the release names seeds and targets the whole directory, including other seeds already queued there. Registry policy and service definitions can change; verify their intended effects separately. Failure is exit 5, and is not a rollback of package changes or already-applied seeds.

upgrade-peios never elevates. peipkg and reg run with the caller's token, exactly as they would if you typed the two commands yourself; KACS decides what each may do.

An installation whose upgrade-peios itself predates the qualified package name is covered too. Its old concrete request for peios-experimental selects the final 2026.8-10 legacy migration release. That dependency-only release requires dev.peios.peios-experimental; the successor's bounded replaces edge removes the legacy package in the same transaction. The migration release therefore never enters the installed result. The successor also conflicts with the legacy name, so trying to install the trampoline into an empty system fails rather than leaving two edition identities behind.

Options #

--on-rebootStop after staging. This still writes the seed queue, including after a plain-CLI no-op or declined package prompt. The seeds apply on the next boot, when peinit runs the drain script before planning its services. Use this when a seed changes something you would rather not change under a running session — the console login, say.
--seeds-onlySkip the package upgrade and only reconcile the installed release's seeds. This is the re-run: after an interrupted upgrade, or to re-apply a release's policy.
--yes, -yPass --yes to peipkg.
--allow-stalePass --allow-stale to peipkg: carry on with a repository's out-of-date information when refreshing it brought nothing newer.
--checkPreview peipkg's package plan with --dry-run; do not apply that plan or stage or apply seeds. Trust-freshness checks can still refresh repository metadata.
--statusShow the installed release, the edition package's version, the seeds waiting for the next boot, and whether you may upgrade.
--root DIROperate on the Peios rooted at DIR rather than /. A non-live root implies --on-reboot: seeds are still staged there, but reg is not run against the live registry. peipkg is run with --root DIR.

To do it on the desktop, use Upgrade Peios. It uses driven mode: a cancelled result stops before seed staging and application. This cancellation handling differs from the ordinary CLI's exit-status check. A driven done result with no package operations is not cancelled; without --check, it can still reach seed reconciliation.

Check the outcome #

Separate the package result, the seed result, and the running system:

  1. Read the package result and warnings. peipkg exit 0 alone does not establish that an upgrade was performed or approved.
  2. Run upgrade-peios --status again, using the same --root DIR for an alternate root. Compare the installed edition version and queued seeds. An unreadable queue is unknown, not empty. After --on-reboot, staged seeds are expected to remain until boot applies them.
  3. If staging or application failed, read the failed step before retrying. Earlier seeds may already be staged or applied. --seeds-only stages the installed release's complete autoapply list again; it is not a read-only check or a reversal of the failed attempt.
  4. Verify affected registry policy and the software that uses it. An empty queue does not by itself prove every intended service or policy is working. Package undo does not restore registry state or undo a feature's setup; see recovery limits.

--status reads installed release/package metadata and the queue. It does not identify the currently running kernel or mapped service binaries. This updater invokes peipkg and reg; that path does not itself run a reboot or boot-image regeneration command. Do not use its successful exit or status as proof that all installed code is active or that every kernel update's boot artifacts are ready. Upgrading from the medium is a separate path with explicit boot-file regeneration.

Check what an update has activated #

Use these inspection steps after reviewing the package and seed results above. They do not regenerate boot files, start services or restart the machine. If a step cannot be verified, record boot activation not yet verified rather than treating the update as ready to boot.

Compare installed and running code #

For a kernel update on the live system, record:

uname -r
peipkg info dev.peios.kernel
peipkg files dev.peios.kernel

uname -r identifies the running kernel release. Compare it with the intended release in the installed kernel's payload paths, not directly with the edition or package version: those are different identifiers. A difference before restart is not proof that a restart will select the new kernel. For alternate roots, select the intended package root separately; uname still describes the running system.

Service status gives state, job, PID and start information, not an executing binary's build identity. svctl --version is the client version. Use the affected service's own reload/restart requirements and functional checks; package installation or a registry-definition reload does not prove it has adopted new code.

Inspect Dynamic Boot's actual state #

The located live regeneration mechanism is the optional Dynamic Boot feature, whose scripts define mkirf-watch and mkuki-watch. Package delivery, feature setup, enablement and running services are separate states:

feat info dynamic-boot
svctl status mkirf-watch
svctl status mkuki-watch
svctl definition show mkirf-watch
svctl definition show mkuki-watch

The reviewed feature initially creates disabled services. Enabling it after boot clears that flag but does not retroactively start them; disabling it does not stop an already-running watcher. Image setup before boot-time service enumeration can differ. Check the actual services rather than inferring their state from the feature record. This section supplies no automatic start or restart sequence.

In the packaged definitions, mkirf-watch writes /system/boot/initramfs.cpio.zst, and mkuki-watch writes /boot/efi/EFI/BOOT/BOOTX64.EFI. Verify the definitions on your machine. The UKI launcher chooses its command-line input at startup: a nonempty /lcl/etc/boot/cmdline, otherwise /usr/share/live-boot/cmdline. Creating the preferred file later does not prove an existing watcher switched to it.

Verify the destination and rebuild evidence #

Before relying on watcher output, use storage inspection to identify the intended EFI System Partition and the filesystem at the output path. Plain mount lists mounts visible in the caller's namespace; that is not proof of firmware selection or another namespace's view.

Warning

mkuki can create missing output directories without checking that they are on the intended ESP. A wrote message can therefore describe an ordinary directory, not the boot partition. The reviewed feature scripts do not supply a complete ESP-mount arrangement. If the mapping is absent or uncertain, stop the boot-readiness claim; do not choose a device or create a mount recipe by guesswork.

In Services Manager, inspect Logs… for each watcher over the update's job/time interval. Both can report rebuild failed and remain running; an Active/Alive state or watching message is not build success. mkuki also rejects zero or multiple regular vmlinuz-* candidates rather than selecting the newest one.

Look for successful cpio replacement and a subsequent successful UKI rebuild using the final settled inputs, retaining all rebuild/watch errors. A wrote message follows output replacement, but the watchers run independently of the package transaction: it is not an all-input generation or bootability attestation. Intermediate builds can observe different update stages. Missing logs or timestamps alone cannot establish success; log visibility and delivery limits still apply.

Check after a separately planned restart #

Only follow the machine's supported restart procedure once its prerequisites and recovery access are established. After it actually boots, run uname -r again and compare the intended release. Check queued-seed state, affected service jobs/status and their functional behavior separately. An accepted restart request, installed edition name or old successful build message does not replace these observations.

The installer-medium upgrade is a separate path that mounts its selected ESP and explicitly rebuilds boot files. It is not evidence that the live watcher destination is configured, nor a harmless generic repair.

Activation evidence and scope #

This checklist follows pinned static source, not a deployed-system test:

  • Dynamic Boot setup, enable, disable, and launcher paths.
  • mkirf failure handling and mkuki watch/build behavior.
  • Kernel payload naming and running-kernel readback.
  • Installer boot-file generation.

Check applicability to your image. These sources establish separate stages, not a complete live ESP setup, combined artifact verifier or successful boot.

For a program #

upgrade-peios --status --json answers with one object:

MemberIs
editionThe edition package's name.
name, version_id, variantPRETTY_NAME, VERSION_ID and VARIANT from os-release.
versionThe edition package's installed version, or null where peipkg's records can't be read.
queued_seedsThe seeds waiting in /lcl/policy/autoapply.d/ for the next boot, by name, or null where the queue can't be read.
may_upgradeWhether you may upgrade: asked by opening peipkg's lock and database for writing and checking the seed queue is writable. Nothing is written.

upgrade-peios --driven upgrades, or with --check looks, and reports it as JSON Lines events on standard output. Its first part is peipkg's driven mode: peipkg is run with upgrade-peios's standard input, so the program answers peipkg's questions as it would answer peipkg's own, and every event peipkg writes is passed on except its terminal one. Then upgrade-peios adds:

EventMembersMeans
progressphase, step, stepsphase is release-stage (the seeds are copied into the queue) or release-apply (they are applied).
messagetextWhat upgrade-peios is doing, or a line reg wrote.
donesummary, edition, seeds_queuedFinished; seeds_queued is true when the seeds were left for the next boot.
cancelledreasonpeipkg's plan wasn't approved; the updater stops before staging or applying seeds.
errorcode, messageFailed. The code is peipkg's own for its part (stale, busy, denied, unresolvable, untrusted, failed…), or usage, no-release, seeds or apply.

Exactly one done, cancelled or error ends the run. --yes is refused with --driven. A program that goes away mid-upgrade doesn't stop it.

From the medium #

A machine with no reachable repository is upgraded from an image instead: boot it from the new medium with its disk attached and choose Upgrade an installation in the installer. That runs the same procedure against the mounted disk — the edition with the bypass, --seeds-only for the seeds, then everything else the medium carries — and rewrites the boot files. The medium's repository index is fixed at manufacture, so the installer always passes peipkg --allow-stale, which this command does only when asked.

Why not peipkg upgrade #

peipkg upgrade upgrades every package it can and holds the edition back, printing the edition's message:

The package "dev.peios.peios-experimental" has an alternate upgrade path.

To upgrade Peios use the `upgrade-peios` command.

Warning: Alternate upgrade paths may bypass normal peipkg protections; ensure you fully trust the authors of the package before running.
held back: dev.peios.peios-experimental 2026.8-12 -> 2026.9-1

peipkg upgrade dev.peios.peios-experimental refuses outright with the same text. peipkg upgrade peios-experimental cannot name the installed qualified package at all: named upgrades intentionally do not resolve through provides. This is not peipkg being unable to move the edition; it is peipkg declining to do only half the job. Moving a release also means reconciling its seeds, and applying registry seeds is exactly what the package manager must never do on a package's behalf. The refusal keeps that line where it is, and the bypass used by upgrade-peios is the deliberate act that crosses it.

Once editions pin their dependencies exactly, this becomes the only way a release moves: peipkg upgrade cannot carry any pinned package past what the installed edition allows, so the system upgrades as a unit or not at all.

What it does not do #

  • It does not upgrade packages outside the edition's closure that the edition merely floors. With >= floors, peipkg upgrade afterwards brings the rest current; with pins there is no "rest".
  • It does not un-apply seeds the new release dropped. See release.toml.
  • It does not reboot or verify runtime activation. Check the running system separately from the installed package version.

Source and release scope #

The CLI/driven distinction above follows peiosutils 3344d469's upgrade path, its seed-copying step, and peipkg 8b588ae8's plan/approval return path. Peipkg performs trust-freshness checks before its dry-run boundary. The status implementation establishes the limits of the inspection command. These are source-backed behaviours, not a live cancellation/upgrade test or proof that a particular released image includes these revisions. Check the tool versions supplied by the image you operate.

Upgrade Peios

Peios / Peiso / Editions and upgrades

Upgrade Peios is the desktop's window on upgrade-peios. Open it from the launcher (type upgrade). It has no authority of its own: it runs upgrade-peios --driven as you, with the same package and seed operations. Driven mode handles an explicit cancellation separately: a cancelled result stops before release seeds are staged or applied. Do not apply the ordinary CLI's declined-prompt warning to that driven cancellation result.

The release #

At the top is the release installed on this machine, as its name (Peios 2026.8 Experimental) and its edition package with that package's version. Beside it, once Upgrade Peios has looked: Up to Date, a newer release Available, or Not Checked when the look was refused.

Looking for a newer release #

Upgrade Peios looks as it opens, and again with Check Now or Check Again. A look refreshes the repositories this machine trusts, updating cached metadata, then previews the package plan without applying it or staging or applying release seeds.

It refreshes only repositories already trusted: a repository set up but never confirmed is confirmed in Package Manager, under Repositories, never as a side effect of looking.

When a newer release is offered, the page shows every change upgrading makes — the edition package and whatever of its release comes with it — with what it downloads and takes once installed.

Upgrading #

Apply Its Settings at the Next Restart chooses when the release's settings (its registry seeds: which services run, which policies apply) take effect. Off, as it starts, they are applied as soon as the release is installed. On, they wait for the next restart; use it when a setting would change something under the people signed in now.

Upgrade to … makes the upgrade. The changes it makes are the ones just shown; if they differ by the time it starts, because a repository changed in between, the new changes are shown and asked about again. Anything peipkg needs your specific approval for, such as a package moving back to an older version, is asked on its own, as in Package Manager.

The page then shows how far it has got: downloading and checking, putting the release in place, recording it, then gathering and applying the release's settings. Once the upgrade is approved it is made to the end even if you close the window, which stays, saying so, until it is done.

The upgrade doesn't restart the machine. Its displayed release and completed package/seed steps do not establish which kernel or service binaries are running. Follow the outcome checks, and follow the activation inspection checklist before relying on a kernel update.

When something stops it #

  • A repository's information is out of date, and refreshing it brought nothing newer. Continue Anyway carries on with what this machine already has, as --allow-stale does, and is recorded. Upgrade Peios remembers it until you close it.
  • A repository isn't trusted yet. Confirm it, or remove it, in Package Manager.
  • Another change is being made. Try Again once it has finished.
  • The release's settings couldn't all be applied. The release is installed; its settings wait for the next restart, which tries them again.

Settings waiting for the next restart #

When some of the release's settings are waiting for the next restart — left by an upgrade made that way, or by one that stopped part-way — the page says so, with Apply Now to apply them at once, as upgrade-peios --seeds-only does.

What you may do #

Whether you may upgrade is asked of the system, by upgrade-peios. As shipped, only Administrators may. Anyone else sees the release, with the reason said once, and nothing to press.

Software other than Peios itself is updated in Package Manager, which holds the edition back and points here.

The driven invocation is established by gxwi-upgrade-peios f40e8d8b. The release updater's source scope also applies; a source revision is not proof of the version in an installed image.

Peios Learn — documentation for the Peios project.

Built with Trail.